Skip to main content
Glama
n-r-w

KnowledgeGraph MCP Server

by n-r-w
tag-system.test.ts15 kB
import { KnowledgeGraphManager, Entity } from '../core.js'; import { StorageConfig, StorageType } from '../storage/index.js'; // Mock PostgreSQL to avoid requiring actual database connections jest.mock('pg', () => ({ Pool: jest.fn().mockImplementation(() => { const mockData = new Map<string, any>(); return { connect: jest.fn().mockResolvedValue({ query: jest.fn().mockImplementation(async (sql: string, params?: any[]) => { // Mock database operations for testing if (sql.includes('SELECT') && sql.includes('entities')) { const project = params?.[0] || 'default'; const entities = mockData.get(`entities_${project}`) || []; return { rows: entities }; } else if (sql.includes('SELECT') && sql.includes('relations')) { const project = params?.[0] || 'default'; const relations = mockData.get(`relations_${project}`) || []; return { rows: relations }; } else if (sql.includes('DELETE FROM entities')) { const project = params?.[0] || 'default'; mockData.delete(`entities_${project}`); return { rows: [] }; } else if (sql.includes('DELETE FROM relations')) { const project = params?.[0] || 'default'; mockData.delete(`relations_${project}`); return { rows: [] }; } else if (sql.includes('INSERT INTO entities')) { const project = params?.[0] || 'default'; const entities = mockData.get(`entities_${project}`) || []; const newEntity = { name: params?.[1], entity_type: params?.[2], observations: JSON.parse(params?.[3] || '[]'), tags: JSON.parse(params?.[4] || '[]') }; entities.push(newEntity); mockData.set(`entities_${project}`, entities); return { rows: [] }; } else if (sql.includes('INSERT INTO relations')) { const project = params?.[0] || 'default'; const relations = mockData.get(`relations_${project}`) || []; const newRelation = { project: params?.[0], from_entity: params?.[1], to_entity: params?.[2], relation_type: params?.[3] }; relations.push(newRelation); mockData.set(`relations_${project}`, relations); return { rows: [] }; } return { rows: [] }; }), release: jest.fn() }), end: jest.fn().mockResolvedValue(undefined) }; }) })); describe('Tag System', () => { let manager: KnowledgeGraphManager; let testCounter = 0; beforeEach(async () => { testCounter++; // Use PostgreSQL configuration with mocked database const config: StorageConfig = { type: StorageType.POSTGRESQL, connectionString: 'postgresql://test:test@localhost:5432/test' }; manager = new KnowledgeGraphManager(config); // Wait for initialization await new Promise(resolve => setTimeout(resolve, 100)); }); afterEach(async () => { await manager.close(); }); // Helper function to get unique project name for each test const getTestProject = () => `tag_test_${testCounter}_${Date.now()}`; describe('Entity Creation with Tags', () => { test('should create entities with tags', async () => { const project = getTestProject(); const entities: Entity[] = [ { name: 'JavaScript', entityType: 'Language', observations: ['Dynamic typing'], tags: ['web', 'frontend', 'backend'] }, { name: 'Python', entityType: 'Language', observations: ['Readable syntax'], tags: ['data-science', 'backend', 'ai'] } ]; await manager.createEntities(entities, project); const graph = await manager.readGraph(project); expect(graph.entities).toHaveLength(2); expect(graph.entities[0].tags).toEqual(['web', 'frontend', 'backend']); expect(graph.entities[1].tags).toEqual(['data-science', 'backend', 'ai']); }); test('should create entities without tags (backward compatibility)', async () => { const project = getTestProject(); const entities: Entity[] = [ { name: 'Java', entityType: 'Language', observations: ['Static typing'], tags: [] } ]; await manager.createEntities(entities, project); const graph = await manager.readGraph(project); expect(graph.entities).toHaveLength(1); expect(graph.entities[0].tags).toEqual([]); }); }); describe('Tag Management', () => { test('should add tags to existing entities', async () => { const project = getTestProject(); const entities: Entity[] = [ { name: 'React', entityType: 'Library', observations: ['Component-based'], tags: ['frontend', 'javascript'] }, { name: 'Vue', entityType: 'Framework', observations: ['Progressive'], tags: ['frontend'] } ]; await manager.createEntities(entities, project); const updates = [ { entityName: 'React', tags: ['popular', 'meta'] }, { entityName: 'Vue', tags: ['progressive', 'easy'] } ]; const results = await manager.addTags(updates, project); expect(results).toHaveLength(2); expect(results[0].addedTags).toEqual(['popular', 'meta']); expect(results[1].addedTags).toEqual(['progressive', 'easy']); const graph = await manager.readGraph(project); const reactEntity = graph.entities.find(e => e.name === 'React'); const vueEntity = graph.entities.find(e => e.name === 'Vue'); expect(reactEntity?.tags).toEqual(['frontend', 'javascript', 'popular', 'meta']); expect(vueEntity?.tags).toEqual(['frontend', 'progressive', 'easy']); }); test('should not add duplicate tags', async () => { const project = getTestProject(); const entities: Entity[] = [ { name: 'React', entityType: 'Library', observations: ['Component-based'], tags: ['frontend', 'javascript'] } ]; await manager.createEntities(entities, project); const updates = [ { entityName: 'React', tags: ['frontend', 'new-tag'] } // 'frontend' already exists ]; const results = await manager.addTags(updates, project); expect(results[0].addedTags).toEqual(['new-tag']); // Only new tag added const graph = await manager.readGraph(project); const reactEntity = graph.entities.find(e => e.name === 'React'); expect(reactEntity?.tags).toEqual(['frontend', 'javascript', 'new-tag']); }); test('should remove tags from entities', async () => { const project = getTestProject(); const entities: Entity[] = [ { name: 'React', entityType: 'Library', observations: ['Component-based'], tags: ['frontend', 'javascript'] }, { name: 'Vue', entityType: 'Framework', observations: ['Progressive'], tags: ['frontend'] } ]; await manager.createEntities(entities, project); const updates = [ { entityName: 'React', tags: ['frontend'] }, { entityName: 'Vue', tags: ['frontend'] } ]; const results = await manager.removeTags(updates, project); expect(results).toHaveLength(2); expect(results[0].removedTags).toEqual(['frontend']); expect(results[1].removedTags).toEqual(['frontend']); const graph = await manager.readGraph(project); const reactEntity = graph.entities.find(e => e.name === 'React'); const vueEntity = graph.entities.find(e => e.name === 'Vue'); expect(reactEntity?.tags).toEqual(['javascript']); expect(vueEntity?.tags).toEqual([]); }); test('should handle removing non-existent tags', async () => { const project = getTestProject(); const entities: Entity[] = [ { name: 'React', entityType: 'Library', observations: ['Component-based'], tags: ['frontend', 'javascript'] } ]; await manager.createEntities(entities, project); const updates = [ { entityName: 'React', tags: ['non-existent'] } ]; const results = await manager.removeTags(updates, project); expect(results[0].removedTags).toEqual([]); // No tags removed const graph = await manager.readGraph(project); const reactEntity = graph.entities.find(e => e.name === 'React'); expect(reactEntity?.tags).toEqual(['frontend', 'javascript']); // Unchanged }); test('should throw error for non-existent entity', async () => { const project = getTestProject(); const updates = [ { entityName: 'NonExistent', tags: ['test'] } ]; await expect(manager.addTags(updates, project)).rejects.toThrow('ENTITY ERROR: \'NonExistent\' not found. ACTION: Create entity first with create_entities'); await expect(manager.removeTags(updates, project)).rejects.toThrow('ENTITY ERROR: \'NonExistent\' not found. ACTION: Create entity first with create_entities'); }); }); describe('Tag Search', () => { const setupTestEntities = async (project: string) => { const entities: Entity[] = [ { name: 'React', entityType: 'Library', observations: ['Component-based'], tags: ['frontend', 'javascript', 'popular'] }, { name: 'Vue', entityType: 'Framework', observations: ['Progressive'], tags: ['frontend', 'javascript'] }, { name: 'Django', entityType: 'Framework', observations: ['Python web framework'], tags: ['backend', 'python', 'web'] }, { name: 'Express', entityType: 'Framework', observations: ['Node.js framework'], tags: ['backend', 'javascript', 'minimal'] } ]; await manager.createEntities(entities, project); }; test('should search by single tag with "any" mode', async () => { const project = getTestProject(); await setupTestEntities(project); const results = await manager.searchNodes('', { exactTags: ['frontend'], tagMatchMode: 'any' }, project); expect(results.entities).toHaveLength(2); const entityNames = results.entities.map(e => e.name); expect(entityNames).toContain('React'); expect(entityNames).toContain('Vue'); }); test('should search by multiple tags with "any" mode', async () => { const project = getTestProject(); await setupTestEntities(project); const results = await manager.searchNodes('', { exactTags: ['frontend', 'python'], tagMatchMode: 'any' }, project); expect(results.entities).toHaveLength(3); const entityNames = results.entities.map(e => e.name); expect(entityNames).toContain('React'); expect(entityNames).toContain('Vue'); expect(entityNames).toContain('Django'); }); test('should search by multiple tags with "all" mode', async () => { const project = getTestProject(); await setupTestEntities(project); const results = await manager.searchNodes('', { exactTags: ['frontend', 'javascript'], tagMatchMode: 'all' }, project); expect(results.entities).toHaveLength(2); const entityNames = results.entities.map(e => e.name); expect(entityNames).toContain('React'); expect(entityNames).toContain('Vue'); }); test('should return empty results for non-matching tags', async () => { const project = getTestProject(); await setupTestEntities(project); const results = await manager.searchNodes('', { exactTags: ['non-existent'], tagMatchMode: 'any' }, project); expect(results.entities).toHaveLength(0); expect(results.relations).toHaveLength(0); }); test('should be case-sensitive for exact matching', async () => { const project = getTestProject(); await setupTestEntities(project); const results = await manager.searchNodes('', { exactTags: ['Frontend'], tagMatchMode: 'any' }, project); // Capital F expect(results.entities).toHaveLength(0); // Should not match 'frontend' }); test('should include relations between filtered entities', async () => { const project = getTestProject(); await setupTestEntities(project); // First add a relation await manager.createRelations([ { from: 'React', to: 'Vue', relationType: 'similar_to' } ], project); const results = await manager.searchNodes('', { exactTags: ['frontend'], tagMatchMode: 'any' }, project); expect(results.entities).toHaveLength(2); expect(results.relations).toHaveLength(1); expect(results.relations[0].from).toBe('React'); expect(results.relations[0].to).toBe('Vue'); }); }); describe('Enhanced Search with Tags', () => { test('should include tags in general search', async () => { const project = getTestProject(); const entities: Entity[] = [ { name: 'React', entityType: 'Library', observations: ['Component-based UI library'], tags: ['frontend', 'javascript'] }, { name: 'Angular', entityType: 'Framework', observations: ['Full-featured framework'], tags: ['frontend', 'typescript'] } ]; await manager.createEntities(entities, project); // Search for 'typescript' should find Angular via tags const results = await manager.searchNodes('typescript', project); expect(results.entities).toHaveLength(1); expect(results.entities[0].name).toBe('Angular'); }); test('should search across names, types, observations, and tags', async () => { const project = getTestProject(); const entities: Entity[] = [ { name: 'React', entityType: 'Library', observations: ['Component-based UI library'], tags: ['frontend', 'javascript'] }, { name: 'Angular', entityType: 'Framework', observations: ['Full-featured framework'], tags: ['frontend', 'typescript'] } ]; await manager.createEntities(entities, project); // Search for 'frontend' should find both entities via tags const results = await manager.searchNodes('frontend', project); expect(results.entities).toHaveLength(2); const entityNames = results.entities.map(e => e.name); expect(entityNames).toContain('React'); expect(entityNames).toContain('Angular'); }); }); });

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/n-r-w/knowledgegraph-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server