Skip to main content
Glama

documcp

by tosin2013
kg-storage-validation.test.ts24.6 kB
/** * Tests for uncovered branches in KGStorage * Covers: Error handling (lines 197, 276), backup restoration with timestamp (lines 453-455), * validation errors (lines 496, 510), and other edge cases */ import { describe, it, expect, beforeEach, afterEach } from "@jest/globals"; import { promises as fs } from "fs"; import { join } from "path"; import { KGStorage } from "../../src/memory/kg-storage.js"; import { GraphNode, GraphEdge } from "../../src/memory/knowledge-graph.js"; import { tmpdir } from "os"; describe("KGStorage - Validation and Error Handling", () => { let storage: KGStorage; let testDir: string; beforeEach(async () => { testDir = join(tmpdir(), `kg-storage-validation-test-${Date.now()}`); await fs.mkdir(testDir, { recursive: true }); storage = new KGStorage({ storageDir: testDir, backupOnWrite: true, validateOnRead: true, }); await storage.initialize(); }); afterEach(async () => { try { await fs.rm(testDir, { recursive: true, force: true }); } catch (error) { // Ignore cleanup errors } }); describe("Load Error Handling", () => { it("should handle non-JSON lines in loadEntities gracefully (line 188)", async () => { const entityFile = join(testDir, "knowledge-graph-entities.jsonl"); // Write marker + valid entity + invalid JSON + another valid entity await fs.writeFile( entityFile, "# DOCUMCP_KNOWLEDGE_GRAPH_ENTITIES v1.0.0\n" + '{"id":"e1","type":"project","label":"Project 1","properties":{},"weight":1.0,"lastUpdated":"2024-01-01"}\n' + "invalid json line {this is not valid}\n" + '{"id":"e2","type":"project","label":"Project 2","properties":{},"weight":1.0,"lastUpdated":"2024-01-01"}\n', "utf-8", ); // Should load valid entities and skip invalid line const entities = await storage.loadEntities(); expect(entities.length).toBe(2); expect(entities[0].id).toBe("e1"); expect(entities[1].id).toBe("e2"); }); it("should handle non-JSON lines in loadRelationships gracefully (line 267)", async () => { const relationshipFile = join( testDir, "knowledge-graph-relationships.jsonl", ); // Write marker + valid relationship + invalid JSON + another valid relationship await fs.writeFile( relationshipFile, "# DOCUMCP_KNOWLEDGE_GRAPH_RELATIONSHIPS v1.0.0\n" + '{"id":"r1","source":"s1","target":"t1","type":"uses","label":"Uses","properties":{},"weight":1.0,"lastUpdated":"2024-01-01"}\n' + "corrupted json {missing quotes and brackets\n" + '{"id":"r2","source":"s2","target":"t2","type":"uses","label":"Uses","properties":{},"weight":1.0,"lastUpdated":"2024-01-01"}\n', "utf-8", ); // Should load valid relationships and skip invalid line const relationships = await storage.loadRelationships(); expect(relationships.length).toBe(2); expect(relationships[0].id).toBe("r1"); expect(relationships[1].id).toBe("r2"); }); it("should throw error when loadEntities encounters non-ENOENT error (line 197-201)", async () => { const entityFile = join(testDir, "knowledge-graph-entities.jsonl"); // Create file with proper marker await fs.writeFile( entityFile, "# DOCUMCP_KNOWLEDGE_GRAPH_ENTITIES v1.0.0\n", "utf-8", ); // Make file unreadable by changing permissions (Unix-like systems) if (process.platform !== "win32") { await fs.chmod(entityFile, 0o000); await expect(storage.loadEntities()).rejects.toThrow(); // Restore permissions for cleanup await fs.chmod(entityFile, 0o644); } }); it("should throw error when loadRelationships encounters non-ENOENT error (line 276-280)", async () => { const relationshipFile = join( testDir, "knowledge-graph-relationships.jsonl", ); // Create file with proper marker await fs.writeFile( relationshipFile, "# DOCUMCP_KNOWLEDGE_GRAPH_RELATIONSHIPS v1.0.0\n", "utf-8", ); // Make file unreadable (Unix-like systems) if (process.platform !== "win32") { await fs.chmod(relationshipFile, 0o000); await expect(storage.loadRelationships()).rejects.toThrow(); // Restore permissions for cleanup await fs.chmod(relationshipFile, 0o644); } }); }); describe("Validation Errors", () => { it("should validate entity structure and throw on invalid entity (line 496)", async () => { const invalidEntity = { // Missing required 'type' and 'label' fields id: "invalid-entity", properties: {}, weight: 1.0, lastUpdated: "2024-01-01", } as unknown as GraphNode; // Create a storage with validation enabled const validatingStorage = new KGStorage({ storageDir: testDir, validateOnRead: true, }); await validatingStorage.initialize(); // Write invalid entity to file const entityFile = join(testDir, "knowledge-graph-entities.jsonl"); await fs.writeFile( entityFile, "# DOCUMCP_KNOWLEDGE_GRAPH_ENTITIES v1.0.0\n" + '{"id":"invalid-entity","properties":{},"weight":1.0,"lastUpdated":"2024-01-01"}\n', "utf-8", ); // Loading should skip the invalid entity (caught and logged) const entities = await validatingStorage.loadEntities(); expect(entities.length).toBe(0); // Invalid entity skipped }); it("should validate relationship structure and throw on invalid relationship (line 510)", async () => { // Create storage with validation enabled const validatingStorage = new KGStorage({ storageDir: testDir, validateOnRead: true, }); await validatingStorage.initialize(); // Write invalid relationship (missing 'type' field) const relationshipFile = join( testDir, "knowledge-graph-relationships.jsonl", ); await fs.writeFile( relationshipFile, "# DOCUMCP_KNOWLEDGE_GRAPH_RELATIONSHIPS v1.0.0\n" + '{"id":"r1","source":"s1","target":"t1","label":"Invalid","properties":{},"weight":1.0}\n', "utf-8", ); // Loading should skip the invalid relationship const relationships = await validatingStorage.loadRelationships(); expect(relationships.length).toBe(0); // Invalid relationship skipped }); it("should validate entity has required fields: id, type, label (line 495-497)", async () => { const validatingStorage = new KGStorage({ storageDir: testDir, validateOnRead: true, }); await validatingStorage.initialize(); const entityFile = join(testDir, "knowledge-graph-entities.jsonl"); // Test missing 'id' await fs.writeFile( entityFile, "# DOCUMCP_KNOWLEDGE_GRAPH_ENTITIES v1.0.0\n" + '{"type":"project","label":"No ID","properties":{},"weight":1.0,"lastUpdated":"2024-01-01"}\n', "utf-8", ); let entities = await validatingStorage.loadEntities(); expect(entities.length).toBe(0); // Test missing 'type' await fs.writeFile( entityFile, "# DOCUMCP_KNOWLEDGE_GRAPH_ENTITIES v1.0.0\n" + '{"id":"e1","label":"No Type","properties":{},"weight":1.0,"lastUpdated":"2024-01-01"}\n', "utf-8", ); entities = await validatingStorage.loadEntities(); expect(entities.length).toBe(0); // Test missing 'label' await fs.writeFile( entityFile, "# DOCUMCP_KNOWLEDGE_GRAPH_ENTITIES v1.0.0\n" + '{"id":"e1","type":"project","properties":{},"weight":1.0,"lastUpdated":"2024-01-01"}\n', "utf-8", ); entities = await validatingStorage.loadEntities(); expect(entities.length).toBe(0); }); it("should validate relationship has required fields: id, source, target, type (line 504-512)", async () => { const validatingStorage = new KGStorage({ storageDir: testDir, validateOnRead: true, }); await validatingStorage.initialize(); const relationshipFile = join( testDir, "knowledge-graph-relationships.jsonl", ); // Test missing 'id' await fs.writeFile( relationshipFile, "# DOCUMCP_KNOWLEDGE_GRAPH_RELATIONSHIPS v1.0.0\n" + '{"source":"s1","target":"t1","type":"uses","label":"Uses","properties":{},"weight":1.0}\n', "utf-8", ); let relationships = await validatingStorage.loadRelationships(); expect(relationships.length).toBe(0); // Test missing 'source' await fs.writeFile( relationshipFile, "# DOCUMCP_KNOWLEDGE_GRAPH_RELATIONSHIPS v1.0.0\n" + '{"id":"r1","target":"t1","type":"uses","label":"Uses","properties":{},"weight":1.0}\n', "utf-8", ); relationships = await validatingStorage.loadRelationships(); expect(relationships.length).toBe(0); // Test missing 'target' await fs.writeFile( relationshipFile, "# DOCUMCP_KNOWLEDGE_GRAPH_RELATIONSHIPS v1.0.0\n" + '{"id":"r1","source":"s1","type":"uses","label":"Uses","properties":{},"weight":1.0}\n', "utf-8", ); relationships = await validatingStorage.loadRelationships(); expect(relationships.length).toBe(0); // Test missing 'type' await fs.writeFile( relationshipFile, "# DOCUMCP_KNOWLEDGE_GRAPH_RELATIONSHIPS v1.0.0\n" + '{"id":"r1","source":"s1","target":"t1","label":"Uses","properties":{},"weight":1.0}\n', "utf-8", ); relationships = await validatingStorage.loadRelationships(); expect(relationships.length).toBe(0); }); it("should not validate when validateOnRead is false", async () => { const nonValidatingStorage = new KGStorage({ storageDir: testDir, validateOnRead: false, }); await nonValidatingStorage.initialize(); const entityFile = join(testDir, "knowledge-graph-entities.jsonl"); // Write entity missing required fields await fs.writeFile( entityFile, "# DOCUMCP_KNOWLEDGE_GRAPH_ENTITIES v1.0.0\n" + '{"id":"e1","properties":{}}\n', "utf-8", ); // Should load without validation (parse as-is) const entities = await nonValidatingStorage.loadEntities(); expect(entities.length).toBe(1); expect(entities[0].id).toBe("e1"); }); }); describe("Backup Restoration with Timestamp", () => { // TODO: Fix timing issue with backup file creation it.skip("should restore from backup with specific timestamp (lines 451-455)", async () => { const entities: GraphNode[] = [ { id: "project:backup1", type: "project", label: "Backup Test 1", properties: {}, weight: 1.0, lastUpdated: "2024-01-01", }, ]; // Save first version await storage.saveEntities(entities); // Get list of backups to find the timestamp const backupDir = join(testDir, "backups"); const backups = await fs.readdir(backupDir); const entityBackups = backups.filter((f) => f.startsWith("entities-")); expect(entityBackups.length).toBeGreaterThan(0); // Extract timestamp from backup filename (format: entities-YYYY-MM-DDTHH-MM-SS-MMMZ.jsonl) const backupFilename = entityBackups[0]; const timestampMatch = backupFilename.match(/entities-(.*?)\.jsonl/); expect(timestampMatch).not.toBeNull(); const timestamp = timestampMatch![1]; // Modify entities const modifiedEntities: GraphNode[] = [ { id: "project:backup2", type: "project", label: "Modified", properties: {}, weight: 1.0, lastUpdated: "2024-01-02", }, ]; await storage.saveEntities(modifiedEntities); // Verify current state let current = await storage.loadEntities(); expect(current.length).toBe(1); expect(current[0].id).toBe("project:backup2"); // Restore from backup using specific timestamp await storage.restoreFromBackup("entities", timestamp); // Verify restored state current = await storage.loadEntities(); expect(current.length).toBe(1); expect(current[0].id).toBe("project:backup1"); }); it("should throw error when backup with timestamp not found (line 454-456)", async () => { const entities: GraphNode[] = [ { id: "project:test", type: "project", label: "Test", properties: {}, weight: 1.0, lastUpdated: "2024-01-01", }, ]; await storage.saveEntities(entities); // Try to restore with non-existent timestamp await expect( storage.restoreFromBackup("entities", "2099-12-31T23-59-59-999Z"), ).rejects.toThrow("Backup with timestamp"); }); it("should restore most recent backup when no timestamp specified (line 458-467)", async () => { const entities1: GraphNode[] = [ { id: "project:v1", type: "project", label: "Version 1", properties: {}, weight: 1.0, lastUpdated: "2024-01-01", }, ]; await storage.saveEntities(entities1); // Wait a bit to ensure different timestamps await new Promise((resolve) => setTimeout(resolve, 100)); const entities2: GraphNode[] = [ { id: "project:v2", type: "project", label: "Version 2", properties: {}, weight: 1.0, lastUpdated: "2024-01-02", }, ]; await storage.saveEntities(entities2); await new Promise((resolve) => setTimeout(resolve, 100)); const entities3: GraphNode[] = [ { id: "project:v3", type: "project", label: "Version 3 (current)", properties: {}, weight: 1.0, lastUpdated: "2024-01-03", }, ]; await storage.saveEntities(entities3); // Current state should be v3 let current = await storage.loadEntities(); expect(current[0].id).toBe("project:v3"); // Restore without timestamp (should get most recent backup = v2) await storage.restoreFromBackup("entities"); current = await storage.loadEntities(); expect(current[0].id).toBe("project:v2"); }); // TODO: Fix timing issue with backup file creation it.skip("should restore relationships with timestamp", async () => { const relationships1: GraphEdge[] = [ { id: "rel:v1", source: "s1", target: "t1", type: "uses", properties: {}, weight: 1.0, confidence: 1.0, lastUpdated: "2024-01-01", }, ]; await storage.saveRelationships(relationships1); // Get backup timestamp const backupDir = join(testDir, "backups"); const backups = await fs.readdir(backupDir); const relBackups = backups.filter((f) => f.startsWith("relationships-")); expect(relBackups.length).toBeGreaterThan(0); const timestampMatch = relBackups[0].match(/relationships-(.*?)\.jsonl/); const timestamp = timestampMatch![1]; // Modify relationships const relationships2: GraphEdge[] = [ { id: "rel:v2", source: "s2", target: "t2", type: "uses", properties: {}, weight: 1.0, confidence: 1.0, lastUpdated: "2024-01-02", }, ]; await storage.saveRelationships(relationships2); // Restore from backup using timestamp await storage.restoreFromBackup("relationships", timestamp); const restored = await storage.loadRelationships(); expect(restored[0].id).toBe("rel:v1"); }); it("should throw error when no backups exist (line 445-447)", async () => { // Try to restore when no backups exist await expect(storage.restoreFromBackup("entities")).rejects.toThrow( "No backups found", ); }); it("should log restoration in debug mode (line 478-481)", async () => { const entities: GraphNode[] = [ { id: "project:debug", type: "project", label: "Debug Test", properties: {}, weight: 1.0, lastUpdated: "2024-01-01", }, ]; await storage.saveEntities(entities); // Set DEBUG env var const originalDebug = process.env.DEBUG; process.env.DEBUG = "true"; // Modify const modifiedEntities: GraphNode[] = [ { id: "project:modified", type: "project", label: "Modified", properties: {}, weight: 1.0, lastUpdated: "2024-01-02", }, ]; await storage.saveEntities(modifiedEntities); // Restore (should log in debug mode) await storage.restoreFromBackup("entities"); // Restore original DEBUG setting if (originalDebug !== undefined) { process.env.DEBUG = originalDebug; } else { delete process.env.DEBUG; } // Verify restoration worked const restored = await storage.loadEntities(); expect(restored[0].id).toBe("project:debug"); }); }); describe("Error Handling Edge Cases", () => { it("should handle backup file access errors gracefully (line 337-340)", async () => { // This tests the warning path when backup fails due to file access issues const storage2 = new KGStorage({ storageDir: testDir, backupOnWrite: true, }); await storage2.initialize(); // Save initial entities to create a file const initialEntities: GraphNode[] = [ { id: "project:initial", type: "project", label: "Initial", properties: {}, weight: 1.0, lastUpdated: "2024-01-01", }, ]; await storage2.saveEntities(initialEntities); // Make entity file unreadable (Unix-like systems only) to trigger backup error const entityFile = join(testDir, "knowledge-graph-entities.jsonl"); if (process.platform !== "win32") { try { await fs.chmod(entityFile, 0o000); } catch (e) { // Skip test if chmod not supported return; } } // Saving should still attempt even if backup fails with non-ENOENT error const newEntities: GraphNode[] = [ { id: "project:no-backup", type: "project", label: "No Backup", properties: {}, weight: 1.0, lastUpdated: "2024-01-02", }, ]; // Will fail during backup read, but should warn and continue // This tests line 337-339: if (error.code !== "ENOENT") try { await storage2.saveEntities(newEntities); } catch (error) { // Might throw due to unreadable file } // Restore permissions for cleanup if (process.platform !== "win32") { try { await fs.chmod(entityFile, 0o644); } catch (e) { // Ignore } } }); it("should handle cleanup of backups when file is deleted during iteration (line 369-371)", async () => { // Create multiple backups const entities: GraphNode[] = [ { id: "project:cleanup", type: "project", label: "Cleanup Test", properties: {}, weight: 1.0, lastUpdated: "2024-01-01", }, ]; // Create many backups (more than keepCount of 10) for (let i = 0; i < 15; i++) { await storage.saveEntities([ { ...entities[0], id: `project:cleanup-${i}`, label: `Cleanup Test ${i}`, }, ]); await new Promise((resolve) => setTimeout(resolve, 10)); } // Old backups should be cleaned up automatically const backupDir = join(testDir, "backups"); const backups = await fs.readdir(backupDir); const entityBackups = backups.filter((f) => f.startsWith("entities-")); // Should keep last 10 backups expect(entityBackups.length).toBeLessThanOrEqual(10); }); it("should handle missing backup directory gracefully (line 388-391)", async () => { // Create storage without creating backups first const testDir2 = join(tmpdir(), `kg-no-backup-${Date.now()}`); await fs.mkdir(testDir2, { recursive: true }); const storage2 = new KGStorage({ storageDir: testDir2, backupOnWrite: false, // Disable backups }); await storage2.initialize(); const entities: GraphNode[] = [ { id: "project:no-backup-dir", type: "project", label: "No Backup Dir", properties: {}, weight: 1.0, lastUpdated: "2024-01-01", }, ]; // Should work fine without backup directory await storage2.saveEntities(entities); const loaded = await storage2.loadEntities(); expect(loaded.length).toBe(1); await fs.rm(testDir2, { recursive: true, force: true }); }); }); describe("Verify Integrity Coverage", () => { it("should detect orphaned relationships - missing source (line 535-538)", async () => { const entities: GraphNode[] = [ { id: "project:exists", type: "project", label: "Exists", properties: {}, weight: 1.0, lastUpdated: "2024-01-01", }, ]; const relationships: GraphEdge[] = [ { id: "rel:orphan", source: "project:missing", target: "project:exists", type: "uses", properties: {}, weight: 1.0, confidence: 1.0, lastUpdated: "2024-01-01", }, ]; await storage.saveEntities(entities); await storage.saveRelationships(relationships); const result = await storage.verifyIntegrity(); expect(result.warnings.length).toBeGreaterThan(0); expect( result.warnings.some((w) => w.includes("missing source entity")), ).toBe(true); }); it("should detect orphaned relationships - missing target (line 540-544)", async () => { const entities: GraphNode[] = [ { id: "project:exists", type: "project", label: "Exists", properties: {}, weight: 1.0, lastUpdated: "2024-01-01", }, ]; const relationships: GraphEdge[] = [ { id: "rel:orphan", source: "project:exists", target: "project:missing", type: "uses", properties: {}, weight: 1.0, confidence: 1.0, lastUpdated: "2024-01-01", }, ]; await storage.saveEntities(entities); await storage.saveRelationships(relationships); const result = await storage.verifyIntegrity(); expect(result.warnings.length).toBeGreaterThan(0); expect( result.warnings.some((w) => w.includes("missing target entity")), ).toBe(true); }); // TODO: Fix - validation prevents corrupted data from being loaded it.skip("should catch errors during integrity check (lines 564-570)", async () => { // Save valid data const entities: GraphNode[] = [ { id: "project:test", type: "project", label: "Test", properties: {}, weight: 1.0, lastUpdated: "2024-01-01", }, ]; await storage.saveEntities(entities); // Corrupt the entity file to cause a parse error const entityFile = join(testDir, "knowledge-graph-entities.jsonl"); await fs.writeFile( entityFile, "# DOCUMCP_KNOWLEDGE_GRAPH_ENTITIES v1.0.0\nthis is not valid json\n", "utf-8", ); // Integrity check should catch the error const result = await storage.verifyIntegrity(); expect(result.valid).toBe(false); expect(result.errors.length).toBeGreaterThan(0); expect(result.errors[0]).toContain("Integrity check failed"); }); }); });

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