/** Tests command-control detection and authorization trigger heuristics. */ import { afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { clearPluginCommands, registerPluginCommand } from "../plugins/commands.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, shouldComputeCommandAuthorized, } from "./command-detection.js"; import { listChatCommands } from "./commands-registry.js"; import { parseActivationCommand } from "./group-activation.js"; import { markInboundContextLabel } from "./reply/inbound-context-marker.js"; import { resolveAuthorizedSessionResetCommand } from "./reply/session-reset-command.js"; import { parseSendPolicyCommand } from "./send-policy.js"; import type { MsgContext } from "./templating.js"; import { installDiscordRegistryHooks } from "./test-helpers/command-auth-registry-fixture.js"; installDiscordRegistryHooks(); afterEach(() => { clearPluginCommands(); }); describe("resolveCommandAuthorization", () => { const formatAllowFrom = ({ allowFrom }: { allowFrom: Array }) => { const values: string[] = []; for (const entry of allowFrom) { const value = String(entry).trim(); if (value) { values.push(value); } } return values; }; function createAllowFromPlugin( id: string, resolveAllowFrom: () => Array | undefined, ) { return { pluginId: id, plugin: { ...createOutboundTestPlugin({ id, outbound: { deliveryMode: "direct" }, }), config: { listAccountIds: () => ["default"], resolveAccount: () => ({}), resolveAllowFrom, formatAllowFrom, }, }, source: "test" as const, }; } function createThrowingAllowFromPlugin(id: string, error: string) { return createAllowFromPlugin(id, () => { throw new Error(error); }); } function createOwnerEnforcingAllowFromPlugin( id: string, resolveAllowFrom: () => Array | undefined, ) { const entry = createAllowFromPlugin(id, resolveAllowFrom); return { ...entry, plugin: { ...entry.plugin, commands: { enforceOwnerForCommands: true }, }, }; } function registerAllowFromPlugins(...plugins: ReturnType[]) { setActivePluginRegistry(createTestRegistry(plugins)); } function resolveTestChannelAuthorization(params: { from: string; senderId?: string; senderE164?: string; allowFrom: string[]; }) { registerAllowFromPlugins(createAllowFromPlugin("mobilechat", () => params.allowFrom)); const cfg = { channels: { mobilechat: { allowFrom: params.allowFrom } }, } as OpenClawConfig; const ctx = { Provider: "mobilechat", Surface: "mobilechat", From: params.from, SenderId: params.senderId, SenderE164: params.senderE164, } as MsgContext; return resolveCommandAuthorization({ ctx, cfg, commandAuthorized: true, }); } it.each([ { name: "falls back from empty SenderId to SenderE164", from: "mobilechat:+999", senderId: "", senderE164: "+123", allowFrom: ["+123"], expectedSenderId: "+123", }, { name: "falls back from whitespace SenderId to SenderE164", from: "mobilechat:+999", senderId: " ", senderE164: "+123", allowFrom: ["+123"], expectedSenderId: "+123", }, { name: "falls back to From when SenderId and SenderE164 are whitespace", from: "+999", senderId: " ", senderE164: " ", allowFrom: ["+999"], expectedSenderId: "+999", }, { name: "falls back from un-normalizable SenderId to SenderE164", from: "mobilechat:+999", senderId: "wat", senderE164: "+123", allowFrom: ["+123"], expectedSenderId: "+123", }, { name: "prefers SenderE164 when SenderId does not match allowFrom", from: "mobilechat:group:room-1", senderId: "opaque-user", senderE164: "+41796666864", allowFrom: ["+41796666864"], expectedSenderId: "+41796666864", }, ])("$name", ({ from, senderId, senderE164, allowFrom, expectedSenderId }) => { const auth = resolveTestChannelAuthorization({ from, senderId, senderE164, allowFrom, }); expect(auth.senderId).toBe(expectedSenderId); expect(auth.isAuthorizedSender).toBe(true); }); it.each([ { name: "uses explicit owner allowlist when allowFrom is wildcard", channelConfig: { allowFrom: ["*"] }, }, { name: "uses explicit owner allowlist when allowFrom is empty", channelConfig: {}, }, ])("$name", ({ channelConfig }) => { const cfg = { commands: { ownerAllowFrom: ["whatsapp:+15551234567"] }, channels: { whatsapp: channelConfig }, } as OpenClawConfig; const resolveSender = (senderId: string) => resolveCommandAuthorization({ ctx: { Provider: "whatsapp", Surface: "whatsapp", From: `whatsapp:${senderId}`, SenderE164: senderId, } as MsgContext, cfg, commandAuthorized: true, }); const ownerAuth = resolveSender("+15551234567"); expect(ownerAuth.senderIsOwner).toBe(true); expect(ownerAuth.isAuthorizedSender).toBe(true); const otherAuth = resolveSender("+19995551234"); expect(otherAuth.senderIsOwner).toBe(false); expect(otherAuth.isAuthorizedSender).toBe(false); }); it("rejects wildcard channel senders when the plugin enforces owner-only commands", () => { setActivePluginRegistry( createTestRegistry([ { pluginId: "discord", plugin: { ...createOutboundTestPlugin({ id: "discord", outbound: { deliveryMode: "direct" }, }), commands: { enforceOwnerForCommands: true }, config: { listAccountIds: () => ["default"], resolveAccount: () => ({}), resolveAllowFrom: () => ["*"], formatAllowFrom, }, }, source: "test", }, ]), ); const cfg = { channels: { discord: { allowFrom: ["*"] } }, } as OpenClawConfig; const auth = resolveCommandAuthorization({ ctx: { Provider: "discord", Surface: "discord", ChatType: "direct", From: "discord:123", SenderId: "123", } as MsgContext, cfg, commandAuthorized: true, }); expect(auth.senderIsOwner).toBe(false); expect(auth.isAuthorizedSender).toBe(false); }); it("rejects channel-validated native commands when plugin owner enforcement has no owner allowlist", () => { setActivePluginRegistry( createTestRegistry([ { pluginId: "discord", plugin: { ...createOutboundTestPlugin({ id: "discord", outbound: { deliveryMode: "direct" }, }), commands: { enforceOwnerForCommands: true }, config: { listAccountIds: () => ["default"], resolveAccount: () => ({}), resolveAllowFrom: () => ["*"], formatAllowFrom, }, }, source: "test", }, ]), ); const cfg = { channels: { discord: { allowFrom: ["*"] } }, } as OpenClawConfig; const auth = resolveCommandAuthorization({ ctx: { Provider: "discord", Surface: "discord", ChatType: "direct", From: "discord:123", SenderId: "123", CommandSource: "native", } as MsgContext, cfg, commandAuthorized: true, }); expect(auth.senderIsOwner).toBe(false); expect(auth.isAuthorizedSender).toBe(false); }); it("uses context owner candidates for command authorization without granting owner status", () => { 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(false); expect(auth.ownerList).toEqual([]); expect(auth.isAuthorizedSender).toBe(true); }); it("suppresses inherited owner status when the context forbids it", () => { const cfg = { channels: { telegram: { allowFrom: ["owner-123"] } }, } as OpenClawConfig; const auth = resolveCommandAuthorization({ ctx: { Provider: "exec-event", Surface: "telegram", OriginatingChannel: "telegram", From: "owner-123", To: "owner-123", } as MsgContext, cfg, commandAuthorized: true, }); expect(auth.senderIsOwner).toBe(false); }); it("does not infer a provider from channel allowlists for webchat command contexts", () => { const cfg = { channels: { whatsapp: { allowFrom: ["+15551234567"] } }, } as OpenClawConfig; const ctx = { Provider: "webchat", Surface: "webchat", OriginatingChannel: "webchat", SenderId: "openclaw-control-ui", } as MsgContext; const auth = resolveCommandAuthorization({ ctx, cfg, commandAuthorized: true, }); expect(auth.providerId).toBeUndefined(); expect(auth.isAuthorizedSender).toBe(true); }); it.each([ { name: "does not apply channel-prefixed owner wildcards to webchat command contexts", owner: "discord:*", provider: "webchat", expectedProvider: undefined, expectedOwner: false, }, { name: "does not apply channel-prefixed owner identities to webchat command contexts", owner: "discord:123456789012345678", provider: "webchat", expectedProvider: undefined, expectedOwner: false, }, { name: "applies channel-prefixed owner identities to matching providers", owner: "discord:123456789012345678", provider: "discord", expectedProvider: "discord", expectedOwner: true, }, { name: "leaves retired channel-qualified owner migration to Doctor", owner: "discord:user:123456789012345678", provider: "discord", expectedProvider: "discord", expectedOwner: false, }, { name: "does not apply channel-prefixed owner wildcards to mismatched providers", owner: "telegram:*", provider: "discord", expectedProvider: "discord", expectedOwner: false, }, ] as const)("$name", ({ owner, provider, expectedProvider, expectedOwner }) => { const webchat = provider === "webchat"; const auth = resolveCommandAuthorization({ ctx: { Provider: provider, Surface: provider, ...(webchat ? { OriginatingChannel: "webchat", GatewayClientScopes: ["operator.write"] } : { From: "discord:123456789012345678" }), SenderId: "123456789012345678", } as MsgContext, cfg: { commands: { ownerAllowFrom: [owner] } } as OpenClawConfig, commandAuthorized: true, }); expect(auth.providerId).toBe(expectedProvider); expect(auth.senderIsOwner).toBe(expectedOwner); }); it("preserves third-party native owner identities outside the bundled Doctor migration", () => { registerAllowFromPlugins(createAllowFromPlugin("custom-chat", () => [])); const auth = resolveCommandAuthorization({ ctx: { Provider: "custom-chat", SenderId: "user:123" }, cfg: { commands: { ownerAllowFrom: ["custom-chat:user:123"] } }, commandAuthorized: true, }); expect(auth.senderIsOwner).toBe(true); }); it("preserves external channel command auth in mixed webchat contexts", () => { const cfg = { commands: { allowFrom: { whatsapp: ["+15551234567"] } }, channels: { whatsapp: { allowFrom: ["+15551234567"] } }, } as OpenClawConfig; const auth = resolveCommandAuthorization({ ctx: { Provider: "webchat", Surface: "whatsapp", OriginatingChannel: "whatsapp", From: "whatsapp:+19995551234", SenderE164: "+19995551234", } as MsgContext, cfg, commandAuthorized: true, }); expect(auth.providerId).toBe("whatsapp"); expect(auth.isAuthorizedSender).toBe(false); }); it("falls back to channel allowFrom when provider allowlist resolution throws", () => { registerAllowFromPlugins( createThrowingAllowFromPlugin("telegram", "channels.telegram.botToken: unresolved SecretRef"), ); const cfg = { channels: { telegram: { allowFrom: ["123"] } }, } as OpenClawConfig; const auth = resolveCommandAuthorization({ ctx: { Provider: "telegram", Surface: "telegram", From: "telegram:123", SenderId: "123", } as MsgContext, cfg, commandAuthorized: true, }); expect(auth.ownerList).toEqual([]); expect(auth.senderIsOwner).toBe(false); expect(auth.isAuthorizedSender).toBe(true); }); describe("commands.allowFrom", () => { const commandsAllowFromConfig = { commands: { allowFrom: { "*": ["user123"], }, }, channels: { whatsapp: { allowFrom: ["+different"] } }, } as OpenClawConfig; function makeWhatsAppContext(senderId: string): MsgContext { return { Provider: "whatsapp", Surface: "whatsapp", From: `whatsapp:${senderId}`, SenderId: senderId, } as MsgContext; } function makeDiscordContext(senderId: string, fromOverride?: string): MsgContext { return { Provider: "discord", Surface: "discord", From: fromOverride ?? `discord:${senderId}`, SenderId: senderId, } as MsgContext; } function resolveWithCommandsAllowFrom(senderId: string, commandAuthorized: boolean) { return resolveCommandAuthorization({ ctx: makeWhatsAppContext(senderId), cfg: commandsAllowFromConfig, commandAuthorized, }); } it("uses commands.allowFrom global list when configured", () => { const authorizedAuth = resolveWithCommandsAllowFrom("user123", true); expect(authorizedAuth.isAuthorizedSender).toBe(true); const unauthorizedAuth = resolveWithCommandsAllowFrom("otheruser", true); expect(unauthorizedAuth.isAuthorizedSender).toBe(false); }); it("ignores commandAuthorized when commands.allowFrom is configured", () => { const authorizedAuth = resolveWithCommandsAllowFrom("user123", false); expect(authorizedAuth.isAuthorizedSender).toBe(true); const unauthorizedAuth = resolveWithCommandsAllowFrom("otheruser", 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; // User in global list but not in whatsapp-specific list const globalUserCtx = { Provider: "whatsapp", Surface: "whatsapp", From: "whatsapp:globaluser", SenderId: "globaluser", } as MsgContext; const globalAuth = resolveCommandAuthorization({ ctx: globalUserCtx, cfg, commandAuthorized: true, }); // Provider-specific list overrides global, so globaluser is not authorized expect(globalAuth.isAuthorizedSender).toBe(false); // User in whatsapp-specific list 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); }); it("requires owner identity before commands.allowFrom when the plugin enforces owner-only commands", () => { registerAllowFromPlugins(createOwnerEnforcingAllowFromPlugin("telegram", () => ["*"])); const cfg = { commands: { allowFrom: { "*": ["*"], }, }, channels: { telegram: { allowFrom: ["*"] } }, } as OpenClawConfig; const auth = resolveCommandAuthorization({ ctx: { Provider: "telegram", Surface: "telegram", ChatType: "group", From: "telegram:999", SenderId: "999", CommandSource: "native", } as MsgContext, cfg, commandAuthorized: true, }); expect(auth.senderIsOwner).toBe(false); expect(auth.isAuthorizedSender).toBe(false); }); it("keeps commands.allowFrom available to non-owner command users when an owner allowlist is configured", () => { const cfg = { commands: { ownerAllowFrom: ["discord:owner"], allowFrom: { discord: ["helper"], }, }, channels: { discord: { allowFrom: ["*"] } }, } as OpenClawConfig; const auth = resolveCommandAuthorization({ ctx: { Provider: "discord", Surface: "discord", ChatType: "group", From: "discord:helper", SenderId: "helper", CommandSource: "native", } as MsgContext, cfg, commandAuthorized: true, }); expect(auth.senderIsOwner).toBe(false); expect(auth.isAuthorizedSender).toBe(true); }); it.each([ { name: "does not treat conversation ids in From as sender identities", provider: "discord", chatType: "channel", from: "discord:channel:123456789012345678", senderId: "999999999999999999", senderE164: undefined, allowFrom: { discord: ["channel:123456789012345678"] }, expected: false, }, { name: "still falls back to From for direct messages when sender fields are absent", provider: "discord", chatType: "direct", from: "discord:123456789012345678", senderId: " ", senderE164: " ", allowFrom: { discord: ["123456789012345678"] }, expected: true, }, { name: "does not fall back to conversation-shaped From when chat type is missing", provider: "whatsapp", chatType: undefined, from: "demo:group:room-1", senderId: " ", senderE164: " ", allowFrom: { "*": ["demo:group:room-1"] }, expected: false, }, ] as const)( "$name", ({ provider, chatType, from, senderId, senderE164, allowFrom, expected }) => { const auth = resolveCommandAuthorization({ ctx: { Provider: provider, Surface: provider, ChatType: chatType, From: from, SenderId: senderId, SenderE164: senderE164, } as MsgContext, cfg: { commands: { allowFrom } } as unknown as OpenClawConfig, commandAuthorized: false, }); expect(auth.isAuthorizedSender).toBe(expected); }, ); it("normalizes Discord commands.allowFrom prefixes and mentions", () => { const cfg = { commands: { allowFrom: { discord: ["user:123", "<@!456>", "pk:member-1"], }, }, } as OpenClawConfig; const userAuth = resolveCommandAuthorization({ ctx: makeDiscordContext("123"), cfg, commandAuthorized: false, }); expect(userAuth.isAuthorizedSender).toBe(true); const mentionAuth = resolveCommandAuthorization({ ctx: makeDiscordContext("456"), cfg, commandAuthorized: false, }); expect(mentionAuth.isAuthorizedSender).toBe(true); const pkAuth = resolveCommandAuthorization({ ctx: makeDiscordContext("member-1", "discord:999"), cfg, commandAuthorized: false, }); expect(pkAuth.isAuthorizedSender).toBe(true); const deniedAuth = resolveCommandAuthorization({ ctx: makeDiscordContext("other"), cfg, commandAuthorized: false, }); expect(deniedAuth.isAuthorizedSender).toBe(false); }); it.each([ { name: "fails closed when provider inference hits unresolved SecretRef allowlists", failingProvider: "telegram", allowKey: "telegram", channelMode: "configured", validTelegram: false, commandAuthorized: false, expectedProvider: "telegram", expectedAuthorized: false, }, { name: "preserves provider resolution errors when inferred fallback allowFrom is empty", failingProvider: "telegram", allowKey: "telegram", channelMode: "empty", validTelegram: false, commandAuthorized: true, expectedProvider: undefined, expectedAuthorized: false, }, { name: "fails closed for global commands.allowFrom when inference errors drop every provider", failingProvider: "slack", allowKey: "*", channelMode: "empty", validTelegram: false, commandAuthorized: false, expectedProvider: undefined, expectedAuthorized: false, }, { name: "does not let an unrelated provider resolution error poison inferred commands.allowFrom", failingProvider: "slack", allowKey: "telegram", channelMode: "configured", validTelegram: true, commandAuthorized: false, expectedProvider: "telegram", expectedAuthorized: true, }, ] as const)( "$name", ({ failingProvider, allowKey, channelMode, validTelegram, commandAuthorized, expectedProvider, expectedAuthorized, }) => { registerAllowFromPlugins( ...(validTelegram ? [createAllowFromPlugin("telegram", () => ["123"])] : []), createThrowingAllowFromPlugin( failingProvider, `channels.${failingProvider}.${failingProvider === "telegram" ? "botToken" : "token"}: unresolved SecretRef`, ), ); const channelId = validTelegram ? "telegram" : failingProvider; const channelConfig = channelMode === "configured" ? { allowFrom: ["123"] } : {}; const params = { ctx: { SenderId: "123", commandText: "/reset", rawText: "/reset" } as MsgContext, cfg: { commands: { allowFrom: { [allowKey]: ["123"] } }, channels: { [channelId]: channelConfig }, } as OpenClawConfig, commandAuthorized, }; const auth = resolveCommandAuthorization(params); const reset = resolveAuthorizedSessionResetCommand({ ...params, agentId: "main", isGroup: false, }); expect(auth.providerId).toBe(expectedProvider); expect(auth.isAuthorizedSender).toBe(expectedAuthorized); expect(reset.resetAuthorized).toBe(expectedAuthorized); expect(reset.resetCommand.matchedResetTriggerLower).toBe( expectedAuthorized ? "/reset" : undefined, ); }, ); it("preserves default-account allowFrom on SecretRef fallback", () => { registerAllowFromPlugins( createThrowingAllowFromPlugin( "telegram", "channels.telegram.botToken: unresolved SecretRef", ), ); const auth = resolveCommandAuthorization({ ctx: { Provider: "telegram", Surface: "telegram", SenderId: "123", } as MsgContext, cfg: { channels: { telegram: { accounts: { default: { allowFrom: ["123"], }, }, }, }, } as OpenClawConfig, commandAuthorized: true, }); expect(auth.ownerList).toEqual([]); expect(auth.senderIsOwner).toBe(false); expect(auth.isAuthorizedSender).toBe(true); }); it("treats undefined allowFrom as an open channel, not a resolution failure", () => { registerAllowFromPlugins(createAllowFromPlugin("discord", () => undefined)); const auth = resolveCommandAuthorization({ ctx: { Provider: "discord", Surface: "discord", SenderId: "123", } as MsgContext, cfg: { channels: { discord: {}, }, } as OpenClawConfig, commandAuthorized: true, }); expect(auth.isAuthorizedSender).toBe(true); }); it("does not log raw resolution messages from thrown allowFrom errors", () => { registerAllowFromPlugins(createThrowingAllowFromPlugin("telegram", "SECRET-TOKEN-123")); const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); try { resolveCommandAuthorization({ ctx: { Provider: "telegram", Surface: "telegram", SenderId: "123", } as MsgContext, cfg: { channels: { telegram: { allowFrom: ["123"], }, }, } as OpenClawConfig, commandAuthorized: true, }); expect(warn).toHaveBeenCalledTimes(1); const warning = String(warn.mock.calls[0]?.[0] ?? ""); expect(warning).toContain("Error"); expect(warning).not.toContain("SECRET-TOKEN-123"); } finally { warn.mockRestore(); } }); }); it.each([ { name: "grants senderIsOwner for internal channel with operator.admin scope", provider: "webchat", scope: "operator.admin", expectedOwner: true, }, { name: "does not grant senderIsOwner for internal channel without admin scope", provider: "webchat", scope: "operator.approvals", expectedOwner: false, }, { name: "does not grant senderIsOwner for external channel even with admin scope", provider: "telegram", scope: "operator.admin", expectedOwner: false, }, ] as const)("$name", ({ provider, scope, expectedOwner }) => { const auth = resolveCommandAuthorization({ ctx: { Provider: provider, Surface: provider, ...(provider === "telegram" ? { From: "telegram:12345" } : {}), GatewayClientScopes: [scope], } as MsgContext, cfg: {} as OpenClawConfig, commandAuthorized: true, }); expect(auth.senderIsOwner).toBe(expectedOwner); }); }); describe("control command parsing", () => { function expectCases( parse: (value: string) => unknown, cases: readonly (readonly [string, unknown])[], ) { cases.forEach(([value, expected]) => expect(parse(value)).toEqual(expected)); } it("requires slash for send policy", () => { expectCases(parseSendPolicyCommand, [ ["/send on", { hasCommand: true, mode: "allow" }], ["/send: on", { hasCommand: true, mode: "allow" }], ["/send", { hasCommand: true }], ["/send:", { hasCommand: true }], ["send on", { hasCommand: false }], ["send", { hasCommand: false }], ]); }); it("requires slash for activation", () => { expectCases(parseActivationCommand, [ ["/activation mention", { hasCommand: true, mode: "mention" }], ["/activation: mention", { hasCommand: true, mode: "mention" }], ["/activation:", { hasCommand: true }], ["activation mention", { 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(true); expect(hasControlCommand("/status plugins")).toBe(true); expect(hasControlCommand("/STATUS plugins")).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 keep CaseSensitivePath")).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(true); 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("/ pair qr")).toBe(false); expect(hasInlineCommandTokens("hey / pair qr")).toBe(false); expect(hasInlineCommandTokens("option A / B")).toBe(false); expect(hasInlineCommandTokens("plain text")).toBe(false); expect(hasInlineCommandTokens("http://example.com/path")).toBe(false); expect(hasInlineCommandTokens("stop")).toBe(false); }); it("detects spaced syntax only for active registered plugin commands", () => { expect(shouldComputeCommandAuthorized("/ pair qr")).toBe(false); registerPluginCommand("device-pair", { name: "pair", description: "Pair command", acceptsArgs: true, handler: async () => ({ text: "ok" }), }); expect(shouldComputeCommandAuthorized("/ pair qr")).toBe(true); expect(shouldComputeCommandAuthorized("@openclaw / pair qr")).toBe(true); expect(shouldComputeCommandAuthorized("hey / pair qr")).toBe(true); clearPluginCommands(); expect(shouldComputeCommandAuthorized("/ pair qr")).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); }); function expectCommandAfterMetadata(label: string, json: string, command: string) { expect(hasControlCommand([label, "```json", json, "```", "", command].join("\n"))).toBe(true); } it("detects commands wrapped in inbound metadata blocks", () => { expectCommandAfterMetadata( markInboundContextLabel("Conversation info:"), '{"message_id":"msg-abc","chat_id":"chat-123"}', "/model spark", ); }); it("detects /new command after metadata prefix", () => { expectCommandAfterMetadata( markInboundContextLabel("Sender:"), '{"name":"Alice","id":"user-1"}', "/new spark", ); }); it("detects /status command after timestamp + metadata prefix", () => { expectCommandAfterMetadata( `[Wed 2026-03-11 23:51 PDT] ${markInboundContextLabel("Conversation info:")}`, '{"chat_id":"chat-123"}', "/status", ); }); });