File size: 5,320 Bytes
35f99ee | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | /** Tests LSP server spawning with Windows shim and sanitized env handling. */
import { describe, expect, it, vi, beforeEach } from "vitest";
import type { sanitizeHostExecEnv } from "../infra/host-env-security.js";
import {
materializeWindowsSpawnProgram,
resolveWindowsSpawnProgram,
type WindowsSpawnProgram,
} from "../plugin-sdk/windows-spawn.js";
import { createOwnedStdioProcess } from "../process/owned-stdio.js";
import { withMockedWindowsPlatform } from "../test-utils/vitest-spies.js";
import { spawnLspServerProcess } from "./agent-bundle-lsp-process.js";
const resolveWindowsSpawnProgramMock = vi.fn<typeof resolveWindowsSpawnProgram>();
const sanitizeHostExecEnvMock = vi.fn<typeof sanitizeHostExecEnv>();
const spawnMock = vi.fn<typeof createOwnedStdioProcess>();
const { spawnWithFallbackMock } = vi.hoisted(() => ({ spawnWithFallbackMock: vi.fn() }));
vi.mock("../process/spawn-utils.js", () => ({ spawnWithFallback: spawnWithFallbackMock }));
describe("spawnLspServerProcess Windows .cmd shim handling", () => {
beforeEach(() => {
vi.clearAllMocks();
spawnMock.mockRejectedValue(new Error("stop after spawn"));
spawnWithFallbackMock.mockRejectedValue(new Error("captured Windows spawn"));
});
const directProgram: WindowsSpawnProgram = {
command: "typescript-language-server",
leadingArgv: [],
resolution: "direct",
shell: false,
windowsHide: true,
};
it.each<{
name: string;
configEnv?: Record<string, string>;
sanitizedEnv: Record<string, string>;
program: WindowsSpawnProgram;
expectedArgv: string[];
}>([
{
name: "calls sanitizeHostExecEnv with baseEnv/overrides, not a flat merged object",
configEnv: { MY_TOKEN: "secret", TOOL_PATH: "/custom" },
sanitizedEnv: { PATH: "/usr/bin", MY_TOKEN: "secret", TOOL_PATH: "/custom" },
program: directProgram,
expectedArgv: ["typescript-language-server", "--stdio"],
},
{
name: "passes sanitized env to resolveWindowsSpawnProgram",
sanitizedEnv: { PATH: "C:\\Windows;C:\\nodejs", PATHEXT: ".COM;.EXE;.BAT;.CMD" },
program: directProgram,
expectedArgv: ["typescript-language-server", "--stdio"],
},
{
name: "passes materialized invocation to spawn with the sanitized env",
sanitizedEnv: { PATH: "/usr/bin" },
program: {
command: "cmd.exe",
leadingArgv: ["/c", "typescript-language-server.cmd"],
resolution: "shell-fallback",
shell: true,
windowsHide: true,
},
expectedArgv: ["cmd.exe", "/c", "typescript-language-server.cmd", "--stdio"],
},
])("$name", async ({ configEnv, sanitizedEnv, program, expectedArgv }) => {
sanitizeHostExecEnvMock.mockReturnValue(sanitizedEnv);
resolveWindowsSpawnProgramMock.mockReturnValue(program);
const abortSignal = new AbortController().signal;
await expect(
spawnLspServerProcess(
{
command: "typescript-language-server",
args: ["--stdio"],
...(configEnv ? { env: configEnv } : {}),
},
{
abortSignal,
dependencies: {
resolveWindowsSpawnProgram: resolveWindowsSpawnProgramMock,
materializeWindowsSpawnProgram,
sanitizeHostExecEnv: sanitizeHostExecEnvMock,
spawn: spawnMock,
},
},
),
).rejects.toThrow("stop after spawn");
const sanitizeParams = sanitizeHostExecEnvMock.mock.calls[0]?.[0];
expect(sanitizeParams?.baseEnv).toBe(process.env);
if (configEnv) {
expect(sanitizeParams?.overrides).toStrictEqual(configEnv);
}
const resolveParams = resolveWindowsSpawnProgramMock.mock.calls[0]?.[0];
expect(resolveParams?.env).toBe(sanitizedEnv);
expect(resolveParams?.allowShellFallback).toBe(true);
expect(spawnMock).toHaveBeenCalledExactlyOnceWith({
argv: expectedArgv,
env: sanitizedEnv,
exactEnv: true,
cwd: undefined,
abortSignal,
...(program.shell ? { windowsShell: true } : {}),
});
});
it("preserves the shipped Windows shell fallback through the owned adapter", async () => {
const sanitizedEnv = { PATH: "", PATHEXT: ".EXE;.CMD;.BAT" };
sanitizeHostExecEnvMock.mockReturnValue(sanitizedEnv);
await expect(
withMockedWindowsPlatform(() =>
spawnLspServerProcess(
{
command: "C:\\Program Files\\language-server.cmd",
args: ["--stdio", "two words", "%LSP_ARGUMENT%", "echo ready & exit /b"],
},
{
dependencies: {
sanitizeHostExecEnv: sanitizeHostExecEnvMock,
resolveWindowsSpawnProgram,
materializeWindowsSpawnProgram,
spawn: createOwnedStdioProcess,
},
},
),
),
).rejects.toThrow("captured Windows spawn");
expect(spawnWithFallbackMock).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({
argv: [
"C:\\Program Files\\language-server.cmd",
"--stdio",
"two words",
"%LSP_ARGUMENT%",
"echo ready & exit /b",
],
options: expect.objectContaining({
env: sanitizedEnv,
shell: true,
windowsHide: true,
}),
}),
);
});
});
|