Skip to main content
Glama

documcp

by tosin2013
drift-detector.test.ts33.7 kB
/** * Drift Detector Tests (Phase 3) */ import { DriftDetector, DriftDetectionResult, } from "../../src/utils/drift-detector.js"; import { promises as fs } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { mkdtemp, rm } from "fs/promises"; describe("DriftDetector", () => { let detector: DriftDetector; let tempDir: string; let projectPath: string; let docsPath: string; beforeAll(async () => { tempDir = await mkdtemp(join(tmpdir(), "drift-test-")); projectPath = join(tempDir, "project"); docsPath = join(tempDir, "docs"); await fs.mkdir(projectPath, { recursive: true }); await fs.mkdir(join(projectPath, "src"), { recursive: true }); await fs.mkdir(docsPath, { recursive: true }); detector = new DriftDetector(tempDir); await detector.initialize(); }); afterAll(async () => { await rm(tempDir, { recursive: true, force: true }); }); describe("Snapshot Creation", () => { test("should create snapshot of codebase and documentation", async () => { // Create sample source file const sourceCode = ` export function calculateSum(a: number, b: number): number { return a + b; } `.trim(); await fs.writeFile(join(projectPath, "src", "math.ts"), sourceCode); // Create sample documentation const docContent = ` # Math Module ## calculateSum Adds two numbers together. \`\`\`typescript calculateSum(a: number, b: number): number \`\`\` `.trim(); await fs.writeFile(join(docsPath, "math.md"), docContent); const snapshot = await detector.createSnapshot(projectPath, docsPath); expect(snapshot).toBeDefined(); expect(snapshot.projectPath).toBe(projectPath); expect(snapshot.timestamp).toBeTruthy(); expect(snapshot.files.size).toBeGreaterThan(0); expect(snapshot.documentation.size).toBeGreaterThan(0); }); test("should store snapshot to disk", async () => { const sourceCode = `export function test(): void {}`; await fs.writeFile(join(projectPath, "src", "test.ts"), sourceCode); const snapshot = await detector.createSnapshot(projectPath, docsPath); // Check that snapshot directory was created const snapshotDir = join(tempDir, ".documcp", "snapshots"); const files = await fs.readdir(snapshotDir); expect(files.length).toBeGreaterThan(0); expect(files.some((f) => f.startsWith("snapshot-"))).toBe(true); }); test("should load latest snapshot", async () => { const sourceCode = `export function loadTest(): void {}`; await fs.writeFile(join(projectPath, "src", "load-test.ts"), sourceCode); await detector.createSnapshot(projectPath, docsPath); const loaded = await detector.loadLatestSnapshot(); expect(loaded).toBeDefined(); expect(loaded?.projectPath).toBe(projectPath); }); }); describe("Drift Detection", () => { test("should detect when function signature changes", async () => { // Create initial version const oldCode = ` export function processData(data: string): void { console.log(data); } `.trim(); await fs.writeFile(join(projectPath, "src", "processor.ts"), oldCode); const oldDoc = ` # Processor ## processData(data: string): void Processes string data. `.trim(); await fs.writeFile(join(docsPath, "processor.md"), oldDoc); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Modify function signature const newCode = ` export function processData(data: string, options: object): Promise<string> { console.log(data, options); return Promise.resolve("done"); } `.trim(); await fs.writeFile(join(projectPath, "src", "processor.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); expect(drifts.length).toBeGreaterThan(0); const processorDrift = drifts.find((d) => d.filePath.includes("processor.ts"), ); expect(processorDrift).toBeDefined(); expect(processorDrift?.hasDrift).toBe(true); expect(processorDrift?.drifts.length).toBeGreaterThan(0); }); test("should detect when functions are removed", async () => { // Initial code with two functions const oldCode = ` export function keepMe(): void {} export function removeMe(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "removal.ts"), oldCode); const oldDoc = ` # Functions ## keepMe ## removeMe `.trim(); await fs.writeFile(join(docsPath, "removal.md"), oldDoc); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Remove one function const newCode = ` export function keepMe(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "removal.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const removalDrift = drifts.find((d) => d.filePath.includes("removal.ts"), ); expect(removalDrift).toBeDefined(); expect( removalDrift?.drifts.some((drift) => drift.type === "breaking"), ).toBe(true); }); test("should detect when new functions are added", async () => { const oldCode = ` export function existing(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "addition.ts"), oldCode); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); const newCode = ` export function existing(): void {} export function newFunction(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "addition.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const additionDrift = drifts.find((d) => d.filePath.includes("addition.ts"), ); expect(additionDrift).toBeDefined(); expect( additionDrift?.drifts.some((drift) => drift.type === "missing"), ).toBe(true); }); test("should classify drift severity correctly", async () => { // Breaking change const oldCode = ` export function criticalFunction(param: string): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "severity.ts"), oldCode); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Remove exported function - breaking change const newCode = ` function criticalFunction(param: string): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "severity.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const severityDrift = drifts.find((d) => d.filePath.includes("severity.ts"), ); expect(severityDrift).toBeDefined(); expect(severityDrift?.severity).toBe("critical"); // Removing export is breaking }); }); describe("Suggestion Generation", () => { test("should generate suggestions for outdated documentation", async () => { const oldCode = ` export function calculate(x: number): number { return x * 2; } `.trim(); await fs.writeFile(join(projectPath, "src", "calc.ts"), oldCode); const oldDoc = ` # Calculator ## calculate(x: number): number Doubles the input. `.trim(); await fs.writeFile(join(docsPath, "calc.md"), oldDoc); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Change function signature const newCode = ` export function calculate(x: number, y: number): number { return x * y; } `.trim(); await fs.writeFile(join(projectPath, "src", "calc.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const calcDrift = drifts.find((d) => d.filePath.includes("calc.ts")); expect(calcDrift).toBeDefined(); expect(calcDrift?.suggestions.length).toBeGreaterThan(0); const suggestion = calcDrift?.suggestions[0]; expect(suggestion).toBeDefined(); expect(suggestion?.suggestedContent).toBeTruthy(); expect(suggestion?.confidence).toBeGreaterThan(0); }); test("should provide auto-applicable flag for safe changes", async () => { const oldCode = ` export function simpleChange(a: number): number { return a; } `.trim(); await fs.writeFile(join(projectPath, "src", "simple.ts"), oldCode); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Minor change const newCode = ` export function simpleChange(a: number): number { return a * 2; } `.trim(); await fs.writeFile(join(projectPath, "src", "simple.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); // Minor internal changes shouldn't require doc updates if signature is same const simpleDrift = drifts.find((d) => d.filePath.includes("simple.ts")); if (simpleDrift && simpleDrift.suggestions.length > 0) { const suggestion = simpleDrift.suggestions[0]; expect(typeof suggestion.autoApplicable).toBe("boolean"); } }); }); describe("Impact Analysis", () => { test("should analyze impact of changes", async () => { const oldCode = ` export function breaking(): void {} export function major(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "impact.ts"), oldCode); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Breaking change - remove function const newCode = ` export function major(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "impact.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const impactDrift = drifts.find((d) => d.filePath.includes("impact.ts")); expect(impactDrift?.impactAnalysis).toBeDefined(); expect(impactDrift?.impactAnalysis.breakingChanges).toBeGreaterThan(0); expect(impactDrift?.impactAnalysis.estimatedUpdateEffort).toBeDefined(); }); test("should identify affected documentation files", async () => { const code = ` export function documented(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "documented.ts"), code); const doc = ` # Documentation \`documented()\` is a function. `.trim(); await fs.writeFile(join(docsPath, "documented.md"), doc); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Change the function const newCode = ` export function documented(param: string): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "documented.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const docDrift = drifts.find((d) => d.filePath.includes("documented.ts")); expect(docDrift?.impactAnalysis.affectedDocFiles.length).toBeGreaterThan( 0, ); }); }); describe("Edge Cases", () => { test("should handle no drift scenario", async () => { const code = ` export function unchangedFunction(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "unchanged.ts"), code); const snapshot1 = await detector.createSnapshot(projectPath, docsPath); const snapshot2 = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(snapshot1, snapshot2); // No changes should mean no drifts const unchangedDrift = drifts.find((d) => d.filePath.includes("unchanged.ts"), ); if (unchangedDrift) { expect(unchangedDrift.hasDrift).toBe(false); } }); test("should handle missing documentation gracefully", async () => { const code = ` export function undocumentedFunction(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "undocumented.ts"), code); // Don't create documentation const snapshot = await detector.createSnapshot(projectPath, docsPath); expect(snapshot).toBeDefined(); expect(snapshot.documentation.size).toBeGreaterThanOrEqual(0); }); test("should handle new files correctly", async () => { const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Add new file const newCode = ` export function brandNew(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "brand-new.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); // New files might not show as drift if they have no corresponding docs expect(Array.isArray(drifts)).toBe(true); }); }); describe("Documentation Section Extraction", () => { test("should extract documentation sections", async () => { const doc = ` # Main Title This is the introduction. ## Section 1 Content for section 1. \`\`\`typescript function example(): void {} \`\`\` ## Section 2 Content for section 2. `.trim(); await fs.writeFile(join(docsPath, "sections.md"), doc); const snapshot = await detector.createSnapshot(projectPath, docsPath); const docSnapshot = snapshot.documentation.get( join(docsPath, "sections.md"), ); expect(docSnapshot).toBeDefined(); expect(docSnapshot?.sections.length).toBeGreaterThan(0); const section1 = docSnapshot?.sections.find( (s) => s.title === "Section 1", ); expect(section1).toBeDefined(); expect(section1?.codeExamples.length).toBeGreaterThan(0); }); test("should extract code references from documentation", async () => { const doc = ` # API Reference See \`calculateSum()\` for details. The function is in \`src/math.ts\`. Check out the \`MathUtils\` class. `.trim(); await fs.writeFile(join(docsPath, "references.md"), doc); const snapshot = await detector.createSnapshot(projectPath, docsPath); const docSnapshot = snapshot.documentation.get( join(docsPath, "references.md"), ); expect(docSnapshot).toBeDefined(); const section = docSnapshot?.sections[0]; expect(section?.referencedFunctions.length).toBeGreaterThan(0); }); }); describe("Suggestion Generation Helper Methods", () => { test("should generate removal suggestion with deprecation notice", async () => { const oldCode = ` export function deprecatedFunc(x: number): number { return x; } `.trim(); await fs.writeFile(join(projectPath, "src", "deprecated.ts"), oldCode); const oldDoc = ` # API ## deprecatedFunc(x: number): number This function does something. `.trim(); await fs.writeFile(join(docsPath, "deprecated.md"), oldDoc); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Remove the function const newCode = `// Function removed`; await fs.writeFile(join(projectPath, "src", "deprecated.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const deprecatedDrift = drifts.find((d) => d.filePath.includes("deprecated.ts"), ); expect(deprecatedDrift).toBeDefined(); expect(deprecatedDrift?.suggestions.length).toBeGreaterThan(0); const suggestion = deprecatedDrift?.suggestions[0]; expect(suggestion?.suggestedContent).toContain("removed"); expect(suggestion?.suggestedContent).toContain("Note"); }); test("should generate addition suggestion with code signature", async () => { const oldCode = `export function existing(): void {}`; await fs.writeFile(join(projectPath, "src", "additions.ts"), oldCode); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Add new function const newCode = ` export function existing(): void {} export function newAddedFunc(a: number, b: string): boolean { return true; } `.trim(); await fs.writeFile(join(projectPath, "src", "additions.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const additionDrift = drifts.find((d) => d.filePath.includes("additions.ts"), ); expect(additionDrift?.drifts.some((d) => d.type === "missing")).toBe( true, ); }); test("should generate modification suggestion with signature update", async () => { const oldCode = ` export function modifyMe(x: number): number { return x; } `.trim(); await fs.writeFile(join(projectPath, "src", "modify.ts"), oldCode); const oldDoc = ` # API ## modifyMe(x: number): number Returns the input number. `.trim(); await fs.writeFile(join(docsPath, "modify.md"), oldDoc); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Modify the function signature const newCode = ` export function modifyMe(x: number, y: number): number { return x + y; } `.trim(); await fs.writeFile(join(projectPath, "src", "modify.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const modifyDrift = drifts.find((d) => d.filePath.includes("modify.ts")); expect(modifyDrift).toBeDefined(); expect(modifyDrift?.suggestions.length).toBeGreaterThan(0); const suggestion = modifyDrift?.suggestions[0]; expect(suggestion?.suggestedContent).toBeTruthy(); }); test("should set auto-applicable flag correctly for safe changes", async () => { const oldCode = ` export function safeChange(x: number): number { return x; } `.trim(); await fs.writeFile(join(projectPath, "src", "safe.ts"), oldCode); const oldDoc = ` # API ## safeChange(x: number): number `.trim(); await fs.writeFile(join(docsPath, "safe.md"), oldDoc); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Internal implementation change (patch level) const newCode = ` export function safeChange(x: number): number { return x * 2; // Changed implementation but not signature } `.trim(); await fs.writeFile(join(projectPath, "src", "safe.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); // If there are any drifts, check their suggestions if (drifts.length > 0) { const safeDrift = drifts.find((d) => d.filePath.includes("safe.ts")); if (safeDrift && safeDrift.suggestions.length > 0) { const suggestion = safeDrift.suggestions[0]; expect(typeof suggestion.autoApplicable).toBe("boolean"); } } }); }); describe("Comparison Helper Methods", () => { test("should correctly identify affected sections by function name", async () => { const code = ` export function targetFunc(): void {} export function otherFunc(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "target.ts"), code); const doc = ` # API See \`targetFunc()\` for details. ## targetFunc This documents the target function. ## otherFunc This documents another function. `.trim(); await fs.writeFile(join(docsPath, "target.md"), doc); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Modify only targetFunc const newCode = ` export function targetFunc(param: string): void {} export function otherFunc(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "target.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const targetDrift = drifts.find((d) => d.filePath.includes("target.ts")); expect(targetDrift).toBeDefined(); // Drift was detected, and impact analysis was performed expect(targetDrift?.impactAnalysis).toBeDefined(); expect( targetDrift?.impactAnalysis.affectedDocFiles.length, ).toBeGreaterThanOrEqual(0); }); test("should correctly classify drift types", async () => { const oldCode = ` export function removedFunc(): void {} export function modifiedFunc(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "classify.ts"), oldCode); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Remove one function, keep the other unchanged const newCode = ` export function modifiedFunc(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "classify.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const classifyDrift = drifts.find((d) => d.filePath.includes("classify.ts"), ); expect(classifyDrift).toBeDefined(); expect(classifyDrift?.drifts.length).toBeGreaterThan(0); // Verify drift types are correctly classified const driftTypes = classifyDrift?.drifts.map((d) => d.type) || []; expect(driftTypes.length).toBeGreaterThan(0); // Should have breaking or incorrect drift for removed function const hasRemovalDrift = classifyDrift?.drifts.some( (d) => d.type === "breaking" || d.type === "incorrect", ); expect(hasRemovalDrift).toBe(true); }); test("should map impact levels to severity correctly", async () => { const oldCode = ` export function critical(): void {} export function major(): void {} export function minor(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "severity-map.ts"), oldCode); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Breaking change const newCode = ` export function major(): void {} export function minor(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "severity-map.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const severityDrift = drifts.find((d) => d.filePath.includes("severity-map.ts"), ); expect(severityDrift).toBeDefined(); expect(severityDrift?.severity).toBe("critical"); }); test("should estimate update effort based on drift count", async () => { const oldCode = ` export function func1(): void {} export function func2(): void {} export function func3(): void {} export function func4(): void {} export function func5(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "effort.ts"), oldCode); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Remove multiple functions - high effort const newCode = ` export function func5(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "effort.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const effortDrift = drifts.find((d) => d.filePath.includes("effort.ts")); expect(effortDrift).toBeDefined(); expect(effortDrift?.impactAnalysis.estimatedUpdateEffort).toBeDefined(); expect( ["low", "medium", "high"].includes( effortDrift!.impactAnalysis.estimatedUpdateEffort, ), ).toBe(true); }); test("should calculate overall severity from multiple drifts", async () => { const oldCode = ` export function criticalChange(): void {} export function minorChange(): void {} `.trim(); await fs.writeFile( join(projectPath, "src", "overall-severity.ts"), oldCode, ); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Breaking change dominates const newCode = ` export function minorChange(x: number): void {} `.trim(); await fs.writeFile( join(projectPath, "src", "overall-severity.ts"), newCode, ); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const overallDrift = drifts.find((d) => d.filePath.includes("overall-severity.ts"), ); expect(overallDrift).toBeDefined(); expect( ["none", "low", "medium", "high", "critical"].includes( overallDrift!.severity, ), ).toBe(true); }); test("should handle multiple documentation files referencing same code", async () => { const code = ` export function sharedFunc(): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "shared.ts"), code); const doc1 = ` # Guide 1 See \`sharedFunc()\` for details. `.trim(); const doc2 = ` # Guide 2 Also uses \`sharedFunc()\`. `.trim(); await fs.writeFile(join(docsPath, "guide1.md"), doc1); await fs.writeFile(join(docsPath, "guide2.md"), doc2); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Change the shared function const newCode = ` export function sharedFunc(param: string): void {} `.trim(); await fs.writeFile(join(projectPath, "src", "shared.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const sharedDrift = drifts.find((d) => d.filePath.includes("shared.ts")); expect(sharedDrift).toBeDefined(); // Should affect both documentation files expect( sharedDrift?.impactAnalysis.affectedDocFiles.length, ).toBeGreaterThanOrEqual(1); }); }); describe("Advanced Suggestion Generation", () => { test("should generate suggestions for added functions with signatures", async () => { const oldCode = `export function existing(): void {}`; await fs.writeFile( join(projectPath, "src", "added-with-sig.ts"), oldCode, ); const oldDoc = ` # API ## existing Existing function documentation. `.trim(); await fs.writeFile(join(docsPath, "added-with-sig.md"), oldDoc); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Add new function with signature const newCode = ` export function existing(): void {} export async function newFunction(param: string, count: number): Promise<boolean> { return true; } `.trim(); await fs.writeFile( join(projectPath, "src", "added-with-sig.ts"), newCode, ); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const addedDrift = drifts.find((d) => d.filePath.includes("added-with-sig.ts"), ); expect(addedDrift).toBeDefined(); expect(addedDrift?.drifts.some((d) => d.type === "missing")).toBe(true); // Should detect the added function const hasAddedFunction = addedDrift?.drifts.some((d) => d.codeChanges.some((c) => c.name === "newFunction"), ); expect(hasAddedFunction).toBe(true); }); test("should handle class changes in suggestions", async () => { const oldCode = ` export class OldClass { method(): void {} } `.trim(); await fs.writeFile(join(projectPath, "src", "class-change.ts"), oldCode); const oldDoc = ` # Classes ## OldClass Documentation for OldClass. `.trim(); await fs.writeFile(join(docsPath, "class-change.md"), oldDoc); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Modify class const newCode = ` export class OldClass { method(): void {} newMethod(): void {} } `.trim(); await fs.writeFile(join(projectPath, "src", "class-change.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); expect(drifts.length).toBeGreaterThanOrEqual(0); }); test("should handle interface changes in suggestions", async () => { const oldCode = ` export interface UserInterface { id: string; } `.trim(); await fs.writeFile( join(projectPath, "src", "interface-change.ts"), oldCode, ); const oldDoc = ` # Interfaces ## UserInterface The UserInterface interface. `.trim(); await fs.writeFile(join(docsPath, "interface-change.md"), oldDoc); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Modify interface const newCode = ` export interface UserInterface { id: string; name: string; } `.trim(); await fs.writeFile( join(projectPath, "src", "interface-change.ts"), newCode, ); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); expect(drifts.length).toBeGreaterThanOrEqual(0); }); test("should handle type alias changes in suggestions", async () => { const oldCode = ` export type Status = "active" | "inactive"; `.trim(); await fs.writeFile(join(projectPath, "src", "type-change.ts"), oldCode); const oldDoc = ` # Types ## Status The Status type. `.trim(); await fs.writeFile(join(docsPath, "type-change.md"), oldDoc); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Modify type const newCode = ` export type Status = "active" | "inactive" | "pending"; `.trim(); await fs.writeFile(join(projectPath, "src", "type-change.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); expect(drifts.length).toBeGreaterThanOrEqual(0); }); test("should detect documentation referencing classes", async () => { const code = ` export class DocumentedClass { public property: string; constructor(prop: string) { this.property = prop; } } `.trim(); await fs.writeFile(join(projectPath, "src", "doc-class.ts"), code); const doc = ` # Classes See the \`DocumentedClass\` for details. ## DocumentedClass This class does something important. `.trim(); await fs.writeFile(join(docsPath, "doc-class.md"), doc); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Modify class const newCode = ` export class DocumentedClass { public property: string; public newProperty: number; constructor(prop: string, num: number) { this.property = prop; this.newProperty = num; } } `.trim(); await fs.writeFile(join(projectPath, "src", "doc-class.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const classDrift = drifts.find((d) => d.filePath.includes("doc-class.ts"), ); // Check that affected docs were identified if (classDrift && classDrift.hasDrift) { expect(classDrift.impactAnalysis).toBeDefined(); } }); test("should detect documentation referencing types", async () => { const code = ` export type ConfigType = { apiKey: string; timeout: number; }; `.trim(); await fs.writeFile(join(projectPath, "src", "doc-type.ts"), code); const doc = ` # Configuration The \`ConfigType\` defines configuration options. `.trim(); await fs.writeFile(join(docsPath, "doc-type.md"), doc); const oldSnapshot = await detector.createSnapshot(projectPath, docsPath); // Modify type const newCode = ` export type ConfigType = { apiKey: string; timeout: number; retries: number; }; `.trim(); await fs.writeFile(join(projectPath, "src", "doc-type.ts"), newCode); const newSnapshot = await detector.createSnapshot(projectPath, docsPath); const drifts = await detector.detectDrift(oldSnapshot, newSnapshot); const typeDrift = drifts.find((d) => d.filePath.includes("doc-type.ts")); if (typeDrift && typeDrift.hasDrift) { expect(typeDrift.impactAnalysis).toBeDefined(); } }); }); });

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