From 422db236fe6d56b1cd8394cd8bffb91bc248c4c5 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 14 Feb 2026 14:02:03 +0900 Subject: [PATCH 1/2] fix(lsp): remove unreliable Windows binary availability check The isBinaryAvailableOnWindows() function used spawnSync("where") which fails even when the binary IS on PATH, causing false negatives. Removed the redundant pre-check and let nodeSpawn handle binary resolution naturally with proper OS-level error messages. Fixes #1805 --- src/tools/lsp/lsp-process.test.ts | 37 +++++++++++++++++++++++++++++++ src/tools/lsp/lsp-process.ts | 27 +--------------------- 2 files changed, 38 insertions(+), 26 deletions(-) create mode 100644 src/tools/lsp/lsp-process.test.ts diff --git a/src/tools/lsp/lsp-process.test.ts b/src/tools/lsp/lsp-process.test.ts new file mode 100644 index 000000000..406a7f7fc --- /dev/null +++ b/src/tools/lsp/lsp-process.test.ts @@ -0,0 +1,37 @@ +import { mkdtempSync, rmSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" + +import { describe, expect, it, spyOn } from "bun:test" + +describe("spawnProcess", () => { + it("proceeds to node spawn on Windows when command is available", async () => { + // #given + const originalPlatform = process.platform + const rootDir = mkdtempSync(join(tmpdir(), "lsp-process-test-")) + const childProcess = await import("node:child_process") + const nodeSpawnSpy = spyOn(childProcess, "spawn") + + try { + Object.defineProperty(process, "platform", { value: "win32" }) + const { spawnProcess } = await import("./lsp-process") + + // #when + let result: ReturnType | null = null + expect(() => { + result = spawnProcess(["node", "--version"], { + cwd: rootDir, + env: process.env, + }) + }).not.toThrow(/Binary 'node' not found/) + + // #then + expect(nodeSpawnSpy).toHaveBeenCalled() + expect(result).not.toBeNull() + } finally { + Object.defineProperty(process, "platform", { value: originalPlatform }) + nodeSpawnSpy.mockRestore() + rmSync(rootDir, { recursive: true, force: true }) + } + }) +}) diff --git a/src/tools/lsp/lsp-process.ts b/src/tools/lsp/lsp-process.ts index a193aa968..358c5c7e5 100644 --- a/src/tools/lsp/lsp-process.ts +++ b/src/tools/lsp/lsp-process.ts @@ -1,5 +1,5 @@ import { spawn as bunSpawn } from "bun" -import { spawn as nodeSpawn, spawnSync, type ChildProcess } from "node:child_process" +import { spawn as nodeSpawn, type ChildProcess } from "node:child_process" import { existsSync, statSync } from "fs" import { log } from "../../shared/logger" // Bun spawn segfaults on Windows (oven-sh/bun#25798) — unfixed as of v1.3.8+ @@ -21,24 +21,6 @@ export function validateCwd(cwd: string): { valid: boolean; error?: string } { return { valid: false, error: `Cannot access working directory: ${cwd} (${err instanceof Error ? err.message : String(err)})` } } } -function isBinaryAvailableOnWindows(command: string): boolean { - if (process.platform !== "win32") return true - - if (command.includes("/") || command.includes("\\")) { - return existsSync(command) - } - - try { - const result = spawnSync("where", [command], { - shell: true, - windowsHide: true, - timeout: 5000, - }) - return result.status === 0 - } catch { - return true - } -} interface StreamReader { read(): Promise<{ done: boolean; value: Uint8Array | undefined }> } @@ -158,13 +140,6 @@ export function spawnProcess( } if (shouldUseNodeSpawn()) { const [cmd, ...args] = command - if (!isBinaryAvailableOnWindows(cmd)) { - throw new Error( - `[LSP] Binary '${cmd}' not found on Windows. ` + - `Ensure the LSP server is installed and available in PATH. ` + - `For npm packages, try: npm install -g ${cmd}` - ) - } log("[LSP] Using Node.js child_process on Windows to avoid Bun spawn segfault") const proc = nodeSpawn(cmd, args, { cwd: options.cwd, From 26ae666bc3a734a6c43e6d981c7ce6933956d165 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 14 Feb 2026 14:03:06 +0900 Subject: [PATCH 2/2] test(lsp): use explicit BDD markers in Windows spawn test --- src/tools/lsp/lsp-process.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tools/lsp/lsp-process.test.ts b/src/tools/lsp/lsp-process.test.ts index 406a7f7fc..a4ecbc0d9 100644 --- a/src/tools/lsp/lsp-process.test.ts +++ b/src/tools/lsp/lsp-process.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it, spyOn } from "bun:test" describe("spawnProcess", () => { it("proceeds to node spawn on Windows when command is available", async () => { - // #given + //#given const originalPlatform = process.platform const rootDir = mkdtempSync(join(tmpdir(), "lsp-process-test-")) const childProcess = await import("node:child_process") @@ -16,7 +16,7 @@ describe("spawnProcess", () => { Object.defineProperty(process, "platform", { value: "win32" }) const { spawnProcess } = await import("./lsp-process") - // #when + //#when let result: ReturnType | null = null expect(() => { result = spawnProcess(["node", "--version"], { @@ -25,7 +25,7 @@ describe("spawnProcess", () => { }) }).not.toThrow(/Binary 'node' not found/) - // #then + //#then expect(nodeSpawnSpy).toHaveBeenCalled() expect(result).not.toBeNull() } finally {