mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-12 04:20:42 +00:00
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>
112 lines
4.0 KiB
TypeScript
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 }),
|
|
);
|
|
});
|
|
});
|