moving
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { join } from 'node:path';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createLspTools, _resetLspState } from '../src/lsp-tools.js';
|
||||
import {
|
||||
createLspTools,
|
||||
_resetLspState,
|
||||
spawnLspServerProcess,
|
||||
stopLspServerProcess,
|
||||
} from '../src/lsp-tools.js';
|
||||
import type { ToolDefinition } from '../src/tools.js';
|
||||
|
||||
// Mock child_process.spawn to prevent actually spawning LSP servers
|
||||
@@ -7,7 +14,15 @@ vi.mock('node:child_process', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('node:child_process')>();
|
||||
return {
|
||||
...original,
|
||||
spawn: vi.fn(() => {
|
||||
spawn: vi.fn((command, args, options) => {
|
||||
if (
|
||||
command === process.execPath
|
||||
&& Array.isArray(args)
|
||||
&& args[0] === '-e'
|
||||
&& args[2] === process.execPath
|
||||
) {
|
||||
return original.spawn(command, args, options);
|
||||
}
|
||||
throw new Error('spawn ENOENT');
|
||||
}),
|
||||
};
|
||||
@@ -30,6 +45,100 @@ describe('LSP Tools', () => {
|
||||
|
||||
// ── Tool registration ─────────────────────────────────────────────────
|
||||
|
||||
it('spawns LSP through the sidecar-owned boundary with a sanitized environment', async () => {
|
||||
const previous = process.env.WAGGLE_LSP_AMBIENT_SECRET;
|
||||
process.env.WAGGLE_LSP_AMBIENT_SECRET = 'must-not-reach-language-server';
|
||||
const fakeProcess = { pid: 4242 };
|
||||
const resolveCommand = vi.fn(async () => ({
|
||||
binary: process.execPath,
|
||||
args: ['/trusted/typescript-language-server.js', '--stdio'],
|
||||
}));
|
||||
const spawnOwned = vi.fn(() => fakeProcess);
|
||||
|
||||
try {
|
||||
const result = await spawnLspServerProcess(workspace, { resolveCommand, spawnOwned });
|
||||
expect(result).toBe(fakeProcess);
|
||||
expect(resolveCommand).toHaveBeenCalledWith(
|
||||
'typescript-language-server',
|
||||
['--stdio'],
|
||||
process.platform,
|
||||
expect.objectContaining({ env: expect.any(Object) }),
|
||||
);
|
||||
const resolvedEnv = resolveCommand.mock.calls[0][3].env as NodeJS.ProcessEnv;
|
||||
expect(resolvedEnv.WAGGLE_LSP_AMBIENT_SECRET).toBeUndefined();
|
||||
expect(spawnOwned).toHaveBeenCalledWith(
|
||||
process.execPath,
|
||||
['/trusted/typescript-language-server.js', '--stdio'],
|
||||
expect.objectContaining({
|
||||
cwd: workspace,
|
||||
env: resolvedEnv,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
}),
|
||||
);
|
||||
expect(spawnOwned.mock.calls[0][2]).not.toHaveProperty('shell');
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.WAGGLE_LSP_AMBIENT_SECRET;
|
||||
else process.env.WAGGLE_LSP_AMBIENT_SECRET = previous;
|
||||
}
|
||||
});
|
||||
|
||||
it.runIf(process.platform === 'win32')(
|
||||
'explicit LSP stop removes the supervised target and its descendant',
|
||||
async () => {
|
||||
const targetSource = [
|
||||
"const { spawn } = require('node:child_process');",
|
||||
"const descendant = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { detached: true, stdio: 'ignore' });",
|
||||
'descendant.unref();',
|
||||
'console.log(JSON.stringify({ pid: process.pid, descendantPid: descendant.pid }));',
|
||||
'setInterval(() => {}, 1000);',
|
||||
].join('\n');
|
||||
const child = await spawnLspServerProcess(process.cwd(), {
|
||||
resolveCommand: async () => ({
|
||||
binary: process.execPath,
|
||||
args: ['-e', targetSource],
|
||||
}),
|
||||
});
|
||||
const receipt = await new Promise<{ pid: number; descendantPid: number }>((resolveReceipt, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error('LSP target produced no ownership receipt')), 5_000);
|
||||
let output = '';
|
||||
child.stdout?.on('data', (chunk: Buffer) => {
|
||||
output += chunk.toString('utf8');
|
||||
const line = output.split(/\r?\n/, 1)[0];
|
||||
try {
|
||||
const parsed = JSON.parse(line) as { pid: number; descendantPid: number };
|
||||
clearTimeout(timer);
|
||||
resolveReceipt(parsed);
|
||||
} catch { /* wait for a complete JSON line */ }
|
||||
});
|
||||
});
|
||||
const isAlive = (pid: number): boolean => {
|
||||
try { process.kill(pid, 0); return true; } catch { return false; }
|
||||
};
|
||||
|
||||
try {
|
||||
await stopLspServerProcess(child, 8_000);
|
||||
await vi.waitUntil(
|
||||
() => !isAlive(receipt.pid) && !isAlive(receipt.descendantPid),
|
||||
{ timeout: 5_000, interval: 50 },
|
||||
);
|
||||
expect(isAlive(receipt.pid)).toBe(false);
|
||||
expect(isAlive(receipt.descendantPid)).toBe(false);
|
||||
} finally {
|
||||
const windowsRoot = process.env.SystemRoot ?? process.env.WINDIR ?? 'C:\\Windows';
|
||||
for (const pid of [receipt.pid, receipt.descendantPid, child.pid]) {
|
||||
if (!pid || !isAlive(pid)) continue;
|
||||
try {
|
||||
execFileSync(join(windowsRoot, 'System32', 'taskkill.exe'), [
|
||||
'/PID', String(pid), '/T', '/F',
|
||||
], { stdio: 'ignore', windowsHide: true });
|
||||
} catch { /* best-effort fixture cleanup */ }
|
||||
}
|
||||
}
|
||||
},
|
||||
20_000,
|
||||
);
|
||||
|
||||
it('creates 4 LSP tools', () => {
|
||||
expect(tools).toHaveLength(4);
|
||||
const names = tools.map(t => t.name);
|
||||
|
||||
Reference in New Issue
Block a user