import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; type EnvValue = string | undefined | ((home: string) => string | undefined); type EnvSnapshot = { home: string | undefined; userProfile: string | undefined; homeDrive: string | undefined; homePath: string | undefined; openclawHome: string | undefined; stateDir: string | undefined; }; type SharedHomeRootState = { rootPromise: Promise; nextCaseId: number; }; const SHARED_HOME_ROOTS = new Map(); function snapshotEnv(): EnvSnapshot { return { home: process.env.HOME, userProfile: process.env.USERPROFILE, homeDrive: process.env.HOMEDRIVE, homePath: process.env.HOMEPATH, openclawHome: process.env.OPENCLAW_HOME, stateDir: process.env.OPENCLAW_STATE_DIR, }; } function restoreEnv(snapshot: EnvSnapshot) { const restoreKey = (key: string, value: string | undefined) => { if (value === undefined) { delete process.env[key]; } else { process.env[key] = value; } }; restoreKey("HOME", snapshot.home); restoreKey("USERPROFILE", snapshot.userProfile); restoreKey("HOMEDRIVE", snapshot.homeDrive); restoreKey("HOMEPATH", snapshot.homePath); restoreKey("OPENCLAW_HOME", snapshot.openclawHome); restoreKey("OPENCLAW_STATE_DIR", snapshot.stateDir); } function snapshotExtraEnv(keys: string[]): Record { const snapshot: Record = {}; for (const key of keys) { snapshot[key] = process.env[key]; } return snapshot; } function restoreExtraEnv(snapshot: Record) { for (const [key, value] of Object.entries(snapshot)) { if (value === undefined) { delete process.env[key]; } else { process.env[key] = value; } } } function setTempHome(base: string) { process.env.HOME = base; process.env.USERPROFILE = base; // Ensure tests using HOME isolation aren't affected by leaked OPENCLAW_HOME. delete process.env.OPENCLAW_HOME; process.env.OPENCLAW_STATE_DIR = path.join(base, ".openclaw"); if (process.platform !== "win32") { return; } const match = base.match(/^([A-Za-z]:)(.*)$/); if (!match) { return; } process.env.HOMEDRIVE = match[1]; process.env.HOMEPATH = match[2] || "\\"; } async function allocateTempHomeBase(prefix: string): Promise { let state = SHARED_HOME_ROOTS.get(prefix); if (!state) { state = { rootPromise: fs.mkdtemp(path.join(os.tmpdir(), prefix)), nextCaseId: 0, }; SHARED_HOME_ROOTS.set(prefix, state); } const root = await state.rootPromise; const base = path.join(root, `case-${state.nextCaseId++}`); await fs.mkdir(base, { recursive: true }); return base; } export async function withTempHome( fn: (home: string) => Promise, opts: { env?: Record; prefix?: string } = {}, ): Promise { const prefix = opts.prefix ?? "openclaw-test-home-"; const base = await allocateTempHomeBase(prefix); const snapshot = snapshotEnv(); const envKeys = Object.keys(opts.env ?? {}); for (const key of envKeys) { if (key === "HOME" || key === "USERPROFILE" || key === "HOMEDRIVE" || key === "HOMEPATH") { throw new Error(`withTempHome: use built-in home env (got ${key})`); } } const envSnapshot = snapshotExtraEnv(envKeys); setTempHome(base); await fs.mkdir(path.join(base, ".openclaw", "agents", "main", "sessions"), { recursive: true }); if (opts.env) { for (const [key, raw] of Object.entries(opts.env)) { const value = typeof raw === "function" ? raw(base) : raw; if (value === undefined) { delete process.env[key]; } else { process.env[key] = value; } } } try { return await fn(base); } finally { restoreExtraEnv(envSnapshot); restoreEnv(snapshot); try { if (process.platform === "win32") { await fs.rm(base, { recursive: true, force: true, maxRetries: 10, retryDelay: 50, }); } else { await fs.rm(base, { recursive: true, force: true, }); } } catch { // ignore cleanup failures in tests } } }