Skip to content

Commit 0c40c9e

Browse files
committed
feat: Add vitest tests for MCP tools
Adds unit tests for the following tools located in `src/tools/`: - `FindVersionTool` - `ListLibrariesTool` - `ScrapeTool` - `SearchTool` - `errors` (custom error classes)
1 parent 9b41856 commit 0c40c9e

File tree

5 files changed

+667
-0
lines changed

5 files changed

+667
-0
lines changed

src/tools/FindVersionTool.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { describe, it, expect, vi, beforeEach, type Mock } from "vitest";
2+
import type { DocumentManagementService } from "../store";
3+
import { FindVersionTool, type FindVersionToolOptions } from "./FindVersionTool";
4+
import { VersionNotFoundError } from "./errors";
5+
import { logger } from "../utils/logger";
6+
7+
// Mock dependencies
8+
vi.mock("../store"); // Mock the entire store module if DocumentManagementService is complex
9+
vi.mock("../utils/logger");
10+
11+
describe("FindVersionTool", () => {
12+
let mockDocService: Partial<DocumentManagementService>;
13+
let findVersionTool: FindVersionTool;
14+
15+
beforeEach(() => {
16+
// Reset mocks before each test
17+
vi.resetAllMocks();
18+
19+
// Setup mock DocumentManagementService
20+
mockDocService = {
21+
findBestVersion: vi.fn(),
22+
};
23+
24+
// Create instance of the tool with the mock service
25+
findVersionTool = new FindVersionTool(
26+
mockDocService as DocumentManagementService,
27+
);
28+
});
29+
30+
it("should return message indicating best match when found", async () => {
31+
const options: FindVersionToolOptions = { library: "react", targetVersion: "18.2.0" };
32+
const mockResult = { bestMatch: "18.2.0", hasUnversioned: false };
33+
(mockDocService.findBestVersion as Mock).mockResolvedValue(mockResult);
34+
35+
const result = await findVersionTool.execute(options);
36+
37+
expect(mockDocService.findBestVersion).toHaveBeenCalledWith("react", "18.2.0");
38+
expect(result).toContain("Best match: 18.2.0");
39+
expect(result).not.toContain("Unversioned docs");
40+
});
41+
42+
it("should return message indicating best match and unversioned docs when both exist", async () => {
43+
const options: FindVersionToolOptions = { library: "react", targetVersion: "18.x" };
44+
const mockResult = { bestMatch: "18.3.1", hasUnversioned: true };
45+
(mockDocService.findBestVersion as Mock).mockResolvedValue(mockResult);
46+
47+
const result = await findVersionTool.execute(options);
48+
49+
expect(mockDocService.findBestVersion).toHaveBeenCalledWith("react", "18.x");
50+
expect(result).toContain("Best match: 18.3.1");
51+
expect(result).toContain("Unversioned docs also available");
52+
});
53+
54+
it("should return message indicating only unversioned docs when no version matches", async () => {
55+
const options: FindVersionToolOptions = { library: "vue", targetVersion: "4.0.0" };
56+
const mockResult = { bestMatch: null, hasUnversioned: true };
57+
(mockDocService.findBestVersion as Mock).mockResolvedValue(mockResult);
58+
59+
const result = await findVersionTool.execute(options);
60+
61+
expect(mockDocService.findBestVersion).toHaveBeenCalledWith("vue", "4.0.0");
62+
expect(result).toContain("No matching version found");
63+
expect(result).toContain("but unversioned docs exist");
64+
});
65+
66+
it("should return message indicating no match when VersionNotFoundError is thrown", async () => {
67+
const options: FindVersionToolOptions = { library: "angular", targetVersion: "1.0.0" };
68+
const available = [{ version: "15.0.0", indexed: true }, { version: "16.1.0", indexed: false }];
69+
const error = new VersionNotFoundError("angular", "1.0.0", available);
70+
(mockDocService.findBestVersion as Mock).mockRejectedValue(error);
71+
72+
const result = await findVersionTool.execute(options);
73+
74+
expect(mockDocService.findBestVersion).toHaveBeenCalledWith("angular", "1.0.0");
75+
expect(result).toContain("No matching version or unversioned documents found");
76+
expect(result).toContain("Available:"); // Check it mentions availability without exact format
77+
expect(result).toContain("15.0.0");
78+
expect(result).toContain("16.1.0");
79+
});
80+
81+
it("should return message indicating no match when VersionNotFoundError is thrown with no available versions", async () => {
82+
const options: FindVersionToolOptions = { library: "unknown-lib" };
83+
const error = new VersionNotFoundError("unknown-lib", "latest", []); // Assuming default is 'latest' if targetVersion omitted
84+
(mockDocService.findBestVersion as Mock).mockRejectedValue(error);
85+
86+
const result = await findVersionTool.execute(options);
87+
88+
expect(mockDocService.findBestVersion).toHaveBeenCalledWith("unknown-lib", undefined); // targetVersion is undefined
89+
expect(result).toContain("No matching version or unversioned documents found");
90+
expect(result).toContain("Available: None");
91+
});
92+
93+
it("should re-throw unexpected errors from docService", async () => {
94+
const options: FindVersionToolOptions = { library: "react" };
95+
const unexpectedError = new Error("Database connection failed");
96+
(mockDocService.findBestVersion as Mock).mockRejectedValue(unexpectedError);
97+
98+
await expect(findVersionTool.execute(options)).rejects.toThrow(
99+
"Database connection failed",
100+
);
101+
expect(logger.error).toHaveBeenCalled();
102+
});
103+
104+
it("should handle missing targetVersion correctly", async () => {
105+
const options: FindVersionToolOptions = { library: "react" }; // No targetVersion
106+
const mockResult = { bestMatch: "18.3.1", hasUnversioned: false };
107+
(mockDocService.findBestVersion as Mock).mockResolvedValue(mockResult);
108+
109+
const result = await findVersionTool.execute(options);
110+
111+
// Check that findBestVersion was called with undefined for targetVersion
112+
expect(mockDocService.findBestVersion).toHaveBeenCalledWith("react", undefined);
113+
expect(result).toContain("Best match: 18.3.1");
114+
});
115+
});
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { describe, it, expect, vi, beforeEach, type Mock } from "vitest";
2+
import type { DocumentManagementService } from "../store/DocumentManagementService";
3+
import { ListLibrariesTool } from "./ListLibrariesTool";
4+
import { logger } from "../utils/logger"; // Assuming logger might be used internally, mock it just in case
5+
6+
// Mock dependencies
7+
vi.mock("../store/DocumentManagementService");
8+
vi.mock("../utils/logger");
9+
10+
describe("ListLibrariesTool", () => {
11+
let mockDocService: Partial<DocumentManagementService>;
12+
let listLibrariesTool: ListLibrariesTool;
13+
14+
beforeEach(() => {
15+
// Reset mocks before each test
16+
vi.resetAllMocks();
17+
18+
// Setup mock DocumentManagementService
19+
mockDocService = {
20+
listLibraries: vi.fn(),
21+
};
22+
23+
// Create instance of the tool with the mock service
24+
listLibrariesTool = new ListLibrariesTool(
25+
mockDocService as DocumentManagementService,
26+
);
27+
});
28+
29+
it("should return a list of libraries with their versions", async () => {
30+
const mockRawLibraries = [
31+
{
32+
library: "react",
33+
versions: [
34+
{ version: "18.2.0", indexed: true },
35+
{ version: "17.0.1", indexed: false },
36+
],
37+
},
38+
{
39+
library: "vue",
40+
versions: [{ version: "3.2.0", indexed: true }],
41+
},
42+
{
43+
library: "unversioned-lib",
44+
versions: [{ version: "", indexed: true }], // Test unversioned case
45+
},
46+
];
47+
(mockDocService.listLibraries as Mock).mockResolvedValue(mockRawLibraries);
48+
49+
const result = await listLibrariesTool.execute();
50+
51+
expect(mockDocService.listLibraries).toHaveBeenCalledOnce();
52+
expect(result).toEqual({
53+
libraries: [
54+
{
55+
name: "react",
56+
versions: [
57+
{ version: "18.2.0", indexed: true },
58+
{ version: "17.0.1", indexed: false },
59+
],
60+
},
61+
{
62+
name: "vue",
63+
versions: [{ version: "3.2.0", indexed: true }],
64+
},
65+
{
66+
name: "unversioned-lib",
67+
versions: [{ version: "", indexed: true }],
68+
},
69+
],
70+
});
71+
// Check structure more generally
72+
expect(result.libraries).toBeInstanceOf(Array);
73+
expect(result.libraries.length).toBe(3);
74+
result.libraries.forEach((lib) => {
75+
expect(lib).toHaveProperty("name");
76+
expect(lib).toHaveProperty("versions");
77+
expect(lib.versions).toBeInstanceOf(Array);
78+
lib.versions.forEach((v) => {
79+
expect(v).toHaveProperty("version");
80+
expect(v).toHaveProperty("indexed");
81+
});
82+
});
83+
});
84+
85+
it("should return an empty list when no libraries are in the store", async () => {
86+
(mockDocService.listLibraries as Mock).mockResolvedValue([]);
87+
88+
const result = await listLibrariesTool.execute();
89+
90+
expect(mockDocService.listLibraries).toHaveBeenCalledOnce();
91+
expect(result).toEqual({ libraries: [] });
92+
});
93+
94+
it("should handle potential errors from the docService", async () => {
95+
const error = new Error("Failed to access store");
96+
(mockDocService.listLibraries as Mock).mockRejectedValue(error);
97+
98+
await expect(listLibrariesTool.execute()).rejects.toThrow(
99+
"Failed to access store",
100+
);
101+
});
102+
});

0 commit comments

Comments
 (0)