import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; import { loadModelCatalog } from "../agents/model-catalog.js"; import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { getReplyFromConfig } from "./reply.js"; vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); vi.mock("../agents/model-catalog.js", () => ({ loadModelCatalog: vi.fn(), })); async function withTempHome(fn: (home: string) => Promise): Promise { return withTempHomeBase( async (home) => { return await fn(home); }, { env: { CLAWDBOT_AGENT_DIR: (home) => path.join(home, ".clawdbot", "agent"), PI_CODING_AGENT_DIR: (home) => path.join(home, ".clawdbot", "agent"), }, prefix: "clawdbot-rawbody-", }, ); } describe("RawBody directive parsing", () => { beforeEach(() => { vi.mocked(runEmbeddedPiAgent).mockReset(); vi.mocked(loadModelCatalog).mockResolvedValue([ { id: "claude-opus-4-5", name: "Opus 4.5", provider: "anthropic" }, ]); }); afterEach(() => { vi.restoreAllMocks(); }); it("/model, /think, /verbose directives detected from RawBody even when Body has structural wrapper", async () => { await withTempHome(async (home) => { vi.mocked(runEmbeddedPiAgent).mockReset(); const groupMessageCtx = { Body: `[Chat messages since your last reply - for context]\\n[WhatsApp ...] Someone: hello\\n\\n[Current message - respond to this]\\n[WhatsApp ...] Jake: /think:high\\n[from: Jake McInteer (+6421807830)]`, RawBody: "/think:high", From: "+1222", To: "+1222", ChatType: "group", }; const res = await getReplyFromConfig( groupMessageCtx, {}, { agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: path.join(home, "clawd"), }, }, whatsapp: { allowFrom: ["*"] }, session: { store: path.join(home, "sessions.json") }, }, ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toContain("Thinking level set to high."); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); it("/model status detected from RawBody", async () => { await withTempHome(async (home) => { vi.mocked(runEmbeddedPiAgent).mockReset(); const groupMessageCtx = { Body: `[Context]\nJake: /model status\n[from: Jake]`, RawBody: "/model status", From: "+1222", To: "+1222", ChatType: "group", }; const res = await getReplyFromConfig( groupMessageCtx, {}, { agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: path.join(home, "clawd"), models: { "anthropic/claude-opus-4-5": {}, }, }, }, whatsapp: { allowFrom: ["*"] }, session: { store: path.join(home, "sessions.json") }, }, ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toContain("anthropic/claude-opus-4-5"); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); it("CommandBody is honored when RawBody is missing", async () => { await withTempHome(async (home) => { vi.mocked(runEmbeddedPiAgent).mockReset(); const groupMessageCtx = { Body: `[Context]\nJake: /verbose on\n[from: Jake]`, CommandBody: "/verbose on", From: "+1222", To: "+1222", ChatType: "group", }; const res = await getReplyFromConfig( groupMessageCtx, {}, { agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: path.join(home, "clawd"), }, }, whatsapp: { allowFrom: ["*"] }, session: { store: path.join(home, "sessions.json") }, }, ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toContain("Verbose logging enabled."); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); it("Integration: WhatsApp group message with structural wrapper and RawBody command", async () => { await withTempHome(async (home) => { vi.mocked(runEmbeddedPiAgent).mockReset(); const groupMessageCtx = { Body: `[Chat messages since your last reply - for context]\\n[WhatsApp ...] Someone: hello\\n\\n[Current message - respond to this]\\n[WhatsApp ...] Jake: /status\\n[from: Jake McInteer (+6421807830)]`, RawBody: "/status", ChatType: "group", From: "+1222", To: "+1222", SessionKey: "agent:main:whatsapp:group:G1", Provider: "whatsapp", Surface: "whatsapp", SenderE164: "+1222", }; const res = await getReplyFromConfig( groupMessageCtx, {}, { agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: path.join(home, "clawd"), }, }, whatsapp: { allowFrom: ["+1222"] }, session: { store: path.join(home, "sessions.json") }, }, ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toContain("Session: agent:main:whatsapp:group:G1"); expect(text).toContain("anthropic/claude-opus-4-5"); expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); it("preserves history when RawBody is provided for command parsing", async () => { await withTempHome(async (home) => { vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1, agentMeta: { sessionId: "s", provider: "p", model: "m" }, }, }); const groupMessageCtx = { Body: [ "[Chat messages since your last reply - for context]", "[WhatsApp ...] Peter: hello", "", "[Current message - respond to this]", "[WhatsApp ...] Jake: /think:high status please", "[from: Jake McInteer (+6421807830)]", ].join("\n"), RawBody: "/think:high status please", From: "+1222", To: "+1222", ChatType: "group", }; const res = await getReplyFromConfig( groupMessageCtx, {}, { agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: path.join(home, "clawd"), }, }, whatsapp: { allowFrom: ["*"] }, session: { store: path.join(home, "sessions.json") }, }, ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("ok"); expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); const prompt = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; expect(prompt).toContain( "[Chat messages since your last reply - for context]", ); expect(prompt).toContain("Peter: hello"); expect(prompt).toContain("status please"); expect(prompt).not.toContain("/think:high"); }); }); });