Skip to main content
Glama
freshness-kg-integration.test.ts23 kB
/** * Tests for Documentation Freshness Knowledge Graph Integration */ import { promises as fs } from "fs"; import path from "path"; import { tmpdir } from "os"; import { storeFreshnessEvent, updateFreshnessEvent, getFreshnessHistory, getStalenessInsights, compareFreshnessAcrossProjects, } from "../../src/memory/freshness-kg-integration.js"; import type { FreshnessScanReport } from "../../src/utils/freshness-tracker.js"; describe("Freshness Knowledge Graph Integration", () => { let testDir: string; beforeEach(async () => { // Create temporary test directory testDir = path.join(tmpdir(), `freshness-kg-test-${Date.now()}`); await fs.mkdir(testDir, { recursive: true }); // Set storage directory to test directory process.env.DOCUMCP_STORAGE_DIR = path.join(testDir, ".documcp/memory"); }); afterEach(async () => { // Clean up test directory try { await fs.rm(testDir, { recursive: true, force: true }); } catch (error) { // Ignore cleanup errors } delete process.env.DOCUMCP_STORAGE_DIR; }); describe("storeFreshnessEvent", () => { it("should store a freshness scan event in KG", async () => { const projectPath = path.join(testDir, "test-project"); const docsPath = path.join(projectPath, "docs"); const report: FreshnessScanReport = { docsPath, scannedAt: new Date().toISOString(), totalFiles: 10, filesWithMetadata: 8, filesWithoutMetadata: 2, freshFiles: 6, warningFiles: 2, staleFiles: 1, criticalFiles: 1, files: [ { filePath: path.join(docsPath, "page1.md"), relativePath: "page1.md", hasMetadata: true, isStale: false, stalenessLevel: "fresh", ageInMs: 1000 * 60 * 60 * 24, // 1 day ageFormatted: "1 day", }, { filePath: path.join(docsPath, "page2.md"), relativePath: "page2.md", hasMetadata: true, isStale: true, stalenessLevel: "critical", ageInMs: 1000 * 60 * 60 * 24 * 100, // 100 days ageFormatted: "100 days", }, ], thresholds: { warning: { value: 7, unit: "days" }, stale: { value: 30, unit: "days" }, critical: { value: 90, unit: "days" }, }, }; const eventId = await storeFreshnessEvent( projectPath, docsPath, report, "scan", ); expect(eventId).toBeDefined(); expect(eventId).toContain("freshness_event:"); }); it("should store event with different event types", async () => { const projectPath = path.join(testDir, "test-project"); const docsPath = path.join(projectPath, "docs"); const report: FreshnessScanReport = { docsPath, scannedAt: new Date().toISOString(), totalFiles: 5, filesWithMetadata: 5, filesWithoutMetadata: 0, freshFiles: 5, warningFiles: 0, staleFiles: 0, criticalFiles: 0, files: [], thresholds: { warning: { value: 7, unit: "days" }, stale: { value: 30, unit: "days" }, critical: { value: 90, unit: "days" }, }, }; const initEventId = await storeFreshnessEvent( projectPath, docsPath, report, "initialization", ); expect(initEventId).toBeDefined(); const updateEventId = await storeFreshnessEvent( projectPath, docsPath, report, "update", ); expect(updateEventId).toBeDefined(); const validationEventId = await storeFreshnessEvent( projectPath, docsPath, report, "validation", ); expect(validationEventId).toBeDefined(); }); }); describe("getFreshnessHistory", () => { it("should retrieve freshness event history", async () => { const projectPath = path.join(testDir, "test-project"); const docsPath = path.join(projectPath, "docs"); const report: FreshnessScanReport = { docsPath, scannedAt: new Date().toISOString(), totalFiles: 5, filesWithMetadata: 5, filesWithoutMetadata: 0, freshFiles: 5, warningFiles: 0, staleFiles: 0, criticalFiles: 0, files: [], thresholds: { warning: { value: 7, unit: "days" }, stale: { value: 30, unit: "days" }, critical: { value: 90, unit: "days" }, }, }; // Store multiple events await storeFreshnessEvent(projectPath, docsPath, report, "scan"); await new Promise((resolve) => setTimeout(resolve, 10)); // Small delay await storeFreshnessEvent(projectPath, docsPath, report, "update"); const history = await getFreshnessHistory(projectPath, 10); expect(history).toBeDefined(); expect(history.length).toBeGreaterThanOrEqual(0); }); it("should return empty array for project with no history", async () => { const projectPath = path.join(testDir, "new-project"); const history = await getFreshnessHistory(projectPath, 10); expect(history).toEqual([]); }); }); describe("getStalenessInsights", () => { it("should return insights for project with no history", async () => { const projectPath = path.join(testDir, "new-project"); const insights = await getStalenessInsights(projectPath); expect(insights).toBeDefined(); expect(insights.totalEvents).toBe(0); expect(insights.averageImprovementScore).toBe(0); expect(insights.trend).toBe("stable"); expect(insights.currentStatus).toBeNull(); expect(insights.recommendations.length).toBeGreaterThan(0); expect(insights.recommendations[0]).toContain( "No freshness tracking history found", ); }); it("should calculate insights from event history", async () => { const projectPath = path.join(testDir, "test-project"); const docsPath = path.join(projectPath, "docs"); const report: FreshnessScanReport = { docsPath, scannedAt: new Date().toISOString(), totalFiles: 10, filesWithMetadata: 10, filesWithoutMetadata: 0, freshFiles: 8, warningFiles: 1, staleFiles: 1, criticalFiles: 0, files: [], thresholds: { warning: { value: 7, unit: "days" }, stale: { value: 30, unit: "days" }, critical: { value: 90, unit: "days" }, }, }; await storeFreshnessEvent(projectPath, docsPath, report, "scan"); const insights = await getStalenessInsights(projectPath); expect(insights).toBeDefined(); expect(insights.trend).toMatch(/improving|declining|stable/); expect(insights.recommendations).toBeDefined(); expect(Array.isArray(insights.recommendations)).toBe(true); }); it("should detect improving trend", async () => { const projectPath = path.join(testDir, "test-project"); const docsPath = path.join(projectPath, "docs"); // Store older event with worse metrics const olderReport: FreshnessScanReport = { docsPath, scannedAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(), totalFiles: 10, filesWithMetadata: 10, filesWithoutMetadata: 0, freshFiles: 5, warningFiles: 2, staleFiles: 2, criticalFiles: 1, files: [], thresholds: { warning: { value: 7, unit: "days" }, stale: { value: 30, unit: "days" }, critical: { value: 90, unit: "days" }, }, }; await storeFreshnessEvent(projectPath, docsPath, olderReport, "scan"); await new Promise((resolve) => setTimeout(resolve, 10)); // Store newer event with better metrics const newerReport: FreshnessScanReport = { ...olderReport, scannedAt: new Date().toISOString(), freshFiles: 9, warningFiles: 1, staleFiles: 0, criticalFiles: 0, }; await storeFreshnessEvent(projectPath, docsPath, newerReport, "scan"); const insights = await getStalenessInsights(projectPath); expect(insights.trend).toMatch(/improving|stable/); }); it("should generate recommendations for critical files", async () => { const projectPath = path.join(testDir, "test-project"); const docsPath = path.join(projectPath, "docs"); const report: FreshnessScanReport = { docsPath, scannedAt: new Date().toISOString(), totalFiles: 10, filesWithMetadata: 10, filesWithoutMetadata: 0, freshFiles: 5, warningFiles: 2, staleFiles: 1, criticalFiles: 2, files: [], thresholds: { warning: { value: 7, unit: "days" }, stale: { value: 30, unit: "days" }, critical: { value: 90, unit: "days" }, }, }; await storeFreshnessEvent(projectPath, docsPath, report, "scan"); const insights = await getStalenessInsights(projectPath); expect(insights).toBeDefined(); expect(insights.recommendations).toBeDefined(); expect(Array.isArray(insights.recommendations)).toBe(true); }); it("should recommend validation for files without metadata", async () => { const projectPath = path.join(testDir, "test-project"); const docsPath = path.join(projectPath, "docs"); const report: FreshnessScanReport = { docsPath, scannedAt: new Date().toISOString(), totalFiles: 10, filesWithMetadata: 7, filesWithoutMetadata: 3, freshFiles: 7, warningFiles: 0, staleFiles: 0, criticalFiles: 0, files: [], thresholds: { warning: { value: 7, unit: "days" }, stale: { value: 30, unit: "days" }, critical: { value: 90, unit: "days" }, }, }; await storeFreshnessEvent(projectPath, docsPath, report, "scan"); const insights = await getStalenessInsights(projectPath); expect(insights).toBeDefined(); expect(insights.recommendations).toBeDefined(); expect(Array.isArray(insights.recommendations)).toBe(true); }); }); describe("compareFreshnessAcrossProjects", () => { it("should handle project with no history", async () => { const projectPath = path.join(testDir, "new-project"); const comparison = await compareFreshnessAcrossProjects(projectPath); expect(comparison).toBeDefined(); expect(comparison.currentProject.path).toBe(projectPath); expect(comparison.currentProject.improvementScore).toBe(0); expect(comparison.similarProjects).toEqual([]); }); it("should calculate ranking for project", async () => { const projectPath = path.join(testDir, "test-project"); const docsPath = path.join(projectPath, "docs"); const report: FreshnessScanReport = { docsPath, scannedAt: new Date().toISOString(), totalFiles: 10, filesWithMetadata: 10, filesWithoutMetadata: 0, freshFiles: 8, warningFiles: 1, staleFiles: 1, criticalFiles: 0, files: [], thresholds: { warning: { value: 7, unit: "days" }, stale: { value: 30, unit: "days" }, critical: { value: 90, unit: "days" }, }, }; await storeFreshnessEvent(projectPath, docsPath, report, "scan"); const comparison = await compareFreshnessAcrossProjects(projectPath); expect(comparison.ranking).toBeGreaterThan(0); }); it("should compare with similar projects", async () => { const projectPath1 = path.join(testDir, "project1"); const docsPath1 = path.join(projectPath1, "docs"); const projectPath2 = path.join(testDir, "project2"); const docsPath2 = path.join(projectPath2, "docs"); // Store events for both projects const report1: FreshnessScanReport = { docsPath: docsPath1, scannedAt: new Date().toISOString(), totalFiles: 10, filesWithMetadata: 10, filesWithoutMetadata: 0, freshFiles: 9, warningFiles: 1, staleFiles: 0, criticalFiles: 0, files: [], thresholds: { warning: { value: 7, unit: "days" }, stale: { value: 30, unit: "days" }, critical: { value: 90, unit: "days" }, }, }; const report2: FreshnessScanReport = { ...report1, docsPath: docsPath2, freshFiles: 7, warningFiles: 2, staleFiles: 1, }; await storeFreshnessEvent(projectPath1, docsPath1, report1, "scan"); await storeFreshnessEvent(projectPath2, docsPath2, report2, "scan"); // The function should work even if there are no similar_to edges // (it will just return empty similarProjects array) const comparison = await compareFreshnessAcrossProjects(projectPath1); expect(comparison.currentProject.path).toBe(projectPath1); expect(comparison.similarProjects).toBeDefined(); expect(Array.isArray(comparison.similarProjects)).toBe(true); }); }); describe("updateFreshnessEvent", () => { it("should update a freshness event with new data", async () => { const projectPath = path.join(testDir, "test-project"); const docsPath = path.join(projectPath, "docs"); const report: FreshnessScanReport = { docsPath, scannedAt: new Date().toISOString(), totalFiles: 10, filesWithMetadata: 8, filesWithoutMetadata: 2, freshFiles: 8, warningFiles: 0, staleFiles: 0, criticalFiles: 0, files: [], thresholds: { warning: { value: 7, unit: "days" }, stale: { value: 30, unit: "days" }, critical: { value: 90, unit: "days" }, }, }; const eventId = await storeFreshnessEvent( projectPath, docsPath, report, "scan", ); await updateFreshnessEvent(eventId, { filesInitialized: 2, filesUpdated: 5, eventType: "update", }); // Verify the update by checking history const history = await getFreshnessHistory(projectPath, 10); expect(history.length).toBeGreaterThan(0); }); it("should throw error for non-existent event", async () => { await expect( updateFreshnessEvent("freshness_event:nonexistent", { filesInitialized: 1, }), ).rejects.toThrow(); }); }); describe("Edge cases and additional coverage", () => { it("should handle more than 10 stale files", async () => { const projectPath = path.join(testDir, "test-project"); const docsPath = path.join(projectPath, "docs"); // Create 15 stale files const staleFiles = Array.from({ length: 15 }, (_, i) => ({ filePath: path.join(docsPath, `stale${i}.md`), relativePath: `stale${i}.md`, hasMetadata: true, isStale: true, stalenessLevel: "stale" as const, ageInMs: 1000 * 60 * 60 * 24 * (40 + i), // 40+ days ageFormatted: `${40 + i} days`, })); const report: FreshnessScanReport = { docsPath, scannedAt: new Date().toISOString(), totalFiles: 15, filesWithMetadata: 15, filesWithoutMetadata: 0, freshFiles: 0, warningFiles: 0, staleFiles: 15, criticalFiles: 0, files: staleFiles, thresholds: { warning: { value: 7, unit: "days" }, stale: { value: 30, unit: "days" }, critical: { value: 90, unit: "days" }, }, }; const eventId = await storeFreshnessEvent( projectPath, docsPath, report, "scan", ); expect(eventId).toBeDefined(); const history = await getFreshnessHistory(projectPath, 1); expect(history[0].event.mostStaleFiles.length).toBeLessThanOrEqual(10); }); it("should recommend action for 30%+ stale files", async () => { const projectPath = path.join(testDir, "test-project"); const docsPath = path.join(projectPath, "docs"); const report: FreshnessScanReport = { docsPath, scannedAt: new Date().toISOString(), totalFiles: 10, filesWithMetadata: 10, filesWithoutMetadata: 0, freshFiles: 6, warningFiles: 0, staleFiles: 4, // 40% stale criticalFiles: 0, files: [], thresholds: { warning: { value: 7, unit: "days" }, stale: { value: 30, unit: "days" }, critical: { value: 90, unit: "days" }, }, }; await storeFreshnessEvent(projectPath, docsPath, report, "scan"); const insights = await getStalenessInsights(projectPath); expect(insights.recommendations).toBeDefined(); expect(insights.recommendations.length).toBeGreaterThan(0); // Check that we get recommendations about stale files const hasStaleRecommendation = insights.recommendations.some( (r) => r.includes("30%") || r.includes("stale"), ); expect(hasStaleRecommendation).toBe(true); }); it("should detect declining trend", async () => { const projectPath = path.join(testDir, "test-project"); const docsPath = path.join(projectPath, "docs"); // Store older event with good metrics const olderReport: FreshnessScanReport = { docsPath, scannedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString(), totalFiles: 10, filesWithMetadata: 10, filesWithoutMetadata: 0, freshFiles: 9, warningFiles: 1, staleFiles: 0, criticalFiles: 0, files: [], thresholds: { warning: { value: 7, unit: "days" }, stale: { value: 30, unit: "days" }, critical: { value: 90, unit: "days" }, }, }; await storeFreshnessEvent(projectPath, docsPath, olderReport, "scan"); await new Promise((resolve) => setTimeout(resolve, 10)); // Store newer event with worse metrics const newerReport: FreshnessScanReport = { ...olderReport, scannedAt: new Date().toISOString(), freshFiles: 5, warningFiles: 2, staleFiles: 2, criticalFiles: 1, }; await storeFreshnessEvent(projectPath, docsPath, newerReport, "scan"); const insights = await getStalenessInsights(projectPath); expect(insights.trend).toMatch(/declining|stable/); }); it("should identify chronically stale files", async () => { const projectPath = path.join(testDir, "test-project"); const docsPath = path.join(projectPath, "docs"); // Create multiple events with same critical/stale files // Need to create enough events so files appear repeatedly for (let i = 0; i < 6; i++) { const report: FreshnessScanReport = { docsPath, scannedAt: new Date( Date.now() - 1000 * 60 * 60 * 24 * (6 - i), ).toISOString(), totalFiles: 10, filesWithMetadata: 10, filesWithoutMetadata: 0, freshFiles: 6, warningFiles: 0, staleFiles: 2, criticalFiles: 2, files: [ { filePath: path.join(docsPath, "always-stale.md"), relativePath: "always-stale.md", hasMetadata: true, isStale: true, stalenessLevel: "critical", ageInMs: 1000 * 60 * 60 * 24 * 100, ageFormatted: "100 days", }, { filePath: path.join(docsPath, "also-stale.md"), relativePath: "also-stale.md", hasMetadata: true, isStale: true, stalenessLevel: "critical", ageInMs: 1000 * 60 * 60 * 24 * 95, ageFormatted: "95 days", }, { filePath: path.join(docsPath, "stale-doc.md"), relativePath: "stale-doc.md", hasMetadata: true, isStale: true, stalenessLevel: "stale", ageInMs: 1000 * 60 * 60 * 24 * 40, ageFormatted: "40 days", }, { filePath: path.join(docsPath, "another-stale.md"), relativePath: "another-stale.md", hasMetadata: true, isStale: true, stalenessLevel: "stale", ageInMs: 1000 * 60 * 60 * 24 * 35, ageFormatted: "35 days", }, ], thresholds: { warning: { value: 7, unit: "days" }, stale: { value: 30, unit: "days" }, critical: { value: 90, unit: "days" }, }, }; await storeFreshnessEvent(projectPath, docsPath, report, "scan"); await new Promise((resolve) => setTimeout(resolve, 10)); } const insights = await getStalenessInsights(projectPath); // With 6 events and files appearing in all of them, // should trigger chronically stale recommendation const hasChronicallyStale = insights.recommendations.some( (r) => r.includes("chronically") || r.includes("critical"), ); expect(hasChronicallyStale).toBe(true); }); it("should handle files without age information", async () => { const projectPath = path.join(testDir, "test-project"); const docsPath = path.join(projectPath, "docs"); const report: FreshnessScanReport = { docsPath, scannedAt: new Date().toISOString(), totalFiles: 5, filesWithMetadata: 3, filesWithoutMetadata: 2, freshFiles: 3, warningFiles: 0, staleFiles: 0, criticalFiles: 0, files: [ { filePath: path.join(docsPath, "no-metadata.md"), relativePath: "no-metadata.md", hasMetadata: false, isStale: false, stalenessLevel: "unknown", }, ], thresholds: { warning: { value: 7, unit: "days" }, stale: { value: 30, unit: "days" }, critical: { value: 90, unit: "days" }, }, }; const eventId = await storeFreshnessEvent( projectPath, docsPath, report, "scan", ); expect(eventId).toBeDefined(); const history = await getFreshnessHistory(projectPath, 1); expect(history.length).toBeGreaterThan(0); if (history.length > 0) { expect(history[0].event.averageAge).toBeUndefined(); expect(history[0].event.oldestFile).toBeUndefined(); } }); }); });

Latest Blog Posts

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