MCP DuckDB Knowledge Graph Memory Server

  • tests
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { DuckDBKnowledgeGraphManager } from "../src/manager"; import { Entity, Relation, Observation } from "../src/types"; import { join } from "path"; import { existsSync, unlinkSync } from "fs"; describe("DuckDBFuseKnowledgeGraphManager", () => { // Test file path const testDbPath = join(process.cwd(), "tmp", "test-knowledge-graph.db"); let manager: DuckDBKnowledgeGraphManager; // More realistic test data const testEntities: Entity[] = [ { name: "John Smith", entityType: "Person", observations: [ "Software engineer with 8 years of experience", "Specializes in TypeScript and React", "Works at Acme Corporation since 2020", ], }, { name: "Acme Corporation", entityType: "Organization", observations: [ "Technology company founded in 2015", "Headquartered in San Francisco", "Specializes in enterprise software solutions", ], }, { name: "Knowledge Graph Project", entityType: "Project", observations: [ "Started in January 2025", "Aims to create a knowledge management system", "Implemented using TypeScript and Node.js", ], }, { name: "GraphQL", entityType: "Technology", observations: [ "API query language developed by Facebook", "Enables efficient data retrieval", "Popular in modern web applications", ], }, ]; // Function to create test data const createTestData = async () => { await manager.createEntities(testEntities); await manager.createRelations(testRelations); }; // More realistic relations const testRelations: Relation[] = [ { from: "John Smith", to: "Acme Corporation", relationType: "works at", }, { from: "John Smith", to: "Knowledge Graph Project", relationType: "leads", }, { from: "Acme Corporation", to: "Knowledge Graph Project", relationType: "sponsors", }, { from: "Knowledge Graph Project", to: "GraphQL", relationType: "uses", }, ]; // More realistic observations const testObservations: Observation[] = [ { entityName: "John Smith", contents: [ "Recently completed advanced GraphQL certification", "Has 3 years of team leadership experience", ], }, { entityName: "Knowledge Graph Project", contents: [ "Phase one scheduled for completion in March 2025", "Main goal is to visualize data relationships", ], }, ]; // Run before each test beforeEach(async () => { // Create test manager manager = new DuckDBKnowledgeGraphManager(() => testDbPath); }); // Run after each test afterEach(() => { // Delete test file if (existsSync(testDbPath)) { unlinkSync(testDbPath); } }); describe("createEntities", () => { it("should create new entities", async () => { const result = await manager.createEntities(testEntities); expect(result).toHaveLength(4); expect(result[0].name).toBe("John Smith"); expect(result[1].name).toBe("Acme Corporation"); // Verify graph const graph = await manager.readGraph(); expect(graph.entities).toHaveLength(4); expect(graph.entities[0].name).toBe("John Smith"); expect(graph.entities[1].name).toBe("Acme Corporation"); }); it("should not create duplicate entities", async () => { // Create first entity await manager.createEntities([testEntities[0]]); // Try to create entities including the existing one const result = await manager.createEntities(testEntities); // Only non-duplicate entities should be created expect(result).toHaveLength(3); expect(result.map((e) => e.name)).not.toContain("John Smith"); // Verify graph const graph = await manager.readGraph(); expect(graph.entities).toHaveLength(4); }); }); describe("createRelations", () => { it("should create relations between existing entities", async () => { // Create entities await manager.createEntities(testEntities); // Create relations const result = await manager.createRelations(testRelations); expect(result).toHaveLength(4); expect(result[0].from).toBe("John Smith"); expect(result[0].to).toBe("Acme Corporation"); // Verify graph const graph = await manager.readGraph(); expect(graph.relations).toHaveLength(4); expect(graph.relations[0].from).toBe("John Smith"); expect(graph.relations[0].to).toBe("Acme Corporation"); }); it("should not create relations for non-existing entities", async () => { // Try to create relations without creating entities const result = await manager.createRelations(testRelations); // No relations should be created expect(result).toHaveLength(0); // Verify graph const graph = await manager.readGraph(); expect(graph.relations).toHaveLength(0); }); it("should not create duplicate relations", async () => { // Create entities await manager.createEntities(testEntities); // Create relations await manager.createRelations(testRelations); // Try to create the same relations again const result = await manager.createRelations(testRelations); // No duplicate relations should be created expect(result).toHaveLength(0); // Verify graph const graph = await manager.readGraph(); expect(graph.relations).toHaveLength(4); }); }); describe("addObservations", () => { it("should add observations to existing entities", async () => { // Create entities await manager.createEntities(testEntities); // Add observations const result = await manager.addObservations(testObservations); expect(result).toHaveLength(2); expect(result[0].entityName).toBe("John Smith"); expect(result[0].contents).toHaveLength(2); expect(result[0].contents).toContain( "Recently completed advanced GraphQL certification" ); // Verify graph const graph = await manager.readGraph(); const entity = graph.entities.find((e) => e.name === "John Smith"); expect(entity).toBeDefined(); expect(entity!.observations).toHaveLength(5); // Original 3 + new 2 expect(entity!.observations).toContain( "Software engineer with 8 years of experience" ); expect(entity!.observations).toContain( "Recently completed advanced GraphQL certification" ); }); it("should not add duplicate observations", async () => { // Create entities await manager.createEntities(testEntities); // Create observations with duplicates const duplicateObservations: Observation[] = [ { entityName: "John Smith", contents: [ "Software engineer with 8 years of experience", // Already exists "Active contributor to open source projects", // New observation ], }, ]; // Add observations const result = await manager.addObservations(duplicateObservations); // Verify graph const graph = await manager.readGraph(); const entity = graph.entities.find((e) => e.name === "John Smith"); expect(entity).toBeDefined(); // Verify observations expect(entity!.observations).toContain( "Software engineer with 8 years of experience" ); expect(entity!.observations).toContain( "Active contributor to open source projects" ); }); it("should ignore observations for non-existing entities", async () => { // Observations for non-existing entity const nonExistingObservations: Observation[] = [ { entityName: "Non-existing Entity", contents: ["Some observation"], }, ]; // Add observations const result = await manager.addObservations(nonExistingObservations); expect(result).toHaveLength(0); // Verify graph const graph = await manager.readGraph(); expect(graph.entities).toHaveLength(0); }); }); describe("deleteObservations", () => { it("should delete observations from entities", async () => { // Create entities await createTestData(); // Observations to delete const deletions: Observation[] = [ { entityName: "John Smith", contents: ["Software engineer with 8 years of experience"], }, ]; // Delete observations await manager.deleteObservations(deletions); // Verify graph const graph = await manager.readGraph(); const entity = graph.entities.find((e) => e.name === "John Smith"); expect(entity).toBeDefined(); // Verify observations were deleted expect(entity!.observations).not.toContain( "Software engineer with 8 years of experience" ); expect(entity!.observations).toContain( "Specializes in TypeScript and React" ); }); it("should handle non-existing entities gracefully", async () => { // Delete observations from non-existing entity const deletions: Observation[] = [ { entityName: "Non-existing Entity", contents: ["Some observation"], }, ]; // Should not throw error await expect( manager.deleteObservations(deletions) ).resolves.not.toThrow(); }); it("should handle non-existing observations gracefully", async () => { // Create entities await manager.createEntities(testEntities); // Delete non-existing observations const deletions: Observation[] = [ { entityName: "John Smith", contents: ["Non-existing observation content"], }, ]; // Delete observations await manager.deleteObservations(deletions); // Verify graph const graph = await manager.readGraph(); const entity = graph.entities.find((e) => e.name === "John Smith"); expect(entity).toBeDefined(); // Verify observations still exist expect(entity!.observations.length).toBeGreaterThan(0); }); }); describe("deleteEntities", () => { it("should delete entities and their relations", async () => { // Create entities and relations await manager.createEntities(testEntities); await manager.createRelations(testRelations); // Delete John Smith entity await manager.deleteEntities(["John Smith"]); // Verify graph const graph = await manager.readGraph(); expect(graph.entities).toHaveLength(3); // Only 3 entities remain (John Smith is deleted) expect(graph.entities.map((e) => e.name)).not.toContain("John Smith"); expect(graph.entities.map((e) => e.name)).toContain("Acme Corporation"); // Relations involving John Smith should be deleted expect( graph.relations.some( (r) => r.from === "John Smith" || r.to === "John Smith" ) ).toBe(false); }); it("should handle non-existing entities gracefully", async () => { // Delete non-existing entity await expect( manager.deleteEntities(["Non-existing Entity"]) ).resolves.not.toThrow(); }); }); describe("deleteRelations", () => { it("should delete specific relations", async () => { // Create entities and relations await manager.createEntities(testEntities); await manager.createRelations(testRelations); // Delete one relation await manager.deleteRelations([testRelations[0]]); // Verify graph const graph = await manager.readGraph(); expect(graph.entities).toHaveLength(4); // All entities remain expect(graph.relations).toHaveLength(3); // One relation is deleted expect( graph.relations.some( (r) => r.from === "John Smith" && r.to === "Acme Corporation" && r.relationType === "works at" ) ).toBe(false); }); it("should handle non-existing relations gracefully", async () => { // Delete non-existing relations const nonExistingRelations: Relation[] = [ { from: "Non-existing Person", to: "Non-existing Organization", relationType: "works at", }, ]; // Should not throw error await expect( manager.deleteRelations(nonExistingRelations) ).resolves.not.toThrow(); }); }); describe("searchNodes", () => { it("should find entities by name", async () => { // Create entities await createTestData(); // Search by name const results = await manager.searchNodes("John Smith"); // Verify results expect(results.entities.length).toBeGreaterThan(0); expect( results.entities.some((entity) => entity.name === "John Smith") ).toBe(true); }); it("should return relations involving found entities", async () => { // Create entities and relations await createTestData(); // Search by name const results = await manager.searchNodes("John Smith"); // Verify relations expect(results.relations.length).toBeGreaterThan(0); // Should include relations where John Smith is the 'from' entity expect( results.relations.some( (relation) => relation.from === "John Smith" && relation.to === "Acme Corporation" && relation.relationType === "works at" ) ).toBe(true); // Should include relations where John Smith is the 'to' entity // (In our test data, there are no such relations, but the logic should be tested) // If we had such relations, we would test them here }); it("should find entities by type", async () => { // Create entities await manager.createEntities(testEntities); // Search by entity type const results = await manager.searchNodes("Organization"); // Verify results expect(results.entities.length).toBeGreaterThan(0); expect( results.entities.some((entity) => entity.entityType === "Organization") ).toBe(true); }); it("should find entities by observation content", async () => { // Create entities await manager.createEntities(testEntities); // Search by observation content const results = await manager.searchNodes("TypeScript"); // Verify results expect(results.entities.length).toBeGreaterThan(0); expect( results.entities.some((entity) => entity.observations.some((obs) => obs.includes("TypeScript")) ) ).toBe(true); }); it("should return empty array for no matches", async () => { // Create entities await manager.createEntities(testEntities); // Search with no matches const results = await manager.searchNodes("Non-existing Term"); expect(results.entities).toHaveLength(0); }); it("should return empty array for empty query", async () => { // Create entities await manager.createEntities(testEntities); // Search with empty query const results = await manager.searchNodes(""); expect(results.entities).toHaveLength(0); }); it("should find entities by multiple keywords", async () => { // Create entities await manager.createEntities(testEntities); // Search by multiple keywords const results = await manager.searchNodes("TypeScript React"); // Verify results expect(results.entities.length).toBeGreaterThan(0); expect( results.entities.some((entity) => entity.name === "John Smith") ).toBe(true); // Verify the entity has observations containing both keywords const johnSmith = results.entities.find( (entity) => entity.name === "John Smith" ); expect(johnSmith).toBeDefined(); expect( johnSmith!.observations.some( (obs) => obs.includes("TypeScript") && obs.includes("React") ) ).toBe(true); }); }); describe("openNodes", () => { it("should retrieve specific entities by name", async () => { // Create entities await manager.createEntities(testEntities); // Retrieve by name const results = await manager.openNodes(["John Smith"]); expect(results.entities).toHaveLength(1); expect(results.entities[0].name).toBe("John Smith"); expect(results.entities[0].entityType).toBe("Person"); // Verify observations expect(results.entities[0].observations.length).toBeGreaterThan(0); }); it("should return relations involving opened entities", async () => { // Create entities and relations await manager.createEntities(testEntities); await manager.createRelations(testRelations); // Open specific entity const results = await manager.openNodes(["John Smith"]); // Verify relations expect(results.relations.length).toBeGreaterThan(0); // Should include relations where John Smith is the 'from' entity expect( results.relations.some( (relation) => relation.from === "John Smith" && relation.to === "Acme Corporation" && relation.relationType === "works at" ) ).toBe(true); expect( results.relations.some( (relation) => relation.from === "John Smith" && relation.to === "Knowledge Graph Project" && relation.relationType === "leads" ) ).toBe(true); // Should include relations where John Smith is the 'to' entity // (In our test data, there are no such relations, but the logic should be tested) // If we had such relations, we would test them here }); it("should retrieve multiple entities", async () => { // Create entities await manager.createEntities(testEntities); // Retrieve multiple entities const results = await manager.openNodes([ "John Smith", "Acme Corporation", ]); expect(results.entities).toHaveLength(2); expect(results.entities.map((e) => e.name)).toContain("John Smith"); expect(results.entities.map((e) => e.name)).toContain("Acme Corporation"); }); it("should return empty array for non-existing entities", async () => { // Create entities await manager.createEntities(testEntities); // Retrieve non-existing entity const results = await manager.openNodes(["Non-existing Entity"]); expect(results.entities).toHaveLength(0); }); it("should return only existing entities from a mixed list", async () => { // Create entities await manager.createEntities(testEntities); // Retrieve mixed list of existing and non-existing entities const results = await manager.openNodes([ "John Smith", "Non-existing Entity", ]); expect(results.entities).toHaveLength(1); expect(results.entities[0].name).toBe("John Smith"); }); }); describe("readGraph", () => { it("should return the entire knowledge graph", async () => { // Create entities and relations await manager.createEntities(testEntities); await manager.createRelations(testRelations); // Read graph const graph = await manager.readGraph(); expect(graph.entities).toHaveLength(4); expect(graph.relations).toHaveLength(4); expect(graph.entities.map((e) => e.name)).toContain("John Smith"); expect(graph.entities.map((e) => e.name)).toContain("Acme Corporation"); expect( graph.relations.some( (r) => r.from === "John Smith" && r.to === "Acme Corporation" ) ).toBe(true); }); it("should return empty graph when no data exists", async () => { // Read empty graph const graph = await manager.readGraph(); expect(graph.entities).toHaveLength(0); expect(graph.relations).toHaveLength(0); }); }); });