diff --git a/src/auto-reply/reply.heartbeat-typing.test.ts b/src/auto-reply/reply.heartbeat-typing.test.ts index d0ef5d9f6..a6c72429a 100644 --- a/src/auto-reply/reply.heartbeat-typing.test.ts +++ b/src/auto-reply/reply.heartbeat-typing.test.ts @@ -1,7 +1,5 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import { join } from "node:path"; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createTempHomeHarness, makeReplyConfig } from "./reply.test-harness.js"; const runEmbeddedPiAgentMock = vi.fn(); @@ -40,95 +38,11 @@ vi.mock("../web/session.js", () => webMocks); import { getReplyFromConfig } from "./reply.js"; -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; - } - } -} - -let fixtureRoot = ""; -let caseId = 0; - -beforeAll(async () => { - fixtureRoot = await fs.mkdtemp(join(os.tmpdir(), "openclaw-typing-")); +const { withTempHome } = createTempHomeHarness({ + prefix: "openclaw-typing-", + beforeEachCase: () => runEmbeddedPiAgentMock.mockClear(), }); -afterAll(async () => { - if (!fixtureRoot) { - return; - } - await fs.rm(fixtureRoot, { recursive: true, force: true }); -}); - -async function withTempHome(fn: (home: string) => Promise): Promise { - const home = join(fixtureRoot, `case-${++caseId}`); - await fs.mkdir(join(home, ".openclaw", "agents", "main", "sessions"), { recursive: true }); - const envSnapshot = snapshotHomeEnv(); - process.env.HOME = home; - process.env.USERPROFILE = home; - process.env.OPENCLAW_STATE_DIR = join(home, ".openclaw"); - process.env.OPENCLAW_AGENT_DIR = join(home, ".openclaw", "agent"); - process.env.PI_CODING_AGENT_DIR = 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 { - runEmbeddedPiAgentMock.mockClear(); - return await fn(home); - } finally { - restoreHomeEnv(envSnapshot); - } -} - -function makeCfg(home: string) { - return { - agents: { - defaults: { - model: "anthropic/claude-opus-4-5", - workspace: join(home, "openclaw"), - }, - }, - channels: { - whatsapp: { - allowFrom: ["*"], - }, - }, - session: { store: join(home, "sessions.json") }, - }; -} - afterEach(() => { vi.restoreAllMocks(); }); @@ -149,7 +63,7 @@ describe("getReplyFromConfig typing (heartbeat)", () => { await getReplyFromConfig( { Body: "hi", From: "+1000", To: "+2000", Provider: "whatsapp" }, { onReplyStart, isHeartbeat: false }, - makeCfg(home), + makeReplyConfig(home), ); expect(onReplyStart).toHaveBeenCalled(); @@ -167,7 +81,7 @@ describe("getReplyFromConfig typing (heartbeat)", () => { await getReplyFromConfig( { Body: "hi", From: "+1000", To: "+2000", Provider: "whatsapp" }, { onReplyStart, isHeartbeat: true }, - makeCfg(home), + makeReplyConfig(home), ); expect(onReplyStart).not.toHaveBeenCalled(); diff --git a/src/auto-reply/reply.raw-body.test.ts b/src/auto-reply/reply.raw-body.test.ts index 3afabf86e..5b52e8029 100644 --- a/src/auto-reply/reply.raw-body.test.ts +++ b/src/auto-reply/reply.raw-body.test.ts @@ -1,7 +1,5 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createTempHomeHarness, makeReplyConfig } from "./reply.test-harness.js"; const agentMocks = vi.hoisted(() => ({ runEmbeddedPiAgent: vi.fn(), @@ -32,75 +30,9 @@ vi.mock("../web/session.js", () => ({ import { getReplyFromConfig } from "./reply.js"; -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; - } - } -} - -let fixtureRoot = ""; -let caseId = 0; - -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 { - return await fn(home); - } finally { - restoreHomeEnv(envSnapshot); - } -} +const { withTempHome } = createTempHomeHarness({ prefix: "openclaw-rawbody-" }); describe("RawBody directive parsing", () => { - beforeAll(async () => { - fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-rawbody-")); - }); - - afterAll(async () => { - await fs.rm(fixtureRoot, { recursive: true, force: true }); - }); - beforeEach(() => { vi.stubEnv("OPENCLAW_TEST_FAST", "1"); agentMocks.runEmbeddedPiAgent.mockReset(); @@ -138,20 +70,7 @@ describe("RawBody directive parsing", () => { CommandAuthorized: true, }; - const res = await getReplyFromConfig( - groupMessageCtx, - {}, - { - agents: { - defaults: { - model: "anthropic/claude-opus-4-5", - workspace: path.join(home, "openclaw"), - }, - }, - channels: { whatsapp: { allowFrom: ["*"] } }, - session: { store: path.join(home, "sessions.json") }, - }, - ); + const res = await getReplyFromConfig(groupMessageCtx, {}, makeReplyConfig(home)); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("ok"); diff --git a/src/auto-reply/reply.test-harness.ts b/src/auto-reply/reply.test-harness.ts new file mode 100644 index 000000000..a75862836 --- /dev/null +++ b/src/auto-reply/reply.test-harness.ts @@ -0,0 +1,97 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterAll, beforeAll } from "vitest"; + +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 { + agents: { + defaults: { + model: "anthropic/claude-opus-4-5", + workspace: path.join(home, "openclaw"), + }, + }, + channels: { + whatsapp: { + allowFrom: ["*"], + }, + }, + session: { store: path.join(home, "sessions.json") }, + }; +}