import fs from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; const runEmbeddedPiAgentMock = vi.fn(); vi.mock("../agents/model-fallback.js", () => ({ runWithModelFallback: async ({ provider, model, run, }: { provider: string; model: string; run: (provider: string, model: string) => Promise; }) => ({ result: await run(provider, model), provider, model, }), })); vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: (params: unknown) => runEmbeddedPiAgentMock(params), queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), })); const webMocks = vi.hoisted(() => ({ webAuthExists: vi.fn().mockResolvedValue(true), getWebAuthAgeMs: vi.fn().mockReturnValue(120_000), readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }), })); vi.mock("../web/session.js", () => webMocks); import { getReplyFromConfig } from "./reply.js"; async function withTempHome(fn: (home: string) => Promise): Promise { const base = await fs.mkdtemp(join(tmpdir(), "clawdbot-typing-")); const previousHome = process.env.HOME; process.env.HOME = base; try { runEmbeddedPiAgentMock.mockClear(); return await fn(base); } finally { process.env.HOME = previousHome; await fs.rm(base, { recursive: true, force: true }); } } function makeCfg(home: string) { return { agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: join(home, "clawd"), }, }, whatsapp: { allowFrom: ["*"], }, session: { store: join(home, "sessions.json") }, }; } afterEach(() => { vi.restoreAllMocks(); }); describe("getReplyFromConfig typing (heartbeat)", () => { it("starts typing for normal runs", async () => { await withTempHome(async (home) => { runEmbeddedPiAgentMock.mockResolvedValueOnce({ payloads: [{ text: "ok" }], meta: {}, }); const onReplyStart = vi.fn(); await getReplyFromConfig( { Body: "hi", From: "+1000", To: "+2000", Provider: "whatsapp" }, { onReplyStart, isHeartbeat: false }, makeCfg(home), ); expect(onReplyStart).toHaveBeenCalled(); }); }); it("does not start typing for heartbeat runs", async () => { await withTempHome(async (home) => { runEmbeddedPiAgentMock.mockResolvedValueOnce({ payloads: [{ text: "ok" }], meta: {}, }); const onReplyStart = vi.fn(); await getReplyFromConfig( { Body: "hi", From: "+1000", To: "+2000", Provider: "whatsapp" }, { onReplyStart, isHeartbeat: true }, makeCfg(home), ); expect(onReplyStart).not.toHaveBeenCalled(); }); }); });