diff --git a/src/auto-reply/reply.triggers.trigger-handling.allows-activation-from-allowfrom-groups.test.ts b/src/auto-reply/reply.triggers.trigger-handling.allows-activation-from-allowfrom-groups.test.ts index 3d97464c7d2..e8e2d1b27fc 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.allows-activation-from-allowfrom-groups.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.allows-activation-from-allowfrom-groups.test.ts @@ -1,8 +1,11 @@ +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { beforeAll, describe, expect, it } from "vitest"; import { getRunEmbeddedPiAgentMock, installTriggerHandlingE2eTestHooks, makeCfg, + runGreetingPromptForBareNewOrReset, withTempHome, } from "./reply.triggers.trigger-handling.test-harness.js"; @@ -13,6 +16,36 @@ beforeAll(async () => { installTriggerHandlingE2eTestHooks(); +async function expectResetBlockedForNonOwner(params: { + home: string; + commandAuthorized: boolean; + getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig; +}): Promise { + const { home, commandAuthorized, getReplyFromConfig } = params; + const cfg = makeCfg(home); + cfg.channels ??= {}; + cfg.channels.whatsapp = { + ...cfg.channels.whatsapp, + allowFrom: ["+1999"], + }; + cfg.session = { + ...cfg.session, + store: join(tmpdir(), `openclaw-session-test-${Date.now()}.json`), + }; + const res = await getReplyFromConfig( + { + Body: "/reset", + From: "+1003", + To: "+2000", + CommandAuthorized: commandAuthorized, + }, + {}, + cfg, + ); + expect(res).toBeUndefined(); + expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled(); +} + describe("trigger handling", () => { it("allows /activation from allowFrom in groups", async () => { await withTempHome(async (home) => { @@ -79,4 +112,32 @@ describe("trigger handling", () => { expect(extra).toContain("Activation: always-on"); }); }); + it("runs a greeting prompt for a bare /reset", async () => { + await withTempHome(async (home) => { + await runGreetingPromptForBareNewOrReset({ home, body: "/reset", getReplyFromConfig }); + }); + }); + it("runs a greeting prompt for a bare /new", async () => { + await withTempHome(async (home) => { + await runGreetingPromptForBareNewOrReset({ home, body: "/new", getReplyFromConfig }); + }); + }); + it("does not reset for unauthorized /reset", async () => { + await withTempHome(async (home) => { + await expectResetBlockedForNonOwner({ + home, + commandAuthorized: false, + getReplyFromConfig, + }); + }); + }); + it("blocks /reset for non-owner senders", async () => { + await withTempHome(async (home) => { + await expectResetBlockedForNonOwner({ + home, + commandAuthorized: true, + getReplyFromConfig, + }); + }); + }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.includes-error-cause-embedded-agent-throws.test.ts b/src/auto-reply/reply.triggers.trigger-handling.includes-error-cause-embedded-agent-throws.test.ts index cb54f1d32f9..73bea2eece5 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.includes-error-cause-embedded-agent-throws.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.includes-error-cause-embedded-agent-throws.test.ts @@ -1,10 +1,15 @@ import fs from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { beforeAll, describe, expect, it } from "vitest"; +import { loadSessionStore, resolveSessionKey } from "../config/sessions.js"; import { + getCompactEmbeddedPiSessionMock, getRunEmbeddedPiAgentMock, installTriggerHandlingE2eTestHooks, MAIN_SESSION_KEY, makeCfg, + mockRunEmbeddedPiAgentOk, withTempHome, } from "./reply.triggers.trigger-handling.test-harness.js"; import { HEARTBEAT_TOKEN } from "./tokens.js"; @@ -57,6 +62,22 @@ async function writeStoredModelOverride(cfg: ReturnType): Promis ); } +function mockSuccessfulCompaction() { + getCompactEmbeddedPiSessionMock().mockResolvedValue({ + ok: true, + compacted: true, + result: { + summary: "summary", + firstKeptEntryId: "x", + tokensBefore: 12000, + }, + }); +} + +function replyText(res: Awaited>) { + return Array.isArray(res) ? res[0]?.text : res?.text; +} + describe("trigger handling", () => { it("includes the error cause when the embedded agent throws", async () => { await withTempHome(async (home) => { @@ -170,4 +191,113 @@ describe("trigger handling", () => { expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); }); }); + + it("runs /compact as a gated command", async () => { + await withTempHome(async (home) => { + const storePath = join(tmpdir(), `openclaw-session-test-${Date.now()}.json`); + const cfg = makeCfg(home); + cfg.session = { ...cfg.session, store: storePath }; + mockSuccessfulCompaction(); + + const res = await getReplyFromConfig( + { + Body: "/compact focus on decisions", + From: "+1003", + To: "+2000", + CommandAuthorized: true, + }, + {}, + cfg, + ); + const text = replyText(res); + expect(text?.startsWith("⚙️ Compacted")).toBe(true); + expect(getCompactEmbeddedPiSessionMock()).toHaveBeenCalledOnce(); + expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled(); + const store = loadSessionStore(storePath); + const sessionKey = resolveSessionKey("per-sender", { + Body: "/compact focus on decisions", + From: "+1003", + To: "+2000", + }); + expect(store[sessionKey]?.compactionCount).toBe(1); + }); + }); + + it("runs /compact for non-default agents without transcript path validation failures", async () => { + await withTempHome(async (home) => { + getCompactEmbeddedPiSessionMock().mockClear(); + mockSuccessfulCompaction(); + + const res = await getReplyFromConfig( + { + Body: "/compact", + From: "+1004", + To: "+2000", + SessionKey: "agent:worker1:telegram:12345", + CommandAuthorized: true, + }, + {}, + makeCfg(home), + ); + + const text = replyText(res); + expect(text?.startsWith("⚙️ Compacted")).toBe(true); + expect(getCompactEmbeddedPiSessionMock()).toHaveBeenCalledOnce(); + expect(getCompactEmbeddedPiSessionMock().mock.calls[0]?.[0]?.sessionFile).toContain( + join("agents", "worker1", "sessions"), + ); + 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), + ); + + const text = replyText(res); + expect(text).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 = replyText(res); + expect(text).toBe("ok"); + expect(text).not.toMatch(/Thinking level set/i); + expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); + }); + }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.runs-compact-as-gated-command.test.ts b/src/auto-reply/reply.triggers.trigger-handling.runs-compact-as-gated-command.test.ts deleted file mode 100644 index 385df13e65a..00000000000 --- a/src/auto-reply/reply.triggers.trigger-handling.runs-compact-as-gated-command.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { beforeAll, describe, expect, it } from "vitest"; -import { loadSessionStore, resolveSessionKey } from "../config/sessions.js"; -import { - getCompactEmbeddedPiSessionMock, - 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 mockSuccessfulCompaction() { - getCompactEmbeddedPiSessionMock().mockResolvedValue({ - ok: true, - compacted: true, - result: { - summary: "summary", - firstKeptEntryId: "x", - tokensBefore: 12000, - }, - }); -} - -function replyText(res: Awaited>) { - return Array.isArray(res) ? res[0]?.text : res?.text; -} - -describe("trigger handling", () => { - it("runs /compact as a gated command", async () => { - await withTempHome(async (home) => { - const storePath = join(tmpdir(), `openclaw-session-test-${Date.now()}.json`); - const cfg = makeCfg(home); - cfg.session = { ...cfg.session, store: storePath }; - mockSuccessfulCompaction(); - - const res = await getReplyFromConfig( - { - Body: "/compact focus on decisions", - From: "+1003", - To: "+2000", - CommandAuthorized: true, - }, - {}, - cfg, - ); - const text = replyText(res); - expect(text?.startsWith("⚙️ Compacted")).toBe(true); - expect(getCompactEmbeddedPiSessionMock()).toHaveBeenCalledOnce(); - expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled(); - const store = loadSessionStore(storePath); - const sessionKey = resolveSessionKey("per-sender", { - Body: "/compact focus on decisions", - From: "+1003", - To: "+2000", - }); - expect(store[sessionKey]?.compactionCount).toBe(1); - }); - }); - it("runs /compact for non-default agents without transcript path validation failures", async () => { - await withTempHome(async (home) => { - getCompactEmbeddedPiSessionMock().mockClear(); - mockSuccessfulCompaction(); - - const res = await getReplyFromConfig( - { - Body: "/compact", - From: "+1004", - To: "+2000", - SessionKey: "agent:worker1:telegram:12345", - CommandAuthorized: true, - }, - {}, - makeCfg(home), - ); - - const text = replyText(res); - expect(text?.startsWith("⚙️ Compacted")).toBe(true); - expect(getCompactEmbeddedPiSessionMock()).toHaveBeenCalledOnce(); - expect(getCompactEmbeddedPiSessionMock().mock.calls[0]?.[0]?.sessionFile).toContain( - join("agents", "worker1", "sessions"), - ); - 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), - ); - - const text = replyText(res); - expect(text).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 = replyText(res); - expect(text).toBe("ok"); - expect(text).not.toMatch(/Thinking level set/i); - expect(getRunEmbeddedPiAgentMock()).toHaveBeenCalledOnce(); - }); - }); -}); diff --git a/src/auto-reply/reply.triggers.trigger-handling.runs-greeting-prompt-bare-reset.test.ts b/src/auto-reply/reply.triggers.trigger-handling.runs-greeting-prompt-bare-reset.test.ts deleted file mode 100644 index 916308069d4..00000000000 --- a/src/auto-reply/reply.triggers.trigger-handling.runs-greeting-prompt-bare-reset.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { beforeAll, describe, expect, it } from "vitest"; -import { - getRunEmbeddedPiAgentMock, - installTriggerHandlingE2eTestHooks, - makeCfg, - runGreetingPromptForBareNewOrReset, - withTempHome, -} from "./reply.triggers.trigger-handling.test-harness.js"; - -let getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig; -beforeAll(async () => { - ({ getReplyFromConfig } = await import("./reply.js")); -}); - -installTriggerHandlingE2eTestHooks(); - -async function expectResetBlockedForNonOwner(params: { - home: string; - commandAuthorized: boolean; - getReplyFromConfig: typeof import("./reply.js").getReplyFromConfig; -}): Promise { - const { home, commandAuthorized, getReplyFromConfig } = params; - const cfg = makeCfg(home); - cfg.channels ??= {}; - cfg.channels.whatsapp = { - ...cfg.channels.whatsapp, - allowFrom: ["+1999"], - }; - cfg.session = { - ...cfg.session, - store: join(tmpdir(), `openclaw-session-test-${Date.now()}.json`), - }; - const res = await getReplyFromConfig( - { - Body: "/reset", - From: "+1003", - To: "+2000", - CommandAuthorized: commandAuthorized, - }, - {}, - cfg, - ); - expect(res).toBeUndefined(); - expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled(); -} - -describe("trigger handling", () => { - it("runs a greeting prompt for a bare /reset", async () => { - await withTempHome(async (home) => { - await runGreetingPromptForBareNewOrReset({ home, body: "/reset", getReplyFromConfig }); - }); - }); - it("runs a greeting prompt for a bare /new", async () => { - await withTempHome(async (home) => { - await runGreetingPromptForBareNewOrReset({ home, body: "/new", getReplyFromConfig }); - }); - }); - it("does not reset for unauthorized /reset", async () => { - await withTempHome(async (home) => { - await expectResetBlockedForNonOwner({ - home, - commandAuthorized: false, - getReplyFromConfig, - }); - }); - }); - it("blocks /reset for non-owner senders", async () => { - await withTempHome(async (home) => { - await expectResetBlockedForNonOwner({ - home, - commandAuthorized: true, - getReplyFromConfig, - }); - }); - }); -});