-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmcp-client.test.ts
More file actions
80 lines (67 loc) · 3.02 KB
/
Copy pathmcp-client.test.ts
File metadata and controls
80 lines (67 loc) · 3.02 KB
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
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
connect: vi.fn(),
request: vi.fn(),
close: vi.fn(),
transport: vi.fn(),
existsSync: vi.fn(),
}));
vi.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
Client: class {
connect = mocks.connect;
request = mocks.request;
close = mocks.close;
},
}));
vi.mock("@modelcontextprotocol/sdk/client/stdio.js", () => ({
StdioClientTransport: class {
constructor(options: unknown) {
mocks.transport(options);
}
},
}));
vi.mock("fs", () => ({ existsSync: mocks.existsSync }));
import { callMcpTool, resolveServerPath } from "../hooks/lib/mcp-client.js";
describe("hook MCP client", () => {
beforeEach(() => {
mocks.close.mockResolvedValue(undefined);
mocks.connect.mockResolvedValue(undefined);
mocks.existsSync.mockReturnValue(false);
});
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
delete process.env.PROMPTIMPROVER_SERVER_PATH;
delete process.env.PROMPTIMPROVER_HOOK_TIMEOUT_MS;
});
it("calls a tool, returns text, honors timeout, and closes the client", async () => {
process.env.PROMPTIMPROVER_SERVER_PATH = "./custom-server.js";
process.env.PROMPTIMPROVER_HOOK_TIMEOUT_MS = "25";
mocks.request.mockResolvedValue({ content: [{ type: "text", text: "result" }] });
await expect(callMcpTool("lint_prompt", { prompt: "test" })).resolves.toBe("result");
expect(mocks.transport).toHaveBeenCalledWith(expect.objectContaining({ args: [resolveServerPath()] }));
expect(mocks.request.mock.calls[0][2]).toEqual({ timeout: 25 });
expect(mocks.close).toHaveBeenCalledOnce();
});
it("throws for missing text and still closes after request failures", async () => {
mocks.request.mockResolvedValueOnce({ content: [] }).mockRejectedValueOnce(new Error("closed"));
await expect(callMcpTool("lint_prompt", {})).rejects.toThrow(/no text/);
await expect(callMcpTool("lint_prompt", {})).rejects.toThrow("closed");
expect(mocks.close).toHaveBeenCalledTimes(2);
});
it("resolves built server candidates and uses the default timeout", async () => {
mocks.request.mockResolvedValue({ content: [{ type: "text", text: "ok" }] });
expect(resolveServerPath()).toMatch(/src[\\/]index\.js$/);
await callMcpTool("lint_prompt", {});
expect(mocks.request.mock.calls[0][2]).toEqual({ timeout: 15_000 });
});
it("selects an existing built candidate, rejects invalid timeouts, and tolerates close failures", async () => {
mocks.existsSync.mockImplementation((candidate: string) => candidate.includes("dist"));
mocks.request.mockResolvedValue({ content: [{ type: "text", text: "ok" }] });
mocks.close.mockRejectedValue(new Error("close failed"));
process.env.PROMPTIMPROVER_HOOK_TIMEOUT_MS = "-1";
expect(resolveServerPath()).toMatch(/dist[\\/]src[\\/]index\.js$/);
await expect(callMcpTool("lint_prompt", {})).resolves.toBe("ok");
expect(mocks.request.mock.calls[0][2]).toEqual({ timeout: 15_000 });
});
});