diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..a533bd0 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,5 @@ +# Bolt's Journal - Critical Performance Learnings + +## 2026-08-09 - [State Pollution in Concurrent Tests under Bun] +**Learning:** Mutating global properties like `process.platform` in concurrent asynchronous test suites causes state pollution and race conditions across tests under Bun. +**Action:** Avoid mutating `process.platform` globally. Instead, pass `platform` or mock dependencies explicitly, or use helper functions that isolate the platform environment. diff --git a/src/commands/init/agent-definitions.test.ts b/src/commands/init/agent-definitions.test.ts index e1efe7c..04b4b9e 100644 --- a/src/commands/init/agent-definitions.test.ts +++ b/src/commands/init/agent-definitions.test.ts @@ -1325,7 +1325,7 @@ describe("detectAgents", () => { }); describe("scanAgents", () => { - function lookupCommandFor(platform: string = process.platform): string { + function lookupCommandFor(platform: string = "linux"): string { return platform === "win32" ? "where" : "which"; } @@ -1352,10 +1352,16 @@ describe("scanAgents", () => { configFiles?: Record; existingFiles?: string[]; execResults?: Record; - pathPlatform?: "posix" | "win32"; + pathPlatform?: "posix" | "win32" | "darwin" | "linux"; }) { - const isWin32 = opts.pathPlatform === "win32"; + const platform = + opts.pathPlatform === "posix" + ? "linux" + : (opts.pathPlatform ?? + (process.platform === "win32" ? "linux" : process.platform)); + const isWin32 = platform === "win32"; const fs = createMockFileSystemService({ + platform, getHomeDir: mock(() => (isWin32 ? "C:\\Users\\test" : "/home/test")), joinPath: mock((...segments: string[]) => isWin32 ? win32.join(...segments) : segments.join("/"), @@ -1374,6 +1380,7 @@ describe("scanAgents", () => { ), }); const execService = createMockExecService({ + platform, exec: mock(async (cmd: string, args: string[], _options?: unknown) => { const key = `${cmd} ${args.join(" ")}`; if (opts.execResults && key in opts.execResults) { @@ -2620,23 +2627,17 @@ describe("scanAgents", () => { }; describe(`comprehensive all-agents scenarios (${platform})`, () => { - const pathPlatform = platform === "win32" ? "win32" : "posix"; const createScenarioScanMocks = ( scenarioOpts: Parameters[0], - ) => createScanMocks({ ...scenarioOpts, pathPlatform }); - const originalPlatform = process.platform; - beforeAll(() => { - Object.defineProperty(process, "platform", { - value: platform, - configurable: true, + ) => + createScanMocks({ + ...scenarioOpts, + pathPlatform: platform as "posix" | "win32" | "darwin" | "linux", }); + beforeAll(() => { opts.envSetup?.(); }); afterAll(() => { - Object.defineProperty(process, "platform", { - value: originalPlatform, - configurable: true, - }); opts.envTeardown?.(); }); diff --git a/src/commands/init/agent-definitions.ts b/src/commands/init/agent-definitions.ts index 9a00e9c..df52e35 100644 --- a/src/commands/init/agent-definitions.ts +++ b/src/commands/init/agent-definitions.ts @@ -211,8 +211,9 @@ export interface AgentDefinition { * Linux: ~/.config/ */ function getAppDataPath(fs: FileSystemService, appName: string): string { + const platform = fs.platform ?? process.platform; const home = fs.getHomeDir(); - switch (process.platform) { + switch (platform) { case "win32": return fs.joinPath( process.env.APPDATA ?? fs.joinPath(home, "AppData", "Roaming"), @@ -232,8 +233,9 @@ function getAppDataPath(fs: FileSystemService, appName: string): string { * Linux: $XDG_DATA_HOME (or ~/.local/share fallback) */ function getUserDataRoot(fs: FileSystemService): string { + const platform = fs.platform ?? process.platform; const home = fs.getHomeDir(); - switch (process.platform) { + switch (platform) { case "win32": return process.env.APPDATA ?? fs.joinPath(home, "AppData", "Roaming"); case "darwin": @@ -244,7 +246,8 @@ function getUserDataRoot(fs: FileSystemService): string { } function getOpenCodeConfigDir(fs: FileSystemService): string { - if (process.platform === "win32") { + const platform = fs.platform ?? process.platform; + if (platform === "win32") { return fs.joinPath(getUserDataRoot(fs), "opencode"); } return fs.joinPath(fs.getHomeDir(), ".config", "opencode"); @@ -417,7 +420,8 @@ async function isExecutableAvailable( executable: string, ): Promise { try { - const lookupCommand = process.platform === "win32" ? "where" : "which"; + const platform = exec.platform ?? process.platform; + const lookupCommand = platform === "win32" ? "where" : "which"; const result = await exec.exec(lookupCommand, [executable], { timeoutMs: BINARY_LOOKUP_TIMEOUT_MS, }); @@ -445,14 +449,17 @@ type PiGlobalBinProbe = (typeof PI_GLOBAL_BIN_PROBES)[number]; const PI_ADAPTER_CONFIGURED_PATTERN = /(?:^|\s|:)(?:npm:)?pi-mcp-adapter(?:[\s@:]|$)/i; -function getPiExecutableNames(): string[] { - return process.platform === "win32" ? ["pi.cmd", "pi.exe", "pi"] : ["pi"]; +function getPiExecutableNames( + platform: NodeJS.Platform = process.platform, +): string[] { + return platform === "win32" ? ["pi.cmd", "pi.exe", "pi"] : ["pi"]; } async function runGlobalBinProbe( exec: ExecService, probe: PiGlobalBinProbe, ): Promise { + const platform = exec.platform ?? process.platform; try { const result = await exec.exec(probe.command, [...probe.args], { timeoutMs: GLOBAL_BIN_PROBE_TIMEOUT_MS, @@ -467,7 +474,7 @@ async function runGlobalBinProbe( if (!probePath) { return null; } - if (probe.output === "prefix" && process.platform !== "win32") { + if (probe.output === "prefix" && platform !== "win32") { return fsJoinPathLike(probePath, "bin"); } return probePath; @@ -484,6 +491,7 @@ async function detectPiExecutable( exec: ExecService, fs: FileSystemService, ): Promise { + const platform = exec.platform ?? fs.platform ?? process.platform; if (await resolveExecutableFromPath(exec, "pi")) { return { command: "pi" }; } @@ -493,7 +501,7 @@ async function detectPiExecutable( if (!binDir) { continue; } - for (const executableName of getPiExecutableNames()) { + for (const executableName of getPiExecutableNames(platform)) { const candidate = fs.joinPath(binDir, executableName); if (await fs.exists(candidate)) { return { command: candidate }; @@ -649,8 +657,9 @@ const claudeDesktop: AgentDefinition = { detectionMethod: "path", setupMethod: "config-file", detectPaths: (fs) => { + const platform = fs.platform ?? process.platform; const appData = getAppDataPath(fs, "Claude"); - if (process.platform === "win32") { + if (platform === "win32") { const home = fs.getHomeDir(); const localAppData = process.env.LOCALAPPDATA ?? fs.joinPath(home, "AppData", "Local"); diff --git a/src/services/exec-service.ts b/src/services/exec-service.ts index c0ac18f..ca9d495 100644 --- a/src/services/exec-service.ts +++ b/src/services/exec-service.ts @@ -101,6 +101,9 @@ export class ExecTimeoutError extends Error { * Abstraction allows for easy testing with mock implementations. */ export interface ExecService { + /** Optional platform override, mainly for testing. */ + platform?: NodeJS.Platform; + /** Execute a command with arguments and return the result */ exec( command: string, @@ -117,6 +120,8 @@ export interface ExecService { * Callers must not pass untrusted input as command or args. */ export class ExecServiceImpl implements ExecService { + platform: NodeJS.Platform = process.platform; + async exec( command: string, args: string[], diff --git a/src/services/filesystem-service.ts b/src/services/filesystem-service.ts index e1cc608..f4bda99 100644 --- a/src/services/filesystem-service.ts +++ b/src/services/filesystem-service.ts @@ -17,6 +17,9 @@ import { dirname, join } from "node:path"; * Abstraction allows for easy testing with mock implementations. */ export interface FileSystemService { + /** Optional platform override, mainly for testing. */ + platform?: NodeJS.Platform; + /** Read file contents as string */ readFile(path: string): Promise; @@ -71,6 +74,8 @@ export interface FileSystemService { * Production implementation using node:fs/promises. */ export class FileSystemServiceImpl implements FileSystemService { + platform: NodeJS.Platform = process.platform; + async readFile(path: string): Promise { return readFile(path, "utf-8"); }