From a3a2c641a7b86590f336c3ed11c4e903da798db2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 18 Jan 2026 05:54:42 +0000 Subject: [PATCH] test(usage): cover modes and full footer --- ...age-summary-current-model-provider.test.ts | 99 +++++++++++ ...agent-runner.response-usage-footer.test.ts | 160 ++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 src/auto-reply/reply/agent-runner.response-usage-footer.test.ts 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 index e1664deb7b4..e9d90110669 100644 --- 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 @@ -1,4 +1,5 @@ import { join } from "node:path"; +import { readFile } from "node:fs/promises"; import { afterEach, describe, expect, it, vi } from "vitest"; import { normalizeTestText } from "../../test/helpers/normalize-text.js"; import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; @@ -90,6 +91,16 @@ function makeCfg(home: string) { }; } +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]; +} + afterEach(() => { vi.restoreAllMocks(); }); @@ -185,6 +196,94 @@ describe("trigger handling", () => { expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); }); + + it("cycles /usage modes and persists to the session store", async () => { + await withTempHome(async (home) => { + const cfg = makeCfg(home); + + 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: tokens", + ); + const s1 = await readSessionStore(home); + expect(pickFirstStoreEntry<{ responseUsage?: string }>(s1)?.responseUsage).toBe("tokens"); + + 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: full", + ); + const s2 = await readSessionStore(home); + expect(pickFirstStoreEntry<{ responseUsage?: string }>(s2)?.responseUsage).toBe("full"); + + 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: off", + ); + const s3 = await readSessionStore(home); + expect(pickFirstStoreEntry<{ responseUsage?: string }>(s3)?.responseUsage).toBeUndefined(); + + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); + + it("treats /usage on as tokens (back-compat)", async () => { + await withTempHome(async (home) => { + const cfg = makeCfg(home); + const res = await getReplyFromConfig( + { + Body: "/usage on", + From: "+1000", + To: "+2000", + Provider: "whatsapp", + SenderE164: "+1000", + CommandAuthorized: true, + }, + undefined, + cfg, + ); + const replies = res ? (Array.isArray(res) ? res : [res]) : []; + expect(replies.length).toBe(1); + expect(String(replies[0]?.text ?? "")).toContain("Usage footer: tokens"); + + const store = await readSessionStore(home); + expect(pickFirstStoreEntry<{ responseUsage?: string }>(store)?.responseUsage).toBe("tokens"); + + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); it("sends one inline status and still returns agent reply for mixed text", async () => { await withTempHome(async (home) => { vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ diff --git a/src/auto-reply/reply/agent-runner.response-usage-footer.test.ts b/src/auto-reply/reply/agent-runner.response-usage-footer.test.ts new file mode 100644 index 00000000000..27511f31229 --- /dev/null +++ b/src/auto-reply/reply/agent-runner.response-usage-footer.test.ts @@ -0,0 +1,160 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import type { SessionEntry } from "../../config/sessions.js"; +import type { TemplateContext } from "../templating.js"; +import type { FollowupRun, QueueSettings } from "./queue.js"; +import { createMockTypingController } from "./test-helpers.js"; + +const runEmbeddedPiAgentMock = vi.fn(); +const runWithModelFallbackMock = vi.fn(); + +vi.mock("../../agents/model-fallback.js", () => ({ + runWithModelFallback: (params: { + provider: string; + model: string; + run: (provider: string, model: string) => Promise; + }) => runWithModelFallbackMock(params), +})); + +vi.mock("../../agents/pi-embedded.js", () => ({ + queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), + runEmbeddedPiAgent: (params: unknown) => runEmbeddedPiAgentMock(params), +})); + +vi.mock("./queue.js", async () => { + const actual = await vi.importActual("./queue.js"); + return { + ...actual, + enqueueFollowupRun: vi.fn(), + scheduleFollowupDrain: vi.fn(), + }; +}); + +import { runReplyAgent } from "./agent-runner.js"; + +function createRun(params: { responseUsage: "tokens" | "full"; sessionKey: string }) { + const typing = createMockTypingController(); + const sessionCtx = { + Provider: "whatsapp", + OriginatingTo: "+15550001111", + AccountId: "primary", + MessageSid: "msg", + } as unknown as TemplateContext; + const resolvedQueue = { mode: "interrupt" } as unknown as QueueSettings; + + const sessionEntry: SessionEntry = { + sessionId: "session", + updatedAt: Date.now(), + responseUsage: params.responseUsage, + }; + + const followupRun = { + prompt: "hello", + summaryLine: "hello", + enqueuedAt: Date.now(), + run: { + agentId: "main", + agentDir: "/tmp/agent", + sessionId: "session", + sessionKey: params.sessionKey, + messageProvider: "whatsapp", + sessionFile: "/tmp/session.jsonl", + workspaceDir: "/tmp", + config: {}, + skillsSnapshot: {}, + provider: "anthropic", + model: "claude", + thinkLevel: "low", + verboseLevel: "off", + elevatedLevel: "off", + bashElevated: { + enabled: false, + allowed: false, + defaultLevel: "off", + }, + timeoutMs: 1_000, + blockReplyBreak: "message_end", + }, + } as unknown as FollowupRun; + + return runReplyAgent({ + commandBody: "hello", + followupRun, + queueKey: "main", + resolvedQueue, + shouldSteer: false, + shouldFollowup: false, + isActive: false, + isStreaming: false, + typing, + sessionCtx, + sessionEntry, + sessionKey: params.sessionKey, + defaultModel: "anthropic/claude-opus-4-5", + resolvedVerboseLevel: "off", + isNewSession: false, + blockStreamingEnabled: false, + resolvedBlockStreamingBreak: "message_end", + shouldInjectGroupIntro: false, + typingMode: "instant", + }); +} + +describe("runReplyAgent response usage footer", () => { + beforeEach(() => { + runEmbeddedPiAgentMock.mockReset(); + runWithModelFallbackMock.mockReset(); + }); + + it("appends session key when responseUsage=full", async () => { + runEmbeddedPiAgentMock.mockResolvedValueOnce({ + payloads: [{ text: "ok" }], + meta: { + agentMeta: { + provider: "anthropic", + model: "claude", + usage: { input: 12, output: 3 }, + }, + }, + }); + runWithModelFallbackMock.mockImplementationOnce( + async ({ run }: { run: (provider: string, model: string) => Promise }) => ({ + result: await run("anthropic", "claude"), + provider: "anthropic", + model: "claude", + }), + ); + + const sessionKey = "agent:main:whatsapp:dm:+1000"; + const res = await createRun({ responseUsage: "full", sessionKey }); + const payload = Array.isArray(res) ? res[0] : res; + expect(String(payload?.text ?? "")).toContain("Usage:"); + expect(String(payload?.text ?? "")).toContain(`· session ${sessionKey}`); + }); + + it("does not append session key when responseUsage=tokens", async () => { + runEmbeddedPiAgentMock.mockResolvedValueOnce({ + payloads: [{ text: "ok" }], + meta: { + agentMeta: { + provider: "anthropic", + model: "claude", + usage: { input: 12, output: 3 }, + }, + }, + }); + runWithModelFallbackMock.mockImplementationOnce( + async ({ run }: { run: (provider: string, model: string) => Promise }) => ({ + result: await run("anthropic", "claude"), + provider: "anthropic", + model: "claude", + }), + ); + + const sessionKey = "agent:main:whatsapp:dm:+1000"; + const res = await createRun({ responseUsage: "tokens", sessionKey }); + const payload = Array.isArray(res) ? res[0] : res; + expect(String(payload?.text ?? "")).toContain("Usage:"); + expect(String(payload?.text ?? "")).not.toContain("· session "); + }); +});