From c88915b72168f8da4ab990902b72d558e791bbe4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 23 Feb 2026 19:40:39 +0000 Subject: [PATCH] test: consolidate trigger handling suites --- ...eply.triggers.group-intro-prompts.cases.ts | 131 ++++ ...reply.triggers.group-intro-prompts.test.ts | 110 --- ...ge-summary-current-model-provider.cases.ts | 401 +++++++++++ ...age-summary-current-model-provider.test.ts | 394 ----------- ...ne-commands-strips-it-before-agent.test.ts | 398 ----------- ...bound-media-into-sandbox-workspace.test.ts | 133 ++-- ...targets-active-session-native-stop.test.ts | 668 ++++++++++-------- 7 files changed, 964 insertions(+), 1271 deletions(-) create mode 100644 src/auto-reply/reply.triggers.group-intro-prompts.cases.ts delete mode 100644 src/auto-reply/reply.triggers.group-intro-prompts.test.ts create mode 100644 src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.ts delete mode 100644 src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.test.ts delete mode 100644 src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts diff --git a/src/auto-reply/reply.triggers.group-intro-prompts.cases.ts b/src/auto-reply/reply.triggers.group-intro-prompts.cases.ts new file mode 100644 index 00000000000..b10c91adf94 --- /dev/null +++ b/src/auto-reply/reply.triggers.group-intro-prompts.cases.ts @@ -0,0 +1,131 @@ +import { describe, expect, it } from "vitest"; +import { + getRunEmbeddedPiAgentMock, + makeCfg, + mockRunEmbeddedPiAgentOk, + withTempHome, +} from "./reply.triggers.trigger-handling.test-harness.js"; + +type GetReplyFromConfig = typeof import("./reply.js").getReplyFromConfig; +type InboundMessage = Parameters[0]; + +function getLastExtraSystemPrompt() { + return getRunEmbeddedPiAgentMock().mock.calls.at(-1)?.[0]?.extraSystemPrompt ?? ""; +} + +export function registerGroupIntroPromptCases(params: { + getReplyFromConfig: () => GetReplyFromConfig; +}): void { + describe("group intro prompts", () => { + type GroupIntroCase = { + name: string; + message: InboundMessage; + expected: string[]; + setup?: (cfg: ReturnType) => void; + }; + const groupParticipationNote = + "Be a good group participant: mostly lurk and follow the conversation; reply only when directly addressed or you can add clear value. Emoji reactions are welcome when available. Write like a human. Avoid Markdown tables. Don't type literal \\n sequences; use real line breaks sparingly."; + it("labels group chats using channel-specific metadata", async () => { + await withTempHome(async (home) => { + const cases: GroupIntroCase[] = [ + { + name: "discord", + message: { + Body: "status update", + From: "discord:group:dev", + To: "+1888", + ChatType: "group", + GroupSubject: "Release Squad", + GroupMembers: "Alice, Bob", + Provider: "discord", + }, + expected: [ + '"channel": "discord"', + `You are in the Discord group chat "Release Squad". Participants: Alice, Bob.`, + `Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`, + ], + }, + { + name: "whatsapp", + message: { + Body: "ping", + From: "123@g.us", + To: "+1999", + ChatType: "group", + GroupSubject: "Ops", + Provider: "whatsapp", + }, + expected: [ + '"channel": "whatsapp"', + `You are in the WhatsApp group chat "Ops".`, + `WhatsApp IDs: SenderId is the participant JID (group participant id).`, + `Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). WhatsApp IDs: SenderId is the participant JID (group participant id). ${groupParticipationNote} Address the specific sender noted in the message context.`, + ], + }, + { + name: "telegram", + message: { + Body: "ping", + From: "telegram:group:tg", + To: "+1777", + ChatType: "group", + GroupSubject: "Dev Chat", + Provider: "telegram", + }, + expected: [ + '"channel": "telegram"', + `You are in the Telegram group chat "Dev Chat".`, + `Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`, + ], + }, + { + name: "whatsapp-always-on", + setup: (cfg) => { + cfg.channels ??= {}; + cfg.channels.whatsapp = { + ...cfg.channels.whatsapp, + allowFrom: ["*"], + groups: { "*": { requireMention: false } }, + }; + cfg.messages = { + ...cfg.messages, + groupChat: {}, + }; + }, + message: { + Body: "hello group", + From: "123@g.us", + To: "+2000", + ChatType: "group", + Provider: "whatsapp", + SenderE164: "+2000", + GroupSubject: "Test Group", + GroupMembers: "Alice (+1), Bob (+2)", + }, + expected: [ + '"channel": "whatsapp"', + '"chat_type": "group"', + "Activation: always-on (you receive every group message).", + ], + }, + ]; + + for (const testCase of cases) { + mockRunEmbeddedPiAgentOk(); + const cfg = makeCfg(home); + testCase.setup?.(cfg); + await params.getReplyFromConfig()(testCase.message, {}, cfg); + + expect(getRunEmbeddedPiAgentMock(), testCase.name).toHaveBeenCalledOnce(); + const extraSystemPrompt = getLastExtraSystemPrompt(); + for (const expectedFragment of testCase.expected) { + expect(extraSystemPrompt, `${testCase.name}:${expectedFragment}`).toContain( + expectedFragment, + ); + } + getRunEmbeddedPiAgentMock().mockClear(); + } + }); + }); + }); +} diff --git a/src/auto-reply/reply.triggers.group-intro-prompts.test.ts b/src/auto-reply/reply.triggers.group-intro-prompts.test.ts deleted file mode 100644 index 9bfb463c397..00000000000 --- a/src/auto-reply/reply.triggers.group-intro-prompts.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { beforeAll, describe, expect, it } from "vitest"; -import { - getRunEmbeddedPiAgentMock, - installTriggerHandlingE2eTestHooks, - loadGetReplyFromConfig, - makeCfg, - mockRunEmbeddedPiAgentOk, - withTempHome, -} from "./reply.triggers.trigger-handling.test-harness.js"; - -let getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig; -beforeAll(async () => { - getReplyFromConfig = await loadGetReplyFromConfig(); -}); - -installTriggerHandlingE2eTestHooks(); - -function getLastExtraSystemPrompt() { - return getRunEmbeddedPiAgentMock().mock.calls.at(-1)?.[0]?.extraSystemPrompt ?? ""; -} - -describe("group intro prompts", () => { - const groupParticipationNote = - "Be a good group participant: mostly lurk and follow the conversation; reply only when directly addressed or you can add clear value. Emoji reactions are welcome when available. Write like a human. Avoid Markdown tables. Don't type literal \\n sequences; use real line breaks sparingly."; - - it("labels Discord groups using the surface metadata", async () => { - await withTempHome(async (home) => { - mockRunEmbeddedPiAgentOk(); - - await getReplyFromConfig( - { - Body: "status update", - From: "discord:group:dev", - To: "+1888", - ChatType: "group", - GroupSubject: "Release Squad", - GroupMembers: "Alice, Bob", - Provider: "discord", - }, - {}, - makeCfg(home), - ); - - expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); - const extraSystemPrompt = getLastExtraSystemPrompt(); - expect(extraSystemPrompt).toContain('"channel": "discord"'); - expect(extraSystemPrompt).toContain( - `You are in the Discord group chat "Release Squad". Participants: Alice, Bob.`, - ); - expect(extraSystemPrompt).toContain( - `Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`, - ); - }); - }); - it("keeps WhatsApp labeling for WhatsApp group chats", async () => { - await withTempHome(async (home) => { - mockRunEmbeddedPiAgentOk(); - - await getReplyFromConfig( - { - Body: "ping", - From: "123@g.us", - To: "+1999", - ChatType: "group", - GroupSubject: "Ops", - Provider: "whatsapp", - }, - {}, - makeCfg(home), - ); - - expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); - const extraSystemPrompt = getLastExtraSystemPrompt(); - expect(extraSystemPrompt).toContain('"channel": "whatsapp"'); - expect(extraSystemPrompt).toContain(`You are in the WhatsApp group chat "Ops".`); - expect(extraSystemPrompt).toContain( - `WhatsApp IDs: SenderId is the participant JID (group participant id).`, - ); - expect(extraSystemPrompt).toContain( - `Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). WhatsApp IDs: SenderId is the participant JID (group participant id). ${groupParticipationNote} Address the specific sender noted in the message context.`, - ); - }); - }); - it("labels Telegram groups using their own surface", async () => { - await withTempHome(async (home) => { - mockRunEmbeddedPiAgentOk(); - - await getReplyFromConfig( - { - Body: "ping", - From: "telegram:group:tg", - To: "+1777", - ChatType: "group", - GroupSubject: "Dev Chat", - Provider: "telegram", - }, - {}, - makeCfg(home), - ); - - expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); - const extraSystemPrompt = getLastExtraSystemPrompt(); - expect(extraSystemPrompt).toContain('"channel": "telegram"'); - expect(extraSystemPrompt).toContain(`You are in the Telegram group chat "Dev Chat".`); - expect(extraSystemPrompt).toContain( - `Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`, - ); - }); - }); -}); diff --git a/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.ts b/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.ts new file mode 100644 index 00000000000..11f975de809 --- /dev/null +++ b/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.ts @@ -0,0 +1,401 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { normalizeTestText } from "../../test/helpers/normalize-text.js"; +import type { OpenClawConfig } from "../config/config.js"; +import { resolveSessionKey } from "../config/sessions.js"; +import { + createBlockReplyCollector, + getProviderUsageMocks, + getRunEmbeddedPiAgentMock, + makeCfg, + requireSessionStorePath, + withTempHome, +} from "./reply.triggers.trigger-handling.test-harness.js"; + +type GetReplyFromConfig = typeof import("./reply.js").getReplyFromConfig; + +const usageMocks = getProviderUsageMocks(); +const modelStatusCtx = { + Body: "/model status", + From: "telegram:111", + To: "telegram:111", + ChatType: "direct", + Provider: "telegram", + Surface: "telegram", + SessionKey: "telegram:slash:111", + CommandAuthorized: true, +} as const; + +async function readSessionStore(storePath: string): Promise> { + const raw = await readFile(storePath, "utf-8"); + return JSON.parse(raw) as Record; +} + +function pickFirstStoreEntry(store: Record): T | undefined { + const entries = Object.values(store) as T[]; + return entries[0]; +} + +function getReplyFromConfigNow(getReplyFromConfig: () => GetReplyFromConfig): GetReplyFromConfig { + return getReplyFromConfig(); +} + +async function runCommandAndCollectReplies(params: { + getReplyFromConfig: () => GetReplyFromConfig; + home: string; + body: string; + from?: string; + senderE164?: string; +}) { + const { blockReplies, handlers } = createBlockReplyCollector(); + const res = await getReplyFromConfigNow(params.getReplyFromConfig)( + { + Body: params.body, + From: params.from ?? "+1000", + To: "+2000", + Provider: "whatsapp", + SenderE164: params.senderE164 ?? params.from ?? "+1000", + CommandAuthorized: true, + }, + handlers, + makeCfg(params.home), + ); + const replies = res ? (Array.isArray(res) ? res : [res]) : []; + return { blockReplies, replies }; +} + +async function expectStopAbortWithoutAgent(params: { + getReplyFromConfig: () => GetReplyFromConfig; + home: string; + body: string; + from: string; +}) { + const res = await getReplyFromConfigNow(params.getReplyFromConfig)( + { + Body: params.body, + From: params.from, + To: "+2000", + CommandAuthorized: true, + }, + {}, + makeCfg(params.home), + ); + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toBe("⚙️ Agent was aborted."); + expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled(); +} + +export function registerTriggerHandlingUsageSummaryCases(params: { + getReplyFromConfig: () => GetReplyFromConfig; +}): void { + describe("usage and status command handling", () => { + it("handles status, usage cycles, restart/stop gating, and auth-profile status details", async () => { + await withTempHome(async (home) => { + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + const getReplyFromConfig = getReplyFromConfigNow(params.getReplyFromConfig); + usageMocks.loadProviderUsageSummary.mockClear(); + usageMocks.loadProviderUsageSummary.mockResolvedValue({ + updatedAt: 0, + providers: [ + { + provider: "anthropic", + displayName: "Anthropic", + windows: [ + { + label: "5h", + usedPercent: 20, + }, + ], + }, + ], + }); + + { + const res = await getReplyFromConfig( + { + Body: "/status", + From: "+1000", + To: "+2000", + Provider: "whatsapp", + SenderE164: "+1000", + CommandAuthorized: true, + }, + {}, + makeCfg(home), + ); + + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toContain("Model:"); + expect(text).toContain("OpenClaw"); + expect(normalizeTestText(text ?? "")).toContain("Usage: Claude 80% left"); + expect(usageMocks.loadProviderUsageSummary).toHaveBeenCalledWith( + expect.objectContaining({ providers: ["anthropic"] }), + ); + expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + } + + { + runEmbeddedPiAgentMock.mockResolvedValue({ + payloads: [{ text: "agent says hi" }], + meta: { + durationMs: 1, + agentMeta: { sessionId: "s", provider: "p", model: "m" }, + }, + }); + const { blockReplies, replies } = await runCommandAndCollectReplies({ + getReplyFromConfig: params.getReplyFromConfig, + home, + body: "here we go /status now", + from: "+1002", + }); + expect(blockReplies.length).toBe(1); + expect(String(blockReplies[0]?.text ?? "")).toContain("Model:"); + expect(replies.length).toBe(1); + expect(replies[0]?.text).toBe("agent says hi"); + const prompt = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.prompt ?? ""; + expect(prompt).not.toContain("/status"); + } + + { + runEmbeddedPiAgentMock.mockClear(); + const defaultCfg = makeCfg(home); + const cfg = { + ...defaultCfg, + models: { + providers: { + minimax: { + baseUrl: "https://api.minimax.io/anthropic", + api: "anthropic-messages", + }, + }, + }, + } as unknown as OpenClawConfig; + const defaultStatus = await getReplyFromConfig(modelStatusCtx, {}, defaultCfg); + const configuredStatus = await getReplyFromConfig(modelStatusCtx, {}, cfg); + + expect( + normalizeTestText( + (Array.isArray(defaultStatus) ? defaultStatus[0]?.text : defaultStatus?.text) ?? "", + ), + ).toContain("endpoint: default"); + const configuredText = Array.isArray(configuredStatus) + ? configuredStatus[0]?.text + : configuredStatus?.text; + expect(normalizeTestText(configuredText ?? "")).toContain( + "[minimax] endpoint: https://api.minimax.io/anthropic api: anthropic-messages auth:", + ); + } + + { + const cfg = makeCfg(home); + cfg.session = { ...cfg.session, store: join(home, "usage-cycle.sessions.json") }; + const usageStorePath = requireSessionStorePath(cfg); + const explicitTokens = await getReplyFromConfig( + { + Body: "/usage tokens", + From: "+1000", + To: "+2000", + Provider: "whatsapp", + SenderE164: "+1000", + CommandAuthorized: true, + }, + undefined, + cfg, + ); + expect( + String( + (Array.isArray(explicitTokens) ? explicitTokens[0]?.text : explicitTokens?.text) ?? + "", + ), + ).toContain("Usage footer: tokens"); + const explicitStore = await readSessionStore(usageStorePath); + expect( + pickFirstStoreEntry<{ responseUsage?: string }>(explicitStore)?.responseUsage, + ).toBe("tokens"); + + const r0 = await getReplyFromConfig( + { + Body: "/usage on", + From: "+1000", + To: "+2000", + Provider: "whatsapp", + SenderE164: "+1000", + CommandAuthorized: true, + }, + undefined, + cfg, + ); + expect(String((Array.isArray(r0) ? r0[0]?.text : r0?.text) ?? "")).toContain( + "Usage footer: tokens", + ); + + const r1 = await getReplyFromConfig( + { + Body: "/usage", + From: "+1000", + To: "+2000", + Provider: "whatsapp", + SenderE164: "+1000", + CommandAuthorized: true, + }, + undefined, + cfg, + ); + expect(String((Array.isArray(r1) ? r1[0]?.text : r1?.text) ?? "")).toContain( + "Usage footer: full", + ); + + const r2 = await getReplyFromConfig( + { + Body: "/usage", + From: "+1000", + To: "+2000", + Provider: "whatsapp", + SenderE164: "+1000", + CommandAuthorized: true, + }, + undefined, + cfg, + ); + expect(String((Array.isArray(r2) ? r2[0]?.text : r2?.text) ?? "")).toContain( + "Usage footer: off", + ); + + const r3 = await getReplyFromConfig( + { + Body: "/usage", + From: "+1000", + To: "+2000", + Provider: "whatsapp", + SenderE164: "+1000", + CommandAuthorized: true, + }, + undefined, + cfg, + ); + expect(String((Array.isArray(r3) ? r3[0]?.text : r3?.text) ?? "")).toContain( + "Usage footer: tokens", + ); + const finalStore = await readSessionStore(usageStorePath); + expect(pickFirstStoreEntry<{ responseUsage?: string }>(finalStore)?.responseUsage).toBe( + "tokens", + ); + expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + } + + { + runEmbeddedPiAgentMock.mockClear(); + await expectStopAbortWithoutAgent({ + getReplyFromConfig: params.getReplyFromConfig, + home, + body: "[Dec 5 10:00] stop", + from: "+1000", + }); + + const enabledRes = await getReplyFromConfig( + { + Body: " [Dec 5] /restart", + From: "+1001", + To: "+2000", + CommandAuthorized: true, + }, + {}, + makeCfg(home), + ); + const enabledText = Array.isArray(enabledRes) ? enabledRes[0]?.text : enabledRes?.text; + expect( + enabledText?.startsWith("⚙️ Restarting") || + enabledText?.startsWith("⚠️ Restart failed"), + ).toBe(true); + + const disabledCfg = { ...makeCfg(home), commands: { restart: false } } as OpenClawConfig; + const disabledRes = await getReplyFromConfig( + { + Body: "/restart", + From: "+1001", + To: "+2000", + CommandAuthorized: true, + }, + {}, + disabledCfg, + ); + + const disabledText = Array.isArray(disabledRes) + ? disabledRes[0]?.text + : disabledRes?.text; + expect(disabledText).toContain("/restart is disabled"); + expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + } + + { + runEmbeddedPiAgentMock.mockClear(); + const cfg = makeCfg(home); + cfg.session = { ...cfg.session, store: join(home, "auth-profile-status.sessions.json") }; + const agentDir = join(home, ".openclaw", "agents", "main", "agent"); + await mkdir(agentDir, { recursive: true }); + await writeFile( + join(agentDir, "auth-profiles.json"), + JSON.stringify( + { + version: 1, + profiles: { + "anthropic:work": { + type: "api_key", + provider: "anthropic", + key: "sk-test-1234567890abcdef", + }, + }, + lastGood: { anthropic: "anthropic:work" }, + }, + null, + 2, + ), + ); + + const sessionKey = resolveSessionKey("per-sender", { + From: "+1002", + To: "+2000", + Provider: "whatsapp", + } as Parameters[1]); + await writeFile( + requireSessionStorePath(cfg), + JSON.stringify( + { + [sessionKey]: { + sessionId: "session-auth", + updatedAt: Date.now(), + authProfileOverride: "anthropic:work", + }, + }, + null, + 2, + ), + ); + + const res = await getReplyFromConfig( + { + Body: "/status", + From: "+1002", + To: "+2000", + Provider: "whatsapp", + SenderE164: "+1002", + CommandAuthorized: true, + }, + {}, + cfg, + ); + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toContain("api-key"); + expect(text).toMatch(/\u2026|\.{3}/); + expect(text).toContain("sk-tes"); + expect(text).toContain("abcdef"); + expect(text).not.toContain("1234567890abcdef"); + expect(text).toContain("(anthropic:work)"); + expect(text).not.toContain("mixed"); + expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + } + }); + }); + }); +} diff --git a/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.test.ts b/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.test.ts deleted file mode 100644 index bfd09ca4beb..00000000000 --- a/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.test.ts +++ /dev/null @@ -1,394 +0,0 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { join } from "node:path"; -import { beforeAll, describe, expect, it } from "vitest"; -import { normalizeTestText } from "../../test/helpers/normalize-text.js"; -import type { OpenClawConfig } from "../config/config.js"; -import { resolveSessionKey } from "../config/sessions.js"; -import { - createBlockReplyCollector, - getProviderUsageMocks, - getRunEmbeddedPiAgentMock, - installTriggerHandlingE2eTestHooks, - makeCfg, - requireSessionStorePath, - withTempHome, -} from "./reply.triggers.trigger-handling.test-harness.js"; - -let getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig; -beforeAll(async () => { - ({ getReplyFromConfig } = await import("./reply.js")); -}); - -installTriggerHandlingE2eTestHooks(); - -const usageMocks = getProviderUsageMocks(); -const modelStatusCtx = { - Body: "/model status", - From: "telegram:111", - To: "telegram:111", - ChatType: "direct", - Provider: "telegram", - Surface: "telegram", - SessionKey: "telegram:slash:111", - CommandAuthorized: true, -} as const; - -async function readSessionStore(home: string): Promise> { - const raw = await readFile(join(home, "sessions.json"), "utf-8"); - return JSON.parse(raw) as Record; -} - -function pickFirstStoreEntry(store: Record): T | undefined { - const entries = Object.values(store) as T[]; - return entries[0]; -} - -async function runCommandAndCollectReplies(params: { - home: string; - body: string; - from?: string; - senderE164?: string; -}) { - const { blockReplies, handlers } = createBlockReplyCollector(); - const res = await getReplyFromConfig( - { - Body: params.body, - From: params.from ?? "+1000", - To: "+2000", - Provider: "whatsapp", - SenderE164: params.senderE164 ?? params.from ?? "+1000", - CommandAuthorized: true, - }, - handlers, - makeCfg(params.home), - ); - const replies = res ? (Array.isArray(res) ? res : [res]) : []; - return { blockReplies, replies }; -} - -async function expectStopAbortWithoutAgent(params: { home: string; body: string; from: string }) { - const res = await getReplyFromConfig( - { - Body: params.body, - From: params.from, - To: "+2000", - CommandAuthorized: true, - }, - {}, - makeCfg(params.home), - ); - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toBe("⚙️ Agent was aborted."); - expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled(); -} - -describe("trigger handling", () => { - it("filters usage summary to the current model provider", async () => { - await withTempHome(async (home) => { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - usageMocks.loadProviderUsageSummary.mockClear(); - usageMocks.loadProviderUsageSummary.mockResolvedValue({ - updatedAt: 0, - providers: [ - { - provider: "anthropic", - displayName: "Anthropic", - windows: [ - { - label: "5h", - usedPercent: 20, - }, - ], - }, - ], - }); - - const res = await getReplyFromConfig( - { - Body: "/status", - From: "+1000", - To: "+2000", - Provider: "whatsapp", - SenderE164: "+1000", - CommandAuthorized: true, - }, - {}, - makeCfg(home), - ); - - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("Model:"); - expect(text).toContain("OpenClaw"); - expect(normalizeTestText(text ?? "")).toContain("Usage: Claude 80% left"); - expect(usageMocks.loadProviderUsageSummary).toHaveBeenCalledWith( - expect.objectContaining({ providers: ["anthropic"] }), - ); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); - }); - }); - it("handles explicit /usage tokens, back-compat, and cycle persistence", async () => { - await withTempHome(async (home) => { - const cfg = makeCfg(home); - - const explicitTokens = await getReplyFromConfig( - { - Body: "/usage tokens", - From: "+1000", - To: "+2000", - Provider: "whatsapp", - SenderE164: "+1000", - CommandAuthorized: true, - }, - undefined, - cfg, - ); - expect( - String( - (Array.isArray(explicitTokens) ? explicitTokens[0]?.text : explicitTokens?.text) ?? "", - ), - ).toContain("Usage footer: tokens"); - const explicitStore = await readSessionStore(home); - expect(pickFirstStoreEntry<{ responseUsage?: string }>(explicitStore)?.responseUsage).toBe( - "tokens", - ); - - const r0 = await getReplyFromConfig( - { - Body: "/usage on", - From: "+1000", - To: "+2000", - Provider: "whatsapp", - SenderE164: "+1000", - CommandAuthorized: true, - }, - undefined, - cfg, - ); - expect(String((Array.isArray(r0) ? r0[0]?.text : r0?.text) ?? "")).toContain( - "Usage footer: tokens", - ); - - const r1 = await getReplyFromConfig( - { - Body: "/usage", - From: "+1000", - To: "+2000", - Provider: "whatsapp", - SenderE164: "+1000", - CommandAuthorized: true, - }, - undefined, - cfg, - ); - expect(String((Array.isArray(r1) ? r1[0]?.text : r1?.text) ?? "")).toContain( - "Usage footer: full", - ); - - const r2 = await getReplyFromConfig( - { - Body: "/usage", - From: "+1000", - To: "+2000", - Provider: "whatsapp", - SenderE164: "+1000", - CommandAuthorized: true, - }, - undefined, - cfg, - ); - expect(String((Array.isArray(r2) ? r2[0]?.text : r2?.text) ?? "")).toContain( - "Usage footer: off", - ); - - const r3 = await getReplyFromConfig( - { - Body: "/usage", - From: "+1000", - To: "+2000", - Provider: "whatsapp", - SenderE164: "+1000", - CommandAuthorized: true, - }, - undefined, - cfg, - ); - expect(String((Array.isArray(r3) ? r3[0]?.text : r3?.text) ?? "")).toContain( - "Usage footer: tokens", - ); - const finalStore = await readSessionStore(home); - expect(pickFirstStoreEntry<{ responseUsage?: string }>(finalStore)?.responseUsage).toBe( - "tokens", - ); - - expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled(); - }); - }); - it("sends one inline status and still returns agent reply for mixed text", async () => { - await withTempHome(async (home) => { - getRunEmbeddedPiAgentMock().mockResolvedValue({ - payloads: [{ text: "agent says hi" }], - meta: { - durationMs: 1, - agentMeta: { sessionId: "s", provider: "p", model: "m" }, - }, - }); - const { blockReplies, replies } = await runCommandAndCollectReplies({ - home, - body: "here we go /status now", - from: "+1002", - }); - expect(blockReplies.length).toBe(1); - expect(String(blockReplies[0]?.text ?? "")).toContain("Model:"); - expect(replies.length).toBe(1); - expect(replies[0]?.text).toBe("agent says hi"); - const prompt = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.prompt ?? ""; - expect(prompt).not.toContain("/status"); - }); - }); - it("handles /stop command variants without invoking the agent", async () => { - await withTempHome(async (home) => { - for (const testCase of [ - { body: "[Dec 5 10:00] stop", from: "+1000" }, - { body: "/stop", from: "+1003" }, - ] as const) { - await expectStopAbortWithoutAgent({ home, body: testCase.body, from: testCase.from }); - } - }); - }); - - it("shows model status defaults and configured endpoint details", async () => { - await withTempHome(async (home) => { - const defaultCfg = makeCfg(home); - const cfg = { - ...defaultCfg, - models: { - providers: { - minimax: { - baseUrl: "https://api.minimax.io/anthropic", - api: "anthropic-messages", - }, - }, - }, - } as unknown as OpenClawConfig; - const defaultStatus = await getReplyFromConfig(modelStatusCtx, {}, defaultCfg); - const configuredStatus = await getReplyFromConfig(modelStatusCtx, {}, cfg); - - expect( - normalizeTestText( - (Array.isArray(defaultStatus) ? defaultStatus[0]?.text : defaultStatus?.text) ?? "", - ), - ).toContain("endpoint: default"); - const configuredText = Array.isArray(configuredStatus) - ? configuredStatus[0]?.text - : configuredStatus?.text; - expect(normalizeTestText(configuredText ?? "")).toContain( - "[minimax] endpoint: https://api.minimax.io/anthropic api: anthropic-messages auth:", - ); - }); - }); - - it("restarts by default and rejects /restart when disabled", async () => { - await withTempHome(async (home) => { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - const enabledRes = await getReplyFromConfig( - { - Body: " [Dec 5] /restart", - From: "+1001", - To: "+2000", - CommandAuthorized: true, - }, - {}, - makeCfg(home), - ); - const enabledText = Array.isArray(enabledRes) ? enabledRes[0]?.text : enabledRes?.text; - expect( - enabledText?.startsWith("⚙️ Restarting") || enabledText?.startsWith("⚠️ Restart failed"), - ).toBe(true); - - const disabledCfg = { ...makeCfg(home), commands: { restart: false } } as OpenClawConfig; - const disabledRes = await getReplyFromConfig( - { - Body: "/restart", - From: "+1001", - To: "+2000", - CommandAuthorized: true, - }, - {}, - disabledCfg, - ); - - const disabledText = Array.isArray(disabledRes) ? disabledRes[0]?.text : disabledRes?.text; - expect(disabledText).toContain("/restart is disabled"); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); - }); - }); - - it("reports active auth profile and key snippet in status", async () => { - await withTempHome(async (home) => { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - const cfg = makeCfg(home); - const agentDir = join(home, ".openclaw", "agents", "main", "agent"); - await mkdir(agentDir, { recursive: true }); - await writeFile( - join(agentDir, "auth-profiles.json"), - JSON.stringify( - { - version: 1, - profiles: { - "anthropic:work": { - type: "api_key", - provider: "anthropic", - key: "sk-test-1234567890abcdef", - }, - }, - lastGood: { anthropic: "anthropic:work" }, - }, - null, - 2, - ), - ); - - const sessionKey = resolveSessionKey("per-sender", { - From: "+1002", - To: "+2000", - Provider: "whatsapp", - } as Parameters[1]); - await writeFile( - requireSessionStorePath(cfg), - JSON.stringify( - { - [sessionKey]: { - sessionId: "session-auth", - updatedAt: Date.now(), - authProfileOverride: "anthropic:work", - }, - }, - null, - 2, - ), - ); - - const res = await getReplyFromConfig( - { - Body: "/status", - From: "+1002", - To: "+2000", - Provider: "whatsapp", - SenderE164: "+1002", - CommandAuthorized: true, - }, - {}, - cfg, - ); - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("api-key"); - expect(text).toMatch(/\u2026|\.{3}/); - expect(text).toContain("sk-tes"); - expect(text).toContain("abcdef"); - expect(text).not.toContain("1234567890abcdef"); - expect(text).toContain("(anthropic:work)"); - expect(text).not.toContain("mixed"); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts b/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts deleted file mode 100644 index b2dbed042ff..00000000000 --- a/src/auto-reply/reply.triggers.trigger-handling.handles-inline-commands-strips-it-before-agent.test.ts +++ /dev/null @@ -1,398 +0,0 @@ -import fs from "node:fs/promises"; -import { join } from "node:path"; -import { beforeAll, describe, expect, it } from "vitest"; -import { - expectInlineCommandHandledAndStripped, - getRunEmbeddedPiAgentMock, - installTriggerHandlingE2eTestHooks, - loadGetReplyFromConfig, - MAIN_SESSION_KEY, - makeCfg, - makeWhatsAppElevatedCfg, - mockRunEmbeddedPiAgentOk, - readSessionStore, - requireSessionStorePath, - runGreetingPromptForBareNewOrReset, - withTempHome, -} from "./reply.triggers.trigger-handling.test-harness.js"; - -let getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig; -beforeAll(async () => { - getReplyFromConfig = await loadGetReplyFromConfig(); -}); - -installTriggerHandlingE2eTestHooks(); - -function makeUnauthorizedWhatsAppCfg(home: string) { - const baseCfg = makeCfg(home); - return { - ...baseCfg, - channels: { - ...baseCfg.channels, - whatsapp: { - allowFrom: ["+1000"], - }, - }, - }; -} - -async function expectResetBlockedForNonOwner(params: { home: string }): Promise { - const { home } = params; - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - runEmbeddedPiAgentMock.mockClear(); - const cfg = makeCfg(home); - cfg.channels ??= {}; - cfg.channels.whatsapp = { - ...cfg.channels.whatsapp, - allowFrom: ["+1999"], - }; - cfg.session = { - ...cfg.session, - store: join(home, "blocked-reset.sessions.json"), - }; - const res = await getReplyFromConfig( - { - Body: "/reset", - From: "+1003", - To: "+2000", - CommandAuthorized: true, - }, - {}, - cfg, - ); - expect(res).toBeUndefined(); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); -} - -async function expectUnauthorizedCommandDropped(home: string, body: "/status") { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - const cfg = makeUnauthorizedWhatsAppCfg(home); - - const res = await getReplyFromConfig( - { - Body: body, - From: "+2001", - To: "+2000", - Provider: "whatsapp", - SenderE164: "+2001", - }, - {}, - cfg, - ); - - expect(res).toBeUndefined(); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); -} - -function mockEmbeddedOk() { - return mockRunEmbeddedPiAgentOk("ok"); -} - -async function runInlineUnauthorizedCommand(params: { home: string; command: "/status" }) { - const cfg = makeUnauthorizedWhatsAppCfg(params.home); - const res = await getReplyFromConfig( - { - Body: `please ${params.command} now`, - From: "+2001", - To: "+2000", - Provider: "whatsapp", - SenderE164: "+2001", - }, - {}, - cfg, - ); - return res; -} - -describe("trigger handling", () => { - it("handles owner-admin commands without invoking the agent", async () => { - await withTempHome(async (home) => { - { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - runEmbeddedPiAgentMock.mockClear(); - const cfg = makeCfg(home); - const res = await getReplyFromConfig( - { - Body: "/activation mention", - From: "123@g.us", - To: "+2000", - ChatType: "group", - Provider: "whatsapp", - SenderE164: "+999", - CommandAuthorized: true, - }, - {}, - cfg, - ); - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toBe("⚙️ Group activation set to mention."); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); - } - - { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - runEmbeddedPiAgentMock.mockClear(); - const cfg = makeUnauthorizedWhatsAppCfg(home); - const res = await getReplyFromConfig( - { - Body: "/send off", - From: "+1000", - To: "+2000", - Provider: "whatsapp", - SenderE164: "+1000", - CommandAuthorized: true, - }, - {}, - cfg, - ); - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("Send policy set to off"); - - const storeRaw = await fs.readFile(requireSessionStorePath(cfg), "utf-8"); - const store = JSON.parse(storeRaw) as Record; - expect(store[MAIN_SESSION_KEY]?.sendPolicy).toBe("deny"); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); - } - }); - }); - - it("injects group activation context into the system prompt", async () => { - await withTempHome(async (home) => { - getRunEmbeddedPiAgentMock().mockResolvedValue({ - payloads: [{ text: "ok" }], - meta: { - durationMs: 1, - agentMeta: { sessionId: "s", provider: "p", model: "m" }, - }, - }); - const cfg = makeCfg(home); - cfg.channels ??= {}; - cfg.channels.whatsapp = { - ...cfg.channels.whatsapp, - allowFrom: ["*"], - groups: { "*": { requireMention: false } }, - }; - cfg.messages = { - ...cfg.messages, - groupChat: {}, - }; - - const res = await getReplyFromConfig( - { - Body: "hello group", - From: "123@g.us", - To: "+2000", - ChatType: "group", - Provider: "whatsapp", - SenderE164: "+2000", - GroupSubject: "Test Group", - GroupMembers: "Alice (+1), Bob (+2)", - }, - {}, - cfg, - ); - - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toBe("ok"); - expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); - const extra = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.extraSystemPrompt ?? ""; - expect(extra).toContain('"chat_type": "group"'); - expect(extra).toContain("Activation: always-on"); - }); - }); - - it("runs a greeting prompt for bare /new and blocks unauthorized /reset", async () => { - await withTempHome(async (home) => { - await runGreetingPromptForBareNewOrReset({ home, body: "/new", getReplyFromConfig }); - await expectResetBlockedForNonOwner({ home }); - }); - }); - - it("handles inline commands and strips directives before the agent", async () => { - await withTempHome(async (home) => { - await expectInlineCommandHandledAndStripped({ - home, - getReplyFromConfig, - body: "please /whoami now", - stripToken: "/whoami", - blockReplyContains: "Identity", - requestOverrides: { SenderId: "12345" }, - }); - }); - }); - - it("enforces top-level command auth while keeping inline text", async () => { - await withTempHome(async (home) => { - await expectUnauthorizedCommandDropped(home, "/status"); - const runEmbeddedPiAgentMock = mockEmbeddedOk(); - const res = await runInlineUnauthorizedCommand({ - home, - command: "/status", - }); - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toBe("ok"); - expect(runEmbeddedPiAgentMock).toHaveBeenCalled(); - const prompt = runEmbeddedPiAgentMock.mock.calls.at(-1)?.[0]?.prompt ?? ""; - expect(prompt).toContain("/status"); - }); - }); - - it("enforces elevated toggles across enabled and mention scenarios", async () => { - await withTempHome(async (home) => { - const isolateStore = (cfg: ReturnType, label: string) => { - cfg.session = { ...cfg.session, store: join(home, `${label}.sessions.json`) }; - return cfg; - }; - - { - const cfg = isolateStore(makeWhatsAppElevatedCfg(home, { elevatedEnabled: false }), "off"); - const res = await getReplyFromConfig( - { - Body: "/elevated on", - From: "+1000", - To: "+2000", - Provider: "whatsapp", - SenderE164: "+1000", - }, - {}, - cfg, - ); - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("tools.elevated.enabled"); - - const storeRaw = await fs.readFile(requireSessionStorePath(cfg), "utf-8"); - const store = JSON.parse(storeRaw) as Record; - expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBeUndefined(); - } - - { - const cfg = isolateStore( - makeWhatsAppElevatedCfg(home, { requireMentionInGroups: true }), - "group-on", - ); - const res = await getReplyFromConfig( - { - Body: "/elevated on", - From: "whatsapp:group:123@g.us", - To: "whatsapp:+2000", - Provider: "whatsapp", - SenderE164: "+1000", - CommandAuthorized: true, - ChatType: "group", - WasMentioned: true, - }, - {}, - cfg, - ); - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("Elevated mode set to ask"); - const store = await readSessionStore(cfg); - expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe("on"); - } - - { - const cfg = isolateStore(makeWhatsAppElevatedCfg(home), "inline-unapproved"); - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - runEmbeddedPiAgentMock.mockClear(); - runEmbeddedPiAgentMock.mockResolvedValue({ - payloads: [{ text: "ok" }], - meta: { - durationMs: 1, - agentMeta: { sessionId: "s", provider: "p", model: "m" }, - }, - }); - - const res = await getReplyFromConfig( - { - Body: "please /elevated on now", - From: "+2000", - To: "+2000", - Provider: "whatsapp", - SenderE164: "+2000", - }, - {}, - cfg, - ); - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).not.toContain("elevated is not available right now"); - expect(runEmbeddedPiAgentMock).toHaveBeenCalled(); - } - }); - }); - - it("handles discord elevated allowlist and override behavior", async () => { - await withTempHome(async (home) => { - { - const cfg = makeCfg(home); - cfg.session = { ...cfg.session, store: join(home, "discord-allow.sessions.json") }; - cfg.tools = { elevated: { allowFrom: { discord: ["123"] } } }; - - const res = await getReplyFromConfig( - { - Body: "/elevated on", - From: "discord:123", - To: "user:123", - Provider: "discord", - SenderName: "Peter Steinberger", - SenderUsername: "steipete", - SenderTag: "steipete", - CommandAuthorized: true, - }, - {}, - cfg, - ); - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("Elevated mode set to ask"); - const store = await readSessionStore(cfg); - expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBe("on"); - } - - { - const cfg = makeCfg(home); - cfg.session = { ...cfg.session, store: join(home, "discord-deny.sessions.json") }; - cfg.tools = { - elevated: { - allowFrom: { discord: [] }, - }, - }; - - const res = await getReplyFromConfig( - { - Body: "/elevated on", - From: "discord:123", - To: "user:123", - Provider: "discord", - SenderName: "steipete", - }, - {}, - cfg, - ); - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("tools.elevated.allowFrom.discord"); - expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled(); - } - }); - }); - - it("returns a context overflow fallback when the embedded agent throws", async () => { - await withTempHome(async (home) => { - getRunEmbeddedPiAgentMock().mockRejectedValue(new Error("Context window exceeded")); - - const res = await getReplyFromConfig( - { - Body: "hello", - From: "+1002", - To: "+2000", - }, - {}, - makeCfg(home), - ); - - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toBe( - "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model.", - ); - expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); - }); - }); -}); diff --git a/src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts b/src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts index 4dfddded047..919e88a5bcd 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts @@ -26,96 +26,79 @@ afterEach(() => { }); describe("stageSandboxMedia", () => { - it("stages inbound media into the sandbox workspace", async () => { + it("stages allowed media and blocks unsafe paths", async () => { await withSandboxMediaTempHome("openclaw-triggers-", async (home) => { - const inboundDir = join(home, ".openclaw", "media", "inbound"); - await fs.mkdir(inboundDir, { recursive: true }); - const mediaPath = join(inboundDir, "photo.jpg"); - await fs.writeFile(mediaPath, "test"); - + const cfg = createSandboxMediaStageConfig(home); + const workspaceDir = join(home, "openclaw"); const sandboxDir = join(home, "sandboxes", "session"); vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({ workspaceDir: sandboxDir, containerWorkdir: "/work", }); - const { ctx, sessionCtx } = createSandboxMediaContexts(mediaPath); + { + const inboundDir = join(home, ".openclaw", "media", "inbound"); + await fs.mkdir(inboundDir, { recursive: true }); + const mediaPath = join(inboundDir, "photo.jpg"); + await fs.writeFile(mediaPath, "test"); + const { ctx, sessionCtx } = createSandboxMediaContexts(mediaPath); - await stageSandboxMedia({ - ctx, - sessionCtx, - cfg: createSandboxMediaStageConfig(home), - sessionKey: "agent:main:main", - workspaceDir: join(home, "openclaw"), - }); + await stageSandboxMedia({ + ctx, + sessionCtx, + cfg, + sessionKey: "agent:main:main", + workspaceDir, + }); - const stagedPath = `media/inbound/${basename(mediaPath)}`; - expect(ctx.MediaPath).toBe(stagedPath); - expect(sessionCtx.MediaPath).toBe(stagedPath); - expect(ctx.MediaUrl).toBe(stagedPath); - expect(sessionCtx.MediaUrl).toBe(stagedPath); + const stagedPath = `media/inbound/${basename(mediaPath)}`; + expect(ctx.MediaPath).toBe(stagedPath); + expect(sessionCtx.MediaPath).toBe(stagedPath); + expect(ctx.MediaUrl).toBe(stagedPath); + expect(sessionCtx.MediaUrl).toBe(stagedPath); + await expect( + fs.stat(join(sandboxDir, "media", "inbound", basename(mediaPath))), + ).resolves.toBeTruthy(); + } - const stagedFullPath = join(sandboxDir, "media", "inbound", basename(mediaPath)); - await expect(fs.stat(stagedFullPath)).resolves.toBeTruthy(); - }); - }); + { + const sensitiveFile = join(home, "secrets.txt"); + await fs.writeFile(sensitiveFile, "SENSITIVE DATA"); + const { ctx, sessionCtx } = createSandboxMediaContexts(sensitiveFile); - it("rejects staging host files from outside the media directory", async () => { - await withSandboxMediaTempHome("openclaw-triggers-bypass-", async (home) => { - // Sensitive host file outside .openclaw - const sensitiveFile = join(home, "secrets.txt"); - await fs.writeFile(sensitiveFile, "SENSITIVE DATA"); + await stageSandboxMedia({ + ctx, + sessionCtx, + cfg, + sessionKey: "agent:main:main", + workspaceDir, + }); - const sandboxDir = join(home, "sandboxes", "session"); - vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({ - workspaceDir: sandboxDir, - containerWorkdir: "/work", - }); + await expect( + fs.stat(join(sandboxDir, "media", "inbound", basename(sensitiveFile))), + ).rejects.toThrow(); + expect(ctx.MediaPath).toBe(sensitiveFile); + } - const { ctx, sessionCtx } = createSandboxMediaContexts(sensitiveFile); + { + childProcessMocks.spawn.mockClear(); + const { ctx, sessionCtx } = createSandboxMediaContexts("/etc/passwd"); + ctx.Provider = "imessage"; + ctx.MediaRemoteHost = "user@gateway-host"; + sessionCtx.Provider = "imessage"; + sessionCtx.MediaRemoteHost = "user@gateway-host"; - // This should fail or skip the file - await stageSandboxMedia({ - ctx, - sessionCtx, - cfg: createSandboxMediaStageConfig(home), - sessionKey: "agent:main:main", - workspaceDir: join(home, "openclaw"), - }); + await stageSandboxMedia({ + ctx, + sessionCtx, + cfg, + sessionKey: "agent:main:main", + workspaceDir, + }); - const stagedFullPath = join(sandboxDir, "media", "inbound", basename(sensitiveFile)); - // Expect the file NOT to be staged - await expect(fs.stat(stagedFullPath)).rejects.toThrow(); - - // Context should NOT be rewritten to a sandbox path if it failed to stage - expect(ctx.MediaPath).toBe(sensitiveFile); - }); - }); - - it("blocks remote SCP staging for non-iMessage attachment paths", async () => { - await withSandboxMediaTempHome("openclaw-triggers-remote-block-", async (home) => { - const sandboxDir = join(home, "sandboxes", "session"); - vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({ - workspaceDir: sandboxDir, - containerWorkdir: "/work", - }); - - const { ctx, sessionCtx } = createSandboxMediaContexts("/etc/passwd"); - ctx.Provider = "imessage"; - ctx.MediaRemoteHost = "user@gateway-host"; - sessionCtx.Provider = "imessage"; - sessionCtx.MediaRemoteHost = "user@gateway-host"; - - await stageSandboxMedia({ - ctx, - sessionCtx, - cfg: createSandboxMediaStageConfig(home), - sessionKey: "agent:main:main", - workspaceDir: join(home, "openclaw"), - }); - - expect(childProcessMocks.spawn).not.toHaveBeenCalled(); - expect(ctx.MediaPath).toBe("/etc/passwd"); + expect(childProcessMocks.spawn).not.toHaveBeenCalled(); + expect(ctx.MediaPath).toBe("/etc/passwd"); + } }); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.test.ts b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.test.ts index c6967192457..4374fd06089 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.test.ts @@ -3,7 +3,10 @@ import { join } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { loadSessionStore, resolveSessionKey } from "../config/sessions.js"; +import { registerGroupIntroPromptCases } from "./reply.triggers.group-intro-prompts.cases.js"; +import { registerTriggerHandlingUsageSummaryCases } from "./reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.js"; import { + expectInlineCommandHandledAndStripped, getAbortEmbeddedPiRunMock, getCompactEmbeddedPiSessionMock, getRunEmbeddedPiAgentMock, @@ -12,6 +15,7 @@ import { makeCfg, mockRunEmbeddedPiAgentOk, requireSessionStorePath, + runGreetingPromptForBareNewOrReset, withTempHome, } from "./reply.triggers.trigger-handling.test-harness.js"; import { enqueueFollowupRun, getFollowupQueueDepth, type FollowupRun } from "./reply/queue.js"; @@ -75,25 +79,183 @@ function mockSuccessfulCompaction() { }); } +function makeUnauthorizedWhatsAppCfg(home: string) { + const baseCfg = makeCfg(home); + return { + ...baseCfg, + channels: { + ...baseCfg.channels, + whatsapp: { + allowFrom: ["+1000"], + }, + }, + }; +} + +async function expectResetBlockedForNonOwner(params: { home: string }): Promise { + const { home } = params; + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + runEmbeddedPiAgentMock.mockClear(); + const cfg = makeCfg(home); + cfg.channels ??= {}; + cfg.channels.whatsapp = { + ...cfg.channels.whatsapp, + allowFrom: ["+1999"], + }; + cfg.session = { + ...cfg.session, + store: join(home, "blocked-reset.sessions.json"), + }; + const res = await getReplyFromConfig( + { + Body: "/reset", + From: "+1003", + To: "+2000", + CommandAuthorized: true, + }, + {}, + cfg, + ); + expect(res).toBeUndefined(); + expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); +} + +async function expectUnauthorizedCommandDropped(home: string, body: "/status") { + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + const cfg = makeUnauthorizedWhatsAppCfg(home); + + const res = await getReplyFromConfig( + { + Body: body, + From: "+2001", + To: "+2000", + Provider: "whatsapp", + SenderE164: "+2001", + }, + {}, + cfg, + ); + + expect(res).toBeUndefined(); + expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); +} + +function mockEmbeddedOk() { + return mockRunEmbeddedPiAgentOk("ok"); +} + +async function runInlineUnauthorizedCommand(params: { home: string; command: "/status" }) { + const cfg = makeUnauthorizedWhatsAppCfg(params.home); + const res = await getReplyFromConfig( + { + Body: `please ${params.command} now`, + From: "+2001", + To: "+2000", + Provider: "whatsapp", + SenderE164: "+2001", + }, + {}, + cfg, + ); + return res; +} + describe("trigger handling", () => { - it("includes the error cause when the embedded agent throws", async () => { - await withTempHome(async (home) => { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - runEmbeddedPiAgentMock.mockRejectedValue(new Error("sandbox is not defined.")); - - const res = await getReplyFromConfig(BASE_MESSAGE, {}, makeCfg(home)); - - expect(maybeReplyText(res)).toBe( - "⚠️ Agent failed before reply: sandbox is not defined.\nLogs: openclaw logs --follow", - ); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); - }); + registerGroupIntroPromptCases({ + getReplyFromConfig: () => getReplyFromConfig, + }); + registerTriggerHandlingUsageSummaryCases({ + getReplyFromConfig: () => getReplyFromConfig, }); - it("uses heartbeat override when configured and falls back to stored model override", async () => { + it("handles trigger command and heartbeat flows end-to-end", async () => { await withTempHome(async (home) => { - const runEmbeddedPiAgentMock = mockEmbeddedOkPayload(); - const cases = [ + const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + const errorCases = [ + { + error: "sandbox is not defined.", + expected: + "⚠️ Agent failed before reply: sandbox is not defined.\nLogs: openclaw logs --follow", + }, + { + error: "Context window exceeded", + expected: + "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model.", + }, + ] as const; + for (const testCase of errorCases) { + runEmbeddedPiAgentMock.mockClear(); + runEmbeddedPiAgentMock.mockRejectedValue(new Error(testCase.error)); + const errorRes = await getReplyFromConfig(BASE_MESSAGE, {}, makeCfg(home)); + expect(maybeReplyText(errorRes), testCase.error).toBe(testCase.expected); + expect(runEmbeddedPiAgentMock, testCase.error).toHaveBeenCalledOnce(); + } + + const tokenCases = [ + { text: HEARTBEAT_TOKEN, expected: undefined }, + { text: `${HEARTBEAT_TOKEN} hello`, expected: "hello" }, + ] as const; + + for (const testCase of tokenCases) { + runEmbeddedPiAgentMock.mockClear(); + runEmbeddedPiAgentMock.mockResolvedValue({ + payloads: [{ text: testCase.text }], + meta: { + durationMs: 1, + agentMeta: { sessionId: "s", provider: "p", model: "m" }, + }, + }); + const res = await getReplyFromConfig(BASE_MESSAGE, {}, makeCfg(home)); + expect(maybeReplyText(res)).toBe(testCase.expected); + expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); + } + + const thinkCases = [ + { + label: "context-wrapper", + request: { + Body: [ + "[Chat messages since your last reply - for context]", + "Peter: /thinking high [2025-12-05T21:45:00.000Z]", + "", + "[Current message - respond to this]", + "Give me the status", + ].join("\n"), + From: "+1002", + To: "+2000", + }, + options: {}, + assertPrompt: true, + }, + { + label: "heartbeat", + request: { + Body: "HEARTBEAT /think:high", + From: "+1003", + To: "+1003", + }, + options: { isHeartbeat: true }, + assertPrompt: false, + }, + ] as const; + runEmbeddedPiAgentMock.mockClear(); + for (const testCase of thinkCases) { + mockRunEmbeddedPiAgentOk(); + const res = await getReplyFromConfig(testCase.request, testCase.options, makeCfg(home)); + const text = maybeReplyText(res); + expect(text, testCase.label).toBe("ok"); + expect(text, testCase.label).not.toMatch(/Thinking level set/i); + expect(getRunEmbeddedPiAgentMock(), testCase.label).toHaveBeenCalledOnce(); + if (testCase.assertPrompt) { + const prompt = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.prompt ?? ""; + expect(prompt).toContain("Give me the status"); + expect(prompt).not.toContain("/thinking high"); + expect(prompt).not.toContain("/think high"); + } + getRunEmbeddedPiAgentMock().mockClear(); + } + + const modelCases = [ { label: "heartbeat-override", setup: (cfg: ReturnType) => { @@ -114,7 +276,8 @@ describe("trigger handling", () => { }, ] as const; - for (const testCase of cases) { + for (const testCase of modelCases) { + mockEmbeddedOkPayload(); runEmbeddedPiAgentMock.mockClear(); const cfg = makeCfg(home); cfg.session = { ...cfg.session, store: join(home, `${testCase.label}.sessions.json`) }; @@ -126,62 +289,6 @@ describe("trigger handling", () => { expect(call?.provider).toBe(testCase.expected.provider); expect(call?.model).toBe(testCase.expected.model); } - }); - }); - - it("suppresses or strips HEARTBEAT_OK replies outside heartbeat runs", async () => { - await withTempHome(async (home) => { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - const cases = [ - { text: HEARTBEAT_TOKEN, expected: undefined }, - { text: `${HEARTBEAT_TOKEN} hello`, expected: "hello" }, - ] as const; - - for (const testCase of cases) { - runEmbeddedPiAgentMock.mockClear(); - runEmbeddedPiAgentMock.mockResolvedValue({ - payloads: [{ text: testCase.text }], - meta: { - durationMs: 1, - agentMeta: { sessionId: "s", provider: "p", model: "m" }, - }, - }); - const res = await getReplyFromConfig(BASE_MESSAGE, {}, makeCfg(home)); - expect(maybeReplyText(res)).toBe(testCase.expected); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); - } - }); - }); - - it("updates group activation when the owner sends /activation", async () => { - await withTempHome(async (home) => { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - const cfg = makeCfg(home); - const res = await getReplyFromConfig( - { - Body: "/activation always", - From: "123@g.us", - To: "+2000", - ChatType: "group", - Provider: "whatsapp", - SenderE164: "+2000", - CommandAuthorized: true, - }, - {}, - cfg, - ); - expect(maybeReplyText(res)).toContain("Group activation set to always"); - const store = JSON.parse(await fs.readFile(requireSessionStorePath(cfg), "utf-8")) as Record< - string, - { groupActivation?: string } - >; - expect(store["agent:main:whatsapp:group:123@g.us"]?.groupActivation).toBe("always"); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); - }); - }); - - it("runs /compact for main and non-default agents without invoking the embedded run path", async () => { - await withTempHome(async (home) => { { const storePath = join(home, "compact-main.sessions.json"); const cfg = makeCfg(home); @@ -213,6 +320,8 @@ describe("trigger handling", () => { { getCompactEmbeddedPiSessionMock().mockClear(); mockSuccessfulCompaction(); + const cfg = makeCfg(home); + cfg.session = { ...cfg.session, store: join(home, "compact-worker.sessions.json") }; const res = await getReplyFromConfig( { Body: "/compact", @@ -222,7 +331,7 @@ describe("trigger handling", () => { CommandAuthorized: true, }, {}, - makeCfg(home), + cfg, ); const text = maybeReplyText(res); @@ -233,240 +342,211 @@ describe("trigger handling", () => { ); } - expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled(); - }); - }); - - it("ignores think directives that only appear in the context wrapper", async () => { - await withTempHome(async (home) => { - mockRunEmbeddedPiAgentOk(); - - const res = await getReplyFromConfig( - { - Body: [ - "[Chat messages since your last reply - for context]", - "Peter: /thinking high [2025-12-05T21:45:00.000Z]", - "", - "[Current message - respond to this]", - "Give me the status", - ].join("\n"), - From: "+1002", - To: "+2000", - }, - {}, - makeCfg(home), - ); - - expect(maybeReplyText(res)).toBe("ok"); - expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); - const prompt = getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]?.prompt ?? ""; - expect(prompt).toContain("Give me the status"); - expect(prompt).not.toContain("/thinking high"); - expect(prompt).not.toContain("/think high"); - }); - }); - - it("does not emit directive acks for heartbeats with /think", async () => { - await withTempHome(async (home) => { - mockRunEmbeddedPiAgentOk(); - - const res = await getReplyFromConfig( - { - Body: "HEARTBEAT /think:high", - From: "+1003", - To: "+1003", - }, - { isHeartbeat: true }, - makeCfg(home), - ); - - const text = maybeReplyText(res); - expect(text).toBe("ok"); - expect(text).not.toMatch(/Thinking level set/i); - expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); - }); - }); - - it("targets the active session for native /stop", async () => { - await withTempHome(async (home) => { - const cfg = makeCfg(home); - const storePath = cfg.session?.store; - if (!storePath) { - throw new Error("missing session store path"); - } - const targetSessionKey = "agent:main:telegram:group:123"; - const targetSessionId = "session-target"; - await fs.writeFile( - storePath, - JSON.stringify({ - [targetSessionKey]: { + { + const cfg = makeCfg(home); + cfg.session = { ...cfg.session, store: join(home, "native-stop.sessions.json") }; + getAbortEmbeddedPiRunMock().mockClear(); + const storePath = cfg.session?.store; + if (!storePath) { + throw new Error("missing session store path"); + } + const targetSessionKey = "agent:main:telegram:group:123"; + const targetSessionId = "session-target"; + await fs.writeFile( + storePath, + JSON.stringify({ + [targetSessionKey]: { + sessionId: targetSessionId, + updatedAt: Date.now(), + }, + }), + ); + const followupRun: FollowupRun = { + prompt: "queued", + enqueuedAt: Date.now(), + run: { + agentId: "main", + agentDir: join(home, "agent"), sessionId: targetSessionId, - updatedAt: Date.now(), + sessionKey: targetSessionKey, + messageProvider: "telegram", + agentAccountId: "acct", + sessionFile: join(home, "session.jsonl"), + workspaceDir: join(home, "workspace"), + config: cfg, + provider: "anthropic", + model: "claude-opus-4-5", + timeoutMs: 10, + blockReplyBreak: "text_end", }, - }), - ); - const followupRun: FollowupRun = { - prompt: "queued", - enqueuedAt: Date.now(), - run: { - agentId: "main", - agentDir: join(home, "agent"), - sessionId: targetSessionId, - sessionKey: targetSessionKey, - messageProvider: "telegram", - agentAccountId: "acct", - sessionFile: join(home, "session.jsonl"), - workspaceDir: join(home, "workspace"), - config: cfg, - provider: "anthropic", - model: "claude-opus-4-5", - timeoutMs: 10, - blockReplyBreak: "text_end", - }, - }; - enqueueFollowupRun( - targetSessionKey, - followupRun, - { mode: "collect", debounceMs: 0, cap: 20, dropPolicy: "summarize" }, - "none", - ); - expect(getFollowupQueueDepth(targetSessionKey)).toBe(1); + }; + enqueueFollowupRun( + targetSessionKey, + followupRun, + { mode: "collect", debounceMs: 0, cap: 20, dropPolicy: "summarize" }, + "none", + ); + expect(getFollowupQueueDepth(targetSessionKey)).toBe(1); - const res = await getReplyFromConfig( - { - Body: "/stop", - From: "telegram:111", - To: "telegram:111", - ChatType: "direct", - Provider: "telegram", - Surface: "telegram", - SessionKey: "telegram:slash:111", - CommandSource: "native", - CommandTargetSessionKey: targetSessionKey, - CommandAuthorized: true, - }, - {}, - cfg, - ); + const res = await getReplyFromConfig( + { + Body: "/stop", + From: "telegram:111", + To: "telegram:111", + ChatType: "direct", + Provider: "telegram", + Surface: "telegram", + SessionKey: "telegram:slash:111", + CommandSource: "native", + CommandTargetSessionKey: targetSessionKey, + CommandAuthorized: true, + }, + {}, + cfg, + ); - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toBe("⚙️ Agent was aborted."); - expect(getAbortEmbeddedPiRunMock()).toHaveBeenCalledWith(targetSessionId); - const store = loadSessionStore(storePath); - expect(store[targetSessionKey]?.abortedLastRun).toBe(true); - expect(getFollowupQueueDepth(targetSessionKey)).toBe(0); - }); - }); - it("applies native /model to the target session", async () => { - await withTempHome(async (home) => { - const cfg = makeCfg(home); - const storePath = cfg.session?.store; - if (!storePath) { - throw new Error("missing session store path"); + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toBe("⚙️ Agent was aborted."); + expect(getAbortEmbeddedPiRunMock()).toHaveBeenCalledWith(targetSessionId); + const store = loadSessionStore(storePath); + expect(store[targetSessionKey]?.abortedLastRun).toBe(true); + expect(getFollowupQueueDepth(targetSessionKey)).toBe(0); } - const slashSessionKey = "telegram:slash:111"; - const targetSessionKey = MAIN_SESSION_KEY; - // Seed the target session to ensure the native command mutates it. - await fs.writeFile( - storePath, - JSON.stringify({ - [targetSessionKey]: { - sessionId: "session-target", - updatedAt: Date.now(), + { + const cfg = makeCfg(home); + cfg.session = { ...cfg.session, store: join(home, "native-model.sessions.json") }; + getRunEmbeddedPiAgentMock().mockClear(); + const storePath = cfg.session?.store; + if (!storePath) { + throw new Error("missing session store path"); + } + const slashSessionKey = "telegram:slash:111"; + const targetSessionKey = MAIN_SESSION_KEY; + + // Seed the target session to ensure the native command mutates it. + await fs.writeFile( + storePath, + JSON.stringify({ + [targetSessionKey]: { + sessionId: "session-target", + updatedAt: Date.now(), + }, + }), + ); + + const res = await getReplyFromConfig( + { + Body: "/model openai/gpt-4.1-mini", + From: "telegram:111", + To: "telegram:111", + ChatType: "direct", + Provider: "telegram", + Surface: "telegram", + SessionKey: slashSessionKey, + CommandSource: "native", + CommandTargetSessionKey: targetSessionKey, + CommandAuthorized: true, }, - }), - ); + {}, + cfg, + ); - const res = await getReplyFromConfig( - { - Body: "/model openai/gpt-4.1-mini", - From: "telegram:111", - To: "telegram:111", - ChatType: "direct", - Provider: "telegram", - Surface: "telegram", - SessionKey: slashSessionKey, - CommandSource: "native", - CommandTargetSessionKey: targetSessionKey, - CommandAuthorized: true, - }, - {}, - cfg, - ); + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toContain("Model set to openai/gpt-4.1-mini"); - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("Model set to openai/gpt-4.1-mini"); + const store = loadSessionStore(storePath); + expect(store[targetSessionKey]?.providerOverride).toBe("openai"); + expect(store[targetSessionKey]?.modelOverride).toBe("gpt-4.1-mini"); + expect(store[slashSessionKey]).toBeUndefined(); - const store = loadSessionStore(storePath); - expect(store[targetSessionKey]?.providerOverride).toBe("openai"); - expect(store[targetSessionKey]?.modelOverride).toBe("gpt-4.1-mini"); - expect(store[slashSessionKey]).toBeUndefined(); + getRunEmbeddedPiAgentMock().mockResolvedValue({ + payloads: [{ text: "ok" }], + meta: { + durationMs: 5, + agentMeta: { sessionId: "s", provider: "p", model: "m" }, + }, + }); - getRunEmbeddedPiAgentMock().mockResolvedValue({ - payloads: [{ text: "ok" }], - meta: { - durationMs: 5, - agentMeta: { sessionId: "s", provider: "p", model: "m" }, - }, + await getReplyFromConfig( + { + Body: "hi", + From: "telegram:111", + To: "telegram:111", + ChatType: "direct", + Provider: "telegram", + Surface: "telegram", + }, + {}, + cfg, + ); + + expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); + expect(getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]).toEqual( + expect.objectContaining({ + provider: "openai", + model: "gpt-4.1-mini", + }), + ); + } + + { + const cfg = makeCfg(home) as unknown as OpenClawConfig; + cfg.session = { ...cfg.session, store: join(home, "native-status.sessions.json") }; + cfg.agents = { + ...cfg.agents, + list: [{ id: "coding", model: "minimax/MiniMax-M2.1" }], + }; + cfg.channels = { + ...cfg.channels, + telegram: { + allowFrom: ["*"], + }, + }; + + const res = await getReplyFromConfig( + { + Body: "/status", + From: "telegram:111", + To: "telegram:111", + ChatType: "group", + Provider: "telegram", + Surface: "telegram", + SessionKey: "telegram:slash:111", + CommandSource: "native", + CommandTargetSessionKey: "agent:coding:telegram:group:123", + CommandAuthorized: true, + }, + {}, + cfg, + ); + + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toContain("minimax/MiniMax-M2.1"); + } + + await runGreetingPromptForBareNewOrReset({ home, body: "/new", getReplyFromConfig }); + await expectResetBlockedForNonOwner({ home }); + await expectInlineCommandHandledAndStripped({ + home, + getReplyFromConfig, + body: "please /whoami now", + stripToken: "/whoami", + blockReplyContains: "Identity", + requestOverrides: { SenderId: "12345" }, + }); + getRunEmbeddedPiAgentMock().mockClear(); + await expectUnauthorizedCommandDropped(home, "/status"); + const inlineRunEmbeddedPiAgentMock = mockEmbeddedOk(); + const res = await runInlineUnauthorizedCommand({ + home, + command: "/status", }); - - await getReplyFromConfig( - { - Body: "hi", - From: "telegram:111", - To: "telegram:111", - ChatType: "direct", - Provider: "telegram", - Surface: "telegram", - }, - {}, - cfg, - ); - - expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); - expect(getRunEmbeddedPiAgentMock().mock.calls[0]?.[0]).toEqual( - expect.objectContaining({ - provider: "openai", - model: "gpt-4.1-mini", - }), - ); - }); - }); - - it("uses the target agent model for native /status", async () => { - await withTempHome(async (home) => { - const cfg = makeCfg(home) as unknown as OpenClawConfig; - cfg.agents = { - ...cfg.agents, - list: [{ id: "coding", model: "minimax/MiniMax-M2.1" }], - }; - cfg.channels = { - ...cfg.channels, - telegram: { - allowFrom: ["*"], - }, - }; - - const res = await getReplyFromConfig( - { - Body: "/status", - From: "telegram:111", - To: "telegram:111", - ChatType: "group", - Provider: "telegram", - Surface: "telegram", - SessionKey: "telegram:slash:111", - CommandSource: "native", - CommandTargetSessionKey: "agent:coding:telegram:group:123", - CommandAuthorized: true, - }, - {}, - cfg, - ); - const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toContain("minimax/MiniMax-M2.1"); + expect(text).toBe("ok"); + expect(inlineRunEmbeddedPiAgentMock).toHaveBeenCalled(); + const prompt = inlineRunEmbeddedPiAgentMock.mock.calls.at(-1)?.[0]?.prompt ?? ""; + expect(prompt).toContain("/status"); }); }); });