dependency-context
Version:
MCP server for providing dependency documentation context to AI assistants
376 lines (318 loc) • 11.5 kB
text/typescript
import {
analyzeAndIndexDependencies,
AnalyzeParamsSchema,
} from "../tools/analyze";
import { searchDependencyDocs, SearchParamsSchema } from "../tools/search";
import * as parsers from "../parsers";
import * as repositories from "../repositories";
import * as documents from "../documents";
import * as vectorstore from "../vectorstore";
import * as config from "../config";
import path from "path";
import fs from "fs-extra";
// Mock dependencies
jest.mock("../parsers");
jest.mock("../repositories");
jest.mock("../documents");
jest.mock("../vectorstore");
jest.mock("../config");
jest.mock("fs-extra");
describe("Tools Tests", () => {
beforeEach(() => {
jest.clearAllMocks();
// Default mock for getConfig
(config.getConfig as jest.Mock).mockReturnValue({
port: 3000,
githubToken: "mock-token",
embeddingModel: "Xenova/all-MiniLM-L6-v2",
storageDir: ".dependency-context",
debugMode: false,
});
// Mock for fs.existsSync
(fs.existsSync as jest.Mock).mockReturnValue(true);
});
describe("analyzeAndIndexDependencies", () => {
test("should return failure when project path is invalid", async () => {
// Mock fs.existsSync to return false for project path
(fs.existsSync as jest.Mock).mockReturnValue(false);
const result = await analyzeAndIndexDependencies({
project_path: "/invalid/path",
});
const parsedResult = JSON.parse(result);
expect(parsedResult.status).toBe("failure");
expect(parsedResult.message).toContain("Invalid project path");
});
test("should return failure when no dependencies are found", async () => {
// Mock parseDependencies to return empty array
(parsers.parseDependencies as jest.Mock).mockResolvedValue([]);
const result = await analyzeAndIndexDependencies({
project_path: "/valid/path",
});
const parsedResult = JSON.parse(result);
expect(parsedResult.status).toBe("failure");
expect(parsedResult.message).toContain("No dependencies found");
});
test("should process dependencies successfully", async () => {
// Mock dependencies
const mockDependencies = [
{ name: "express", version: "4.17.1", type: "npm" as const },
{ name: "axios", version: "1.0.0", type: "npm" as const },
];
// Mock repositories
const mockRepo = {
name: "express",
owner: "expressjs",
url: "https://github.com/expressjs/express",
ref: "main",
};
// Mock documents
const mockDocs = [
{ content: "Express docs", path: "/README.md", filename: "README.md" },
];
// Setup mocks
(parsers.parseDependencies as jest.Mock).mockResolvedValue(
mockDependencies
);
(repositories.findGitHubRepository as jest.Mock).mockResolvedValue(
mockRepo
);
(documents.fetchDocs as jest.Mock).mockResolvedValue(mockDocs);
(vectorstore.indexDocumentation as jest.Mock).mockResolvedValue(
undefined
);
// Create mock context with log and reportProgress
const mockContext = {
log: {
info: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
},
reportProgress: jest.fn().mockResolvedValue(undefined),
};
const result = await analyzeAndIndexDependencies(
{ project_path: "/valid/path" },
mockContext
);
const parsedResult = JSON.parse(result);
// Check result status
expect(parsedResult.status).toBe("success");
expect(parsedResult.message).toContain("Successfully indexed: 2");
// Verify function calls
expect(parsers.parseDependencies).toHaveBeenCalledWith("/valid/path");
expect(repositories.findGitHubRepository).toHaveBeenCalledTimes(2);
expect(documents.fetchDocs).toHaveBeenCalledTimes(2);
expect(vectorstore.indexDocumentation).toHaveBeenCalledTimes(2);
// Verify progress reporting
expect(mockContext.reportProgress).toHaveBeenCalledTimes(10); // Initial + 20% + 3 steps per 2 dependencies + final 100%
});
test("should handle errors during dependency processing", async () => {
// Mock dependencies
const mockDependencies = [
{ name: "express", version: "4.17.1", type: "npm" as const },
{ name: "axios", version: "1.0.0", type: "npm" as const },
];
// Setup mocks with one success and one failure
(parsers.parseDependencies as jest.Mock).mockResolvedValue(
mockDependencies
);
// First dependency succeeds
(repositories.findGitHubRepository as jest.Mock)
.mockResolvedValueOnce({
name: "express",
owner: "expressjs",
url: "https://github.com/expressjs/express",
ref: "main",
})
// Second dependency fails
.mockResolvedValueOnce(null);
(documents.fetchDocs as jest.Mock).mockResolvedValue([
{ content: "Express docs", path: "/README.md", filename: "README.md" },
]);
(vectorstore.indexDocumentation as jest.Mock).mockResolvedValue(
undefined
);
const result = await analyzeAndIndexDependencies({
project_path: "/valid/path",
});
const parsedResult = JSON.parse(result);
// Check result
expect(parsedResult.status).toBe("success"); // Still success because at least one dependency succeeded
expect(parsedResult.message).toContain("Successfully indexed: 1");
expect(parsedResult.message).toContain("Errors: 1");
});
test("should handle environment variables from params", async () => {
// Mock dependencies
(parsers.parseDependencies as jest.Mock).mockResolvedValue([
{ name: "express", version: "4.17.1", type: "npm" as const },
]);
// Other necessary mocks
(repositories.findGitHubRepository as jest.Mock).mockResolvedValue({
name: "express",
owner: "expressjs",
url: "https://github.com/expressjs/express",
ref: "main",
});
(documents.fetchDocs as jest.Mock).mockResolvedValue([
{ content: "Express docs", path: "/README.md", filename: "README.md" },
]);
// Store original env
const originalEnv = { ...process.env };
try {
// Call function with env_vars
await analyzeAndIndexDependencies({
project_path: "/valid/path",
env_vars: {
GITHUB_TOKEN: "test-token",
DEBUG: "true",
},
});
// Check that environment variables were set
expect(process.env.GITHUB_TOKEN).toBe("test-token");
expect(process.env.DEBUG).toBe("true");
} finally {
// Restore original env
process.env = originalEnv;
}
});
});
describe("searchDependencyDocs", () => {
test("should return search results", async () => {
// Mock vector store search results
const mockResults = [
{
text_chunk:
"Express is a minimal and flexible Node.js web application framework",
source_repository: "https://github.com/expressjs/express",
source_file: "/README.md",
similarity_score: 0.92,
},
{
text_chunk:
"Express provides a robust set of features for web applications",
source_repository: "https://github.com/expressjs/express",
source_file: "/docs/features.md",
similarity_score: 0.85,
},
];
// Setup mocks
(vectorstore.searchVectorStore as jest.Mock).mockResolvedValue(
mockResults
);
// Create mock context with log
const mockContext = {
log: {
info: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
},
};
const result = await searchDependencyDocs(
{
project_path: "/valid/path",
query: "express routing",
},
mockContext
);
const parsedResult = JSON.parse(result);
// Check result
expect(parsedResult.results).toEqual(mockResults);
expect(mockContext.log.info).toHaveBeenCalledTimes(2);
// Verify function calls
expect(vectorstore.searchVectorStore).toHaveBeenCalledWith(
"/valid/path",
"express routing",
undefined,
expect.any(Object)
);
});
test("should handle repository context filtering", async () => {
// Setup mocks
(vectorstore.searchVectorStore as jest.Mock).mockResolvedValue([]);
await searchDependencyDocs({
project_path: "/valid/path",
query: "express routing",
repository_context: "express",
});
// Verify repository context was passed through
expect(vectorstore.searchVectorStore).toHaveBeenCalledWith(
"/valid/path",
"express routing",
"express",
expect.any(Object)
);
});
test("should handle search errors", async () => {
// Setup mock to throw error
(vectorstore.searchVectorStore as jest.Mock).mockRejectedValue(
new Error("Search failed")
);
const result = await searchDependencyDocs({
project_path: "/valid/path",
query: "express routing",
});
const parsedResult = JSON.parse(result);
// Check result contains empty results and error
expect(parsedResult.results).toEqual([]);
expect(parsedResult.error).toBe("Search failed");
});
test("should handle environment variables from params", async () => {
// Mock vector store search results
(vectorstore.searchVectorStore as jest.Mock).mockResolvedValue([]);
// Store original env
const originalEnv = { ...process.env };
try {
// Call function with env_vars
await searchDependencyDocs({
project_path: "/valid/path",
query: "express routing",
env_vars: {
GITHUB_TOKEN: "search-token",
DEBUG: "true",
},
});
// Check that environment variables were set
expect(process.env.GITHUB_TOKEN).toBe("search-token");
expect(process.env.DEBUG).toBe("true");
} finally {
// Restore original env
process.env = originalEnv;
}
});
test("should log debug information when debug mode is enabled", async () => {
// Setup config with debug mode enabled
(config.getConfig as jest.Mock).mockReturnValue({
port: 3000,
githubToken: "mock-token",
embeddingModel: "Xenova/all-MiniLM-L6-v2",
storageDir: ".dependency-context",
debugMode: true,
});
// Mock vector store search results
(vectorstore.searchVectorStore as jest.Mock).mockResolvedValue([]);
// Create mock context with log
const mockContext = {
log: {
info: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
},
};
await searchDependencyDocs(
{
project_path: "/valid/path",
query: "express routing",
repository_context: "express",
},
mockContext
);
// Verify debug log was called
expect(mockContext.log.debug).toHaveBeenCalledWith(
"Search details:",
expect.objectContaining({
query: "express routing",
repository_context: "express",
})
);
});
});
});