diff --git a/src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.test.ts b/src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.test.ts index b82dc1f915b..06daf55bb45 100644 --- a/src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.test.ts +++ b/src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.test.ts @@ -1,7 +1,8 @@ import "./isolated-agent.mocks.js"; import fs from "node:fs/promises"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; +import os from "node:os"; +import path from "node:path"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { runSubagentAnnounceFlow } from "../agents/subagent-announce.js"; import type { CliDeps } from "../cli/deps.js"; import { @@ -14,8 +15,67 @@ import { runCronIsolatedAgentTurn } from "./isolated-agent.js"; import { makeCfg, makeJob, writeSessionStore } from "./isolated-agent.test-harness.js"; import { setupIsolatedAgentTurnMocks } from "./isolated-agent.test-setup.js"; +type HomeEnvSnapshot = { + HOME: string | undefined; + USERPROFILE: string | undefined; + HOMEDRIVE: string | undefined; + HOMEPATH: string | undefined; + OPENCLAW_HOME: string | undefined; + OPENCLAW_STATE_DIR: string | undefined; +}; + +const TELEGRAM_TARGET = { mode: "announce", channel: "telegram", to: "123" } as const; +let suiteTempHomeRoot = ""; +let suiteTempHomeCaseId = 0; + +function snapshotHomeEnv(): HomeEnvSnapshot { + return { + HOME: process.env.HOME, + USERPROFILE: process.env.USERPROFILE, + HOMEDRIVE: process.env.HOMEDRIVE, + HOMEPATH: process.env.HOMEPATH, + OPENCLAW_HOME: process.env.OPENCLAW_HOME, + OPENCLAW_STATE_DIR: process.env.OPENCLAW_STATE_DIR, + }; +} + +function restoreHomeEnv(snapshot: HomeEnvSnapshot) { + const restoreValue = (key: keyof HomeEnvSnapshot) => { + const value = snapshot[key]; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + }; + restoreValue("HOME"); + restoreValue("USERPROFILE"); + restoreValue("HOMEDRIVE"); + restoreValue("HOMEPATH"); + restoreValue("OPENCLAW_HOME"); + restoreValue("OPENCLAW_STATE_DIR"); +} + async function withTempHome(fn: (home: string) => Promise): Promise { - return withTempHomeBase(fn, { prefix: "openclaw-cron-delivery-suite-" }); + const home = path.join(suiteTempHomeRoot, `case-${suiteTempHomeCaseId++}`); + await fs.mkdir(path.join(home, ".openclaw", "agents", "main", "sessions"), { recursive: true }); + const snapshot = snapshotHomeEnv(); + process.env.HOME = home; + process.env.USERPROFILE = home; + delete process.env.OPENCLAW_HOME; + process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw"); + if (process.platform === "win32") { + const parsed = path.parse(home); + if (parsed.root) { + process.env.HOMEDRIVE = parsed.root.replace(/[\\/]+$/, ""); + process.env.HOMEPATH = home.slice(process.env.HOMEDRIVE.length) || "\\"; + } + } + try { + return await fn(home); + } finally { + restoreHomeEnv(snapshot); + } } async function runExplicitTelegramAnnounceTurn(params: { @@ -25,7 +85,7 @@ async function runExplicitTelegramAnnounceTurn(params: { }): Promise>> { return runTelegramAnnounceTurn({ ...params, - delivery: { mode: "announce", channel: "telegram", to: "123" }, + delivery: TELEGRAM_TARGET, }); } @@ -77,9 +137,7 @@ async function expectStructuredTelegramFailure(params: { storePath, deps, delivery: { - mode: "announce", - channel: "telegram", - to: "123", + ...TELEGRAM_TARGET, ...(params.bestEffort ? { bestEffort: true } : {}), }, }); @@ -168,6 +226,19 @@ async function assertExplicitTelegramTargetAnnounce(params: { } describe("runCronIsolatedAgentTurn", () => { + beforeAll(async () => { + suiteTempHomeRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cron-delivery-suite-")); + }); + + afterAll(async () => { + if (!suiteTempHomeRoot) { + return; + } + await fs.rm(suiteTempHomeRoot, { recursive: true, force: true }); + suiteTempHomeRoot = ""; + suiteTempHomeCaseId = 0; + }); + beforeEach(() => { setupIsolatedAgentTurnMocks(); }); diff --git a/src/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.ts b/src/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.ts index 579d70dfc75..922dd6f7643 100644 --- a/src/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.ts +++ b/src/cron/isolated-agent.uses-last-non-empty-agent-text-as.test.ts @@ -1,8 +1,8 @@ import "./isolated-agent.mocks.js"; import fs from "node:fs/promises"; +import os from "node:os"; import path from "node:path"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { loadModelCatalog } from "../agents/model-catalog.js"; import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import type { CliDeps } from "../cli/deps.js"; @@ -15,8 +15,66 @@ import { } from "./isolated-agent.test-harness.js"; import type { CronJob } from "./types.js"; +type HomeEnvSnapshot = { + HOME: string | undefined; + USERPROFILE: string | undefined; + HOMEDRIVE: string | undefined; + HOMEPATH: string | undefined; + OPENCLAW_HOME: string | undefined; + OPENCLAW_STATE_DIR: string | undefined; +}; + +let suiteTempHomeRoot = ""; +let suiteTempHomeCaseId = 0; + +function snapshotHomeEnv(): HomeEnvSnapshot { + return { + HOME: process.env.HOME, + USERPROFILE: process.env.USERPROFILE, + HOMEDRIVE: process.env.HOMEDRIVE, + HOMEPATH: process.env.HOMEPATH, + OPENCLAW_HOME: process.env.OPENCLAW_HOME, + OPENCLAW_STATE_DIR: process.env.OPENCLAW_STATE_DIR, + }; +} + +function restoreHomeEnv(snapshot: HomeEnvSnapshot) { + const restoreValue = (key: keyof HomeEnvSnapshot) => { + const value = snapshot[key]; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + }; + restoreValue("HOME"); + restoreValue("USERPROFILE"); + restoreValue("HOMEDRIVE"); + restoreValue("HOMEPATH"); + restoreValue("OPENCLAW_HOME"); + restoreValue("OPENCLAW_STATE_DIR"); +} + async function withTempHome(fn: (home: string) => Promise): Promise { - return withTempHomeBase(fn, { prefix: "openclaw-cron-turn-suite-" }); + const home = path.join(suiteTempHomeRoot, `case-${suiteTempHomeCaseId++}`); + await fs.mkdir(path.join(home, ".openclaw", "agents", "main", "sessions"), { recursive: true }); + const snapshot = snapshotHomeEnv(); + process.env.HOME = home; + process.env.USERPROFILE = home; + delete process.env.OPENCLAW_HOME; + process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw"); + if (process.platform === "win32") { + const parsed = path.parse(home); + if (parsed.root) { + process.env.HOMEDRIVE = parsed.root.replace(/[\\/]+$/, ""); + process.env.HOMEPATH = home.slice(process.env.HOMEDRIVE.length) || "\\"; + } + } + try { + return await fn(home); + } finally { + restoreHomeEnv(snapshot); + } } function makeDeps(): CliDeps { @@ -166,6 +224,19 @@ async function runStoredOverrideAndExpectModel(params: { } describe("runCronIsolatedAgentTurn", () => { + beforeAll(async () => { + suiteTempHomeRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cron-turn-suite-")); + }); + + afterAll(async () => { + if (!suiteTempHomeRoot) { + return; + } + await fs.rm(suiteTempHomeRoot, { recursive: true, force: true }); + suiteTempHomeRoot = ""; + suiteTempHomeCaseId = 0; + }); + beforeEach(() => { vi.mocked(runEmbeddedPiAgent).mockClear(); vi.mocked(loadModelCatalog).mockResolvedValue([]); diff --git a/src/memory/index.test.ts b/src/memory/index.test.ts index 4da434c55de..43ebcca58c2 100644 --- a/src/memory/index.test.ts +++ b/src/memory/index.test.ts @@ -38,6 +38,26 @@ describe("memory index", () => { let indexVectorPath = ""; let indexMainPath = ""; let indexExtraPath = ""; + let indexStatusPath = ""; + let indexSourceChangePath = ""; + let indexModelPath = ""; + let sourceChangeStateDir = ""; + const sourceChangeSessionLogLines = [ + JSON.stringify({ + type: "message", + message: { + role: "user", + content: [{ type: "text", text: "session change test user line" }], + }, + }), + JSON.stringify({ + type: "message", + message: { + role: "assistant", + content: [{ type: "text", text: "session change test assistant line" }], + }, + }), + ].join("\n"); // Perf: keep managers open across tests, but only reset the one a test uses. const managersByStorePath = new Map(); @@ -51,6 +71,10 @@ describe("memory index", () => { indexMainPath = path.join(workspaceDir, "index-main.sqlite"); indexVectorPath = path.join(workspaceDir, "index-vector.sqlite"); indexExtraPath = path.join(workspaceDir, "index-extra.sqlite"); + indexStatusPath = path.join(workspaceDir, "index-status.sqlite"); + indexSourceChangePath = path.join(workspaceDir, "index-source-change.sqlite"); + indexModelPath = path.join(workspaceDir, "index-model-change.sqlite"); + sourceChangeStateDir = path.join(fixtureRoot, "state-source-change"); await fs.mkdir(memoryDir, { recursive: true }); await fs.writeFile( @@ -194,7 +218,6 @@ describe("memory index", () => { }); it("keeps dirty false in status-only manager after prior indexing", async () => { - const indexStatusPath = path.join(workspaceDir, `index-status-${Date.now()}.sqlite`); const cfg = createCfg({ storePath: indexStatusPath }); const first = await getMemorySearchManager({ cfg, agentId: "main" }); @@ -214,31 +237,13 @@ describe("memory index", () => { }); it("reindexes sessions when source config adds sessions to an existing index", async () => { - const indexSourceChangePath = path.join( - workspaceDir, - `index-source-change-${Date.now()}.sqlite`, - ); - const stateDir = path.join(fixtureRoot, `state-source-change-${Date.now()}`); + const stateDir = sourceChangeStateDir; const sessionDir = path.join(stateDir, "agents", "main", "sessions"); + await fs.rm(stateDir, { recursive: true, force: true }); await fs.mkdir(sessionDir, { recursive: true }); await fs.writeFile( path.join(sessionDir, "session-source-change.jsonl"), - [ - JSON.stringify({ - type: "message", - message: { - role: "user", - content: [{ type: "text", text: "session change test user line" }], - }, - }), - JSON.stringify({ - type: "message", - message: { - role: "assistant", - content: [{ type: "text", text: "session change test assistant line" }], - }, - }), - ].join("\n") + "\n", + `${sourceChangeSessionLogLines}\n`, ); const previousStateDir = process.env.OPENCLAW_STATE_DIR; @@ -287,7 +292,6 @@ describe("memory index", () => { }); it("reindexes when the embedding model changes", async () => { - const indexModelPath = path.join(workspaceDir, `index-model-change-${Date.now()}.sqlite`); const base = createCfg({ storePath: indexModelPath }); const baseAgents = base.agents!; const baseDefaults = baseAgents.defaults!; diff --git a/src/memory/qmd-manager.test.ts b/src/memory/qmd-manager.test.ts index 47814da7452..0532dd6099e 100644 --- a/src/memory/qmd-manager.test.ts +++ b/src/memory/qmd-manager.test.ts @@ -133,9 +133,10 @@ describe("QmdMemoryManager", () => { tmpRoot = path.join(fixtureRoot, `case-${fixtureCount++}`); workspaceDir = path.join(tmpRoot, "workspace"); stateDir = path.join(tmpRoot, "state"); + await fs.mkdir(tmpRoot); // Only workspace must exist for configured collection paths; state paths are // created lazily by manager code when needed. - await fs.mkdir(workspaceDir, { recursive: true }); + await fs.mkdir(workspaceDir); process.env.OPENCLAW_STATE_DIR = stateDir; cfg = { agents: { diff --git a/src/secrets/resolve.test.ts b/src/secrets/resolve.test.ts index a7ea8909431..d49bfe71a3c 100644 --- a/src/secrets/resolve.test.ts +++ b/src/secrets/resolve.test.ts @@ -31,7 +31,7 @@ describe("secret ref resolver", () => { const createCaseDir = async (label: string): Promise => { const dir = path.join(fixtureRoot, `${label}-${caseId++}`); - await fs.mkdir(dir, { recursive: true }); + await fs.mkdir(dir); return dir; }; @@ -202,7 +202,7 @@ describe("secret ref resolver", () => { "#!/usr/bin/env node", "setTimeout(() => {", " process.stdout.write(JSON.stringify({ protocolVersion: 1, values: { delayed: 'ok' } }));", - "}, 120);", + "}, 30);", ].join("\n"), 0o700, );