Skip to main content
Glama

documcp

by tosin2013
detect-gaps.test.ts22.8 kB
import { promises as fs } from "fs"; import path from "path"; import { tmpdir } from "os"; // Mock dependencies that don't involve filesystem const mockAnalyzeRepository = jest.fn(); const mockValidateContent = jest.fn(); jest.mock("../../src/tools/analyze-repository.js", () => ({ analyzeRepository: mockAnalyzeRepository, })); jest.mock("../../src/tools/validate-content.js", () => ({ handleValidateDiataxisContent: mockValidateContent, })); jest.mock("../../src/utils/code-scanner.js", () => ({ CodeScanner: jest.fn().mockImplementation(() => ({ analyzeRepository: jest.fn().mockResolvedValue({ summary: { totalFiles: 5, parsedFiles: 3, functions: 10, classes: 2, interfaces: 3, types: 1, constants: 2, apiEndpoints: 1, }, files: ["src/test.ts"], functions: [ { name: "testFunction", filePath: "src/test.ts", line: 1, exported: true, hasJSDoc: false, }, ], classes: [ { name: "TestClass", filePath: "src/test.ts", line: 5, exported: true, hasJSDoc: false, }, ], interfaces: [ { name: "TestInterface", filePath: "src/test.ts", line: 10, exported: true, hasJSDoc: false, }, ], types: [], constants: [], apiEndpoints: [], imports: [], exports: [], frameworks: [], }), })), })); // Helper functions for creating test directories and files async function createTestDirectory(name: string): Promise<string> { const testDir = path.join( tmpdir(), "documcp-test-" + Date.now() + "-" + Math.random().toString(36).substring(7), ); await fs.mkdir(testDir, { recursive: true }); return testDir; } async function createTestFile( filePath: string, content: string, ): Promise<void> { await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, content); } async function cleanupTestDirectory(dirPath: string): Promise<void> { try { await fs.rm(dirPath, { recursive: true, force: true }); } catch (error) { // Ignore cleanup errors } } // Now import the module under test import { detectDocumentationGaps } from "../../src/tools/detect-gaps.js"; describe("detectDocumentationGaps (Real Filesystem)", () => { const mockRepositoryAnalysis = { id: "analysis_123", structure: { hasTests: true, hasCI: true, hasDocs: true, }, dependencies: { ecosystem: "javascript", packages: ["react", "express"], }, hasApiEndpoints: true, packageManager: "npm", hasDocker: true, hasCICD: true, }; const mockValidationResult = { success: true, confidence: { overall: 85 }, issues: [{ type: "warning", description: "Missing API examples" }], validationResults: [ { status: "pass", message: "Good structure" }, { status: "fail", message: "Missing references", recommendation: "Add API docs", }, ], }; let testRepoDir: string; const createdDirs: string[] = []; beforeEach(async () => { jest.clearAllMocks(); // Create a fresh test directory for each test testRepoDir = await createTestDirectory("test-repo"); createdDirs.push(testRepoDir); // Default successful repository analysis mockAnalyzeRepository.mockResolvedValue({ content: [ { type: "text", text: JSON.stringify(mockRepositoryAnalysis), }, ], }); // Default validation result mockValidateContent.mockResolvedValue({ content: [ { type: "text", text: JSON.stringify({ success: true, data: mockValidationResult }), }, ], } as any); }); afterEach(async () => { // Cleanup all created directories await Promise.all(createdDirs.map((dir) => cleanupTestDirectory(dir))); createdDirs.length = 0; }); describe("basic functionality", () => { it("should detect gaps in repository without documentation", async () => { // No docs directory created - test repo is empty const result = await detectDocumentationGaps({ repositoryPath: testRepoDir, depth: "quick", }); expect(result.content).toBeDefined(); expect(result.content[0]).toBeDefined(); const data = JSON.parse(result.content[0].text); expect(data.repositoryPath).toBe(testRepoDir); expect(data.analysisId).toBe("analysis_123"); expect(data.overallScore).toBe(0); expect(data.gaps).toContainEqual( expect.objectContaining({ category: "general", gapType: "missing_section", description: "No documentation directory found", priority: "critical", }), ); }); it("should detect missing Diataxis sections", async () => { // Create docs directory with some sections but missing tutorials and how-to const docsDir = path.join(testRepoDir, "docs"); await fs.mkdir(docsDir); await createTestFile( path.join(docsDir, "index.md"), "# Main Documentation", ); // Create reference and explanation sections await fs.mkdir(path.join(docsDir, "reference")); await createTestFile( path.join(docsDir, "reference", "api.md"), "# API Reference", ); await fs.mkdir(path.join(docsDir, "explanation")); await createTestFile( path.join(docsDir, "explanation", "concepts.md"), "# Concepts", ); // tutorials and how-to are missing const result = await detectDocumentationGaps({ repositoryPath: testRepoDir, documentationPath: docsDir, depth: "standard", }); const data = JSON.parse(result.content[0].text); expect(data.gaps).toContainEqual( expect.objectContaining({ category: "tutorials", gapType: "missing_section", priority: "high", }), ); expect(data.gaps).toContainEqual( expect.objectContaining({ category: "how-to", gapType: "missing_section", priority: "medium", }), ); }); it("should identify existing documentation strengths", async () => { // Create comprehensive docs structure const docsDir = path.join(testRepoDir, "docs"); await fs.mkdir(docsDir); await createTestFile( path.join(docsDir, "README.md"), "# Project Documentation", ); // Create all Diataxis sections await fs.mkdir(path.join(docsDir, "tutorials")); await createTestFile( path.join(docsDir, "tutorials", "getting-started.md"), "# Getting Started", ); await fs.mkdir(path.join(docsDir, "how-to")); await createTestFile( path.join(docsDir, "how-to", "deployment.md"), "# How to Deploy", ); await fs.mkdir(path.join(docsDir, "reference")); await createTestFile( path.join(docsDir, "reference", "api.md"), "# API Reference", ); await fs.mkdir(path.join(docsDir, "explanation")); await createTestFile( path.join(docsDir, "explanation", "architecture.md"), "# Architecture", ); const result = await detectDocumentationGaps({ repositoryPath: testRepoDir, documentationPath: docsDir, depth: "comprehensive", }); const data = JSON.parse(result.content[0].text); expect(data.strengths).toContain("Has main documentation index file"); expect(data.strengths).toContain( "Well-organized sections: tutorials, how-to, reference, explanation", ); expect(data.overallScore).toBeGreaterThan(50); // Adjust expectation to match actual scoring }); }); describe("error handling", () => { it("should handle repository analysis failure", async () => { mockAnalyzeRepository.mockResolvedValue({ content: [ { type: "text", text: JSON.stringify({ success: false, error: "Analysis failed" }), }, ], }); const result = await detectDocumentationGaps({ repositoryPath: testRepoDir, }); expect(result.content[0].text).toContain("GAP_DETECTION_FAILED"); expect(result).toHaveProperty("isError", true); }); it("should handle file system errors gracefully", async () => { // Create a docs directory but then make it inaccessible const docsDir = path.join(testRepoDir, "docs"); await fs.mkdir(docsDir); const result = await detectDocumentationGaps({ repositoryPath: testRepoDir, documentationPath: docsDir, }); const data = JSON.parse(result.content[0].text); expect(data.analysisId).toBe("analysis_123"); expect(data.gaps).toBeInstanceOf(Array); }); }); describe("code-based gap detection", () => { it("should detect missing API documentation when endpoints exist", async () => { // Create docs directory without API documentation const docsDir = path.join(testRepoDir, "docs"); await fs.mkdir(docsDir); await createTestFile(path.join(docsDir, "index.md"), "# Documentation"); // Mock CodeScanner to return API endpoints const { CodeScanner } = require("../../src/utils/code-scanner.js"); CodeScanner.mockImplementationOnce(() => ({ analyzeRepository: jest.fn().mockResolvedValue({ summary: { totalFiles: 5, parsedFiles: 3, functions: 10, classes: 2, interfaces: 3, types: 1, constants: 2, apiEndpoints: 3, }, files: ["src/api.ts", "src/routes.ts"], functions: [], classes: [], interfaces: [], types: [], constants: [], apiEndpoints: [ { method: "GET", path: "/api/users", filePath: "src/api.ts", line: 10, hasDocumentation: true, }, { method: "POST", path: "/api/users", filePath: "src/api.ts", line: 20, hasDocumentation: true, }, { method: "DELETE", path: "/api/users/:id", filePath: "src/routes.ts", line: 5, hasDocumentation: true, }, ], imports: [], exports: [], frameworks: [], }), })); const result = await detectDocumentationGaps({ repositoryPath: testRepoDir, documentationPath: docsDir, depth: "comprehensive", }); const data = JSON.parse(result.content[0].text); // Should detect missing API documentation section expect(data.gaps).toContainEqual( expect.objectContaining({ category: "reference", gapType: "missing_section", description: expect.stringContaining("API endpoints"), priority: "critical", }), ); }); it("should detect undocumented API endpoints", async () => { // Create docs directory with API section const docsDir = path.join(testRepoDir, "docs"); await fs.mkdir(docsDir); await fs.mkdir(path.join(docsDir, "reference")); await createTestFile( path.join(docsDir, "reference", "api.md"), "# API Reference", ); // Mock CodeScanner to return endpoints without documentation const { CodeScanner } = require("../../src/utils/code-scanner.js"); CodeScanner.mockImplementationOnce(() => ({ analyzeRepository: jest.fn().mockResolvedValue({ summary: { totalFiles: 5, parsedFiles: 3, functions: 10, classes: 2, interfaces: 3, types: 1, constants: 2, apiEndpoints: 2, }, files: ["src/api.ts"], functions: [], classes: [], interfaces: [], types: [], constants: [], apiEndpoints: [ { method: "GET", path: "/api/data", filePath: "src/api.ts", line: 15, hasDocumentation: false, // No JSDoc }, { method: "POST", path: "/api/data", filePath: "src/api.ts", line: 25, hasDocumentation: false, // No JSDoc }, ], imports: [], exports: [], frameworks: [], }), })); const result = await detectDocumentationGaps({ repositoryPath: testRepoDir, documentationPath: docsDir, depth: "comprehensive", }); const data = JSON.parse(result.content[0].text); // Should detect undocumented endpoints expect(data.gaps).toContainEqual( expect.objectContaining({ category: "reference", gapType: "missing_examples", description: expect.stringContaining("2 API endpoints lack"), priority: "high", }), ); }); it("should detect undocumented exported classes", async () => { const docsDir = path.join(testRepoDir, "docs"); await fs.mkdir(docsDir); await createTestFile(path.join(docsDir, "index.md"), "# Documentation"); // Mock CodeScanner to return undocumented classes const { CodeScanner } = require("../../src/utils/code-scanner.js"); CodeScanner.mockImplementationOnce(() => ({ analyzeRepository: jest.fn().mockResolvedValue({ summary: { totalFiles: 5, parsedFiles: 3, functions: 10, classes: 3, interfaces: 2, types: 1, constants: 2, apiEndpoints: 0, }, files: ["src/models.ts"], functions: [], classes: [ { name: "UserModel", filePath: "src/models.ts", line: 10, exported: true, hasJSDoc: false, }, { name: "PostModel", filePath: "src/models.ts", line: 30, exported: true, hasJSDoc: false, }, { name: "InternalHelper", filePath: "src/models.ts", line: 50, exported: false, // Not exported, should be ignored hasJSDoc: false, }, ], interfaces: [], types: [], constants: [], apiEndpoints: [], imports: [], exports: [], frameworks: [], }), })); const result = await detectDocumentationGaps({ repositoryPath: testRepoDir, documentationPath: docsDir, depth: "comprehensive", }); const data = JSON.parse(result.content[0].text); // Should detect undocumented exported classes (only 2, not the non-exported one) expect(data.gaps).toContainEqual( expect.objectContaining({ category: "reference", gapType: "incomplete_content", description: expect.stringContaining("2 exported classes lack"), priority: "medium", }), ); }); it("should detect undocumented exported interfaces", async () => { const docsDir = path.join(testRepoDir, "docs"); await fs.mkdir(docsDir); await createTestFile(path.join(docsDir, "index.md"), "# Documentation"); // Mock CodeScanner to return undocumented interfaces const { CodeScanner } = require("../../src/utils/code-scanner.js"); CodeScanner.mockImplementationOnce(() => ({ analyzeRepository: jest.fn().mockResolvedValue({ summary: { totalFiles: 5, parsedFiles: 3, functions: 10, classes: 2, interfaces: 3, types: 1, constants: 2, apiEndpoints: 0, }, files: ["src/types.ts"], functions: [], classes: [], interfaces: [ { name: "IUser", filePath: "src/types.ts", line: 5, exported: true, hasJSDoc: false, }, { name: "IConfig", filePath: "src/types.ts", line: 15, exported: true, hasJSDoc: false, }, { name: "IInternalState", filePath: "src/types.ts", line: 25, exported: false, // Not exported hasJSDoc: false, }, ], types: [], constants: [], apiEndpoints: [], imports: [], exports: [], frameworks: [], }), })); const result = await detectDocumentationGaps({ repositoryPath: testRepoDir, documentationPath: docsDir, depth: "comprehensive", }); const data = JSON.parse(result.content[0].text); // Should detect undocumented exported interfaces expect(data.gaps).toContainEqual( expect.objectContaining({ category: "reference", gapType: "incomplete_content", description: expect.stringContaining("2 exported interfaces lack"), priority: "medium", }), ); }); it("should handle validation errors gracefully", async () => { const docsDir = path.join(testRepoDir, "docs"); await fs.mkdir(docsDir); await createTestFile(path.join(docsDir, "index.md"), "# Documentation"); // Mock validation to throw an error mockValidateContent.mockRejectedValueOnce( new Error("Validation service unavailable"), ); const result = await detectDocumentationGaps({ repositoryPath: testRepoDir, documentationPath: docsDir, depth: "comprehensive", }); const data = JSON.parse(result.content[0].text); // Should still succeed without validation data expect(data.analysisId).toBe("analysis_123"); expect(data.gaps).toBeInstanceOf(Array); expect(data.repositoryPath).toBe(testRepoDir); }); it("should handle empty repository analysis result", async () => { // Mock analyze_repository to return empty/no content mockAnalyzeRepository.mockResolvedValueOnce({ content: [], // Empty content array }); const result = await detectDocumentationGaps({ repositoryPath: testRepoDir, depth: "quick", }); // Should return error about failed analysis expect(result.content[0].text).toContain("GAP_DETECTION_FAILED"); expect(result.content[0].text).toContain("Repository analysis failed"); }); it("should detect missing React framework documentation", async () => { const docsDir = path.join(testRepoDir, "docs"); await fs.mkdir(docsDir); await createTestFile(path.join(docsDir, "index.md"), "# Documentation"); // Mock CodeScanner to return React framework const { CodeScanner } = require("../../src/utils/code-scanner.js"); CodeScanner.mockImplementationOnce(() => ({ analyzeRepository: jest.fn().mockResolvedValue({ summary: { totalFiles: 5, parsedFiles: 3, functions: 10, classes: 2, interfaces: 3, types: 1, constants: 2, apiEndpoints: 0, }, files: ["src/App.tsx"], functions: [], classes: [], interfaces: [], types: [], constants: [], apiEndpoints: [], imports: [], exports: [], frameworks: ["React"], // Indicate React is used }), })); const result = await detectDocumentationGaps({ repositoryPath: testRepoDir, documentationPath: docsDir, depth: "comprehensive", }); const data = JSON.parse(result.content[0].text); // Should detect missing React documentation expect(data.gaps).toContainEqual( expect.objectContaining({ category: "how-to", gapType: "missing_section", description: expect.stringContaining("React framework detected"), priority: "medium", }), ); }); it("should detect missing Express framework documentation", async () => { const docsDir = path.join(testRepoDir, "docs"); await fs.mkdir(docsDir); await createTestFile(path.join(docsDir, "index.md"), "# Documentation"); // Mock CodeScanner to return Express framework const { CodeScanner } = require("../../src/utils/code-scanner.js"); CodeScanner.mockImplementationOnce(() => ({ analyzeRepository: jest.fn().mockResolvedValue({ summary: { totalFiles: 5, parsedFiles: 3, functions: 10, classes: 2, interfaces: 3, types: 1, constants: 2, apiEndpoints: 0, }, files: ["src/server.ts"], functions: [], classes: [], interfaces: [], types: [], constants: [], apiEndpoints: [], imports: [], exports: [], frameworks: ["Express"], // Indicate Express is used }), })); const result = await detectDocumentationGaps({ repositoryPath: testRepoDir, documentationPath: docsDir, depth: "comprehensive", }); const data = JSON.parse(result.content[0].text); // Should detect missing Express documentation expect(data.gaps).toContainEqual( expect.objectContaining({ category: "how-to", gapType: "missing_section", description: expect.stringContaining("Express framework detected"), priority: "medium", }), ); }); }); describe("input validation", () => { it("should require repositoryPath", async () => { await expect(detectDocumentationGaps({} as any)).rejects.toThrow(); }); it("should handle invalid depth parameter", async () => { await expect( detectDocumentationGaps({ repositoryPath: testRepoDir, depth: "invalid" as any, }), ).rejects.toThrow(); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tosin2013/documcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server