@theupsider/lsp-mcp 1.0.1 → 1.0.8

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,436 @@ 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,
94
+ });
95
+ promises_1.stat.mockResolvedValueOnce({
96
+ isDirectory: () => false,
69
97
  });
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' }],
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 and returns formatted definition results", async () => {
89
130
  const registrar = new FakeRegistrar();
90
- const client = createClient({ contents: 'hover docs' });
131
+ const client = createClient({
132
+ uri: "file:///workspace/src/defs.ts",
133
+ range: {
134
+ start: { line: 3, character: 1 },
135
+ end: { line: 3, character: 2 },
136
+ },
137
+ });
91
138
  const lifecycle = createLifecycle({ fileClient: client });
92
139
  (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 });
140
+ const definition = await getHandler(registrar, "lsp_definition")({ file: "/workspace/src/index.ts", line: 2, character: 4 });
141
+ await getHandler(registrar, "lsp_definition")({ file: "/workspace/src/index.ts", line: 2, character: 4 });
95
142
  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 }
143
+ expect(client.ensureDidOpen).toHaveBeenCalledWith("/workspace/src/index.ts");
144
+ expect(client.request).toHaveBeenCalledWith("textDocument/definition", {
145
+ textDocument: { uri: "file:///workspace/src/index.ts" },
146
+ position: { line: 2, character: 4 },
100
147
  }, 5000);
101
- expect(hover).toEqual({
102
- content: [{ type: 'text', text: 'hover docs' }],
103
- raw: { contents: 'hover docs' }
148
+ expect(definition).toEqual({
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: "Found 1 definition: `/workspace/src/defs.ts:4:2`",
153
+ },
154
+ ],
155
+ raw: [
156
+ {
157
+ path: "/workspace/src/defs.ts",
158
+ range: {
159
+ start: { line: 3, character: 1 },
160
+ end: { line: 3, character: 2 },
161
+ },
162
+ },
163
+ ],
104
164
  });
105
165
  });
106
- it('formats definitions and converts URIs back to paths', async () => {
166
+ it("formats definitions and converts URIs back to paths", async () => {
107
167
  const registrar = new FakeRegistrar();
108
168
  const lifecycle = createLifecycle({
109
169
  fileClient: createClient([
110
170
  {
111
- uri: 'file:///workspace/src/defs.ts',
112
- range: { start: { line: 3, character: 1 }, end: { line: 3, character: 2 } }
113
- }
114
- ])
171
+ uri: "file:///workspace/src/defs.ts",
172
+ range: {
173
+ start: { line: 3, character: 1 },
174
+ end: { line: 3, character: 2 },
175
+ },
176
+ },
177
+ ]),
115
178
  });
116
179
  (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`' }],
180
+ await expect(getHandler(registrar, "lsp_definition")({ file: "/workspace/src/index.ts", line: 1, character: 1 })).resolves.toEqual({
181
+ content: [
182
+ {
183
+ type: "text",
184
+ text: "Found 1 definition: `/workspace/src/defs.ts:4:2`",
185
+ },
186
+ ],
119
187
  raw: [
120
188
  {
121
- path: '/workspace/src/defs.ts',
122
- range: { start: { line: 3, character: 1 }, end: { line: 3, character: 2 } }
123
- }
124
- ]
189
+ path: "/workspace/src/defs.ts",
190
+ range: {
191
+ start: { line: 3, character: 1 },
192
+ end: { line: 3, character: 2 },
193
+ },
194
+ },
195
+ ],
125
196
  });
126
197
  });
127
- it('uses the declaration flag when requesting references', async () => {
198
+ it("uses the declaration flag when requesting references", async () => {
128
199
  const registrar = new FakeRegistrar();
129
200
  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' },
201
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
202
+ initializeManager: jest.fn(),
203
+ });
204
+ await getHandler(registrar, "lsp_references")({
205
+ file: "/workspace/src/index.ts",
206
+ line: 0,
207
+ character: 0,
208
+ includeDeclaration: true,
209
+ });
210
+ expect(client.request).toHaveBeenCalledWith("textDocument/references", {
211
+ textDocument: { uri: "file:///workspace/src/index.ts" },
134
212
  position: { line: 0, character: 0 },
135
- context: { includeDeclaration: true }
213
+ context: { includeDeclaration: true },
136
214
  }, 15000);
137
215
  });
138
- it('merges workspace symbols across ready clients', async () => {
216
+ it("merges workspace symbols across ready clients", async () => {
139
217
  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] });
218
+ const firstClient = createClient([
219
+ {
220
+ name: "UserService",
221
+ kind: 5,
222
+ location: {
223
+ uri: "file:///workspace/src/user.ts",
224
+ range: {
225
+ start: { line: 0, character: 0 },
226
+ end: { line: 0, character: 4 },
227
+ },
228
+ },
229
+ },
230
+ ]);
231
+ const secondClient = createClient([
232
+ {
233
+ name: "login",
234
+ kind: 12,
235
+ location: {
236
+ uri: "file:///workspace/src/auth.ts",
237
+ range: {
238
+ start: { line: 1, character: 0 },
239
+ end: { line: 1, character: 3 },
240
+ },
241
+ },
242
+ },
243
+ ]);
244
+ const lifecycle = createLifecycle({
245
+ workspaceClients: [firstClient, secondClient],
246
+ });
143
247
  (0, read_tools_1.registerReadTools)(registrar, lifecycle, { initializeManager: jest.fn() });
144
- const result = await getHandler(registrar, 'lsp_workspace_symbols')({ query: 'log' });
248
+ const result = await getHandler(registrar, "lsp_workspace_symbols")({ query: "log" });
145
249
  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);
250
+ expect(firstClient.request).toHaveBeenCalledWith("workspace/symbol", { query: "log" }, 30000);
251
+ expect(secondClient.request).toHaveBeenCalledWith("workspace/symbol", { query: "log" }, 30000);
148
252
  expect(result).toEqual({
149
- content: [{ type: 'text', text: expect.stringContaining('UserService') }],
253
+ content: [{ type: "text", text: expect.stringContaining("UserService") }],
150
254
  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
- ]
255
+ {
256
+ name: "UserService",
257
+ kind: 5,
258
+ path: "/workspace/src/user.ts",
259
+ range: {
260
+ start: { line: 0, character: 0 },
261
+ end: { line: 0, character: 4 },
262
+ },
263
+ },
264
+ {
265
+ name: "login",
266
+ kind: 12,
267
+ path: "/workspace/src/auth.ts",
268
+ range: {
269
+ start: { line: 1, character: 0 },
270
+ end: { line: 1, character: 3 },
271
+ },
272
+ },
273
+ ],
154
274
  });
155
275
  });
156
- it('returns cached file diagnostics and aggregates workspace diagnostics', async () => {
276
+ it("returns cached file diagnostics and aggregates workspace diagnostics", async () => {
157
277
  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 } } }];
278
+ const diagnostics = [
279
+ {
280
+ uri: "file:///workspace/src/index.ts",
281
+ message: "Boom",
282
+ severity: vscode_languageserver_protocol_1.DiagnosticSeverity.Error,
283
+ range: {
284
+ start: { line: 0, character: 0 },
285
+ end: { line: 0, character: 1 },
286
+ },
287
+ },
288
+ ];
159
289
  (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
290
+ await expect(getHandler(registrar, "lsp_diagnostics")({ file: "/workspace/src/index.ts" })).resolves.toEqual({
291
+ content: [
292
+ {
293
+ type: "text",
294
+ text: expect.stringContaining("File diagnostics: 1 issue(s)"),
295
+ },
296
+ ],
297
+ raw: diagnostics,
163
298
  });
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
299
+ await expect(getHandler(registrar, "lsp_diagnostics")({ scope: "workspace" })).resolves.toEqual({
300
+ content: [
301
+ {
302
+ type: "text",
303
+ text: expect.stringContaining("Workspace diagnostics: 1 issue(s)"),
304
+ },
305
+ ],
306
+ raw: diagnostics,
167
307
  });
168
308
  });
169
- it('returns health instantly without LSP requests', async () => {
309
+ it("returns health instantly without LSP requests", async () => {
170
310
  const registrar = new FakeRegistrar();
171
- const lifecycle = createLifecycle({ health: [{ language: 'typescript', status: 'ready' }] });
311
+ const lifecycle = createLifecycle({
312
+ health: [{ language: "typescript", status: "ready" }],
313
+ });
172
314
  (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' }]
315
+ await expect(getHandler(registrar, "lsp_health")({})).resolves.toEqual({
316
+ content: [
317
+ {
318
+ type: "text",
319
+ text: "| Language | Status | Error |\n| --- | --- | --- |\n| typescript | ready | |",
320
+ },
321
+ ],
322
+ raw: [{ language: "typescript", status: "ready" }],
176
323
  });
177
324
  });
178
- it('supports document symbols, completion lists, and signature help fallbacks', async () => {
325
+ it("supports document symbols", async () => {
179
326
  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 }]
185
- });
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 } } }]
327
+ const client = createClient([
328
+ {
329
+ name: "DocSymbol",
330
+ kind: 5,
331
+ range: {
332
+ start: { line: 0, character: 0 },
333
+ end: { line: 0, character: 1 },
334
+ },
335
+ selectionRange: {
336
+ start: { line: 0, character: 0 },
337
+ end: { line: 0, character: 1 },
338
+ },
339
+ },
340
+ ]);
341
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
342
+ initializeManager: jest.fn(),
190
343
  });
191
- 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
344
+ await expect(getHandler(registrar, "lsp_document_symbols")({ file: "/workspace/src/index.ts" })).resolves.toEqual({
345
+ content: [{ type: "text", text: "- 📦 `DocSymbol`" }],
346
+ raw: [
347
+ {
348
+ name: "DocSymbol",
349
+ kind: 5,
350
+ range: {
351
+ start: { line: 0, character: 0 },
352
+ end: { line: 0, character: 1 },
353
+ },
354
+ selectionRange: {
355
+ start: { line: 0, character: 0 },
356
+ end: { line: 0, character: 1 },
357
+ },
358
+ },
359
+ ],
195
360
  });
196
361
  });
197
- it('supports type and implementation lookups plus empty completion results', async () => {
362
+ it("supports type and implementation lookups", async () => {
198
363
  const registrar = new FakeRegistrar();
199
364
  const client = createClient({
200
- uri: 'file:///workspace/src/types.ts',
201
- range: { start: { line: 1, character: 2 }, end: { line: 1, character: 6 } }
365
+ uri: "file:///workspace/src/types.ts",
366
+ range: {
367
+ start: { line: 1, character: 2 },
368
+ end: { line: 1, character: 6 },
369
+ },
202
370
  });
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 } } }]
371
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
372
+ initializeManager: jest.fn(),
207
373
  });
208
- 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
374
+ await expect(getHandler(registrar, "lsp_type_definition")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
375
+ content: [
376
+ {
377
+ type: "text",
378
+ text: "Found 1 definition: `/workspace/src/types.ts:2:3`",
379
+ },
380
+ ],
381
+ raw: [
382
+ {
383
+ path: "/workspace/src/types.ts",
384
+ range: {
385
+ start: { line: 1, character: 2 },
386
+ end: { line: 1, character: 6 },
387
+ },
388
+ },
389
+ ],
212
390
  });
213
391
  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
217
- });
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)' }] }
392
+ await expect(getHandler(registrar, "lsp_implementation")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
393
+ content: [{ type: "text", text: "No result" }],
394
+ raw: null,
222
395
  });
223
396
  });
224
- it('turns LSP timeouts into retry guidance', async () => {
397
+ it("turns LSP timeouts into retry guidance", async () => {
225
398
  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' }],
399
+ const client = createClient(new Error("LSP request timed out: textDocument/definition"));
400
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
401
+ initializeManager: jest.fn(),
402
+ });
403
+ await expect(getHandler(registrar, "lsp_definition")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
404
+ content: [
405
+ {
406
+ type: "text",
407
+ text: "Operation timed out after 5s — try a more specific query or check the LSP server health",
408
+ },
409
+ ],
230
410
  error: true,
231
- raw: null
411
+ raw: null,
232
412
  });
233
413
  });
234
- it('returns a no-server error when no language server matches the file', async () => {
414
+ it("returns a no-server error when no language server matches the file", async () => {
235
415
  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.' }],
416
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: null }), {
417
+ initializeManager: jest.fn(),
418
+ });
419
+ await expect(getHandler(registrar, "lsp_definition")({ file: "/workspace/README.md", line: 0, character: 0 })).resolves.toEqual({
420
+ content: [
421
+ {
422
+ type: "text",
423
+ text: "No language server available for .md files. Run lsp_health for details.",
424
+ },
425
+ ],
239
426
  error: true,
240
- raw: null
427
+ raw: null,
241
428
  });
242
429
  });
243
- it('returns restart guidance when the LSP crashed', async () => {
430
+ it("returns restart guidance when the LSP crashed", async () => {
244
431
  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.' }],
432
+ const client = createClient(new Error("LSP server exited unexpectedly (code: 1, signal: null)"));
433
+ (0, read_tools_1.registerReadTools)(registrar, createLifecycle({ fileClient: client }), {
434
+ initializeManager: jest.fn(),
435
+ });
436
+ await expect(getHandler(registrar, "lsp_definition")({ file: "/workspace/src/index.ts", line: 0, character: 0 })).resolves.toEqual({
437
+ content: [
438
+ {
439
+ type: "text",
440
+ text: "Der Language Server ist neu gestartet, bitte versuche es erneut.",
441
+ },
442
+ ],
249
443
  error: true,
250
- raw: null
444
+ raw: null,
251
445
  });
252
446
  });
253
447
  });
@@ -262,11 +456,12 @@ function createLifecycle(options) {
262
456
  return {
263
457
  getClientForFile: jest.fn((_) => options.fileClient ?? null),
264
458
  getReadyClients: jest.fn((_) => options.workspaceClients ?? []),
265
- getFileDiagnostics: jest.fn((_) => (options.diagnostics ?? []).filter((diagnostic) => diagnostic.uri === 'file:///workspace/src/index.ts')),
459
+ getFileDiagnostics: jest.fn((_) => (options.diagnostics ?? []).filter((diagnostic) => diagnostic.uri === "file:///workspace/src/index.ts")),
266
460
  getWorkspaceDiagnostics: jest.fn((_) => options.diagnostics ?? []),
267
461
  getHealth: jest.fn(() => options.health ?? []),
268
462
  ensureLanguageForFile: jest.fn().mockResolvedValue(undefined),
269
- ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined)
463
+ ensureSeedFilesOpen: jest.fn().mockResolvedValue(undefined),
464
+ analyzeWorkspace: jest.fn().mockResolvedValue(undefined),
270
465
  };
271
466
  }
272
467
  function createClient(result) {
@@ -278,6 +473,6 @@ function createClient(result) {
278
473
  getCapabilities: jest.fn(() => ({ renameProvider: true })),
279
474
  ensureDidOpen: jest.fn().mockResolvedValue(undefined),
280
475
  waitForDiagnosticsPublish: jest.fn().mockResolvedValue(undefined),
281
- ensureSeedFileOpen: jest.fn().mockResolvedValue(undefined)
476
+ ensureSeedFileOpen: jest.fn().mockResolvedValue(undefined),
282
477
  };
283
478
  }