diff --git a/src/agents/auth-profiles.test.ts b/src/agents/auth-profiles.test.ts index 91513b8c5..1e75668e1 100644 --- a/src/agents/auth-profiles.test.ts +++ b/src/agents/auth-profiles.test.ts @@ -13,6 +13,40 @@ import { resolveAuthProfileOrder, } from "./auth-profiles.js"; +const HOME_ENV_KEYS = ["HOME", "USERPROFILE", "HOMEDRIVE", "HOMEPATH"] as const; +type HomeEnvSnapshot = Record< + (typeof HOME_ENV_KEYS)[number], + string | undefined +>; + +const snapshotHomeEnv = (): HomeEnvSnapshot => ({ + HOME: process.env.HOME, + USERPROFILE: process.env.USERPROFILE, + HOMEDRIVE: process.env.HOMEDRIVE, + HOMEPATH: process.env.HOMEPATH, +}); + +const restoreHomeEnv = (snapshot: HomeEnvSnapshot) => { + for (const key of HOME_ENV_KEYS) { + const value = snapshot[key]; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } +}; + +const setTempHome = (tempHome: string) => { + process.env.HOME = tempHome; + if (process.platform === "win32") { + process.env.USERPROFILE = tempHome; + const root = path.parse(tempHome).root; + process.env.HOMEDRIVE = root.replace(/\\$/, ""); + process.env.HOMEPATH = tempHome.slice(root.length - 1); + } +}; + describe("resolveAuthProfileOrder", () => { const store: AuthProfileStore = { version: 1, @@ -347,12 +381,12 @@ describe("external CLI credential sync", () => { const agentDir = fs.mkdtempSync( path.join(os.tmpdir(), "clawdbot-cli-sync-"), ); - const originalHome = process.env.HOME; + const originalHome = snapshotHomeEnv(); try { // Create a temp home with Claude CLI credentials const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-home-")); - process.env.HOME = tempHome; + setTempHome(tempHome); // Create Claude CLI credentials const claudeDir = path.join(tempHome, ".claude"); @@ -400,7 +434,7 @@ describe("external CLI credential sync", () => { (store.profiles[CLAUDE_CLI_PROFILE_ID] as { expires: number }).expires, ).toBeGreaterThan(Date.now()); } finally { - process.env.HOME = originalHome; + restoreHomeEnv(originalHome); fs.rmSync(agentDir, { recursive: true, force: true }); } }); @@ -409,11 +443,11 @@ describe("external CLI credential sync", () => { const agentDir = fs.mkdtempSync( path.join(os.tmpdir(), "clawdbot-codex-sync-"), ); - const originalHome = process.env.HOME; + const originalHome = snapshotHomeEnv(); try { const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-home-")); - process.env.HOME = tempHome; + setTempHome(tempHome); // Create Codex CLI credentials const codexDir = path.join(tempHome, ".codex"); @@ -444,7 +478,7 @@ describe("external CLI credential sync", () => { (store.profiles[CODEX_CLI_PROFILE_ID] as { access: string }).access, ).toBe("codex-access-token"); } finally { - process.env.HOME = originalHome; + restoreHomeEnv(originalHome); fs.rmSync(agentDir, { recursive: true, force: true }); } }); @@ -453,11 +487,11 @@ describe("external CLI credential sync", () => { const agentDir = fs.mkdtempSync( path.join(os.tmpdir(), "clawdbot-no-overwrite-"), ); - const originalHome = process.env.HOME; + const originalHome = snapshotHomeEnv(); try { const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-home-")); - process.env.HOME = tempHome; + setTempHome(tempHome); // Create Claude CLI credentials const claudeDir = path.join(tempHome, ".claude"); @@ -498,7 +532,7 @@ describe("external CLI credential sync", () => { ); expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined(); } finally { - process.env.HOME = originalHome; + restoreHomeEnv(originalHome); fs.rmSync(agentDir, { recursive: true, force: true }); } }); @@ -507,11 +541,11 @@ describe("external CLI credential sync", () => { const agentDir = fs.mkdtempSync( path.join(os.tmpdir(), "clawdbot-cli-no-downgrade-"), ); - const originalHome = process.env.HOME; + const originalHome = snapshotHomeEnv(); try { const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-home-")); - process.env.HOME = tempHome; + setTempHome(tempHome); const claudeDir = path.join(tempHome, ".claude"); fs.mkdirSync(claudeDir, { recursive: true }); @@ -548,7 +582,7 @@ describe("external CLI credential sync", () => { (store.profiles[CLAUDE_CLI_PROFILE_ID] as { access: string }).access, ).toBe("store-access"); } finally { - process.env.HOME = originalHome; + restoreHomeEnv(originalHome); fs.rmSync(agentDir, { recursive: true, force: true }); } }); @@ -557,11 +591,11 @@ describe("external CLI credential sync", () => { const agentDir = fs.mkdtempSync( path.join(os.tmpdir(), "clawdbot-codex-refresh-sync-"), ); - const originalHome = process.env.HOME; + const originalHome = snapshotHomeEnv(); try { const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-home-")); - process.env.HOME = tempHome; + setTempHome(tempHome); const codexDir = path.join(tempHome, ".codex"); fs.mkdirSync(codexDir, { recursive: true }); @@ -596,7 +630,7 @@ describe("external CLI credential sync", () => { (store.profiles[CODEX_CLI_PROFILE_ID] as { refresh: string }).refresh, ).toBe("new-refresh"); } finally { - process.env.HOME = originalHome; + restoreHomeEnv(originalHome); fs.rmSync(agentDir, { recursive: true, force: true }); } }); diff --git a/src/agents/bash-tools.test.ts b/src/agents/bash-tools.test.ts index 9214ab2c7..abc10f815 100644 --- a/src/agents/bash-tools.test.ts +++ b/src/agents/bash-tools.test.ts @@ -7,6 +7,12 @@ import { processTool, } from "./bash-tools.js"; +const nodePath = process.execPath.includes(" ") + ? `"${process.execPath}"` + : process.execPath; +const nodeEval = (script: string) => + `${nodePath} -e "${script.replaceAll('"', '\\"')}"`; + const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function waitForCompletion(sessionId: string) { @@ -32,7 +38,7 @@ beforeEach(() => { describe("bash tool backgrounding", () => { it("backgrounds after yield and can be polled", async () => { const result = await bashTool.execute("call1", { - command: "node -e \"setTimeout(() => { console.log('done') }, 50)\"", + command: nodeEval("setTimeout(function(){ console.log('done') }, 50)"), yieldMs: 10, }); @@ -62,7 +68,7 @@ describe("bash tool backgrounding", () => { it("supports explicit background", async () => { const result = await bashTool.execute("call1", { - command: "node -e \"setTimeout(() => { console.log('later') }, 50)\"", + command: nodeEval("setTimeout(function(){ console.log('later') }, 50)"), background: true, }); @@ -97,7 +103,7 @@ describe("bash tool backgrounding", () => { const customProcess = createProcessTool(); const result = await customBash.execute("call1", { - command: 'node -e "setInterval(() => {}, 1000)"', + command: nodeEval("setInterval(function(){}, 1000)"), background: true, }); @@ -148,8 +154,9 @@ describe("bash tool backgrounding", () => { it("logs line-based slices and defaults to last lines", async () => { const result = await bashTool.execute("call1", { - command: - "node -e \"console.log('one'); console.log('two'); console.log('three');\"", + command: nodeEval( + "console.log('one'); console.log('two'); console.log('three');", + ), background: true, }); const sessionId = (result.details as { sessionId: string }).sessionId; @@ -169,8 +176,9 @@ describe("bash tool backgrounding", () => { it("supports line offsets for log slices", async () => { const result = await bashTool.execute("call1", { - command: - "node -e \"console.log('alpha'); console.log('beta'); console.log('gamma');\"", + command: nodeEval( + "console.log('alpha'); console.log('beta'); console.log('gamma');", + ), background: true, }); const sessionId = (result.details as { sessionId: string }).sessionId; @@ -193,11 +201,11 @@ describe("bash tool backgrounding", () => { const processB = createProcessTool({ scopeKey: "agent:beta" }); const resultA = await bashA.execute("call1", { - command: 'node -e "setTimeout(() => {}, 50)"', + command: nodeEval("setTimeout(function(){}, 50)"), background: true, }); const resultB = await bashB.execute("call2", { - command: 'node -e "setTimeout(() => {}, 50)"', + command: nodeEval("setTimeout(function(){}, 50)"), background: true, }); diff --git a/src/agents/sandbox-agent-config.test.ts b/src/agents/sandbox-agent-config.test.ts index aee0dcc01..26d71595e 100644 --- a/src/agents/sandbox-agent-config.test.ts +++ b/src/agents/sandbox-agent-config.test.ts @@ -1,4 +1,5 @@ import { EventEmitter } from "node:events"; +import path from "node:path"; import { Readable } from "node:stream"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../config/config.js"; @@ -349,7 +350,9 @@ describe("Agent-specific sandbox config", () => { }); expect(context).toBeDefined(); - expect(context?.workspaceDir).toContain("/tmp/isolated-sandboxes"); + expect(context?.workspaceDir).toContain( + path.resolve("/tmp/isolated-sandboxes"), + ); }); it("should prefer agent config over global for multiple agents", async () => { diff --git a/src/auto-reply/status.test.ts b/src/auto-reply/status.test.ts index 549334c7f..2e9f5ba80 100644 --- a/src/auto-reply/status.test.ts +++ b/src/auto-reply/status.test.ts @@ -4,6 +4,40 @@ import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { buildStatusMessage } from "./status.js"; +const HOME_ENV_KEYS = ["HOME", "USERPROFILE", "HOMEDRIVE", "HOMEPATH"] as const; +type HomeEnvSnapshot = Record< + (typeof HOME_ENV_KEYS)[number], + string | undefined +>; + +const snapshotHomeEnv = (): HomeEnvSnapshot => ({ + HOME: process.env.HOME, + USERPROFILE: process.env.USERPROFILE, + HOMEDRIVE: process.env.HOMEDRIVE, + HOMEPATH: process.env.HOMEPATH, +}); + +const restoreHomeEnv = (snapshot: HomeEnvSnapshot) => { + for (const key of HOME_ENV_KEYS) { + const value = snapshot[key]; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } +}; + +const setTempHome = (tempHome: string) => { + process.env.HOME = tempHome; + if (process.platform === "win32") { + process.env.USERPROFILE = tempHome; + const root = path.parse(tempHome).root; + process.env.HOMEDRIVE = root.replace(/\\$/, ""); + process.env.HOMEPATH = tempHome.slice(root.length - 1); + } +}; + afterEach(() => { vi.restoreAllMocks(); }); @@ -136,8 +170,8 @@ describe("buildStatusMessage", () => { it("prefers cached prompt tokens from the session log", async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-status-")); - const previousHome = process.env.HOME; - process.env.HOME = dir; + const previousHome = snapshotHomeEnv(); + setTempHome(dir); try { vi.resetModules(); const { buildStatusMessage: buildStatusMessageDynamic } = await import( @@ -195,7 +229,7 @@ describe("buildStatusMessage", () => { expect(text).toContain("Context: 1.0k/32k"); } finally { - process.env.HOME = previousHome; + restoreHomeEnv(previousHome); fs.rmSync(dir, { recursive: true, force: true }); } }); diff --git a/src/cli/nodes-screen.test.ts b/src/cli/nodes-screen.test.ts index c709e89bc..ccde0672d 100644 --- a/src/cli/nodes-screen.test.ts +++ b/src/cli/nodes-screen.test.ts @@ -1,3 +1,4 @@ +import path from "node:path"; import { describe, expect, it } from "vitest"; import { @@ -35,6 +36,6 @@ describe("nodes screen helpers", () => { tmpDir: "/tmp", id: "id1", }); - expect(p).toBe("/tmp/clawdbot-screen-record-id1.mp4"); + expect(p).toBe(path.join("/tmp", "clawdbot-screen-record-id1.mp4")); }); }); diff --git a/src/cli/profile.test.ts b/src/cli/profile.test.ts index 09ca2dde8..131b6d25d 100644 --- a/src/cli/profile.test.ts +++ b/src/cli/profile.test.ts @@ -72,10 +72,11 @@ describe("applyCliProfileEnv", () => { env, homedir: () => "/home/peter", }); + const expectedStateDir = path.join("/home/peter", ".clawdbot-dev"); expect(env.CLAWDBOT_PROFILE).toBe("dev"); - expect(env.CLAWDBOT_STATE_DIR).toBe("/home/peter/.clawdbot-dev"); + expect(env.CLAWDBOT_STATE_DIR).toBe(expectedStateDir); expect(env.CLAWDBOT_CONFIG_PATH).toBe( - path.join("/home/peter/.clawdbot-dev", "clawdbot.json"), + path.join(expectedStateDir, "clawdbot.json"), ); expect(env.CLAWDBOT_GATEWAY_PORT).toBe("19001"); }); diff --git a/src/commands/agents.test.ts b/src/commands/agents.test.ts index b1e4214dd..375794655 100644 --- a/src/commands/agents.test.ts +++ b/src/commands/agents.test.ts @@ -40,7 +40,7 @@ describe("agents helpers", () => { const work = summaries.find((summary) => summary.id === "work"); expect(main).toBeTruthy(); - expect(main?.workspace).toBe("/main-ws"); + expect(main?.workspace).toBe(path.resolve("/main-ws")); expect(main?.bindings).toBe(1); expect(main?.model).toBe("anthropic/claude"); expect(main?.agentDir.endsWith(path.join("agents", "main", "agent"))).toBe( @@ -49,8 +49,8 @@ describe("agents helpers", () => { expect(work).toBeTruthy(); expect(work?.name).toBe("Work"); - expect(work?.workspace).toBe("/work-ws"); - expect(work?.agentDir).toBe("/state/agents/work/agent"); + expect(work?.workspace).toBe(path.resolve("/work-ws")); + expect(work?.agentDir).toBe(path.resolve("/state/agents/work/agent")); expect(work?.bindings).toBe(1); expect(work?.isDefault).toBe(true); }); diff --git a/src/commands/doctor.test.ts b/src/commands/doctor.test.ts index f5be00569..bd9f98291 100644 --- a/src/commands/doctor.test.ts +++ b/src/commands/doctor.test.ts @@ -532,7 +532,7 @@ describe("doctor", () => { ([message, title]) => title === "Legacy workspace" && typeof message === "string" && - message.includes("/Users/steipete/clawdis"), + message.includes(path.join("/Users/steipete", "clawdis")), ), ).toBe(true); diff --git a/src/config/paths.ts b/src/config/paths.ts index 134062561..7c095e977 100644 --- a/src/config/paths.ts +++ b/src/config/paths.ts @@ -36,7 +36,8 @@ function resolveUserPath(input: string): string { const trimmed = input.trim(); if (!trimmed) return trimmed; if (trimmed.startsWith("~")) { - return path.resolve(trimmed.replace("~", os.homedir())); + const expanded = trimmed.replace(/^~(?=$|[\\/])/, os.homedir()); + return path.resolve(expanded); } return path.resolve(trimmed); } diff --git a/src/config/sessions.ts b/src/config/sessions.ts index cbb348a27..93e4c0d93 100644 --- a/src/config/sessions.ts +++ b/src/config/sessions.ts @@ -202,12 +202,14 @@ export function resolveStorePath(store?: string, opts?: { agentId?: string }) { const agentId = normalizeAgentId(opts?.agentId ?? DEFAULT_AGENT_ID); if (!store) return resolveDefaultSessionStorePath(agentId); if (store.includes("{agentId}")) { - return path.resolve( - store.replaceAll("{agentId}", agentId).replace("~", os.homedir()), - ); + const expanded = store.replaceAll("{agentId}", agentId); + if (expanded.startsWith("~")) { + return path.resolve(expanded.replace(/^~(?=$|[\\/])/, os.homedir())); + } + return path.resolve(expanded); } if (store.startsWith("~")) - return path.resolve(store.replace("~", os.homedir())); + return path.resolve(store.replace(/^~(?=$|[\\/])/, os.homedir())); return path.resolve(store); } diff --git a/src/utils.ts b/src/utils.ts index 879bf6f47..d10ee478c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -99,7 +99,8 @@ export function resolveUserPath(input: string): string { const trimmed = input.trim(); if (!trimmed) return trimmed; if (trimmed.startsWith("~")) { - return path.resolve(trimmed.replace("~", os.homedir())); + const expanded = trimmed.replace(/^~(?=$|[\\/])/, os.homedir()); + return path.resolve(expanded); } return path.resolve(trimmed); }