pi-lens 3.1.2 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +16 -12
- package/clients/ast-grep-client.js +8 -1
- package/clients/ast-grep-client.ts +9 -1
- package/clients/biome-client.js +51 -38
- package/clients/biome-client.ts +60 -58
- package/clients/dependency-checker.js +30 -1
- package/clients/dependency-checker.ts +35 -1
- package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
- package/clients/dispatch/bus-dispatcher.js +15 -14
- package/clients/dispatch/bus-dispatcher.ts +32 -25
- package/clients/dispatch/dispatcher.js +18 -25
- package/clients/dispatch/dispatcher.test.ts +2 -1
- package/clients/dispatch/dispatcher.ts +17 -28
- package/clients/dispatch/plan.js +77 -32
- package/clients/dispatch/plan.ts +78 -32
- package/clients/dispatch/runners/ast-grep-napi.js +36 -376
- package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
- package/clients/dispatch/runners/index.js +8 -4
- package/clients/dispatch/runners/index.ts +8 -4
- package/clients/dispatch/runners/lsp.js +65 -0
- package/clients/dispatch/runners/lsp.ts +125 -0
- package/clients/dispatch/runners/oxlint.js +2 -2
- package/clients/dispatch/runners/oxlint.ts +2 -2
- package/clients/dispatch/runners/pyright.js +24 -8
- package/clients/dispatch/runners/pyright.ts +28 -14
- package/clients/dispatch/runners/rust-clippy.js +2 -2
- package/clients/dispatch/runners/rust-clippy.ts +2 -4
- package/clients/dispatch/runners/tree-sitter.js +14 -2
- package/clients/dispatch/runners/tree-sitter.ts +15 -2
- package/clients/dispatch/runners/ts-lsp.js +3 -3
- package/clients/dispatch/runners/ts-lsp.ts +8 -5
- package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
- package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
- package/clients/dispatch/types.js +3 -0
- package/clients/dispatch/types.ts +3 -0
- package/clients/formatters.js +67 -14
- package/clients/formatters.ts +68 -15
- package/clients/installer/index.js +78 -10
- package/clients/installer/index.ts +519 -426
- package/clients/jscpd-client.js +28 -0
- package/clients/jscpd-client.ts +41 -3
- package/clients/knip-client.js +30 -1
- package/clients/knip-client.ts +34 -2
- package/clients/lsp/__tests__/client.test.ts +64 -41
- package/clients/lsp/__tests__/config.test.ts +25 -17
- package/clients/lsp/__tests__/launch.test.ts +108 -43
- package/clients/lsp/__tests__/service.test.ts +76 -48
- package/clients/lsp/client.js +87 -2
- package/clients/lsp/client.ts +150 -6
- package/clients/lsp/config.js +8 -11
- package/clients/lsp/config.ts +24 -21
- package/clients/lsp/index.js +69 -0
- package/clients/lsp/index.ts +82 -0
- package/clients/lsp/interactive-install.js +19 -8
- package/clients/lsp/interactive-install.ts +52 -27
- package/clients/lsp/launch.js +182 -32
- package/clients/lsp/launch.ts +241 -38
- package/clients/lsp/path-utils.js +3 -46
- package/clients/lsp/path-utils.ts +11 -51
- package/clients/lsp/server.js +93 -71
- package/clients/lsp/server.ts +173 -131
- package/clients/path-utils.js +142 -0
- package/clients/path-utils.ts +153 -0
- package/clients/ruff-client.js +33 -4
- package/clients/ruff-client.ts +44 -13
- package/clients/safe-spawn.js +3 -1
- package/clients/safe-spawn.ts +3 -1
- package/clients/services/effect-integration.js +11 -7
- package/clients/services/effect-integration.ts +34 -26
- package/clients/sg-runner.js +51 -9
- package/clients/sg-runner.ts +58 -15
- package/clients/tree-sitter-client.js +12 -0
- package/clients/tree-sitter-client.ts +12 -0
- package/clients/typescript-client.js +6 -2
- package/clients/typescript-client.ts +9 -2
- package/commands/booboo.js +2 -4
- package/commands/booboo.ts +2 -4
- package/index.ts +377 -93
- package/package.json +2 -1
- package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
- package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
- package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
- package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
- package/tsconfig.json +1 -1
- package/clients/__tests__/file-time.test.js +0 -216
- package/clients/__tests__/format-service.test.js +0 -245
- package/clients/__tests__/formatters.test.js +0 -271
- package/clients/agent-behavior-client.test.js +0 -94
- package/clients/ast-grep-client.test.js +0 -129
- package/clients/ast-grep-client.test.ts +0 -155
- package/clients/biome-client.test.js +0 -144
- package/clients/cache-manager.test.js +0 -197
- package/clients/complexity-client.test.js +0 -234
- package/clients/dependency-checker.test.js +0 -60
- package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
- package/clients/dispatch/__tests__/runner-registration.test.js +0 -236
- package/clients/dispatch/dispatcher.edge.test.js +0 -82
- package/clients/dispatch/dispatcher.format.test.js +0 -46
- package/clients/dispatch/dispatcher.inline.test.js +0 -74
- package/clients/dispatch/dispatcher.test.js +0 -115
- package/clients/dispatch/runners/architect.test.js +0 -138
- package/clients/dispatch/runners/ast-grep-napi.test.js +0 -106
- package/clients/dispatch/runners/oxlint.test.js +0 -230
- package/clients/dispatch/runners/pyright.test.js +0 -98
- package/clients/dispatch/runners/python-slop.test.js +0 -203
- package/clients/dispatch/runners/scan_codebase.test.js +0 -89
- package/clients/dispatch/runners/shellcheck.test.js +0 -98
- package/clients/dispatch/runners/spellcheck.test.js +0 -158
- package/clients/dispatch/runners/ts-slop.test.js +0 -180
- package/clients/dispatch/runners/ts-slop.test.ts +0 -230
- package/clients/dogfood.test.js +0 -201
- package/clients/file-kinds.test.js +0 -169
- package/clients/go-client.test.js +0 -127
- package/clients/jscpd-client.test.js +0 -127
- package/clients/knip-client.test.js +0 -112
- package/clients/lsp/__tests__/client.test.js +0 -325
- package/clients/lsp/__tests__/config.test.js +0 -166
- package/clients/lsp/__tests__/error-recovery.test.js +0 -213
- package/clients/lsp/__tests__/integration.test.js +0 -127
- package/clients/lsp/__tests__/launch.test.js +0 -260
- package/clients/lsp/__tests__/server.test.js +0 -259
- package/clients/lsp/__tests__/service.test.js +0 -417
- package/clients/metrics-client.test.js +0 -141
- package/clients/ruff-client.test.js +0 -132
- package/clients/rust-client.test.js +0 -108
- package/clients/sanitize.test.js +0 -177
- package/clients/secrets-scanner.test.js +0 -100
- package/clients/services/__tests__/effect-integration.test.js +0 -86
- package/clients/test-runner-client.test.js +0 -192
- package/clients/todo-scanner.test.js +0 -301
- package/clients/type-coverage-client.test.js +0 -105
- package/clients/typescript-client.codefix.test.js +0 -157
- package/clients/typescript-client.test.js +0 -105
- package/commands/clients/ast-grep-client.js +0 -250
- package/commands/clients/ast-grep-parser.js +0 -86
- package/commands/clients/ast-grep-rule-manager.js +0 -91
- package/commands/clients/ast-grep-types.js +0 -9
- package/commands/clients/biome-client.js +0 -380
- package/commands/clients/complexity-client.js +0 -667
- package/commands/clients/file-kinds.js +0 -177
- package/commands/clients/file-utils.js +0 -40
- package/commands/clients/jscpd-client.js +0 -169
- package/commands/clients/knip-client.js +0 -211
- package/commands/clients/ruff-client.js +0 -297
- package/commands/clients/safe-spawn.js +0 -88
- package/commands/clients/scan-utils.js +0 -83
- package/commands/clients/sg-runner.js +0 -190
- package/commands/clients/types.js +0 -11
- package/commands/clients/typescript-client.js +0 -505
- package/commands/rate.test.js +0 -119
- package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
- package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
- package/rules/ast-grep-rules/rules/no-eval.yml +0 -13
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LSP Launch Utilities Test Suite
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Tests for launching LSP servers including:
|
|
5
5
|
* - Direct binary execution
|
|
6
6
|
* - Package manager execution (npx/bun)
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
* - Process cleanup
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
14
14
|
import {
|
|
15
15
|
launchLSP,
|
|
16
|
-
launchViaPackageManager,
|
|
17
16
|
launchViaNode,
|
|
17
|
+
launchViaPackageManager,
|
|
18
18
|
launchViaPython,
|
|
19
19
|
stopLSP,
|
|
20
20
|
} from "../launch.js";
|
|
@@ -27,16 +27,30 @@ vi.mock("child_process", () => ({
|
|
|
27
27
|
const mockSpawn = vi.mocked(spawn);
|
|
28
28
|
|
|
29
29
|
// Helper to create mock child process
|
|
30
|
-
function createMockChildProcess(
|
|
31
|
-
|
|
30
|
+
function createMockChildProcess(
|
|
31
|
+
pid: number = 123,
|
|
32
|
+
): Partial<ChildProcess> & { _emit: (event: string, ...args: any[]) => void } {
|
|
33
|
+
const handlers = new Map<string, Array<(...args: any[]) => void>>();
|
|
34
|
+
const mockObj: any = {
|
|
32
35
|
pid,
|
|
33
36
|
stdin: { write: vi.fn() } as any,
|
|
34
37
|
stdout: { on: vi.fn(), pipe: vi.fn() } as any,
|
|
35
38
|
stderr: { on: vi.fn(), pipe: vi.fn() } as any,
|
|
36
39
|
kill: vi.fn(),
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
exitCode: null,
|
|
41
|
+
killed: false,
|
|
42
|
+
on(event: string, handler: (...args: any[]) => void) {
|
|
43
|
+
if (!handlers.has(event)) {
|
|
44
|
+
handlers.set(event, []);
|
|
45
|
+
}
|
|
46
|
+
handlers.get(event)?.push(handler);
|
|
47
|
+
return mockObj;
|
|
48
|
+
},
|
|
49
|
+
_emit(event: string, ...args: any[]) {
|
|
50
|
+
handlers.get(event)?.forEach((h) => h(...args));
|
|
51
|
+
},
|
|
39
52
|
};
|
|
53
|
+
return mockObj;
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
describe("launchLSP", () => {
|
|
@@ -48,11 +62,20 @@ describe("launchLSP", () => {
|
|
|
48
62
|
const mockProcess = createMockChildProcess();
|
|
49
63
|
mockSpawn.mockReturnValue(mockProcess as ChildProcess);
|
|
50
64
|
|
|
51
|
-
|
|
65
|
+
// Start the async operation
|
|
66
|
+
const launchPromise = launchLSP("typescript-language-server", ["--stdio"], {
|
|
67
|
+
cwd: "/test",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Let the 50ms timeout pass
|
|
71
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
72
|
+
|
|
73
|
+
// Now complete by resolving
|
|
74
|
+
const _result = await launchPromise;
|
|
52
75
|
|
|
53
76
|
expect(mockSpawn).toHaveBeenCalled();
|
|
54
|
-
const [cmd,
|
|
55
|
-
|
|
77
|
+
const [cmd, _args, options] = mockSpawn.mock.calls[0];
|
|
78
|
+
|
|
56
79
|
// Command should be provided (format depends on platform)
|
|
57
80
|
expect(cmd).toBeTruthy();
|
|
58
81
|
expect(options).toMatchObject({
|
|
@@ -68,7 +91,10 @@ describe("launchLSP", () => {
|
|
|
68
91
|
mockSpawn.mockReturnValue(mockProcess as ChildProcess);
|
|
69
92
|
|
|
70
93
|
const customEnv = { CUSTOM_VAR: "value" };
|
|
71
|
-
launchLSP("server", [], { env: customEnv });
|
|
94
|
+
const launchPromise = launchLSP("server", [], { env: customEnv });
|
|
95
|
+
|
|
96
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
97
|
+
await launchPromise;
|
|
72
98
|
|
|
73
99
|
const [, , options] = mockSpawn.mock.calls[0];
|
|
74
100
|
expect(options?.env).toMatchObject({
|
|
@@ -80,7 +106,9 @@ describe("launchLSP", () => {
|
|
|
80
106
|
const mockProcess = createMockChildProcess(456);
|
|
81
107
|
mockSpawn.mockReturnValue(mockProcess as ChildProcess);
|
|
82
108
|
|
|
83
|
-
const
|
|
109
|
+
const launchPromise = launchLSP("server", [], {});
|
|
110
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
111
|
+
const result = await launchPromise;
|
|
84
112
|
|
|
85
113
|
expect(result.pid).toBe(456);
|
|
86
114
|
expect(result.stdin).toBeDefined();
|
|
@@ -89,31 +117,55 @@ describe("launchLSP", () => {
|
|
|
89
117
|
});
|
|
90
118
|
|
|
91
119
|
it("should throw if stdin is not available", async () => {
|
|
92
|
-
const
|
|
120
|
+
const baseMock = createMockChildProcess();
|
|
121
|
+
const mockProcess = {
|
|
122
|
+
...baseMock,
|
|
123
|
+
stdin: null,
|
|
124
|
+
on: baseMock.on,
|
|
125
|
+
_emit: baseMock._emit,
|
|
126
|
+
};
|
|
93
127
|
mockSpawn.mockReturnValue(mockProcess as ChildProcess);
|
|
94
128
|
|
|
95
|
-
|
|
129
|
+
// Use wrapper function for proper async error catching
|
|
130
|
+
const launchFn = async () => launchLSP("server", [], {});
|
|
131
|
+
await expect(launchFn()).rejects.toThrow("Failed to spawn LSP server");
|
|
96
132
|
});
|
|
97
133
|
|
|
98
134
|
it("should throw if stdout is not available", async () => {
|
|
99
|
-
const
|
|
135
|
+
const baseMock = createMockChildProcess();
|
|
136
|
+
const mockProcess = {
|
|
137
|
+
...baseMock,
|
|
138
|
+
stdout: null,
|
|
139
|
+
on: baseMock.on,
|
|
140
|
+
_emit: baseMock._emit,
|
|
141
|
+
};
|
|
100
142
|
mockSpawn.mockReturnValue(mockProcess as ChildProcess);
|
|
101
143
|
|
|
102
|
-
|
|
144
|
+
const launchFn = async () => launchLSP("server", [], {});
|
|
145
|
+
await expect(launchFn()).rejects.toThrow("Failed to spawn LSP server");
|
|
103
146
|
});
|
|
104
147
|
|
|
105
148
|
it("should throw if stderr is not available", async () => {
|
|
106
|
-
const
|
|
149
|
+
const baseMock = createMockChildProcess();
|
|
150
|
+
const mockProcess = {
|
|
151
|
+
...baseMock,
|
|
152
|
+
stderr: null,
|
|
153
|
+
on: baseMock.on,
|
|
154
|
+
_emit: baseMock._emit,
|
|
155
|
+
};
|
|
107
156
|
mockSpawn.mockReturnValue(mockProcess as ChildProcess);
|
|
108
157
|
|
|
109
|
-
|
|
158
|
+
const launchFn = async () => launchLSP("server", [], {});
|
|
159
|
+
await expect(launchFn()).rejects.toThrow("Failed to spawn LSP server");
|
|
110
160
|
});
|
|
111
161
|
|
|
112
162
|
it("should default to process.cwd() when cwd not provided", async () => {
|
|
113
163
|
const mockProcess = createMockChildProcess();
|
|
114
164
|
mockSpawn.mockReturnValue(mockProcess as ChildProcess);
|
|
115
165
|
|
|
116
|
-
launchLSP("server", [], {});
|
|
166
|
+
const launchPromise = launchLSP("server", [], {});
|
|
167
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
168
|
+
await launchPromise;
|
|
117
169
|
|
|
118
170
|
const [, , options] = mockSpawn.mock.calls[0];
|
|
119
171
|
expect(options?.cwd).toBe(process.cwd());
|
|
@@ -124,11 +176,16 @@ describe("launchLSP", () => {
|
|
|
124
176
|
mockSpawn.mockReturnValue(mockProcess as ChildProcess);
|
|
125
177
|
|
|
126
178
|
// Save original platform
|
|
127
|
-
const originalPlatform = Object.getOwnPropertyDescriptor(
|
|
179
|
+
const originalPlatform = Object.getOwnPropertyDescriptor(
|
|
180
|
+
process,
|
|
181
|
+
"platform",
|
|
182
|
+
);
|
|
128
183
|
Object.defineProperty(process, "platform", { value: "win32" });
|
|
129
184
|
|
|
130
185
|
try {
|
|
131
|
-
launchLSP("server.cmd", ["--arg"], {});
|
|
186
|
+
const launchPromise = launchLSP("server.cmd", ["--arg"], {});
|
|
187
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
188
|
+
await launchPromise;
|
|
132
189
|
|
|
133
190
|
const [, , options] = mockSpawn.mock.calls[0];
|
|
134
191
|
expect(options?.shell).toBe(true);
|
|
@@ -171,8 +228,9 @@ describe("launchViaPackageManager", () => {
|
|
|
171
228
|
const [cmd, args] = mockSpawn.mock.calls[0];
|
|
172
229
|
// On Windows with shell mode, args may be concatenated into cmd
|
|
173
230
|
const cmdStr = String(cmd);
|
|
174
|
-
const hasBun =
|
|
175
|
-
|
|
231
|
+
const hasBun =
|
|
232
|
+
cmdStr.includes("bun") || args?.some((a: string) => a?.includes("bun"));
|
|
233
|
+
const _hasX = cmdStr.includes(" x ") || args?.includes("x");
|
|
176
234
|
expect(hasBun).toBe(true);
|
|
177
235
|
// Note: 'x' arg may be in command string on Windows
|
|
178
236
|
});
|
|
@@ -200,8 +258,8 @@ describe("launchViaNode", () => {
|
|
|
200
258
|
launchViaNode("/path/to/script.js", ["--arg"], { cwd: "/test" });
|
|
201
259
|
|
|
202
260
|
expect(mockSpawn).toHaveBeenCalled();
|
|
203
|
-
const [cmd,
|
|
204
|
-
|
|
261
|
+
const [cmd, _args, options] = mockSpawn.mock.calls[0];
|
|
262
|
+
|
|
205
263
|
// On Windows, command is combined; on Unix, it's separate
|
|
206
264
|
expect(String(cmd)).toContain("node");
|
|
207
265
|
expect(options?.cwd).toBe("/test");
|
|
@@ -228,14 +286,19 @@ describe("launchViaPython", () => {
|
|
|
228
286
|
const mockProcess = createMockChildProcess();
|
|
229
287
|
mockSpawn.mockReturnValue(mockProcess as ChildProcess);
|
|
230
288
|
|
|
231
|
-
launchViaPython("pylsp", ["--verbose", "--log-file", "/tmp/log"], {});
|
|
289
|
+
await launchViaPython("pylsp", ["--verbose", "--log-file", "/tmp/log"], {});
|
|
232
290
|
|
|
233
291
|
expect(mockSpawn).toHaveBeenCalled();
|
|
234
292
|
const [cmd, args] = mockSpawn.mock.calls[0];
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
293
|
+
// On Windows with shell mode, args may be combined into cmd string
|
|
294
|
+
// Check that the command contains all expected parts
|
|
295
|
+
const fullCommand =
|
|
296
|
+
typeof cmd === "string" && Array.isArray(args) && args.length === 0
|
|
297
|
+
? cmd // shell mode: everything in cmd
|
|
298
|
+
: `${cmd} ${args.join(" ")}`; // normal mode
|
|
299
|
+
expect(fullCommand).toContain("-m");
|
|
300
|
+
expect(fullCommand).toContain("pylsp");
|
|
301
|
+
expect(fullCommand).toContain("--verbose");
|
|
239
302
|
});
|
|
240
303
|
});
|
|
241
304
|
|
|
@@ -252,7 +315,7 @@ describe("stopLSP", () => {
|
|
|
252
315
|
it("should send SIGTERM first", async () => {
|
|
253
316
|
const mockKill = vi.fn();
|
|
254
317
|
const exitHandlers: Array<() => void> = [];
|
|
255
|
-
|
|
318
|
+
|
|
256
319
|
const mockProcess = {
|
|
257
320
|
...createMockChildProcess(),
|
|
258
321
|
kill: mockKill,
|
|
@@ -265,7 +328,7 @@ describe("stopLSP", () => {
|
|
|
265
328
|
}),
|
|
266
329
|
};
|
|
267
330
|
|
|
268
|
-
const
|
|
331
|
+
const _stopPromise = stopLSP({
|
|
269
332
|
process: mockProcess as any,
|
|
270
333
|
stdin: {} as any,
|
|
271
334
|
stdout: {} as any,
|
|
@@ -274,8 +337,8 @@ describe("stopLSP", () => {
|
|
|
274
337
|
});
|
|
275
338
|
|
|
276
339
|
// Let the exit handler fire
|
|
277
|
-
await new Promise(r => setTimeout(r, 20));
|
|
278
|
-
|
|
340
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
341
|
+
|
|
279
342
|
expect(mockKill).toHaveBeenCalledWith("SIGTERM");
|
|
280
343
|
});
|
|
281
344
|
|
|
@@ -295,13 +358,15 @@ describe("stopLSP", () => {
|
|
|
295
358
|
}),
|
|
296
359
|
};
|
|
297
360
|
|
|
298
|
-
await expect(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
361
|
+
await expect(
|
|
362
|
+
stopLSP({
|
|
363
|
+
process: mockProcess as any,
|
|
364
|
+
stdin: {} as any,
|
|
365
|
+
stdout: {} as any,
|
|
366
|
+
stderr: {} as any,
|
|
367
|
+
pid: 123,
|
|
368
|
+
}),
|
|
369
|
+
).resolves.toBeUndefined();
|
|
305
370
|
});
|
|
306
371
|
|
|
307
372
|
it("should resolve on error event", async () => {
|
|
@@ -323,7 +388,7 @@ describe("stopLSP", () => {
|
|
|
323
388
|
});
|
|
324
389
|
|
|
325
390
|
// Let the error handler fire
|
|
326
|
-
await new Promise(r => setTimeout(r, 20));
|
|
391
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
327
392
|
await expect(stopPromise).resolves.toBeUndefined();
|
|
328
393
|
});
|
|
329
394
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LSP Service Test Suite
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Tests for the LSP Service layer including:
|
|
5
5
|
* - Client lifecycle management
|
|
6
6
|
* - Effect-TS integration
|
|
@@ -9,15 +9,21 @@
|
|
|
9
9
|
* - Cleanup and shutdown
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
13
12
|
import { Effect } from "effect";
|
|
14
|
-
import {
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
15
14
|
import type { LSPClientInfo } from "../client.js";
|
|
15
|
+
import {
|
|
16
|
+
getLSPService,
|
|
17
|
+
LSPService,
|
|
18
|
+
lspEffect,
|
|
19
|
+
resetLSPService,
|
|
20
|
+
} from "../index.js";
|
|
16
21
|
import type { LSPServerInfo } from "../server.js";
|
|
17
22
|
|
|
18
23
|
// Mock the dependencies
|
|
19
24
|
vi.mock("../server.js", async () => {
|
|
20
|
-
const actual =
|
|
25
|
+
const actual =
|
|
26
|
+
await vi.importActual<typeof import("../server.js")>("../server.js");
|
|
21
27
|
return {
|
|
22
28
|
...actual,
|
|
23
29
|
getServersForFileWithConfig: vi.fn(),
|
|
@@ -41,8 +47,8 @@ vi.mock("../client.js", () => ({
|
|
|
41
47
|
createLSPClient: vi.fn(),
|
|
42
48
|
}));
|
|
43
49
|
|
|
44
|
-
import { getServersForFileWithConfig as mockGetServersForFile } from "../config.js";
|
|
45
50
|
import { createLSPClient as mockCreateLSPClient } from "../client.js";
|
|
51
|
+
import { getServersForFileWithConfig as mockGetServersForFile } from "../config.js";
|
|
46
52
|
|
|
47
53
|
describe("LSPService", () => {
|
|
48
54
|
let service: LSPService;
|
|
@@ -74,7 +80,7 @@ describe("LSPService", () => {
|
|
|
74
80
|
describe("hasLSP", () => {
|
|
75
81
|
it("should return false when no servers match file extension", async () => {
|
|
76
82
|
vi.mocked(mockGetServersForFile).mockReturnValue([]);
|
|
77
|
-
|
|
83
|
+
|
|
78
84
|
const result = await service.hasLSP("/test/file.unknown");
|
|
79
85
|
expect(result).toBe(false);
|
|
80
86
|
});
|
|
@@ -88,7 +94,7 @@ describe("LSPService", () => {
|
|
|
88
94
|
spawn: vi.fn(),
|
|
89
95
|
};
|
|
90
96
|
vi.mocked(mockGetServersForFile).mockReturnValue([mockServer]);
|
|
91
|
-
|
|
97
|
+
|
|
92
98
|
const result = await service.hasLSP("/project/test.ts");
|
|
93
99
|
expect(result).toBe(true);
|
|
94
100
|
});
|
|
@@ -102,7 +108,7 @@ describe("LSPService", () => {
|
|
|
102
108
|
spawn: vi.fn(),
|
|
103
109
|
};
|
|
104
110
|
vi.mocked(mockGetServersForFile).mockReturnValue([mockServer]);
|
|
105
|
-
|
|
111
|
+
|
|
106
112
|
const result = await service.hasLSP("/project/test.ts");
|
|
107
113
|
expect(result).toBe(false);
|
|
108
114
|
});
|
|
@@ -123,7 +129,7 @@ describe("LSPService", () => {
|
|
|
123
129
|
spawn: vi.fn(),
|
|
124
130
|
};
|
|
125
131
|
vi.mocked(mockGetServersForFile).mockReturnValue([server1, server2]);
|
|
126
|
-
|
|
132
|
+
|
|
127
133
|
const result = await service.hasLSP("/project/test.ts");
|
|
128
134
|
expect(result).toBe(true);
|
|
129
135
|
expect(server1.root).toHaveBeenCalled();
|
|
@@ -134,7 +140,7 @@ describe("LSPService", () => {
|
|
|
134
140
|
describe("getClientForFile", () => {
|
|
135
141
|
it("should return undefined when no servers match", async () => {
|
|
136
142
|
vi.mocked(mockGetServersForFile).mockReturnValue([]);
|
|
137
|
-
|
|
143
|
+
|
|
138
144
|
const result = await service.getClientForFile("/test/file.unknown");
|
|
139
145
|
expect(result).toBeUndefined();
|
|
140
146
|
});
|
|
@@ -152,12 +158,12 @@ describe("LSPService", () => {
|
|
|
152
158
|
};
|
|
153
159
|
vi.mocked(mockGetServersForFile).mockReturnValue([mockServer]);
|
|
154
160
|
vi.mocked(mockCreateLSPClient).mockResolvedValue(mockClient as any);
|
|
155
|
-
|
|
161
|
+
|
|
156
162
|
// First call creates client
|
|
157
163
|
const result1 = await service.getClientForFile("/project/test.ts");
|
|
158
164
|
expect(result1).toBeDefined();
|
|
159
165
|
expect(mockCreateLSPClient).toHaveBeenCalledTimes(1);
|
|
160
|
-
|
|
166
|
+
|
|
161
167
|
// Second call returns cached client
|
|
162
168
|
const result2 = await service.getClientForFile("/project/other.ts");
|
|
163
169
|
expect(result2?.client).toBe(result1?.client);
|
|
@@ -174,7 +180,7 @@ describe("LSPService", () => {
|
|
|
174
180
|
spawn: mockSpawn as any,
|
|
175
181
|
};
|
|
176
182
|
vi.mocked(mockGetServersForFile).mockReturnValue([mockServer]);
|
|
177
|
-
|
|
183
|
+
|
|
178
184
|
const result = await service.getClientForFile("/project/test.ts");
|
|
179
185
|
expect(result).toBeUndefined();
|
|
180
186
|
});
|
|
@@ -197,9 +203,12 @@ describe("LSPService", () => {
|
|
|
197
203
|
process: { pid: 456 } as any,
|
|
198
204
|
}),
|
|
199
205
|
};
|
|
200
|
-
vi.mocked(mockGetServersForFile).mockReturnValue([
|
|
206
|
+
vi.mocked(mockGetServersForFile).mockReturnValue([
|
|
207
|
+
failingServer,
|
|
208
|
+
workingServer,
|
|
209
|
+
]);
|
|
201
210
|
vi.mocked(mockCreateLSPClient).mockResolvedValue(mockClient as any);
|
|
202
|
-
|
|
211
|
+
|
|
203
212
|
const result = await service.getClientForFile("/project/test.ts");
|
|
204
213
|
expect(result).toBeDefined();
|
|
205
214
|
expect(result?.info.id).toBe("server2");
|
|
@@ -220,22 +229,23 @@ describe("LSPService", () => {
|
|
|
220
229
|
};
|
|
221
230
|
vi.mocked(mockGetServersForFile).mockReturnValue([mockServer]);
|
|
222
231
|
vi.mocked(mockCreateLSPClient).mockResolvedValue(mockClient as any);
|
|
223
|
-
|
|
232
|
+
|
|
224
233
|
await service.openFile("/project/test.ts", "const x = 1;");
|
|
225
|
-
|
|
234
|
+
|
|
226
235
|
expect(mockClient.notify.open).toHaveBeenCalledWith(
|
|
227
236
|
"/project/test.ts",
|
|
228
237
|
"const x = 1;",
|
|
229
|
-
"typescript"
|
|
238
|
+
"typescript",
|
|
230
239
|
);
|
|
231
240
|
});
|
|
232
241
|
|
|
233
242
|
it("should do nothing when no LSP available", async () => {
|
|
234
243
|
vi.mocked(mockGetServersForFile).mockReturnValue([]);
|
|
235
|
-
|
|
244
|
+
|
|
236
245
|
// Should not throw
|
|
237
|
-
await expect(
|
|
238
|
-
.
|
|
246
|
+
await expect(
|
|
247
|
+
service.openFile("/test.unknown", "content"),
|
|
248
|
+
).resolves.not.toThrow();
|
|
239
249
|
});
|
|
240
250
|
});
|
|
241
251
|
|
|
@@ -253,12 +263,12 @@ describe("LSPService", () => {
|
|
|
253
263
|
};
|
|
254
264
|
vi.mocked(mockGetServersForFile).mockReturnValue([mockServer]);
|
|
255
265
|
vi.mocked(mockCreateLSPClient).mockResolvedValue(mockClient as any);
|
|
256
|
-
|
|
266
|
+
|
|
257
267
|
await service.updateFile("/project/test.ts", "const x = 2;");
|
|
258
|
-
|
|
268
|
+
|
|
259
269
|
expect(mockClient.notify.change).toHaveBeenCalledWith(
|
|
260
270
|
"/project/test.ts",
|
|
261
|
-
"const x = 2;"
|
|
271
|
+
"const x = 2;",
|
|
262
272
|
);
|
|
263
273
|
});
|
|
264
274
|
});
|
|
@@ -287,16 +297,16 @@ describe("LSPService", () => {
|
|
|
287
297
|
};
|
|
288
298
|
vi.mocked(mockGetServersForFile).mockReturnValue([mockServer]);
|
|
289
299
|
vi.mocked(mockCreateLSPClient).mockResolvedValue(mockClient as any);
|
|
290
|
-
|
|
300
|
+
|
|
291
301
|
const result = await service.getDiagnostics("/project/test.ts");
|
|
292
|
-
|
|
302
|
+
|
|
293
303
|
expect(result).toEqual(mockDiagnostics);
|
|
294
304
|
expect(mockClient.waitForDiagnostics).toHaveBeenCalled();
|
|
295
305
|
});
|
|
296
306
|
|
|
297
307
|
it("should return empty array when no LSP available", async () => {
|
|
298
308
|
vi.mocked(mockGetServersForFile).mockReturnValue([]);
|
|
299
|
-
|
|
309
|
+
|
|
300
310
|
const result = await service.getDiagnostics("/test.unknown");
|
|
301
311
|
expect(result).toEqual([]);
|
|
302
312
|
});
|
|
@@ -306,7 +316,7 @@ describe("LSPService", () => {
|
|
|
306
316
|
it("should shutdown all clients", async () => {
|
|
307
317
|
const mockClient1 = createMockClient();
|
|
308
318
|
const mockClient2 = createMockClient();
|
|
309
|
-
|
|
319
|
+
|
|
310
320
|
// Add clients to service
|
|
311
321
|
const mockServer1: LSPServerInfo = {
|
|
312
322
|
id: "typescript",
|
|
@@ -322,29 +332,31 @@ describe("LSPService", () => {
|
|
|
322
332
|
root: vi.fn().mockResolvedValue("/project2"),
|
|
323
333
|
spawn: vi.fn().mockResolvedValue({ process: { pid: 2 } as any }),
|
|
324
334
|
};
|
|
325
|
-
|
|
335
|
+
|
|
326
336
|
vi.mocked(mockGetServersForFile)
|
|
327
337
|
.mockReturnValueOnce([mockServer1])
|
|
328
338
|
.mockReturnValueOnce([mockServer2]);
|
|
329
|
-
|
|
339
|
+
|
|
330
340
|
vi.mocked(mockCreateLSPClient)
|
|
331
341
|
.mockResolvedValueOnce(mockClient1)
|
|
332
342
|
.mockResolvedValueOnce(mockClient2);
|
|
333
|
-
|
|
343
|
+
|
|
334
344
|
// Create clients
|
|
335
345
|
await service.getClientForFile("/project1/test.ts");
|
|
336
346
|
await service.getClientForFile("/project2/test.py");
|
|
337
|
-
|
|
347
|
+
|
|
338
348
|
await service.shutdown();
|
|
339
|
-
|
|
349
|
+
|
|
340
350
|
expect(mockClient1.shutdown).toHaveBeenCalled();
|
|
341
351
|
expect(mockClient2.shutdown).toHaveBeenCalled();
|
|
342
352
|
});
|
|
343
353
|
|
|
344
354
|
it("should handle shutdown errors gracefully", async () => {
|
|
345
355
|
const mockClient = createMockClient();
|
|
346
|
-
(mockClient.shutdown as any).mockRejectedValue(
|
|
347
|
-
|
|
356
|
+
(mockClient.shutdown as any).mockRejectedValue(
|
|
357
|
+
new Error("Shutdown failed"),
|
|
358
|
+
);
|
|
359
|
+
|
|
348
360
|
const mockServer: LSPServerInfo = {
|
|
349
361
|
id: "typescript",
|
|
350
362
|
name: "TypeScript Server",
|
|
@@ -354,9 +366,9 @@ describe("LSPService", () => {
|
|
|
354
366
|
};
|
|
355
367
|
vi.mocked(mockGetServersForFile).mockReturnValue([mockServer]);
|
|
356
368
|
vi.mocked(mockCreateLSPClient).mockResolvedValue(mockClient as any);
|
|
357
|
-
|
|
369
|
+
|
|
358
370
|
await service.getClientForFile("/project/test.ts");
|
|
359
|
-
|
|
371
|
+
|
|
360
372
|
// Should not throw even when client shutdown fails
|
|
361
373
|
await expect(service.shutdown()).resolves.not.toThrow();
|
|
362
374
|
});
|
|
@@ -374,9 +386,9 @@ describe("LSPService", () => {
|
|
|
374
386
|
};
|
|
375
387
|
vi.mocked(mockGetServersForFile).mockReturnValue([mockServer]);
|
|
376
388
|
vi.mocked(mockCreateLSPClient).mockResolvedValue(mockClient as any);
|
|
377
|
-
|
|
389
|
+
|
|
378
390
|
await service.getClientForFile("/project/test.ts");
|
|
379
|
-
|
|
391
|
+
|
|
380
392
|
const status = service.getStatus();
|
|
381
393
|
expect(status).toHaveLength(1);
|
|
382
394
|
expect(status[0]).toEqual({
|
|
@@ -416,17 +428,17 @@ describe("lspEffect", () => {
|
|
|
416
428
|
};
|
|
417
429
|
vi.mocked(mockGetServersForFile).mockReturnValue([mockServer]);
|
|
418
430
|
vi.mocked(mockCreateLSPClient).mockResolvedValue(mockClient as any);
|
|
419
|
-
|
|
431
|
+
|
|
420
432
|
const program = effect.openFile("/project/test.ts", "content");
|
|
421
433
|
const result = await Effect.runPromise(program);
|
|
422
|
-
|
|
434
|
+
|
|
423
435
|
expect(result).toBeUndefined();
|
|
424
436
|
expect(mockClient.notify.open).toHaveBeenCalled();
|
|
425
437
|
});
|
|
426
438
|
|
|
427
439
|
it("should handle errors in Effect", async () => {
|
|
428
440
|
vi.mocked(mockGetServersForFile).mockReturnValue([]);
|
|
429
|
-
|
|
441
|
+
|
|
430
442
|
const program = effect.openFile("/test.unknown", "content");
|
|
431
443
|
// Should complete successfully (no-op for unknown files)
|
|
432
444
|
await expect(Effect.runPromise(program)).resolves.not.toThrow();
|
|
@@ -435,7 +447,16 @@ describe("lspEffect", () => {
|
|
|
435
447
|
|
|
436
448
|
describe("getDiagnostics Effect", () => {
|
|
437
449
|
it("should wrap getDiagnostics in Effect", async () => {
|
|
438
|
-
const mockDiagnostics = [
|
|
450
|
+
const mockDiagnostics = [
|
|
451
|
+
{
|
|
452
|
+
severity: 1,
|
|
453
|
+
message: "Error",
|
|
454
|
+
range: {
|
|
455
|
+
start: { line: 0, character: 0 },
|
|
456
|
+
end: { line: 0, character: 1 },
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
];
|
|
439
460
|
const mockClient = createMockClient(mockDiagnostics);
|
|
440
461
|
const mockServer: LSPServerInfo = {
|
|
441
462
|
id: "typescript",
|
|
@@ -446,10 +467,10 @@ describe("lspEffect", () => {
|
|
|
446
467
|
};
|
|
447
468
|
vi.mocked(mockGetServersForFile).mockReturnValue([mockServer]);
|
|
448
469
|
vi.mocked(mockCreateLSPClient).mockResolvedValue(mockClient as any);
|
|
449
|
-
|
|
470
|
+
|
|
450
471
|
const program = effect.getDiagnostics("/project/test.ts");
|
|
451
472
|
const result = await Effect.runPromise(program);
|
|
452
|
-
|
|
473
|
+
|
|
453
474
|
expect(result).toEqual(mockDiagnostics);
|
|
454
475
|
});
|
|
455
476
|
});
|
|
@@ -464,10 +485,10 @@ describe("lspEffect", () => {
|
|
|
464
485
|
spawn: vi.fn(),
|
|
465
486
|
};
|
|
466
487
|
vi.mocked(mockGetServersForFile).mockReturnValue([mockServer]);
|
|
467
|
-
|
|
488
|
+
|
|
468
489
|
const program = effect.hasLSP("/project/test.ts");
|
|
469
490
|
const result = await Effect.runPromise(program);
|
|
470
|
-
|
|
491
|
+
|
|
471
492
|
expect(result).toBe(true);
|
|
472
493
|
});
|
|
473
494
|
});
|
|
@@ -476,7 +497,7 @@ describe("lspEffect", () => {
|
|
|
476
497
|
it("should wrap shutdown in Effect", async () => {
|
|
477
498
|
const program = effect.shutdown();
|
|
478
499
|
const result = await Effect.runPromise(program);
|
|
479
|
-
|
|
500
|
+
|
|
480
501
|
expect(result).toBeUndefined();
|
|
481
502
|
});
|
|
482
503
|
});
|
|
@@ -493,7 +514,14 @@ function createMockClient(diagnostics: any[] = []): LSPClientInfo {
|
|
|
493
514
|
change: vi.fn().mockResolvedValue(undefined),
|
|
494
515
|
},
|
|
495
516
|
getDiagnostics: vi.fn().mockReturnValue(diagnostics),
|
|
517
|
+
getAllDiagnostics: vi.fn().mockReturnValue(new Map()),
|
|
496
518
|
waitForDiagnostics: vi.fn().mockResolvedValue(undefined),
|
|
519
|
+
definition: vi.fn().mockResolvedValue([]),
|
|
520
|
+
references: vi.fn().mockResolvedValue([]),
|
|
521
|
+
hover: vi.fn().mockResolvedValue(null),
|
|
522
|
+
documentSymbol: vi.fn().mockResolvedValue([]),
|
|
523
|
+
workspaceSymbol: vi.fn().mockResolvedValue([]),
|
|
524
|
+
implementation: vi.fn().mockResolvedValue([]),
|
|
497
525
|
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
498
526
|
};
|
|
499
527
|
}
|