| import { afterEach, beforeEach, describe, expect, it } from "vitest"; |
| import type { OpenClawConfig } from "../config/config.js"; |
| import type { MsgContext } from "./templating.js"; |
| import { setActivePluginRegistry } from "../plugins/runtime.js"; |
| import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js"; |
| import { resolveCommandAuthorization } from "./command-auth.js"; |
| import { hasControlCommand, hasInlineCommandTokens } from "./command-detection.js"; |
| import { listChatCommands } from "./commands-registry.js"; |
| import { parseActivationCommand } from "./group-activation.js"; |
| import { parseSendPolicyCommand } from "./send-policy.js"; |
|
|
| const createRegistry = () => |
| createTestRegistry([ |
| { |
| pluginId: "discord", |
| plugin: createOutboundTestPlugin({ id: "discord", outbound: { deliveryMode: "direct" } }), |
| source: "test", |
| }, |
| ]); |
|
|
| beforeEach(() => { |
| setActivePluginRegistry(createRegistry()); |
| }); |
|
|
| afterEach(() => { |
| setActivePluginRegistry(createRegistry()); |
| }); |
|
|
| describe("resolveCommandAuthorization", () => { |
| it("falls back from empty SenderId to SenderE164", () => { |
| const cfg = { |
| channels: { whatsapp: { allowFrom: ["+123"] } }, |
| } as OpenClawConfig; |
|
|
| const ctx = { |
| Provider: "whatsapp", |
| Surface: "whatsapp", |
| From: "whatsapp:+999", |
| SenderId: "", |
| SenderE164: "+123", |
| } as MsgContext; |
|
|
| const auth = resolveCommandAuthorization({ |
| ctx, |
| cfg, |
| commandAuthorized: true, |
| }); |
|
|
| expect(auth.senderId).toBe("+123"); |
| expect(auth.isAuthorizedSender).toBe(true); |
| }); |
|
|
| it("falls back from whitespace SenderId to SenderE164", () => { |
| const cfg = { |
| channels: { whatsapp: { allowFrom: ["+123"] } }, |
| } as OpenClawConfig; |
|
|
| const ctx = { |
| Provider: "whatsapp", |
| Surface: "whatsapp", |
| From: "whatsapp:+999", |
| SenderId: " ", |
| SenderE164: "+123", |
| } as MsgContext; |
|
|
| const auth = resolveCommandAuthorization({ |
| ctx, |
| cfg, |
| commandAuthorized: true, |
| }); |
|
|
| expect(auth.senderId).toBe("+123"); |
| expect(auth.isAuthorizedSender).toBe(true); |
| }); |
|
|
| it("falls back to From when SenderId and SenderE164 are whitespace", () => { |
| const cfg = { |
| channels: { whatsapp: { allowFrom: ["+999"] } }, |
| } as OpenClawConfig; |
|
|
| const ctx = { |
| Provider: "whatsapp", |
| Surface: "whatsapp", |
| From: "whatsapp:+999", |
| SenderId: " ", |
| SenderE164: " ", |
| } as MsgContext; |
|
|
| const auth = resolveCommandAuthorization({ |
| ctx, |
| cfg, |
| commandAuthorized: true, |
| }); |
|
|
| expect(auth.senderId).toBe("+999"); |
| expect(auth.isAuthorizedSender).toBe(true); |
| }); |
|
|
| it("falls back from un-normalizable SenderId to SenderE164", () => { |
| const cfg = { |
| channels: { whatsapp: { allowFrom: ["+123"] } }, |
| } as OpenClawConfig; |
|
|
| const ctx = { |
| Provider: "whatsapp", |
| Surface: "whatsapp", |
| From: "whatsapp:+999", |
| SenderId: "wat", |
| SenderE164: "+123", |
| } as MsgContext; |
|
|
| const auth = resolveCommandAuthorization({ |
| ctx, |
| cfg, |
| commandAuthorized: true, |
| }); |
|
|
| expect(auth.senderId).toBe("+123"); |
| expect(auth.isAuthorizedSender).toBe(true); |
| }); |
|
|
| it("prefers SenderE164 when SenderId does not match allowFrom", () => { |
| const cfg = { |
| channels: { whatsapp: { allowFrom: ["+41796666864"] } }, |
| } as OpenClawConfig; |
|
|
| const ctx = { |
| Provider: "whatsapp", |
| Surface: "whatsapp", |
| From: "whatsapp:120363401234567890@g.us", |
| SenderId: "123@lid", |
| SenderE164: "+41796666864", |
| } as MsgContext; |
|
|
| const auth = resolveCommandAuthorization({ |
| ctx, |
| cfg, |
| commandAuthorized: true, |
| }); |
|
|
| expect(auth.senderId).toBe("+41796666864"); |
| expect(auth.isAuthorizedSender).toBe(true); |
| }); |
|
|
| it("uses explicit owner allowlist when allowFrom is wildcard", () => { |
| const cfg = { |
| commands: { ownerAllowFrom: ["whatsapp:+15551234567"] }, |
| channels: { whatsapp: { allowFrom: ["*"] } }, |
| } as OpenClawConfig; |
|
|
| const ownerCtx = { |
| Provider: "whatsapp", |
| Surface: "whatsapp", |
| From: "whatsapp:+15551234567", |
| SenderE164: "+15551234567", |
| } as MsgContext; |
| const ownerAuth = resolveCommandAuthorization({ |
| ctx: ownerCtx, |
| cfg, |
| commandAuthorized: true, |
| }); |
| expect(ownerAuth.senderIsOwner).toBe(true); |
| expect(ownerAuth.isAuthorizedSender).toBe(true); |
|
|
| const otherCtx = { |
| Provider: "whatsapp", |
| Surface: "whatsapp", |
| From: "whatsapp:+19995551234", |
| SenderE164: "+19995551234", |
| } as MsgContext; |
| const otherAuth = resolveCommandAuthorization({ |
| ctx: otherCtx, |
| cfg, |
| commandAuthorized: true, |
| }); |
| expect(otherAuth.senderIsOwner).toBe(false); |
| expect(otherAuth.isAuthorizedSender).toBe(false); |
| }); |
|
|
| it("uses owner allowlist override from context when configured", () => { |
| setActivePluginRegistry( |
| createTestRegistry([ |
| { |
| pluginId: "discord", |
| plugin: createOutboundTestPlugin({ |
| id: "discord", |
| outbound: { deliveryMode: "direct" }, |
| }), |
| source: "test", |
| }, |
| ]), |
| ); |
| const cfg = { |
| channels: { discord: {} }, |
| } as OpenClawConfig; |
|
|
| const ctx = { |
| Provider: "discord", |
| Surface: "discord", |
| From: "discord:123", |
| SenderId: "123", |
| OwnerAllowFrom: ["discord:123"], |
| } as MsgContext; |
|
|
| const auth = resolveCommandAuthorization({ |
| ctx, |
| cfg, |
| commandAuthorized: true, |
| }); |
|
|
| expect(auth.senderIsOwner).toBe(true); |
| expect(auth.ownerList).toEqual(["123"]); |
| }); |
|
|
| describe("commands.allowFrom", () => { |
| it("uses commands.allowFrom global list when configured", () => { |
| const cfg = { |
| commands: { |
| allowFrom: { |
| "*": ["user123"], |
| }, |
| }, |
| channels: { whatsapp: { allowFrom: ["+different"] } }, |
| } as OpenClawConfig; |
|
|
| const authorizedCtx = { |
| Provider: "whatsapp", |
| Surface: "whatsapp", |
| From: "whatsapp:user123", |
| SenderId: "user123", |
| } as MsgContext; |
|
|
| const authorizedAuth = resolveCommandAuthorization({ |
| ctx: authorizedCtx, |
| cfg, |
| commandAuthorized: true, |
| }); |
|
|
| expect(authorizedAuth.isAuthorizedSender).toBe(true); |
|
|
| const unauthorizedCtx = { |
| Provider: "whatsapp", |
| Surface: "whatsapp", |
| From: "whatsapp:otheruser", |
| SenderId: "otheruser", |
| } as MsgContext; |
|
|
| const unauthorizedAuth = resolveCommandAuthorization({ |
| ctx: unauthorizedCtx, |
| cfg, |
| commandAuthorized: true, |
| }); |
|
|
| expect(unauthorizedAuth.isAuthorizedSender).toBe(false); |
| }); |
|
|
| it("ignores commandAuthorized when commands.allowFrom is configured", () => { |
| const cfg = { |
| commands: { |
| allowFrom: { |
| "*": ["user123"], |
| }, |
| }, |
| channels: { whatsapp: { allowFrom: ["+different"] } }, |
| } as OpenClawConfig; |
|
|
| const authorizedCtx = { |
| Provider: "whatsapp", |
| Surface: "whatsapp", |
| From: "whatsapp:user123", |
| SenderId: "user123", |
| } as MsgContext; |
|
|
| const authorizedAuth = resolveCommandAuthorization({ |
| ctx: authorizedCtx, |
| cfg, |
| commandAuthorized: false, |
| }); |
|
|
| expect(authorizedAuth.isAuthorizedSender).toBe(true); |
|
|
| const unauthorizedCtx = { |
| Provider: "whatsapp", |
| Surface: "whatsapp", |
| From: "whatsapp:otheruser", |
| SenderId: "otheruser", |
| } as MsgContext; |
|
|
| const unauthorizedAuth = resolveCommandAuthorization({ |
| ctx: unauthorizedCtx, |
| cfg, |
| commandAuthorized: false, |
| }); |
|
|
| expect(unauthorizedAuth.isAuthorizedSender).toBe(false); |
| }); |
|
|
| it("uses commands.allowFrom provider-specific list over global", () => { |
| const cfg = { |
| commands: { |
| allowFrom: { |
| "*": ["globaluser"], |
| whatsapp: ["+15551234567"], |
| }, |
| }, |
| channels: { whatsapp: { allowFrom: ["*"] } }, |
| } as OpenClawConfig; |
|
|
| |
| const globalUserCtx = { |
| Provider: "whatsapp", |
| Surface: "whatsapp", |
| From: "whatsapp:globaluser", |
| SenderId: "globaluser", |
| } as MsgContext; |
|
|
| const globalAuth = resolveCommandAuthorization({ |
| ctx: globalUserCtx, |
| cfg, |
| commandAuthorized: true, |
| }); |
|
|
| |
| expect(globalAuth.isAuthorizedSender).toBe(false); |
|
|
| |
| const whatsappUserCtx = { |
| Provider: "whatsapp", |
| Surface: "whatsapp", |
| From: "whatsapp:+15551234567", |
| SenderE164: "+15551234567", |
| } as MsgContext; |
|
|
| const whatsappAuth = resolveCommandAuthorization({ |
| ctx: whatsappUserCtx, |
| cfg, |
| commandAuthorized: true, |
| }); |
|
|
| expect(whatsappAuth.isAuthorizedSender).toBe(true); |
| }); |
|
|
| it("falls back to channel allowFrom when commands.allowFrom not set", () => { |
| const cfg = { |
| channels: { whatsapp: { allowFrom: ["+15551234567"] } }, |
| } as OpenClawConfig; |
|
|
| const authorizedCtx = { |
| Provider: "whatsapp", |
| Surface: "whatsapp", |
| From: "whatsapp:+15551234567", |
| SenderE164: "+15551234567", |
| } as MsgContext; |
|
|
| const auth = resolveCommandAuthorization({ |
| ctx: authorizedCtx, |
| cfg, |
| commandAuthorized: true, |
| }); |
|
|
| expect(auth.isAuthorizedSender).toBe(true); |
| }); |
|
|
| it("allows all senders when commands.allowFrom includes wildcard", () => { |
| const cfg = { |
| commands: { |
| allowFrom: { |
| "*": ["*"], |
| }, |
| }, |
| channels: { whatsapp: { allowFrom: ["+specific"] } }, |
| } as OpenClawConfig; |
|
|
| const anyUserCtx = { |
| Provider: "whatsapp", |
| Surface: "whatsapp", |
| From: "whatsapp:anyuser", |
| SenderId: "anyuser", |
| } as MsgContext; |
|
|
| const auth = resolveCommandAuthorization({ |
| ctx: anyUserCtx, |
| cfg, |
| commandAuthorized: true, |
| }); |
|
|
| expect(auth.isAuthorizedSender).toBe(true); |
| }); |
| }); |
| }); |
|
|
| describe("control command parsing", () => { |
| it("requires slash for send policy", () => { |
| expect(parseSendPolicyCommand("/send on")).toEqual({ |
| hasCommand: true, |
| mode: "allow", |
| }); |
| expect(parseSendPolicyCommand("/send: on")).toEqual({ |
| hasCommand: true, |
| mode: "allow", |
| }); |
| expect(parseSendPolicyCommand("/send")).toEqual({ hasCommand: true }); |
| expect(parseSendPolicyCommand("/send:")).toEqual({ hasCommand: true }); |
| expect(parseSendPolicyCommand("send on")).toEqual({ hasCommand: false }); |
| expect(parseSendPolicyCommand("send")).toEqual({ hasCommand: false }); |
| }); |
|
|
| it("requires slash for activation", () => { |
| expect(parseActivationCommand("/activation mention")).toEqual({ |
| hasCommand: true, |
| mode: "mention", |
| }); |
| expect(parseActivationCommand("/activation: mention")).toEqual({ |
| hasCommand: true, |
| mode: "mention", |
| }); |
| expect(parseActivationCommand("/activation:")).toEqual({ |
| hasCommand: true, |
| }); |
| expect(parseActivationCommand("activation mention")).toEqual({ |
| hasCommand: false, |
| }); |
| }); |
|
|
| it("treats bare commands as non-control", () => { |
| expect(hasControlCommand("send")).toBe(false); |
| expect(hasControlCommand("help")).toBe(false); |
| expect(hasControlCommand("/commands")).toBe(true); |
| expect(hasControlCommand("/commands:")).toBe(true); |
| expect(hasControlCommand("commands")).toBe(false); |
| expect(hasControlCommand("/status")).toBe(true); |
| expect(hasControlCommand("/status:")).toBe(true); |
| expect(hasControlCommand("status")).toBe(false); |
| expect(hasControlCommand("usage")).toBe(false); |
|
|
| for (const command of listChatCommands()) { |
| for (const alias of command.textAliases) { |
| expect(hasControlCommand(alias)).toBe(true); |
| expect(hasControlCommand(`${alias}:`)).toBe(true); |
| } |
| } |
| expect(hasControlCommand("/compact")).toBe(true); |
| expect(hasControlCommand("/compact:")).toBe(true); |
| expect(hasControlCommand("compact")).toBe(false); |
| }); |
|
|
| it("respects disabled config/debug commands", () => { |
| const cfg = { commands: { config: false, debug: false } }; |
| expect(hasControlCommand("/config show", cfg)).toBe(false); |
| expect(hasControlCommand("/debug show", cfg)).toBe(false); |
| }); |
|
|
| it("requires commands to be the full message", () => { |
| expect(hasControlCommand("hello /status")).toBe(false); |
| expect(hasControlCommand("/status please")).toBe(false); |
| expect(hasControlCommand("prefix /send on")).toBe(false); |
| expect(hasControlCommand("/send on")).toBe(true); |
| }); |
|
|
| it("detects inline command tokens", () => { |
| expect(hasInlineCommandTokens("hello /status")).toBe(true); |
| expect(hasInlineCommandTokens("hey /think high")).toBe(true); |
| expect(hasInlineCommandTokens("plain text")).toBe(false); |
| expect(hasInlineCommandTokens("http://example.com/path")).toBe(false); |
| expect(hasInlineCommandTokens("stop")).toBe(false); |
| }); |
|
|
| it("ignores telegram commands addressed to other bots", () => { |
| expect( |
| hasControlCommand("/help@otherbot", undefined, { |
| botUsername: "openclaw", |
| }), |
| ).toBe(false); |
| expect( |
| hasControlCommand("/help@openclaw", undefined, { |
| botUsername: "openclaw", |
| }), |
| ).toBe(true); |
| }); |
| }); |
|
|