summaryrefslogtreecommitdiffhomepage
path: root/packages/core/tests/tools/lsp-tool.test.ts
blob: 7f26522d9b17d5598dbac29c28a4955ec390af4e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import { describe, expect, it, vi } from "vitest";
import type { LspManager } from "../../src/lsp/manager.js";
import type { ResolvedLspServer } from "../../src/lsp/server.js";
import { createLspTool, type LspToolContext } from "../../src/tools/lsp.js";

const SERVER: ResolvedLspServer = {
	id: "luau-lsp",
	extensions: [".luau"],
	spawn: () => ({ process: {} as never }),
};

function makeManager(overrides: Partial<LspManager> = {}): LspManager {
	return {
		hasServerForFile: vi.fn(() => true),
		touchFile: vi.fn(async () => {}),
		getDiagnostics: vi.fn(() => ({})),
		request: vi.fn(async () => []),
		getClients: vi.fn(async () => []),
		shutdownAll: vi.fn(async () => {}),
		...overrides,
	} as unknown as LspManager;
}

function ctx(manager: LspManager, servers = [SERVER]): () => LspToolContext {
	return () => ({ manager, workingDirectory: "/work", servers });
}

describe("createLspTool", () => {
	it("exposes the expected schema/name", () => {
		const tool = createLspTool(ctx(makeManager()));
		expect(tool.name).toBe("lsp");
		expect(tool.description).toMatch(/luau-lsp/i);
	});

	it("errors when no servers are configured", async () => {
		const tool = createLspTool(ctx(makeManager(), []));
		const out = await tool.execute({ operation: "diagnostics", path: "a.luau" });
		expect(out).toMatch(/no LSP servers are configured/i);
	});

	it("errors when no server matches the file", async () => {
		const manager = makeManager({ hasServerForFile: vi.fn(() => false) as never });
		const tool = createLspTool(ctx(manager));
		const out = await tool.execute({ operation: "diagnostics", path: "a.ts" });
		expect(out).toMatch(/no configured LSP server matches/i);
	});

	it("diagnostics: touches the file then reports errors", async () => {
		const touchFile = vi.fn(async () => {});
		const getDiagnostics = vi.fn(() => ({
			"/work/a.luau": [
				{
					range: { start: { line: 2, character: 1 }, end: { line: 2, character: 9 } },
					severity: 1,
					message: "bad type",
				},
			],
		}));
		const manager = makeManager({
			touchFile: touchFile as never,
			getDiagnostics: getDiagnostics as never,
		});
		const tool = createLspTool(ctx(manager));
		const out = await tool.execute({ operation: "diagnostics", path: "a.luau" });
		expect(touchFile).toHaveBeenCalledOnce();
		expect(out).toContain("ERROR [3:2] bad type");
	});

	it("diagnostics: reports clean when no errors", async () => {
		const tool = createLspTool(ctx(makeManager()));
		const out = await tool.execute({ operation: "diagnostics", path: "a.luau" });
		expect(out).toMatch(/No errors reported/i);
	});

	it("hover: requires line and character", async () => {
		const tool = createLspTool(ctx(makeManager()));
		const out = await tool.execute({ operation: "hover", path: "a.luau" });
		expect(out).toMatch(/requires both 'line' and 'character'/i);
	});

	it("hover: converts 1-based coords to 0-based on the wire", async () => {
		const request = vi.fn(async () => [{ contents: "hi" }]);
		const manager = makeManager({ request: request as never });
		const tool = createLspTool(ctx(manager));
		await tool.execute({ operation: "hover", path: "a.luau", line: 5, character: 3 });
		expect(request).toHaveBeenCalledOnce();
		const arg = request.mock.calls[0]?.[0] as { method: string; params: { position: unknown } };
		expect(arg.method).toBe("textDocument/hover");
		expect(arg.params.position).toEqual({ line: 4, character: 2 });
	});

	it("references: includes declaration context", async () => {
		const request = vi.fn(async () => []);
		const manager = makeManager({ request: request as never });
		const tool = createLspTool(ctx(manager));
		await tool.execute({ operation: "references", path: "a.luau", line: 1, character: 1 });
		const arg = request.mock.calls[0]?.[0] as { params: { context?: unknown } };
		expect(arg.params.context).toEqual({ includeDeclaration: true });
	});

	it("documentSymbol: does not require a position", async () => {
		const request = vi.fn(async () => [{ name: "foo" }]);
		const manager = makeManager({ request: request as never });
		const tool = createLspTool(ctx(manager));
		const out = await tool.execute({ operation: "documentSymbol", path: "a.luau" });
		const arg = request.mock.calls[0]?.[0] as { method: string };
		expect(arg.method).toBe("textDocument/documentSymbol");
		expect(out).toContain("foo");
	});
});