Skip to main content
Glama

documcp

by tosin2013
sync-code-to-docs.test.ts44.3 kB
/** * Sync Code to Docs Tool Tests (Phase 3) */ import { handleSyncCodeToDocs } from "../../src/tools/sync-code-to-docs.js"; import { promises as fs } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { mkdtemp, rm } from "fs/promises"; import { DriftDetector } from "../../src/utils/drift-detector.js"; describe("sync_code_to_docs tool", () => { let tempDir: string; let projectPath: string; let docsPath: string; beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "sync-test-")); projectPath = join(tempDir, "project"); docsPath = join(tempDir, "docs"); await fs.mkdir(join(projectPath, "src"), { recursive: true }); await fs.mkdir(docsPath, { recursive: true }); }); afterEach(async () => { await rm(tempDir, { recursive: true, force: true }); }); describe("Detect Mode", () => { test("should detect drift without making changes", async () => { // Create source file const sourceCode = ` export function calculate(x: number): number { return x * 2; } `.trim(); await fs.writeFile(join(projectPath, "src", "calc.ts"), sourceCode); // Create documentation const docContent = ` # Calculator ## calculate(x: number): number Doubles the input. `.trim(); await fs.writeFile(join(docsPath, "calc.md"), docContent); // Run in detect mode const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", createSnapshot: true, }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); expect(result.content[0]).toBeDefined(); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.mode).toBe("detect"); // Verify no changes were made const docAfter = await fs.readFile(join(docsPath, "calc.md"), "utf-8"); expect(docAfter).toBe(docContent); }); test("should create baseline snapshot on first run", async () => { const sourceCode = `export function test(): void {}`; await fs.writeFile(join(projectPath, "src", "test.ts"), sourceCode); const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", createSnapshot: true, }); expect(result).toBeDefined(); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.snapshotId).toBeTruthy(); // Check snapshot was created const snapshotDir = join(tempDir, "project", ".documcp", "snapshots"); const files = await fs.readdir(snapshotDir); expect(files.length).toBeGreaterThan(0); }); test("should report drift statistics", async () => { // Create initial snapshot const oldCode = ` export function oldFunction(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "changes.ts"), oldCode); await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", createSnapshot: true, }); // Make changes const newCode = ` export function newFunction(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "changes.ts"), newCode); // Detect drift const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", createSnapshot: true, }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.stats).toBeDefined(); expect(data.data.stats.filesAnalyzed).toBeGreaterThanOrEqual(0); }); }); describe("Apply Mode", () => { test("should apply high-confidence changes automatically", async () => { // Create code with JSDoc const sourceCode = ` /** * Calculates the sum of two numbers * @param a First number * @param b Second number * @returns The sum */ export function add(a: number, b: number): number { return a + b; } `.trim(); await fs.writeFile(join(projectPath, "src", "math.ts"), sourceCode); // Create minimal documentation const docContent = ` # Math Module Documentation needed. `.trim(); await fs.writeFile(join(docsPath, "math.md"), docContent); // Create baseline await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", createSnapshot: true, }); // Run in apply mode with high threshold const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "apply", autoApplyThreshold: 0.9, createSnapshot: true, }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.mode).toBe("apply"); // Stats should show applied or pending changes const stats = data.data.stats; expect( stats.changesApplied + stats.changesPending, ).toBeGreaterThanOrEqual(0); }); test("should respect confidence threshold", async () => { // Setup code and docs const sourceCode = `export function test(): void {}`; await fs.writeFile(join(projectPath, "src", "test.ts"), sourceCode); const docContent = `# Test`; await fs.writeFile(join(docsPath, "test.md"), docContent); // Create baseline await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); // Apply with very high threshold (most changes won't meet it) const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "apply", autoApplyThreshold: 0.99, }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); // With high threshold, most changes should be pending if (data.data.stats.driftsDetected > 0) { expect(data.data.pendingChanges.length).toBeGreaterThanOrEqual(0); } }); test("should create snapshot before applying changes", async () => { const sourceCode = `export function test(): void {}`; await fs.writeFile( join(projectPath, "src", "snapshot-test.ts"), sourceCode, ); await handleSyncCodeToDocs({ projectPath, docsPath, mode: "apply", createSnapshot: true, }); // Verify snapshot exists const snapshotDir = join(projectPath, ".documcp", "snapshots"); const files = await fs.readdir(snapshotDir); expect(files.length).toBeGreaterThan(0); }); }); describe("Auto Mode", () => { test("should apply all changes in auto mode", async () => { const sourceCode = ` export function autoFunction(param: string): string { return param.toUpperCase(); } `.trim(); await fs.writeFile(join(projectPath, "src", "auto.ts"), sourceCode); const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "auto", createSnapshot: true, }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.mode).toBe("auto"); }); }); describe("Error Handling", () => { test("should handle invalid project path", async () => { const result = await handleSyncCodeToDocs({ projectPath: "/nonexistent/path", docsPath, mode: "detect", }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); const data = JSON.parse(result.content[0].text); // Should either fail gracefully or handle missing path expect(data).toBeDefined(); }); test("should handle invalid docs path", async () => { const sourceCode = `export function test(): void {}`; await fs.writeFile(join(projectPath, "src", "test.ts"), sourceCode); const result = await handleSyncCodeToDocs({ projectPath, docsPath: "/nonexistent/docs", mode: "detect", }); expect(result).toBeDefined(); const data = JSON.parse(result.content[0].text); expect(data).toBeDefined(); }); test("should handle empty project", async () => { // Empty project directory const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); expect(result).toBeDefined(); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.stats.filesAnalyzed).toBe(0); }); }); describe("Recommendations and Next Steps", () => { test("should provide recommendations based on results", async () => { const sourceCode = ` export function critical(param: number): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "critical.ts"), sourceCode); // Create baseline await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); // Make breaking change const newCode = ` export function critical(param: string, extra: boolean): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "critical.ts"), newCode); // Detect changes const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.recommendations).toBeDefined(); expect(Array.isArray(data.recommendations)).toBe(true); }); test("should provide next steps", async () => { const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.nextSteps).toBeDefined(); expect(Array.isArray(data.nextSteps)).toBe(true); }); }); describe("Integration with Knowledge Graph", () => { test("should store sync events", async () => { const sourceCode = `export function kgTest(): void {}`; await fs.writeFile(join(projectPath, "src", "kg-test.ts"), sourceCode); const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); // Sync event should be created (even if storage fails, shouldn't error) expect(data.data).toBeDefined(); }); }); describe("Preview Mode", () => { test("should show changes in preview mode without applying", async () => { const sourceCode = ` export function previewFunc(x: number): number { return x * 3; } `.trim(); await fs.writeFile(join(projectPath, "src", "preview.ts"), sourceCode); const docContent = ` # Preview Old documentation. `.trim(); await fs.writeFile(join(docsPath, "preview.md"), docContent); // Create baseline await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); // Change code const newCode = ` export function previewFunc(x: number, y: number): number { return x * y; } `.trim(); await fs.writeFile(join(projectPath, "src", "preview.ts"), newCode); // Preview changes const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "preview", }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.mode).toBe("preview"); // Verify documentation wasn't changed const docAfter = await fs.readFile(join(docsPath, "preview.md"), "utf-8"); expect(docAfter).toBe(docContent); }); }); describe("Documentation Change Application", () => { test("should apply changes when low-confidence changes exist in auto mode", async () => { // Create a source file with documentation const sourceCode = ` /** * Multiplies two numbers together * @param x First number * @param y Second number */ export function multiply(x: number, y: number): number { return x * y; } `.trim(); await fs.writeFile(join(projectPath, "src", "math.ts"), sourceCode); // Create outdated documentation const docContent = ` # Math Module ## multiply Adds two numbers. `.trim(); await fs.writeFile(join(docsPath, "math.md"), docContent); // Create baseline await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); // Run in auto mode (applies all changes) const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "auto", }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.mode).toBe("auto"); }); test("should handle apply errors gracefully", async () => { // Create source file const sourceCode = `export function testFunc(): void {}`; await fs.writeFile(join(projectPath, "src", "test.ts"), sourceCode); // Create documentation in a read-only parent directory would fail // But for this test, we'll just verify the error handling path exists const docContent = `# Test`; await fs.writeFile(join(docsPath, "test.md"), docContent); // Create baseline await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); // Modify code const newCode = `export function testFunc(param: string): void {}`; await fs.writeFile(join(projectPath, "src", "test.ts"), newCode); // Try to apply changes const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "apply", autoApplyThreshold: 0.0, // Very low threshold }); // Should complete without crashing expect(result).toBeDefined(); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); }); }); describe("Recommendation Edge Cases", () => { test("should recommend review for breaking changes", async () => { // Create initial code const oldCode = ` export function oldApi(x: number): string { return x.toString(); } `.trim(); await fs.writeFile(join(projectPath, "src", "api.ts"), oldCode); // Create baseline await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); // Make breaking change const newCode = ` export function newApi(x: number, y: string): boolean { return x > 0; } `.trim(); await fs.writeFile(join(projectPath, "src", "api.ts"), newCode); // Detect changes const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); // Should have recommendations expect(data.recommendations).toBeDefined(); expect(Array.isArray(data.recommendations)).toBe(true); }); test("should show info when no drift detected", async () => { // Create code const sourceCode = `export function stable(): void {}`; await fs.writeFile(join(projectPath, "src", "stable.ts"), sourceCode); // Create baseline await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); // Run again without changes const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.recommendations).toBeDefined(); // Should have "No Drift Detected" recommendation const noDriftRec = data.recommendations.find( (r: any) => r.title?.includes("No Drift"), ); expect(noDriftRec).toBeDefined(); }); test("should recommend validation after applying changes", async () => { const sourceCode = ` /** * Test function */ export function test(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "validated.ts"), sourceCode); // Create baseline await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); // Modify code const newCode = ` /** * Modified test function */ export function test(param: string): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "validated.ts"), newCode); // Apply changes const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "auto", }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); // Should have next steps expect(data.nextSteps).toBeDefined(); expect(Array.isArray(data.nextSteps)).toBe(true); }); }); describe("Next Steps Generation", () => { test("should suggest apply mode when in detect mode with pending changes", async () => { const sourceCode = `export function needsSync(): void {}`; await fs.writeFile(join(projectPath, "src", "sync.ts"), sourceCode); // Create baseline await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); // Change code const newCode = `export function needsSync(param: number): void {}`; await fs.writeFile(join(projectPath, "src", "sync.ts"), newCode); // Detect in detect mode const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.nextSteps).toBeDefined(); // If there are pending changes, should suggest apply mode if (data.data.pendingChanges?.length > 0) { const applyStep = data.nextSteps.find( (s: any) => s.action?.includes("Apply"), ); expect(applyStep).toBeDefined(); } }); test("should suggest review for pending manual changes", async () => { const sourceCode = `export function complex(): void {}`; await fs.writeFile(join(projectPath, "src", "complex.ts"), sourceCode); // Create baseline await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); // Change code const newCode = `export function complex(a: number, b: string): boolean { return true; }`; await fs.writeFile(join(projectPath, "src", "complex.ts"), newCode); // Detect with very high threshold (forces manual review) const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "apply", autoApplyThreshold: 0.99, }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.nextSteps).toBeDefined(); }); }); describe("Snapshot Management", () => { test("should not create snapshot when createSnapshot is false in detect mode", async () => { const sourceCode = `export function noSnapshot(): void {}`; await fs.writeFile(join(projectPath, "src", "nosnapshot.ts"), sourceCode); await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", createSnapshot: false, }); // Should still work even without snapshot const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", createSnapshot: false, }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); }); }); describe("Error Path Coverage", () => { test("should handle KG storage failures gracefully", async () => { const sourceCode = `export function kgError(): void {}`; await fs.writeFile(join(projectPath, "src", "kg-error.ts"), sourceCode); // The tool should complete even if KG storage fails const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); }); test("should handle zero drift detections", async () => { const sourceCode = `export function stable(): void {}`; await fs.writeFile(join(projectPath, "src", "stable.ts"), sourceCode); // Create baseline await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); // Run again with no changes const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.stats.driftsDetected).toBe(0); }); test("should handle files with no drift suggestions", async () => { const sourceCode = `export function noDrift(): void {}`; await fs.writeFile(join(projectPath, "src", "nodrift.ts"), sourceCode); const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "apply", autoApplyThreshold: 0.5, }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); // Should handle case with no drift gracefully expect(data.data.appliedChanges).toBeDefined(); expect(data.data.pendingChanges).toBeDefined(); }); test("should handle recommendations with zero breaking changes", async () => { const sourceCode = `export function minor(): void {}`; await fs.writeFile(join(projectPath, "src", "minor.ts"), sourceCode); const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); // Should not have critical recommendations for no breaking changes const criticalRecs = data.recommendations?.filter( (r: any) => r.type === "critical", ); expect(criticalRecs || []).toHaveLength(0); }); test("should handle pending changes without manual review", async () => { const sourceCode = `export function autoApply(): void {}`; await fs.writeFile(join(projectPath, "src", "autoapply.ts"), sourceCode); // Create baseline await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); // Modify const newCode = `export function autoApply(x: number): number { return x; }`; await fs.writeFile(join(projectPath, "src", "autoapply.ts"), newCode); const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "auto", // Auto applies all }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); }); test("should handle next steps when no breaking changes exist", async () => { const sourceCode = `export function noBreaking(): void {}`; await fs.writeFile(join(projectPath, "src", "nobreaking.ts"), sourceCode); const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); // Should not suggest reviewing breaking changes when there are none const breakingStep = data.nextSteps?.find( (s: any) => s.action?.toLowerCase().includes("breaking"), ); expect(breakingStep).toBeUndefined(); }); test("should handle next steps when no changes were applied", async () => { const sourceCode = `export function noApplied(): void {}`; await fs.writeFile(join(projectPath, "src", "noapplied.ts"), sourceCode); const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", // Detect mode doesn't apply }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.appliedChanges).toHaveLength(0); }); test("should handle next steps when no pending changes require review", async () => { const sourceCode = `export function noPending(): void {}`; await fs.writeFile(join(projectPath, "src", "nopending.ts"), sourceCode); const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "auto", // Auto applies everything }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); }); test("should handle apply mode with suggestions below threshold", async () => { const sourceCode = `export function lowConfidence(): void {}`; await fs.writeFile( join(projectPath, "src", "lowconfidence.ts"), sourceCode, ); // Create baseline await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); // Modify const newCode = `export function lowConfidence(param: string): void {}`; await fs.writeFile(join(projectPath, "src", "lowconfidence.ts"), newCode); // Very high threshold - suggestions won't meet it const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "apply", autoApplyThreshold: 1.0, }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); }); test("should handle context parameter with info logging", async () => { const sourceCode = `export function withContext(): void {}`; await fs.writeFile(join(projectPath, "src", "context.ts"), sourceCode); const mockContext = { info: jest.fn(), warn: jest.fn(), }; const result = await handleSyncCodeToDocs( { projectPath, docsPath, mode: "detect", }, mockContext, ); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); // Context info should have been called expect(mockContext.info).toHaveBeenCalled(); }); test("should handle snapshot creation in non-detect modes", async () => { const sourceCode = `export function modeSnapshot(): void {}`; await fs.writeFile( join(projectPath, "src", "modesnapshot.ts"), sourceCode, ); // Apply mode should create snapshot even if createSnapshot not specified const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "apply", createSnapshot: false, // But mode !== "detect" overrides this }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); }); }); describe("Mocked Drift Detector Tests", () => { let mockDetector: jest.Mocked<DriftDetector>; beforeEach(() => { // Create a real detector instance but spy on its methods mockDetector = new DriftDetector( projectPath, ) as jest.Mocked<DriftDetector>; }); test("should apply high-confidence suggestions automatically", async () => { // Create real documentation file const docPath = join(docsPath, "api.md"); const originalDoc = `# API ## oldFunction This is outdated.`; await fs.writeFile(docPath, originalDoc); // Mock drift detector to return suggestions jest.spyOn(DriftDetector.prototype, "initialize").mockResolvedValue(); jest.spyOn(DriftDetector.prototype, "createSnapshot").mockResolvedValue({ timestamp: "2025-01-01T00:00:00.000Z", projectPath, files: new Map([ [ "src/api.ts", { filePath: "src/api.ts", language: "typescript", functions: [], classes: [], interfaces: [], types: [], imports: [], exports: [], contentHash: "abc123", lastModified: "2025-01-01T00:00:00.000Z", linesOfCode: 10, complexity: 1, }, ], ]), documentation: new Map(), }); jest .spyOn(DriftDetector.prototype, "loadLatestSnapshot") .mockResolvedValue({ timestamp: "2025-01-01T00:00:00.000Z", projectPath, files: new Map(), documentation: new Map(), }); jest.spyOn(DriftDetector.prototype, "detectDrift").mockResolvedValue([ { filePath: "src/api.ts", hasDrift: true, severity: "medium", drifts: [ { type: "outdated", affectedDocs: [docPath], codeChanges: [ { type: "modified", category: "function", name: "oldFunction", details: "Function renamed to newFunction", impactLevel: "major", }, ], description: "Function signature changed", detectedAt: "2025-01-01T00:00:00.000Z", severity: "medium", }, ], impactAnalysis: { breakingChanges: 0, majorChanges: 1, minorChanges: 0, affectedDocFiles: [docPath], estimatedUpdateEffort: "medium", requiresManualReview: false, }, suggestions: [ { docFile: docPath, section: "oldFunction", currentContent: "This is outdated.", suggestedContent: `## newFunction Updated documentation for new function.`, reasoning: "Function was renamed from oldFunction to newFunction", confidence: 0.95, autoApplicable: true, }, ], }, ]); // Run in apply mode const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "apply", autoApplyThreshold: 0.8, }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.appliedChanges.length).toBeGreaterThan(0); // Verify the file was actually modified const updatedDoc = await fs.readFile(docPath, "utf-8"); expect(updatedDoc).toContain("newFunction"); }); test("should not apply low-confidence suggestions", async () => { const docPath = join(docsPath, "lowconf.md"); const originalDoc = `# Low Confidence ## someFunction Original content.`; await fs.writeFile(docPath, originalDoc); jest.spyOn(DriftDetector.prototype, "initialize").mockResolvedValue(); jest.spyOn(DriftDetector.prototype, "createSnapshot").mockResolvedValue({ timestamp: "2025-01-01T00:00:00.000Z", projectPath, files: new Map(), documentation: new Map(), }); jest .spyOn(DriftDetector.prototype, "loadLatestSnapshot") .mockResolvedValue({ timestamp: "2025-01-01T00:00:00.000Z", projectPath, files: new Map(), documentation: new Map(), }); jest.spyOn(DriftDetector.prototype, "detectDrift").mockResolvedValue([ { filePath: "src/lowconf.ts", hasDrift: true, severity: "low", drifts: [ { type: "outdated", affectedDocs: [docPath], codeChanges: [ { type: "modified", category: "function", name: "someFunction", details: "Minor change", impactLevel: "minor", }, ], description: "Minor change", detectedAt: "2025-01-01T00:00:00.000Z", severity: "low", }, ], impactAnalysis: { breakingChanges: 0, majorChanges: 0, minorChanges: 1, affectedDocFiles: [docPath], estimatedUpdateEffort: "low", requiresManualReview: false, }, suggestions: [ { docFile: docPath, section: "someFunction", currentContent: "Original content.", suggestedContent: "Suggested content.", reasoning: "Minor update needed", confidence: 0.5, // Low confidence autoApplicable: true, }, ], }, ]); const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "apply", autoApplyThreshold: 0.8, // Higher than suggestion confidence }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.pendingChanges.length).toBeGreaterThan(0); expect(data.data.appliedChanges.length).toBe(0); // Verify file was NOT modified const unchangedDoc = await fs.readFile(docPath, "utf-8"); expect(unchangedDoc).toBe(originalDoc); }); test("should apply all suggestions in auto mode regardless of confidence", async () => { const docPath = join(docsPath, "auto.md"); const originalDoc = `# Auto Mode ## function1 Old content.`; await fs.writeFile(docPath, originalDoc); jest.spyOn(DriftDetector.prototype, "initialize").mockResolvedValue(); jest.spyOn(DriftDetector.prototype, "createSnapshot").mockResolvedValue({ timestamp: "2025-01-01T00:00:00.000Z", projectPath, files: new Map(), documentation: new Map(), }); jest .spyOn(DriftDetector.prototype, "loadLatestSnapshot") .mockResolvedValue({ timestamp: "2025-01-01T00:00:00.000Z", projectPath, files: new Map(), documentation: new Map(), }); jest.spyOn(DriftDetector.prototype, "detectDrift").mockResolvedValue([ { filePath: "src/auto.ts", hasDrift: true, severity: "low", drifts: [ { type: "outdated", affectedDocs: [docPath], codeChanges: [ { type: "modified", category: "function", name: "function1", details: "Change", impactLevel: "minor", }, ], description: "Change", detectedAt: "2025-01-01T00:00:00.000Z", severity: "low", }, ], impactAnalysis: { breakingChanges: 0, majorChanges: 0, minorChanges: 1, affectedDocFiles: [docPath], estimatedUpdateEffort: "low", requiresManualReview: false, }, suggestions: [ { docFile: docPath, section: "function1", currentContent: "Old content.", suggestedContent: `## function1 New content from auto mode.`, reasoning: "Auto-applied update", confidence: 0.3, // Very low confidence autoApplicable: false, // Not auto-applicable }, ], }, ]); const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "auto", // Auto mode applies everything }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.appliedChanges.length).toBeGreaterThan(0); // Verify file was modified const updatedDoc = await fs.readFile(docPath, "utf-8"); expect(updatedDoc).toContain("New content from auto mode"); }); test("should handle apply errors and mark as pending", async () => { const docPath = join(docsPath, "error.md"); // Don't create the file - this will cause an error jest.spyOn(DriftDetector.prototype, "initialize").mockResolvedValue(); jest.spyOn(DriftDetector.prototype, "createSnapshot").mockResolvedValue({ timestamp: "2025-01-01T00:00:00.000Z", projectPath, files: new Map(), documentation: new Map(), }); jest .spyOn(DriftDetector.prototype, "loadLatestSnapshot") .mockResolvedValue({ timestamp: "2025-01-01T00:00:00.000Z", projectPath, files: new Map(), documentation: new Map(), }); jest.spyOn(DriftDetector.prototype, "detectDrift").mockResolvedValue([ { filePath: "src/error.ts", hasDrift: true, severity: "medium", drifts: [ { type: "outdated", affectedDocs: [docPath], codeChanges: [ { type: "modified", category: "function", name: "errorFunction", details: "Change", impactLevel: "minor", }, ], description: "Change", detectedAt: "2025-01-01T00:00:00.000Z", severity: "medium", }, ], impactAnalysis: { breakingChanges: 0, majorChanges: 1, minorChanges: 0, affectedDocFiles: [docPath], estimatedUpdateEffort: "medium", requiresManualReview: false, }, suggestions: [ { docFile: docPath, // File doesn't exist section: "errorSection", currentContent: "N/A", suggestedContent: "New content", reasoning: "Should fail", confidence: 0.95, autoApplicable: true, }, ], }, ]); const mockContext = { info: jest.fn(), warn: jest.fn(), }; const result = await handleSyncCodeToDocs( { projectPath, docsPath, mode: "apply", autoApplyThreshold: 0.8, }, mockContext, ); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); // Failed applies should be added to pending changes expect(data.data.pendingChanges.length).toBeGreaterThan(0); expect(mockContext.warn).toHaveBeenCalled(); }); test("should add new section when section doesn't exist", async () => { const docPath = join(docsPath, "newsection.md"); const originalDoc = `# New Section Test ## existingSection Existing content.`; await fs.writeFile(docPath, originalDoc); jest.spyOn(DriftDetector.prototype, "initialize").mockResolvedValue(); jest.spyOn(DriftDetector.prototype, "createSnapshot").mockResolvedValue({ timestamp: "2025-01-01T00:00:00.000Z", projectPath, files: new Map(), documentation: new Map(), }); jest .spyOn(DriftDetector.prototype, "loadLatestSnapshot") .mockResolvedValue({ timestamp: "2025-01-01T00:00:00.000Z", projectPath, files: new Map(), documentation: new Map(), }); jest.spyOn(DriftDetector.prototype, "detectDrift").mockResolvedValue([ { filePath: "src/new.ts", hasDrift: true, severity: "low", drifts: [ { type: "missing", affectedDocs: [docPath], codeChanges: [ { type: "added", category: "function", name: "newFunction", details: "New function added", impactLevel: "minor", }, ], description: "New function", detectedAt: "2025-01-01T00:00:00.000Z", severity: "low", }, ], impactAnalysis: { breakingChanges: 0, majorChanges: 0, minorChanges: 1, affectedDocFiles: [docPath], estimatedUpdateEffort: "low", requiresManualReview: false, }, suggestions: [ { docFile: docPath, section: "newSection", // This section doesn't exist currentContent: "", suggestedContent: `## newSection This is a brand new section.`, reasoning: "New function added", confidence: 0.9, autoApplicable: true, }, ], }, ]); const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "apply", autoApplyThreshold: 0.8, }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.appliedChanges.length).toBeGreaterThan(0); // Verify new section was appended const updatedDoc = await fs.readFile(docPath, "utf-8"); expect(updatedDoc).toContain("newSection"); expect(updatedDoc).toContain("brand new section"); }); test("should handle breaking changes recommendation", async () => { const docPath = join(docsPath, "breaking.md"); await fs.writeFile(docPath, "# Breaking"); jest.spyOn(DriftDetector.prototype, "initialize").mockResolvedValue(); jest.spyOn(DriftDetector.prototype, "createSnapshot").mockResolvedValue({ timestamp: "2025-01-01T00:00:00.000Z", projectPath, files: new Map(), documentation: new Map(), }); jest .spyOn(DriftDetector.prototype, "loadLatestSnapshot") .mockResolvedValue({ timestamp: "2025-01-01T00:00:00.000Z", projectPath, files: new Map(), documentation: new Map(), }); jest.spyOn(DriftDetector.prototype, "detectDrift").mockResolvedValue([ { filePath: "src/breaking.ts", hasDrift: true, severity: "critical", drifts: [ { type: "breaking", affectedDocs: [docPath], codeChanges: [ { type: "removed", category: "function", name: "oldAPI", details: "Breaking change", impactLevel: "breaking", }, ], description: "Breaking change", detectedAt: "2025-01-01T00:00:00.000Z", severity: "critical", }, ], impactAnalysis: { breakingChanges: 2, // Multiple breaking changes majorChanges: 0, minorChanges: 0, affectedDocFiles: [docPath], estimatedUpdateEffort: "high", requiresManualReview: true, }, suggestions: [ { docFile: docPath, section: "API", currentContent: "Old API", suggestedContent: "New API", reasoning: "Breaking change", confidence: 0.9, autoApplicable: true, }, ], }, ]); const result = await handleSyncCodeToDocs({ projectPath, docsPath, mode: "detect", }); const data = JSON.parse(result.content[0].text); expect(data.success).toBe(true); expect(data.data.stats.breakingChanges).toBe(2); // Should have critical recommendation const criticalRec = data.recommendations.find( (r: any) => r.type === "critical", ); expect(criticalRec).toBeDefined(); expect(criticalRec.title).toContain("Breaking"); }); afterEach(() => { jest.restoreAllMocks(); }); }); });

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