Files
openclaw/src/agents/pi-bundle-lsp-runtime.windows-spawn.test.ts
Elliot Drel 3e4f076723 fix(lsp): resolve Windows .cmd shims (#75343)
Resolve Windows npm .cmd shim startup failures for bundled LSP servers by routing LSP process spawning through the shared Windows spawn resolver with a sanitized child environment.

The change reuses existing PATH/PATHEXT and .cmd shim handling, keeps non-Windows behavior unchanged, and adds focused regression coverage for resolver wiring, env sanitization, and spawn materialization.

Fixes #75352.

Tests:
- pnpm test src/agents/pi-bundle-lsp-runtime.windows-spawn.test.ts src/agents/pi-bundle-lsp-runtime.test.ts
- pnpm check:changed

Thanks @ElliotDrel.

Co-authored-by: Elliot Drel <156480527+ElliotDrel@users.noreply.github.com>
Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
2026-05-04 20:08:00 -05:00

112 lines
4.0 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from "vitest";
const resolveWindowsSpawnProgramMock = vi.hoisted(() => vi.fn());
const materializeWindowsSpawnProgramMock = vi.hoisted(() => vi.fn());
const sanitizeHostExecEnvMock = vi.hoisted(() => vi.fn());
const spawnMock = vi.hoisted(() => vi.fn());
vi.mock("../plugin-sdk/windows-spawn.js", () => ({
resolveWindowsSpawnProgram: resolveWindowsSpawnProgramMock,
materializeWindowsSpawnProgram: materializeWindowsSpawnProgramMock,
}));
vi.mock("../infra/host-env-security.js", () => ({
sanitizeHostExecEnv: sanitizeHostExecEnvMock,
}));
vi.mock("node:child_process", async () => ({
...(await vi.importActual<typeof import("node:child_process")>("node:child_process")),
spawn: spawnMock,
}));
vi.mock("../logger.js", () => ({
logDebug: vi.fn(),
logWarn: vi.fn(),
}));
vi.mock("../process/kill-tree.js", () => ({
killProcessTree: vi.fn(),
}));
vi.mock("./embedded-pi-lsp.js", () => ({
loadEmbeddedPiLspConfig: vi.fn().mockReturnValue({ lspServers: {}, diagnostics: [] }),
}));
const FAKE_CHILD = {
stdout: { setEncoding: vi.fn(), on: vi.fn() },
stderr: { setEncoding: vi.fn(), on: vi.fn() },
on: vi.fn(),
pid: 1234,
} as unknown as import("node:child_process").ChildProcess;
describe("spawnLspServerProcess Windows .cmd shim handling", () => {
beforeEach(() => {
vi.clearAllMocks();
spawnMock.mockReturnValue(FAKE_CHILD);
});
it("calls sanitizeHostExecEnv with baseEnv/overrides, not a flat merged object", async () => {
const configEnv = { MY_TOKEN: "secret", TOOL_PATH: "/custom" };
const sanitizedEnv = { PATH: "/usr/bin", MY_TOKEN: "secret", TOOL_PATH: "/custom" };
sanitizeHostExecEnvMock.mockReturnValue(sanitizedEnv);
resolveWindowsSpawnProgramMock.mockReturnValue({ resolvedCommand: "tls", isShim: false });
materializeWindowsSpawnProgramMock.mockReturnValue({
command: "typescript-language-server",
argv: ["--stdio"],
shell: false,
windowsHide: true,
});
const { spawnLspServerProcess } = await import("./pi-bundle-lsp-runtime.js");
spawnLspServerProcess({ command: "typescript-language-server", args: ["--stdio"], env: configEnv });
// Must use structured params so config.env entries are not dropped
expect(sanitizeHostExecEnvMock).toHaveBeenCalledWith(
expect.objectContaining({ baseEnv: process.env, overrides: configEnv }),
);
});
it("passes sanitized env to resolveWindowsSpawnProgram", async () => {
const sanitizedEnv = { PATH: "C:\\Windows;C:\\nodejs", PATHEXT: ".COM;.EXE;.BAT;.CMD" };
sanitizeHostExecEnvMock.mockReturnValue(sanitizedEnv);
resolveWindowsSpawnProgramMock.mockReturnValue({ resolvedCommand: "tls", isShim: false });
materializeWindowsSpawnProgramMock.mockReturnValue({
command: "typescript-language-server",
argv: ["--stdio"],
shell: false,
windowsHide: true,
});
const { spawnLspServerProcess } = await import("./pi-bundle-lsp-runtime.js");
spawnLspServerProcess({ command: "typescript-language-server", args: ["--stdio"] });
expect(resolveWindowsSpawnProgramMock).toHaveBeenCalledWith(
expect.objectContaining({ env: sanitizedEnv, allowShellFallback: true }),
);
});
it("passes materialized invocation to spawn with the sanitized env", async () => {
const sanitizedEnv = { PATH: "/usr/bin" };
sanitizeHostExecEnvMock.mockReturnValue(sanitizedEnv);
resolveWindowsSpawnProgramMock.mockReturnValue({ resolvedCommand: "tls", isShim: true });
materializeWindowsSpawnProgramMock.mockReturnValue({
command: "cmd.exe",
argv: ["/c", "typescript-language-server.cmd", "--stdio"],
shell: true,
windowsHide: true,
});
const { spawnLspServerProcess } = await import("./pi-bundle-lsp-runtime.js");
spawnLspServerProcess({ command: "typescript-language-server", args: ["--stdio"] });
expect(spawnMock).toHaveBeenCalledWith(
"cmd.exe",
["/c", "typescript-language-server.cmd", "--stdio"],
expect.objectContaining({ env: sanitizedEnv, shell: true, windowsHide: true }),
);
});
});