import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, vi, type Mock } from "vitest"; import { withFastReplyConfig } from "./reply/get-reply-fast-path.js"; export type ReplyRuntimeMocks = { runEmbeddedPiAgent: Mock; loadModelCatalog: Mock; webAuthExists: Mock; getWebAuthAgeMs: Mock; readWebSelfId: Mock; }; const replyRuntimeMockState = vi.hoisted(() => ({ mocks: { runEmbeddedPiAgent: vi.fn(), loadModelCatalog: vi.fn(), webAuthExists: vi.fn().mockResolvedValue(true), getWebAuthAgeMs: vi.fn().mockReturnValue(120_000), readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }), } as ReplyRuntimeMocks, })); vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: (...args: unknown[]) => replyRuntimeMockState.mocks.runEmbeddedPiAgent(...args), 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.runtime.js", () => ({ loadModelCatalog: (...args: unknown[]) => replyRuntimeMockState.mocks.loadModelCatalog(...args), })); vi.mock("../agents/auth-profiles/session-override.js", () => ({ clearSessionAuthProfileOverride: vi.fn(), resolveSessionAuthProfileOverride: vi.fn().mockResolvedValue(undefined), })); vi.mock("../commands-registry.runtime.js", () => ({ listChatCommands: () => [], })); vi.mock("../skill-commands.runtime.js", () => ({ listSkillCommandsForWorkspace: () => [], })); vi.mock("../plugins/runtime/runtime-web-channel-plugin.js", () => ({ webAuthExists: (...args: unknown[]) => replyRuntimeMockState.mocks.webAuthExists(...args), getWebAuthAgeMs: (...args: unknown[]) => replyRuntimeMockState.mocks.getWebAuthAgeMs(...args), readWebSelfId: (...args: unknown[]) => replyRuntimeMockState.mocks.readWebSelfId(...args), })); vi.mock("../agents/pi-embedded.runtime.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), resolveActiveEmbeddedRunSessionId: vi.fn().mockReturnValue(undefined), resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, waitForEmbeddedPiRunEnd: vi.fn(async () => undefined), })); vi.mock("./reply/agent-runner.runtime.js", () => ({ runReplyAgent: async (params: { commandBody: string; followupRun: { prompt: string; run: { agentDir: string; agentId: string; config: unknown; execOverrides?: unknown; inputProvenance?: unknown; messageProvider?: string; model: string; ownerNumbers?: string[]; provider: string; reasoningLevel?: unknown; senderIsOwner?: boolean; sessionFile: string; sessionId: string; sessionKey: string; skillsSnapshot?: unknown; thinkLevel?: unknown; timeoutMs?: number; verboseLevel?: unknown; workspaceDir: string; bashElevated?: unknown; }; }; }) => { const result = await replyRuntimeMockState.mocks.runEmbeddedPiAgent({ prompt: params.followupRun.prompt || params.commandBody, agentDir: params.followupRun.run.agentDir, agentId: params.followupRun.run.agentId, config: params.followupRun.run.config, execOverrides: params.followupRun.run.execOverrides, inputProvenance: params.followupRun.run.inputProvenance, messageProvider: params.followupRun.run.messageProvider, model: params.followupRun.run.model, ownerNumbers: params.followupRun.run.ownerNumbers, provider: params.followupRun.run.provider, reasoningLevel: params.followupRun.run.reasoningLevel, senderIsOwner: params.followupRun.run.senderIsOwner, sessionFile: params.followupRun.run.sessionFile, sessionId: params.followupRun.run.sessionId, sessionKey: params.followupRun.run.sessionKey, skillsSnapshot: params.followupRun.run.skillsSnapshot, thinkLevel: params.followupRun.run.thinkLevel, timeoutMs: params.followupRun.run.timeoutMs, verboseLevel: params.followupRun.run.verboseLevel, workspaceDir: params.followupRun.run.workspaceDir, bashElevated: params.followupRun.run.bashElevated, }); return result?.payloads?.[0]; }, })); type HomeEnvSnapshot = { HOME: string | undefined; USERPROFILE: string | undefined; HOMEDRIVE: string | undefined; HOMEPATH: string | undefined; OPENCLAW_STATE_DIR: string | undefined; OPENCLAW_AGENT_DIR: string | undefined; PI_CODING_AGENT_DIR: string | undefined; }; function snapshotHomeEnv(): HomeEnvSnapshot { return { HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE, HOMEDRIVE: process.env.HOMEDRIVE, HOMEPATH: process.env.HOMEPATH, OPENCLAW_STATE_DIR: process.env.OPENCLAW_STATE_DIR, OPENCLAW_AGENT_DIR: process.env.OPENCLAW_AGENT_DIR, PI_CODING_AGENT_DIR: process.env.PI_CODING_AGENT_DIR, }; } function restoreHomeEnv(snapshot: HomeEnvSnapshot) { for (const [key, value] of Object.entries(snapshot)) { if (value === undefined) { delete process.env[key]; } else { process.env[key] = value; } } } export function createTempHomeHarness(options: { prefix: string; beforeEachCase?: () => void }) { let fixtureRoot = ""; let caseId = 0; beforeAll(async () => { fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), options.prefix)); }); afterAll(async () => { if (!fixtureRoot) { return; } await fs.rm(fixtureRoot, { recursive: true, force: true }); }); async function withTempHome(fn: (home: string) => Promise): Promise { const home = path.join(fixtureRoot, `case-${++caseId}`); await fs.mkdir(path.join(home, ".openclaw", "agents", "main", "sessions"), { recursive: true }); const envSnapshot = snapshotHomeEnv(); process.env.HOME = home; process.env.USERPROFILE = home; process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw"); process.env.OPENCLAW_AGENT_DIR = path.join(home, ".openclaw", "agent"); process.env.PI_CODING_AGENT_DIR = path.join(home, ".openclaw", "agent"); if (process.platform === "win32") { const match = home.match(/^([A-Za-z]:)(.*)$/); if (match) { process.env.HOMEDRIVE = match[1]; process.env.HOMEPATH = match[2] || "\\"; } } try { options.beforeEachCase?.(); return await fn(home); } finally { restoreHomeEnv(envSnapshot); } } return { withTempHome }; } export function makeReplyConfig(home: string) { return withFastReplyConfig({ agents: { defaults: { model: "anthropic/claude-opus-4-6", workspace: path.join(home, "openclaw"), }, }, channels: { whatsapp: { allowFrom: ["*"], }, }, session: { store: path.join(home, "sessions.json") }, }); } export function createReplyRuntimeMocks(): ReplyRuntimeMocks { return { runEmbeddedPiAgent: vi.fn(), loadModelCatalog: vi.fn(), webAuthExists: vi.fn().mockResolvedValue(true), getWebAuthAgeMs: vi.fn().mockReturnValue(120_000), readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }), }; } export function installReplyRuntimeMocks(mocks: ReplyRuntimeMocks) { replyRuntimeMockState.mocks = mocks; } export function resetReplyRuntimeMocks(mocks: ReplyRuntimeMocks) { mocks.runEmbeddedPiAgent.mockClear(); mocks.loadModelCatalog.mockClear(); mocks.loadModelCatalog.mockResolvedValue([ { id: "claude-opus-4-6", name: "Opus 4.5", provider: "anthropic" }, ]); } export function makeEmbeddedTextResult(text: string) { return { payloads: [{ text }], meta: { durationMs: 5, agentMeta: { sessionId: "s", provider: "p", model: "m" }, }, }; }