@theupsider/lsp-mcp 1.0.0 → 1.0.7

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.
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const promises_1 = require("node:fs/promises");
4
4
  const read_tools_1 = require("../tools/read-tools");
5
5
  const vscode_languageserver_protocol_1 = require("vscode-languageserver-protocol");
6
- jest.mock('node:fs/promises', () => ({
7
- stat: jest.fn()
6
+ jest.mock("node:fs/promises", () => ({
7
+ stat: jest.fn(),
8
8
  }));
9
9
  class FakeRegistrar {
10
10
  tools = new Map();
@@ -12,242 +12,449 @@ class FakeRegistrar {
12
12
  this.tools.set(name, { description: config.description, handler });
13
13
  }
14
14
  }
15
- describe('registerReadTools', () => {
15
+ describe("registerReadTools", () => {
16
16
  beforeEach(() => {
17
17
  jest.clearAllMocks();
18
- promises_1.stat.mockResolvedValue({ isDirectory: () => true });
18
+ promises_1.stat.mockResolvedValue({
19
+ isDirectory: () => true,
20
+ });
19
21
  });
20
- it('initializes LSP with a valid root and reports health', async () => {
22
+ it("initializes LSP with a valid root and reports health", async () => {
21
23
  const registrar = new FakeRegistrar();
22
24
  const initializeManager = jest.fn().mockResolvedValue({
23
- root: '/workspace',
25
+ root: "/workspace",
24
26
  health: [
25
- { language: 'typescript', status: 'ready' },
26
- { language: 'python', status: 'error', error: 'missing pylsp' }
27
- ]
28
- });
29
- (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), { initializeManager });
30
- await expect(getHandler(registrar, 'lsp_init')({ root: '/workspace' })).resolves.toEqual({
31
- content: [{ type: 'text', text: 'Initialized LSP for /workspace. Detected languages: Typescript, Python. LSP servers: 1 started, 1 errors.' }],
32
- text: 'Initialized LSP for /workspace. Detected languages: Typescript, Python. LSP servers: 1 started, 1 errors.',
27
+ { language: "typescript", status: "ready" },
28
+ { language: "python", status: "error", error: "missing pylsp" },
29
+ ],
30
+ });
31
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
32
+ initializeManager,
33
+ });
34
+ await expect(getHandler(registrar, "lsp_init")({ root: "/workspace" })).resolves.toEqual({
35
+ content: [
36
+ {
37
+ type: "text",
38
+ text: "Initialized LSP for /workspace. Detected languages: Typescript, Python. LSP servers: 1 started, 1 errors.",
39
+ },
40
+ ],
41
+ text: "Initialized LSP for /workspace. Detected languages: Typescript, Python. LSP servers: 1 started, 1 errors.",
33
42
  raw: {
34
- root: '/workspace',
35
- languages: ['Typescript', 'Python'],
43
+ root: "/workspace",
44
+ languages: ["Typescript", "Python"],
36
45
  health: [
37
- { language: 'typescript', status: 'ready' },
38
- { language: 'python', status: 'error', error: 'missing pylsp' }
39
- ]
40
- }
46
+ { language: "typescript", status: "ready" },
47
+ { language: "python", status: "error", error: "missing pylsp" },
48
+ ],
49
+ },
41
50
  });
42
- expect(initializeManager).toHaveBeenCalledWith('/workspace', undefined);
51
+ expect(initializeManager).toHaveBeenCalledWith("/workspace", undefined);
43
52
  });
44
- it('rejects invalid lsp_init roots clearly', async () => {
53
+ it("rejects invalid lsp_init roots clearly", async () => {
45
54
  const registrar = new FakeRegistrar();
46
55
  const initializeManager = jest.fn();
47
- promises_1.stat.mockRejectedValueOnce(new Error('ENOENT'));
48
- (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), { initializeManager });
49
- await expect(getHandler(registrar, 'lsp_init')({ root: '/missing/project' })).resolves.toEqual({
50
- content: [{ type: 'text', text: 'Project root does not exist: /missing/project' }],
56
+ promises_1.stat.mockRejectedValueOnce(new Error("ENOENT"));
57
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
58
+ initializeManager,
59
+ });
60
+ await expect(getHandler(registrar, "lsp_init")({ root: "/missing/project" })).resolves.toEqual({
61
+ content: [
62
+ { type: "text", text: "Project root does not exist: /missing/project" },
63
+ ],
51
64
  error: true,
52
- raw: null
65
+ raw: null,
53
66
  });
54
67
  expect(initializeManager).not.toHaveBeenCalled();
55
68
  });
56
- it('rejects missing, relative, and non-directory lsp_init roots', async () => {
69
+ it("rejects missing, relative, and non-directory lsp_init roots", async () => {
57
70
  const registrar = new FakeRegistrar();
58
71
  const initializeManager = jest.fn();
59
- (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), { initializeManager });
60
- await expect(getHandler(registrar, 'lsp_init')({})).resolves.toEqual({
61
- content: [{ type: 'text', text: 'Project root is required. Provide lsp_init({ root: \'/absolute/path\' }).' }],
72
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
73
+ initializeManager,
74
+ });
75
+ await expect(getHandler(registrar, "lsp_init")({})).resolves.toEqual({
76
+ content: [
77
+ {
78
+ type: "text",
79
+ text: "Project root is required. Provide lsp_init({ root: '/absolute/path' }).",
80
+ },
81
+ ],
62
82
  error: true,
63
- raw: null
83
+ raw: null,
64
84
  });
65
- await expect(getHandler(registrar, 'lsp_init')({ root: 'relative/path' })).resolves.toEqual({
66
- content: [{ type: 'text', text: 'Project root must be an absolute path: relative/path' }],
85
+ await expect(getHandler(registrar, "lsp_init")({ root: "relative/path" })).resolves.toEqual({
86
+ content: [
87
+ {
88
+ type: "text",
89
+ text: "Project root must be an absolute path: relative/path",
90
+ },
91
+ ],
67
92
  error: true,
68
- raw: null
93
+ raw: null,
69
94
  });
70
- promises_1.stat.mockResolvedValueOnce({ isDirectory: () => false });
71
- await expect(getHandler(registrar, 'lsp_init')({ root: '/workspace/file.ts' })).resolves.toEqual({
72
- content: [{ type: 'text', text: 'Project root is not a directory: /workspace/file.ts' }],
95
+ promises_1.stat.mockResolvedValueOnce({
96
+ isDirectory: () => false,
97
+ });
98
+ await expect(getHandler(registrar, "lsp_init")({ root: "/workspace/file.ts" })).resolves.toEqual({
99
+ content: [
100
+ {
101
+ type: "text",
102
+ text: "Project root is not a directory: /workspace/file.ts",
103
+ },
104
+ ],
73
105
  error: true,
74
- raw: null
106
+ raw: null,
75
107
  });
76
108
  expect(initializeManager).not.toHaveBeenCalled();
77
109
  });
78
- it('maps lsp_init startup failures into tool errors', async () => {
110
+ it("maps lsp_init startup failures into tool errors", async () => {
79
111
  const registrar = new FakeRegistrar();
80
- const initializeManager = jest.fn().mockRejectedValue(new Error('Lifecycle start timed out'));
81
- (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), { initializeManager });
82
- await expect(getHandler(registrar, 'lsp_init')({ root: '/workspace' })).resolves.toEqual({
83
- content: [{ type: 'text', text: 'Operation timed out after 30s — try a more specific query or check the LSP server health' }],
112
+ const initializeManager = jest
113
+ .fn()
114
+ .mockRejectedValue(new Error("Lifecycle start timed out"));
115
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
116
+ initializeManager,
117
+ });
118
+ await expect(getHandler(registrar, "lsp_init")({ root: "/workspace" })).resolves.toEqual({
119
+ content: [
120
+ {
121
+ type: "text",
122
+ text: "Operation timed out after 30s — try a more specific query or check the LSP server health",
123
+ },
124
+ ],
84
125
  error: true,
85
- raw: null
126
+ raw: null,
86
127
  });
87
128
  });
88
- it('sends didOpen once and returns formatted hover results', async () => {
129
+ it("sends didOpen once and returns formatted hover results", async () => {
89
130
  const registrar = new FakeRegistrar();
90
- const client = createClient({ contents: 'hover docs' });
131
+ const client = createClient({ contents: "hover docs" });
91
132
  const lifecycle = createLifecycle({ fileClient: client });
92
133
  (0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
93
- const hover = await getHandler(registrar, 'lsp_hover')({ file: '/workspace/src/index.ts', line: 2, character: 4 });
94
- await getHandler(registrar, 'lsp_hover')({ file: '/workspace/src/index.ts', line: 2, character: 4 });
134
+ const hover = await getHandler(registrar, "lsp_hover")({ file: "/workspace/src/index.ts", line: 2, character: 4 });
135
+ await getHandler(registrar, "lsp_hover")({ file: "/workspace/src/index.ts", line: 2, character: 4 });
95
136
  expect(client.ensureDidOpen).toHaveBeenCalledTimes(2);
96
- expect(client.ensureDidOpen).toHaveBeenCalledWith('/workspace/src/index.ts');
97
- expect(client.request).toHaveBeenCalledWith('textDocument/hover', {
98
- textDocument: { uri: 'file:///workspace/src/index.ts' },
99
- position: { line: 2, character: 4 }
137
+ expect(client.ensureDidOpen).toHaveBeenCalledWith("/workspace/src/index.ts");
138
+ expect(client.request).toHaveBeenCalledWith("textDocument/hover", {
139
+ textDocument: { uri: "file:///workspace/src/index.ts" },
140
+ position: { line: 2, character: 4 },
100
141
  }, 5000);
101
142
  expect(hover).toEqual({
102
- content: [{ type: 'text', text: 'hover docs' }],
103
- raw: { contents: 'hover docs' }
143
+ content: [{ type: "text", text: "hover docs" }],
144
+ raw: { contents: "hover docs" },
104
145
  });
105
146
  });
106
- it('formats definitions and converts URIs back to paths', async () => {
147
+ it("formats definitions and converts URIs back to paths", async () => {
107
148
  const registrar = new FakeRegistrar();
108
149
  const lifecycle = createLifecycle({
109
150
  fileClient: createClient([
110
151
  {
111
- uri: 'file:///workspace/src/defs.ts',
112
- range: { start: { line: 3, character: 1 }, end: { line: 3, character: 2 } }
113
- }
114
- ])
152
+ uri: "file:///workspace/src/defs.ts",
153
+ range: {
154
+ start: { line: 3, character: 1 },
155
+ end: { line: 3, character: 2 },
156
+ },
157
+ },
158
+ ]),
115
159
  });
116
160
  (0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
117
- await expect(getHandler(registrar, 'lsp_definition')({ file: '/workspace/src/index.ts', line: 1, character: 1 })).resolves.toEqual({
118
- content: [{ type: 'text', text: 'Found 1 definition: `/workspace/src/defs.ts:4:2`' }],
161
+ await expect(getHandler(registrar, "lsp_definition")({ file: "/workspace/src/index.ts", line: 1, character: 1 })).resolves.toEqual({
162
+ content: [
163
+ {
164
+ type: "text",
165
+ text: "Found 1 definition: `/workspace/src/defs.ts:4:2`",
166
+ },
167
+ ],
119
168
  raw: [
120
169
  {
121
- path: '/workspace/src/defs.ts',
122
- range: { start: { line: 3, character: 1 }, end: { line: 3, character: 2 } }
123
- }
124
- ]
170
+ path: "/workspace/src/defs.ts",
171
+ range: {
172
+ start: { line: 3, character: 1 },
173
+ end: { line: 3, character: 2 },
174
+ },
175
+ },
176
+ ],
125
177
  });
126
178
  });
127
- it('uses the declaration flag when requesting references', async () => {
179
+ it("uses the declaration flag when requesting references", async () => {
128
180
  const registrar = new FakeRegistrar();
129
181
  const client = createClient([]);
130
- (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), { initializeManager: jest.fn() });
131
- await getHandler(registrar, 'lsp_references')({ file: '/workspace/src/index.ts', line: 0, character: 0, includeDeclaration: true });
132
- expect(client.request).toHaveBeenCalledWith('textDocument/references', {
133
- textDocument: { uri: 'file:///workspace/src/index.ts' },
182
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
183
+ initializeManager: jest.fn(),
184
+ });
185
+ await getHandler(registrar, "lsp_references")({
186
+ file: "/workspace/src/index.ts",
187
+ line: 0,
188
+ character: 0,
189
+ includeDeclaration: true,
190
+ });
191
+ expect(client.request).toHaveBeenCalledWith("textDocument/references", {
192
+ textDocument: { uri: "file:///workspace/src/index.ts" },
134
193
  position: { line: 0, character: 0 },
135
- context: { includeDeclaration: true }
194
+ context: { includeDeclaration: true },
136
195
  }, 15000);
137
196
  });
138
- it('merges workspace symbols across ready clients', async () => {
197
+ it("merges workspace symbols across ready clients", async () => {
139
198
  const registrar = new FakeRegistrar();
140
- const firstClient = createClient([{ name: 'UserService', kind: 5, location: { uri: 'file:///workspace/src/user.ts', range: { start: { line: 0, character: 0 }, end: { line: 0, character: 4 } } } }]);
141
- const secondClient = createClient([{ name: 'login', kind: 12, location: { uri: 'file:///workspace/src/auth.ts', range: { start: { line: 1, character: 0 }, end: { line: 1, character: 3 } } } }]);
142
- const lifecycle = createLifecycle({ workspaceClients: [firstClient, secondClient] });
199
+ const firstClient = createClient([
200
+ {
201
+ name: "UserService",
202
+ kind: 5,
203
+ location: {
204
+ uri: "file:///workspace/src/user.ts",
205
+ range: {
206
+ start: { line: 0, character: 0 },
207
+ end: { line: 0, character: 4 },
208
+ },
209
+ },
210
+ },
211
+ ]);
212
+ const secondClient = createClient([
213
+ {
214
+ name: "login",
215
+ kind: 12,
216
+ location: {
217
+ uri: "file:///workspace/src/auth.ts",
218
+ range: {
219
+ start: { line: 1, character: 0 },
220
+ end: { line: 1, character: 3 },
221
+ },
222
+ },
223
+ },
224
+ ]);
225
+ const lifecycle = createLifecycle({
226
+ workspaceClients: [firstClient, secondClient],
227
+ });
143
228
  (0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
144
- const result = await getHandler(registrar, 'lsp_workspace_symbols')({ query: 'log' });
229
+ const result = await getHandler(registrar, "lsp_workspace_symbols")({ query: "log" });
145
230
  expect(lifecycle.ensureSeedFilesOpen).toHaveBeenCalledTimes(1);
146
- expect(firstClient.request).toHaveBeenCalledWith('workspace/symbol', { query: 'log' }, 30000);
147
- expect(secondClient.request).toHaveBeenCalledWith('workspace/symbol', { query: 'log' }, 30000);
231
+ expect(firstClient.request).toHaveBeenCalledWith("workspace/symbol", { query: "log" }, 30000);
232
+ expect(secondClient.request).toHaveBeenCalledWith("workspace/symbol", { query: "log" }, 30000);
148
233
  expect(result).toEqual({
149
- content: [{ type: 'text', text: expect.stringContaining('UserService') }],
234
+ content: [{ type: "text", text: expect.stringContaining("UserService") }],
150
235
  raw: [
151
- { name: 'UserService', kind: 5, path: '/workspace/src/user.ts', range: { start: { line: 0, character: 0 }, end: { line: 0, character: 4 } } },
152
- { name: 'login', kind: 12, path: '/workspace/src/auth.ts', range: { start: { line: 1, character: 0 }, end: { line: 1, character: 3 } } }
153
- ]
236
+ {
237
+ name: "UserService",
238
+ kind: 5,
239
+ path: "/workspace/src/user.ts",
240
+ range: {
241
+ start: { line: 0, character: 0 },
242
+ end: { line: 0, character: 4 },
243
+ },
244
+ },
245
+ {
246
+ name: "login",
247
+ kind: 12,
248
+ path: "/workspace/src/auth.ts",
249
+ range: {
250
+ start: { line: 1, character: 0 },
251
+ end: { line: 1, character: 3 },
252
+ },
253
+ },
254
+ ],
154
255
  });
155
256
  });
156
- it('returns cached file diagnostics and aggregates workspace diagnostics', async () => {
257
+ it("returns cached file diagnostics and aggregates workspace diagnostics", async () => {
157
258
  const registrar = new FakeRegistrar();
158
- const diagnostics = [{ uri: 'file:///workspace/src/index.ts', message: 'Boom', severity: vscode_languageserver_protocol_1.DiagnosticSeverity.Error, range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } } }];
259
+ const diagnostics = [
260
+ {
261
+ uri: "file:///workspace/src/index.ts",
262
+ message: "Boom",
263
+ severity: vscode_languageserver_protocol_1.DiagnosticSeverity.Error,
264
+ range: {
265
+ start: { line: 0, character: 0 },
266
+ end: { line: 0, character: 1 },
267
+ },
268
+ },
269
+ ];
159
270
  (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ diagnostics, workspaceClients: [createClient([])] }), { initializeManager: jest.fn() });
160
- await expect(getHandler(registrar, 'lsp_diagnostics')({ file: '/workspace/src/index.ts' })).resolves.toEqual({
161
- content: [{ type: 'text', text: expect.stringContaining('File diagnostics: 1 issue(s)') }],
162
- raw: diagnostics
271
+ await expect(getHandler(registrar, "lsp_diagnostics")({ file: "/workspace/src/index.ts" })).resolves.toEqual({
272
+ content: [
273
+ {
274
+ type: "text",
275
+ text: expect.stringContaining("File diagnostics: 1 issue(s)"),
276
+ },
277
+ ],
278
+ raw: diagnostics,
163
279
  });
164
- await expect(getHandler(registrar, 'lsp_diagnostics')({ scope: 'workspace' })).resolves.toEqual({
165
- content: [{ type: 'text', text: expect.stringContaining('Workspace diagnostics: 1 issue(s)') }],
166
- raw: diagnostics
280
+ await expect(getHandler(registrar, "lsp_diagnostics")({ scope: "workspace" })).resolves.toEqual({
281
+ content: [
282
+ {
283
+ type: "text",
284
+ text: expect.stringContaining("Workspace diagnostics: 1 issue(s)"),
285
+ },
286
+ ],
287
+ raw: diagnostics,
167
288
  });
168
289
  });
169
- it('returns health instantly without LSP requests', async () => {
290
+ it("returns health instantly without LSP requests", async () => {
170
291
  const registrar = new FakeRegistrar();
171
- const lifecycle = createLifecycle({ health: [{ language: 'typescript', status: 'ready' }] });
292
+ const lifecycle = createLifecycle({
293
+ health: [{ language: "typescript", status: "ready" }],
294
+ });
172
295
  (0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
173
- await expect(getHandler(registrar, 'lsp_health')({})).resolves.toEqual({
174
- content: [{ type: 'text', text: '| Language | Status | Error |\n| --- | --- | --- |\n| typescript | ready | |' }],
175
- raw: [{ language: 'typescript', status: 'ready' }]
296
+ await expect(getHandler(registrar, "lsp_health")({})).resolves.toEqual({
297
+ content: [
298
+ {
299
+ type: "text",
300
+ text: "| Language | Status | Error |\n| --- | --- | --- |\n| typescript | ready | |",
301
+ },
302
+ ],
303
+ raw: [{ language: "typescript", status: "ready" }],
176
304
  });
177
305
  });
178
- it('supports document symbols, completion lists, and signature help fallbacks', async () => {
306
+ it("supports document symbols, completion lists, and signature help fallbacks", async () => {
179
307
  const registrar = new FakeRegistrar();
180
- const client = createClient({ items: [{ label: 'x', kind: 3 }] });
181
- (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), { initializeManager: jest.fn() });
182
- await expect(getHandler(registrar, 'lsp_completion')({ file: '/workspace/src/index.ts', line: 0, character: 0 })).resolves.toEqual({
183
- content: [{ type: 'text', text: 'Showing 1 of 1 completion item(s)\n\n### Functions\n- `x`' }],
184
- raw: [{ label: 'x', kind: 3 }]
308
+ const client = createClient({ items: [{ label: "x", kind: 3 }] });
309
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
310
+ initializeManager: jest.fn(),
311
+ });
312
+ await expect(getHandler(registrar, "lsp_completion")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
313
+ content: [
314
+ {
315
+ type: "text",
316
+ text: "Showing 1 of 1 completion item(s)\n\n### Functions\n- `x`",
317
+ },
318
+ ],
319
+ raw: [{ label: "x", kind: 3 }],
185
320
  });
186
- client.request.mockResolvedValueOnce([{ name: 'DocSymbol', kind: 5, range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } }, selectionRange: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } } }]);
187
- await expect(getHandler(registrar, 'lsp_document_symbols')({ file: '/workspace/src/index.ts' })).resolves.toEqual({
188
- content: [{ type: 'text', text: '- 📦 `DocSymbol`' }],
189
- raw: [{ name: 'DocSymbol', kind: 5, range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } }, selectionRange: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } } }]
321
+ client.request.mockResolvedValueOnce([
322
+ {
323
+ name: "DocSymbol",
324
+ kind: 5,
325
+ range: {
326
+ start: { line: 0, character: 0 },
327
+ end: { line: 0, character: 1 },
328
+ },
329
+ selectionRange: {
330
+ start: { line: 0, character: 0 },
331
+ end: { line: 0, character: 1 },
332
+ },
333
+ },
334
+ ]);
335
+ await expect(getHandler(registrar, "lsp_document_symbols")({ file: "/workspace/src/index.ts" })).resolves.toEqual({
336
+ content: [{ type: "text", text: "- 📦 `DocSymbol`" }],
337
+ raw: [
338
+ {
339
+ name: "DocSymbol",
340
+ kind: 5,
341
+ range: {
342
+ start: { line: 0, character: 0 },
343
+ end: { line: 0, character: 1 },
344
+ },
345
+ selectionRange: {
346
+ start: { line: 0, character: 0 },
347
+ end: { line: 0, character: 1 },
348
+ },
349
+ },
350
+ ],
190
351
  });
191
352
  client.request.mockResolvedValueOnce(null);
192
- await expect(getHandler(registrar, 'lsp_signature_help')({ file: '/workspace/src/index.ts', line: 0, character: 0 })).resolves.toEqual({
193
- content: [{ type: 'text', text: 'No result' }],
194
- raw: null
353
+ await expect(getHandler(registrar, "lsp_signature_help")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
354
+ content: [{ type: "text", text: "No result" }],
355
+ raw: null,
195
356
  });
196
357
  });
197
- it('supports type and implementation lookups plus empty completion results', async () => {
358
+ it("supports type and implementation lookups plus empty completion results", async () => {
198
359
  const registrar = new FakeRegistrar();
199
360
  const client = createClient({
200
- uri: 'file:///workspace/src/types.ts',
201
- range: { start: { line: 1, character: 2 }, end: { line: 1, character: 6 } }
361
+ uri: "file:///workspace/src/types.ts",
362
+ range: {
363
+ start: { line: 1, character: 2 },
364
+ end: { line: 1, character: 6 },
365
+ },
202
366
  });
203
- (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), { initializeManager: jest.fn() });
204
- await expect(getHandler(registrar, 'lsp_type_definition')({ file: '/workspace/src/index.ts', line: 0, character: 0 })).resolves.toEqual({
205
- content: [{ type: 'text', text: 'Found 1 definition: `/workspace/src/types.ts:2:3`' }],
206
- raw: [{ path: '/workspace/src/types.ts', range: { start: { line: 1, character: 2 }, end: { line: 1, character: 6 } } }]
367
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
368
+ initializeManager: jest.fn(),
369
+ });
370
+ await expect(getHandler(registrar, "lsp_type_definition")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
371
+ content: [
372
+ {
373
+ type: "text",
374
+ text: "Found 1 definition: `/workspace/src/types.ts:2:3`",
375
+ },
376
+ ],
377
+ raw: [
378
+ {
379
+ path: "/workspace/src/types.ts",
380
+ range: {
381
+ start: { line: 1, character: 2 },
382
+ end: { line: 1, character: 6 },
383
+ },
384
+ },
385
+ ],
207
386
  });
208
387
  client.request.mockResolvedValueOnce(null);
209
- await expect(getHandler(registrar, 'lsp_implementation')({ file: '/workspace/src/index.ts', line: 0, character: 0 })).resolves.toEqual({
210
- content: [{ type: 'text', text: 'No result' }],
211
- raw: null
388
+ await expect(getHandler(registrar, "lsp_implementation")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
389
+ content: [{ type: "text", text: "No result" }],
390
+ raw: null,
212
391
  });
213
392
  client.request.mockResolvedValueOnce(null);
214
- await expect(getHandler(registrar, 'lsp_completion')({ file: '/workspace/src/index.ts', line: 0, character: 0 })).resolves.toEqual({
215
- content: [{ type: 'text', text: 'No result' }],
216
- raw: null
393
+ await expect(getHandler(registrar, "lsp_completion")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
394
+ content: [{ type: "text", text: "No result" }],
395
+ raw: null,
217
396
  });
218
- client.request.mockResolvedValueOnce({ signatures: [{ label: 'fn(x: string)' }] });
219
- await expect(getHandler(registrar, 'lsp_signature_help')({ file: '/workspace/src/index.ts', line: 0, character: 0 })).resolves.toEqual({
220
- content: [{ type: 'text', text: JSON.stringify({ signatures: [{ label: 'fn(x: string)' }] }, null, 2) }],
221
- raw: { signatures: [{ label: 'fn(x: string)' }] }
397
+ client.request.mockResolvedValueOnce({
398
+ signatures: [{ label: "fn(x: string)" }],
399
+ });
400
+ await expect(getHandler(registrar, "lsp_signature_help")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
401
+ content: [
402
+ {
403
+ type: "text",
404
+ text: JSON.stringify({ signatures: [{ label: "fn(x: string)" }] }, null, 2),
405
+ },
406
+ ],
407
+ raw: { signatures: [{ label: "fn(x: string)" }] },
222
408
  });
223
409
  });
224
- it('turns LSP timeouts into retry guidance', async () => {
410
+ it("turns LSP timeouts into retry guidance", async () => {
225
411
  const registrar = new FakeRegistrar();
226
- const client = createClient(new Error('LSP request timed out: textDocument/hover'));
227
- (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), { initializeManager: jest.fn() });
228
- await expect(getHandler(registrar, 'lsp_hover')({ file: '/workspace/src/index.ts', line: 0, character: 0 })).resolves.toEqual({
229
- content: [{ type: 'text', text: 'Operation timed out after 5s — try a more specific query or check the LSP server health' }],
412
+ const client = createClient(new Error("LSP request timed out: textDocument/hover"));
413
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
414
+ initializeManager: jest.fn(),
415
+ });
416
+ await expect(getHandler(registrar, "lsp_hover")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
417
+ content: [
418
+ {
419
+ type: "text",
420
+ text: "Operation timed out after 5s — try a more specific query or check the LSP server health",
421
+ },
422
+ ],
230
423
  error: true,
231
- raw: null
424
+ raw: null,
232
425
  });
233
426
  });
234
- it('returns a no-server error when no language server matches the file', async () => {
427
+ it("returns a no-server error when no language server matches the file", async () => {
235
428
  const registrar = new FakeRegistrar();
236
- (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), { initializeManager: jest.fn() });
237
- await expect(getHandler(registrar, 'lsp_hover')({ file: '/workspace/README.md', line: 0, character: 0 })).resolves.toEqual({
238
- content: [{ type: 'text', text: 'No language server available for .md files. Run lsp_health for details.' }],
429
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
430
+ initializeManager: jest.fn(),
431
+ });
432
+ await expect(getHandler(registrar, "lsp_hover")({ file: "/workspace/README.md", line: 0, character: 0 })).resolves.toEqual({
433
+ content: [
434
+ {
435
+ type: "text",
436
+ text: "No language server available for .md files. Run lsp_health for details.",
437
+ },
438
+ ],
239
439
  error: true,
240
- raw: null
440
+ raw: null,
241
441
  });
242
442
  });
243
- it('returns restart guidance when the LSP crashed', async () => {
443
+ it("returns restart guidance when the LSP crashed", async () => {
244
444
  const registrar = new FakeRegistrar();
245
- const client = createClient(new Error('LSP server exited unexpectedly (code: 1, signal: null)'));
246
- (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), { initializeManager: jest.fn() });
247
- await expect(getHandler(registrar, 'lsp_hover')({ file: '/workspace/src/index.ts', line: 0, character: 0 })).resolves.toEqual({
248
- content: [{ type: 'text', text: 'Der Language Server ist neu gestartet, bitte versuche es erneut.' }],
445
+ const client = createClient(new Error("LSP server exited unexpectedly (code: 1, signal: null)"));
446
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
447
+ initializeManager: jest.fn(),
448
+ });
449
+ await expect(getHandler(registrar, "lsp_hover")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
450
+ content: [
451
+ {
452
+ type: "text",
453
+ text: "Der Language Server ist neu gestartet, bitte versuche es erneut.",
454
+ },
455
+ ],
249
456
  error: true,
250
- raw: null
457
+ raw: null,
251
458
  });
252
459
  });
253
460
  });
@@ -262,11 +469,12 @@ function createLifecycle(options) {
262
469
  return {
263
470
  getClientForFile: jest.fn((_) => options.fileClient ?? null),
264
471
  getReadyClients: jest.fn((_) => options.workspaceClients ?? []),
265
- getFileDiagnostics: jest.fn((_) => (options.diagnostics ?? []).filter((diagnostic) => diagnostic.uri === 'file:///workspace/src/index.ts')),
472
+ getFileDiagnostics: jest.fn((_) => (options.diagnostics ?? []).filter((diagnostic) => diagnostic.uri === "file:///workspace/src/index.ts")),
266
473
  getWorkspaceDiagnostics: jest.fn((_) => options.diagnostics ?? []),
267
474
  getHealth: jest.fn(() => options.health ?? []),
268
475
  ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
269
- ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined)
476
+ ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined),
477
+ analyzeWorkspace: jest.fn().mockResolvedValue(undefined),
270
478
  };
271
479
  }
272
480
  function createClient(result) {
@@ -278,6 +486,6 @@ function createClient(result) {
278
486
  getCapabilities: jest.fn(() => ({ renameProvider: true })),
279
487
  ensureDidOpen: jest.fn().mockResolvedValue(undefined),
280
488
  waitForDiagnosticsPublish: jest.fn().mockResolvedValue(undefined),
281
- ensureSeedFileOpen: jest.fn().mockResolvedValue(undefined)
489
+ ensureSeedFileOpen: jest.fn().mockResolvedValue(undefined),
282
490
  };
283
491
  }