diff --git a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.resets-corrupted-gemini-sessions-deletes-transcripts.test.ts b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.resets-corrupted-gemini-sessions-deletes-transcripts.test.ts index 9caaccf649..c115a59f0e 100644 --- a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.resets-corrupted-gemini-sessions-deletes-transcripts.test.ts +++ b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.resets-corrupted-gemini-sessions-deletes-transcripts.test.ts @@ -1,126 +1,18 @@ import fs from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; -import type { SessionEntry } from "../../config/sessions.js"; -import type { TypingMode } from "../../config/types.js"; -import type { TemplateContext } from "../templating.js"; -import type { GetReplyOptions } from "../types.js"; -import type { FollowupRun, QueueSettings } from "./queue.js"; +import { describe, expect, it } from "vitest"; import * as sessions from "../../config/sessions.js"; -import { createMockTypingController } from "./test-helpers.js"; - -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", () => ({ - 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 createMinimalRun(params?: { - opts?: GetReplyOptions; - resolvedVerboseLevel?: "off" | "on"; - sessionStore?: Record; - sessionEntry?: SessionEntry; - sessionKey?: string; - storePath?: string; - typingMode?: TypingMode; - blockStreamingEnabled?: boolean; -}) { - const typing = createMockTypingController(); - const opts = params?.opts; - const sessionCtx = { - Provider: "whatsapp", - MessageSid: "msg", - } as unknown as TemplateContext; - const resolvedQueue = { mode: "interrupt" } as unknown as QueueSettings; - const sessionKey = params?.sessionKey ?? "main"; - const followupRun = { - prompt: "hello", - summaryLine: "hello", - enqueuedAt: Date.now(), - run: { - sessionId: "session", - sessionKey, - messageProvider: "whatsapp", - sessionFile: "/tmp/session.jsonl", - workspaceDir: "/tmp", - config: {}, - skillsSnapshot: {}, - provider: "anthropic", - model: "claude", - thinkLevel: "low", - verboseLevel: params?.resolvedVerboseLevel ?? "off", - elevatedLevel: "off", - bashElevated: { - enabled: false, - allowed: false, - defaultLevel: "off", - }, - timeoutMs: 1_000, - blockReplyBreak: "message_end", - }, - } as unknown as FollowupRun; - - return { - typing, - opts, - run: () => - runReplyAgent({ - commandBody: "hello", - followupRun, - queueKey: "main", - resolvedQueue, - shouldSteer: false, - shouldFollowup: false, - isActive: false, - isStreaming: false, - opts, - typing, - sessionEntry: params?.sessionEntry, - sessionStore: params?.sessionStore, - sessionKey, - storePath: params?.storePath, - sessionCtx, - defaultModel: "anthropic/claude-opus-4-5", - resolvedVerboseLevel: params?.resolvedVerboseLevel ?? "off", - isNewSession: false, - blockStreamingEnabled: params?.blockStreamingEnabled ?? false, - resolvedBlockStreamingBreak: "message_end", - shouldInjectGroupIntro: false, - typingMode: params?.typingMode ?? "instant", - }), - }; -} +import { + createMinimalRun, + getRunEmbeddedPiAgentMock, + installRunReplyAgentTypingHeartbeatTestHooks, +} from "./agent-runner.heartbeat-typing.test-harness.js"; +const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); describe("runReplyAgent typing (heartbeat)", () => { + installRunReplyAgentTypingHeartbeatTestHooks(); + it("resets corrupted Gemini sessions and deletes transcripts", async () => { const prevStateDir = process.env.OPENCLAW_STATE_DIR; const stateDir = await fs.mkdtemp(path.join(tmpdir(), "openclaw-session-reset-")); diff --git a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.retries-after-compaction-failure-by-resetting-session.test.ts b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.retries-after-compaction-failure-by-resetting-session.test.ts index 7f63443dfa..65184e5798 100644 --- a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.retries-after-compaction-failure-by-resetting-session.test.ts +++ b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.retries-after-compaction-failure-by-resetting-session.test.ts @@ -1,129 +1,17 @@ import fs from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { SessionEntry } from "../../config/sessions.js"; -import type { TypingMode } from "../../config/types.js"; -import type { TemplateContext } from "../templating.js"; -import type { GetReplyOptions } from "../types.js"; -import type { FollowupRun, QueueSettings } from "./queue.js"; +import { describe, expect, it } from "vitest"; import * as sessions from "../../config/sessions.js"; -import { createMockTypingController } from "./test-helpers.js"; - -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", () => ({ - 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 createMinimalRun(params?: { - opts?: GetReplyOptions; - resolvedVerboseLevel?: "off" | "on"; - sessionStore?: Record; - sessionEntry?: SessionEntry; - sessionKey?: string; - storePath?: string; - typingMode?: TypingMode; - blockStreamingEnabled?: boolean; -}) { - const typing = createMockTypingController(); - const opts = params?.opts; - const sessionCtx = { - Provider: "whatsapp", - MessageSid: "msg", - } as unknown as TemplateContext; - const resolvedQueue = { mode: "interrupt" } as unknown as QueueSettings; - const sessionKey = params?.sessionKey ?? "main"; - const followupRun = { - prompt: "hello", - summaryLine: "hello", - enqueuedAt: Date.now(), - run: { - sessionId: "session", - sessionKey, - messageProvider: "whatsapp", - sessionFile: "/tmp/session.jsonl", - workspaceDir: "/tmp", - config: {}, - skillsSnapshot: {}, - provider: "anthropic", - model: "claude", - thinkLevel: "low", - verboseLevel: params?.resolvedVerboseLevel ?? "off", - elevatedLevel: "off", - bashElevated: { - enabled: false, - allowed: false, - defaultLevel: "off", - }, - timeoutMs: 1_000, - blockReplyBreak: "message_end", - }, - } as unknown as FollowupRun; - - return { - typing, - opts, - run: () => - runReplyAgent({ - commandBody: "hello", - followupRun, - queueKey: "main", - resolvedQueue, - shouldSteer: false, - shouldFollowup: false, - isActive: false, - isStreaming: false, - opts, - typing, - sessionEntry: params?.sessionEntry, - sessionStore: params?.sessionStore, - sessionKey, - storePath: params?.storePath, - sessionCtx, - defaultModel: "anthropic/claude-opus-4-5", - resolvedVerboseLevel: params?.resolvedVerboseLevel ?? "off", - isNewSession: false, - blockStreamingEnabled: params?.blockStreamingEnabled ?? false, - resolvedBlockStreamingBreak: "message_end", - shouldInjectGroupIntro: false, - typingMode: params?.typingMode ?? "instant", - }), - }; -} +import { + createMinimalRun, + getRunEmbeddedPiAgentMock, + installRunReplyAgentTypingHeartbeatTestHooks, +} from "./agent-runner.heartbeat-typing.test-harness.js"; +const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); describe("runReplyAgent typing (heartbeat)", () => { - beforeEach(() => { - runEmbeddedPiAgentMock.mockReset(); - }); + installRunReplyAgentTypingHeartbeatTestHooks(); it("retries after compaction failure by resetting the session", async () => { const prevStateDir = process.env.OPENCLAW_STATE_DIR; diff --git a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.signals-typing-block-replies.test.ts b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.signals-typing-block-replies.test.ts index 0082d13db6..10cd56699a 100644 --- a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.signals-typing-block-replies.test.ts +++ b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.signals-typing-block-replies.test.ts @@ -2,124 +2,16 @@ import fs from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; -import type { SessionEntry } from "../../config/sessions.js"; -import type { TypingMode } from "../../config/types.js"; -import type { TemplateContext } from "../templating.js"; -import type { GetReplyOptions } from "../types.js"; -import type { FollowupRun, QueueSettings } from "./queue.js"; -import { createMockTypingController } from "./test-helpers.js"; - -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", () => ({ - 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 createMinimalRun(params?: { - opts?: GetReplyOptions; - resolvedVerboseLevel?: "off" | "on"; - sessionStore?: Record; - sessionEntry?: SessionEntry; - sessionKey?: string; - storePath?: string; - typingMode?: TypingMode; - blockStreamingEnabled?: boolean; -}) { - const typing = createMockTypingController(); - const opts = params?.opts; - const sessionCtx = { - Provider: "whatsapp", - MessageSid: "msg", - } as unknown as TemplateContext; - const resolvedQueue = { mode: "interrupt" } as unknown as QueueSettings; - const sessionKey = params?.sessionKey ?? "main"; - const followupRun = { - prompt: "hello", - summaryLine: "hello", - enqueuedAt: Date.now(), - run: { - sessionId: "session", - sessionKey, - messageProvider: "whatsapp", - sessionFile: "/tmp/session.jsonl", - workspaceDir: "/tmp", - config: {}, - skillsSnapshot: {}, - provider: "anthropic", - model: "claude", - thinkLevel: "low", - verboseLevel: params?.resolvedVerboseLevel ?? "off", - elevatedLevel: "off", - bashElevated: { - enabled: false, - allowed: false, - defaultLevel: "off", - }, - timeoutMs: 1_000, - blockReplyBreak: "message_end", - }, - } as unknown as FollowupRun; - - return { - typing, - opts, - run: () => - runReplyAgent({ - commandBody: "hello", - followupRun, - queueKey: "main", - resolvedQueue, - shouldSteer: false, - shouldFollowup: false, - isActive: false, - isStreaming: false, - opts, - typing, - sessionEntry: params?.sessionEntry, - sessionStore: params?.sessionStore, - sessionKey, - storePath: params?.storePath, - sessionCtx, - defaultModel: "anthropic/claude-opus-4-5", - resolvedVerboseLevel: params?.resolvedVerboseLevel ?? "off", - isNewSession: false, - blockStreamingEnabled: params?.blockStreamingEnabled ?? false, - resolvedBlockStreamingBreak: "message_end", - shouldInjectGroupIntro: false, - typingMode: params?.typingMode ?? "instant", - }), - }; -} +import { + createMinimalRun, + getRunEmbeddedPiAgentMock, + installRunReplyAgentTypingHeartbeatTestHooks, +} from "./agent-runner.heartbeat-typing.test-harness.js"; +const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); describe("runReplyAgent typing (heartbeat)", () => { + installRunReplyAgentTypingHeartbeatTestHooks(); + it("signals typing on block replies", async () => { const onBlockReply = vi.fn(); runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => { diff --git a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.signals-typing-normal-runs.test.ts b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.signals-typing-normal-runs.test.ts index 31d3249bbf..575456feec 100644 --- a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.signals-typing-normal-runs.test.ts +++ b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.signals-typing-normal-runs.test.ts @@ -1,122 +1,15 @@ import { describe, expect, it, vi } from "vitest"; -import type { SessionEntry } from "../../config/sessions.js"; -import type { TypingMode } from "../../config/types.js"; -import type { TemplateContext } from "../templating.js"; -import type { GetReplyOptions } from "../types.js"; -import type { FollowupRun, QueueSettings } from "./queue.js"; -import { createMockTypingController } from "./test-helpers.js"; +import { + createMinimalRun, + getRunEmbeddedPiAgentMock, + installRunReplyAgentTypingHeartbeatTestHooks, +} from "./agent-runner.heartbeat-typing.test-harness.js"; -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", () => ({ - 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 createMinimalRun(params?: { - opts?: GetReplyOptions; - resolvedVerboseLevel?: "off" | "on"; - sessionStore?: Record; - sessionEntry?: SessionEntry; - sessionKey?: string; - storePath?: string; - typingMode?: TypingMode; - blockStreamingEnabled?: boolean; -}) { - const typing = createMockTypingController(); - const opts = params?.opts; - const sessionCtx = { - Provider: "whatsapp", - MessageSid: "msg", - } as unknown as TemplateContext; - const resolvedQueue = { mode: "interrupt" } as unknown as QueueSettings; - const sessionKey = params?.sessionKey ?? "main"; - const followupRun = { - prompt: "hello", - summaryLine: "hello", - enqueuedAt: Date.now(), - run: { - sessionId: "session", - sessionKey, - messageProvider: "whatsapp", - sessionFile: "/tmp/session.jsonl", - workspaceDir: "/tmp", - config: {}, - skillsSnapshot: {}, - provider: "anthropic", - model: "claude", - thinkLevel: "low", - verboseLevel: params?.resolvedVerboseLevel ?? "off", - elevatedLevel: "off", - bashElevated: { - enabled: false, - allowed: false, - defaultLevel: "off", - }, - timeoutMs: 1_000, - blockReplyBreak: "message_end", - }, - } as unknown as FollowupRun; - - return { - typing, - opts, - run: () => - runReplyAgent({ - commandBody: "hello", - followupRun, - queueKey: "main", - resolvedQueue, - shouldSteer: false, - shouldFollowup: false, - isActive: false, - isStreaming: false, - opts, - typing, - sessionEntry: params?.sessionEntry, - sessionStore: params?.sessionStore, - sessionKey, - storePath: params?.storePath, - sessionCtx, - defaultModel: "anthropic/claude-opus-4-5", - resolvedVerboseLevel: params?.resolvedVerboseLevel ?? "off", - isNewSession: false, - blockStreamingEnabled: params?.blockStreamingEnabled ?? false, - resolvedBlockStreamingBreak: "message_end", - shouldInjectGroupIntro: false, - typingMode: params?.typingMode ?? "instant", - }), - }; -} +const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); describe("runReplyAgent typing (heartbeat)", () => { + installRunReplyAgentTypingHeartbeatTestHooks(); + it("signals typing for normal runs", async () => { const onPartialReply = vi.fn(); runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedPiAgentParams) => { diff --git a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.still-replies-even-if-session-reset-fails.test.ts b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.still-replies-even-if-session-reset-fails.test.ts index 34a2ab73e1..8082db09d5 100644 --- a/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.still-replies-even-if-session-reset-fails.test.ts +++ b/src/auto-reply/reply/agent-runner.heartbeat-typing.runreplyagent-typing-heartbeat.still-replies-even-if-session-reset-fails.test.ts @@ -2,125 +2,17 @@ import fs from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; -import type { SessionEntry } from "../../config/sessions.js"; -import type { TypingMode } from "../../config/types.js"; -import type { TemplateContext } from "../templating.js"; -import type { GetReplyOptions } from "../types.js"; -import type { FollowupRun, QueueSettings } from "./queue.js"; import * as sessions from "../../config/sessions.js"; -import { createMockTypingController } from "./test-helpers.js"; - -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", () => ({ - 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 createMinimalRun(params?: { - opts?: GetReplyOptions; - resolvedVerboseLevel?: "off" | "on"; - sessionStore?: Record; - sessionEntry?: SessionEntry; - sessionKey?: string; - storePath?: string; - typingMode?: TypingMode; - blockStreamingEnabled?: boolean; -}) { - const typing = createMockTypingController(); - const opts = params?.opts; - const sessionCtx = { - Provider: "whatsapp", - MessageSid: "msg", - } as unknown as TemplateContext; - const resolvedQueue = { mode: "interrupt" } as unknown as QueueSettings; - const sessionKey = params?.sessionKey ?? "main"; - const followupRun = { - prompt: "hello", - summaryLine: "hello", - enqueuedAt: Date.now(), - run: { - sessionId: "session", - sessionKey, - messageProvider: "whatsapp", - sessionFile: "/tmp/session.jsonl", - workspaceDir: "/tmp", - config: {}, - skillsSnapshot: {}, - provider: "anthropic", - model: "claude", - thinkLevel: "low", - verboseLevel: params?.resolvedVerboseLevel ?? "off", - elevatedLevel: "off", - bashElevated: { - enabled: false, - allowed: false, - defaultLevel: "off", - }, - timeoutMs: 1_000, - blockReplyBreak: "message_end", - }, - } as unknown as FollowupRun; - - return { - typing, - opts, - run: () => - runReplyAgent({ - commandBody: "hello", - followupRun, - queueKey: "main", - resolvedQueue, - shouldSteer: false, - shouldFollowup: false, - isActive: false, - isStreaming: false, - opts, - typing, - sessionEntry: params?.sessionEntry, - sessionStore: params?.sessionStore, - sessionKey, - storePath: params?.storePath, - sessionCtx, - defaultModel: "anthropic/claude-opus-4-5", - resolvedVerboseLevel: params?.resolvedVerboseLevel ?? "off", - isNewSession: false, - blockStreamingEnabled: params?.blockStreamingEnabled ?? false, - resolvedBlockStreamingBreak: "message_end", - shouldInjectGroupIntro: false, - typingMode: params?.typingMode ?? "instant", - }), - }; -} +import { + createMinimalRun, + getRunEmbeddedPiAgentMock, + installRunReplyAgentTypingHeartbeatTestHooks, +} from "./agent-runner.heartbeat-typing.test-harness.js"; +const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); describe("runReplyAgent typing (heartbeat)", () => { + installRunReplyAgentTypingHeartbeatTestHooks(); + it("still replies even if session reset fails to persist", async () => { const prevStateDir = process.env.OPENCLAW_STATE_DIR; const stateDir = await fs.mkdtemp(path.join(tmpdir(), "openclaw-session-reset-fail-")); diff --git a/src/auto-reply/reply/agent-runner.heartbeat-typing.test-harness.ts b/src/auto-reply/reply/agent-runner.heartbeat-typing.test-harness.ts new file mode 100644 index 0000000000..64dacf3a7b --- /dev/null +++ b/src/auto-reply/reply/agent-runner.heartbeat-typing.test-harness.ts @@ -0,0 +1,129 @@ +import { beforeEach, vi } from "vitest"; +import type { SessionEntry } from "../../config/sessions.js"; +import type { TypingMode } from "../../config/types.js"; +import type { TemplateContext } from "../templating.js"; +import type { GetReplyOptions } from "../types.js"; +import type { FollowupRun, QueueSettings } from "./queue.js"; +import { createMockTypingController } from "./test-helpers.js"; + +const state = vi.hoisted(() => ({ + runEmbeddedPiAgentMock: vi.fn(), +})); + +export function getRunEmbeddedPiAgentMock() { + return state.runEmbeddedPiAgentMock; +} + +export function installRunReplyAgentTypingHeartbeatTestHooks() { + beforeEach(() => { + state.runEmbeddedPiAgentMock.mockReset(); + }); +} + +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", () => ({ + queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), + runEmbeddedPiAgent: (params: unknown) => state.runEmbeddedPiAgentMock(params), +})); + +vi.mock("./queue.js", async () => { + const actual = await vi.importActual("./queue.js"); + return { + ...actual, + enqueueFollowupRun: vi.fn(), + scheduleFollowupDrain: vi.fn(), + }; +}); + +export function createMinimalRun(params?: { + opts?: GetReplyOptions; + resolvedVerboseLevel?: "off" | "on"; + sessionStore?: Record; + sessionEntry?: SessionEntry; + sessionKey?: string; + storePath?: string; + typingMode?: TypingMode; + blockStreamingEnabled?: boolean; +}) { + const typing = createMockTypingController(); + const opts = params?.opts; + const sessionCtx = { + Provider: "whatsapp", + MessageSid: "msg", + } as unknown as TemplateContext; + const resolvedQueue = { mode: "interrupt" } as unknown as QueueSettings; + const sessionKey = params?.sessionKey ?? "main"; + const followupRun = { + prompt: "hello", + summaryLine: "hello", + enqueuedAt: Date.now(), + run: { + sessionId: "session", + sessionKey, + messageProvider: "whatsapp", + sessionFile: "/tmp/session.jsonl", + workspaceDir: "/tmp", + config: {}, + skillsSnapshot: {}, + provider: "anthropic", + model: "claude", + thinkLevel: "low", + verboseLevel: params?.resolvedVerboseLevel ?? "off", + elevatedLevel: "off", + bashElevated: { + enabled: false, + allowed: false, + defaultLevel: "off", + }, + timeoutMs: 1_000, + blockReplyBreak: "message_end", + }, + } as unknown as FollowupRun; + + return { + typing, + opts, + run: async () => { + const { runReplyAgent } = await import("./agent-runner.js"); + return runReplyAgent({ + commandBody: "hello", + followupRun, + queueKey: "main", + resolvedQueue, + shouldSteer: false, + shouldFollowup: false, + isActive: false, + isStreaming: false, + opts, + typing, + sessionEntry: params?.sessionEntry, + sessionStore: params?.sessionStore, + sessionKey, + storePath: params?.storePath, + sessionCtx, + defaultModel: "anthropic/claude-opus-4-5", + resolvedVerboseLevel: params?.resolvedVerboseLevel ?? "off", + isNewSession: false, + blockStreamingEnabled: params?.blockStreamingEnabled ?? false, + resolvedBlockStreamingBreak: "message_end", + shouldInjectGroupIntro: false, + typingMode: params?.typingMode ?? "instant", + }); + }, + }; +}