UNPKG

lorehub

Version:

Capture and surface the collective wisdom of your codebase

894 lines (775 loc) 30 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { LoreHubServer } from './server.js'; import { Database } from '../db/database.js'; import type { Lore, Realm } from '../core/types.js'; // We'll create a mock database instance instead of mocking the constructor describe('LoreHubServer', () => { let server: LoreHubServer; let mockDb: any; const mockRealm: Realm = { id: 'realm-123', name: 'test-realm', path: '/test/realm', isMonorepo: false, provinces: [], lastSeen: new Date(), createdAt: new Date(), }; const mockLores: Lore[] = [ { id: 'lore-1', realmId: 'realm-123', content: 'Use PostgreSQL for data storage', why: 'Need ACID compliance', type: 'decree', provinces: [], sigils: ['database', 'postgresql'], confidence: 90, origin: { type: 'manual', reference: 'cli' }, status: 'living', createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'), }, { id: 'lore-2', realmId: 'realm-123', content: 'Redis for caching', type: 'decree', provinces: [], sigils: ['cache', 'redis'], confidence: 85, origin: { type: 'manual', reference: 'cli' }, status: 'living', createdAt: new Date('2024-01-02'), updatedAt: new Date('2024-01-02'), }, ]; beforeEach(() => { vi.clearAllMocks(); vi.resetAllMocks(); // Create a mock database object mockDb = { findRealmByPath: vi.fn(), searchLores: vi.fn(), listLoresByRealm: vi.fn(), listLoresByType: vi.fn(), listLoresByService: vi.fn(), findLore: vi.fn(), listRealms: vi.fn(), createLore: vi.fn(), updateLore: vi.fn(), deleteLore: vi.fn(), softDeleteLore: vi.fn(), restoreLore: vi.fn(), createRealm: vi.fn(), createRelation: vi.fn(), deleteRelation: vi.fn(), listRelationsByLore: vi.fn(), getRealmLoreCount: vi.fn(), close: vi.fn(), } as any; // Pass the mock database directly to the server server = new LoreHubServer(mockDb); }); describe('Server Info', () => { it('should return correct server info', async () => { const info = await server.getServerInfo(); expect(info.name).toBe('lorehub'); expect(info.version).toBeDefined(); expect(info.capabilities).toEqual({ tools: {}, }); }); }); describe('Tools', () => { it('should list available tools', async () => { const tools = await server.listTools(); expect(tools).toContainEqual( expect.objectContaining({ name: 'search_lores', description: expect.stringContaining('Search lores'), }) ); expect(tools).toContainEqual( expect.objectContaining({ name: 'list_lores', description: expect.stringContaining('List lores'), }) ); expect(tools).toContainEqual( expect.objectContaining({ name: 'get_lore', description: expect.stringContaining('Get a specific lore'), }) ); expect(tools).toContainEqual( expect.objectContaining({ name: 'list_realms', description: expect.stringContaining('List all realms'), }) ); }); describe('search_lores', () => { beforeEach(() => { vi.clearAllMocks(); mockDb.findRealmByPath = vi.fn().mockReturnValue(mockRealm); mockDb.listRealms = vi.fn().mockReturnValue([mockRealm]); mockDb.searchLores = vi.fn().mockReturnValue(mockLores); }); it('should search lores with query', async () => { const result = await server.callTool('search_lores', { query: 'database', realm_path: '/test/realm', }); expect(mockDb.searchLores).toHaveBeenCalledWith('realm-123', 'database'); expect(result).toHaveProperty('lores'); expect(result.lores).toHaveLength(2); // Lores are sorted by date, lore-2 has a later date expect(result.lores[0]).toMatchObject({ id: 'lore-2', content: 'Redis for caching', type: 'decree', realm: { name: 'test-realm', path: '/test/realm', }, }); expect(result.lores[1]).toMatchObject({ id: 'lore-1', content: 'Use PostgreSQL for data storage', type: 'decree', realm: { name: 'test-realm', path: '/test/realm', }, }); }); it('should filter by type', async () => { // searchLores applies the filter after searching, not using listLoresByType const result = await server.callTool('search_lores', { query: 'database', realm_path: '/test/realm', type: 'decree', }); // Both lores match "database" but we're filtering by type expect(result.lores).toHaveLength(2); expect(result.lores.every((l: any) => l.type === 'decree')).toBe(true); }); it('should handle realm not found', async () => { mockDb.findRealmByPath = vi.fn().mockReturnValue(null); await expect( server.callTool('search_lores', { query: 'test', realm_path: '/nonexistent', }) ).rejects.toThrow('Realm not found'); }); it('should search across all realms when no realm_path specified', async () => { const realm2 = { ...mockRealm, id: 'realm-456', name: 'another-realm' }; mockDb.listRealms = vi.fn().mockReturnValue([mockRealm, realm2]); mockDb.searchLores = vi.fn() .mockReturnValueOnce([mockLores[0]]) // First realm .mockReturnValueOnce([mockLores[1]]); // Second realm const result = await server.callTool('search_lores', { query: 'test', }); expect(mockDb.listRealms).toHaveBeenCalled(); expect(mockDb.searchLores).toHaveBeenCalledTimes(2); expect(result.searchedRealms).toBe(2); expect(result.lores).toHaveLength(2); }); }); describe('list_lores', () => { beforeEach(() => { vi.clearAllMocks(); mockDb.findRealmByPath = vi.fn().mockReturnValue(mockRealm); mockDb.listRealms = vi.fn().mockReturnValue([mockRealm]); mockDb.listLoresByRealm = vi.fn().mockReturnValue(mockLores); }); it('should list all lores for a realm', async () => { const result = await server.callTool('list_lores', { realm_path: '/test/realm', }); expect(mockDb.listLoresByRealm).toHaveBeenCalledWith('realm-123'); expect(result.lores).toHaveLength(2); expect(result.total).toBe(2); }); it('should respect limit parameter', async () => { const result = await server.callTool('list_lores', { realm_path: '/test/realm', limit: 1, }); expect(result.lores).toHaveLength(1); }); it('should filter by type', async () => { mockDb.listLoresByType = vi.fn().mockReturnValue([mockLores[0]]); const result = await server.callTool('list_lores', { realm_path: '/test/realm', type: 'decree', }); expect(mockDb.listLoresByType).toHaveBeenCalledWith('realm-123', 'decree'); expect(result.lores).toHaveLength(1); }); it('should filter by province', async () => { mockDb.listLoresByProvince = vi.fn().mockReturnValue([]); const result = await server.callTool('list_lores', { realm_path: '/test/realm', province: 'api', }); expect(mockDb.listLoresByProvince).toHaveBeenCalledWith('realm-123', 'api'); expect(result.lores).toHaveLength(0); }); }); describe('get_lore', () => { beforeEach(() => { // Reset mocks before each test in this describe block vi.clearAllMocks(); }); it('should return a specific lore by ID', async () => { // Create a fresh lore object to avoid any pollution const loreToReturn = { id: 'lore-1', realmId: 'realm-123', content: 'Use PostgreSQL for data storage', why: 'Need ACID compliance', type: 'decree', provinces: [], sigils: ['database', 'postgresql'], confidence: 90, origin: { type: 'manual', reference: 'cli' }, status: 'living', createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'), }; mockDb.findLore.mockReturnValue(loreToReturn); mockDb.listRealms.mockReturnValue([mockRealm]); const result = await server.callTool('get_lore', { lore_id: 'lore-1', }); expect(mockDb.findLore).toHaveBeenCalledWith('lore-1'); expect(result.lore.id).toBe('lore-1'); expect(result.lore.content).toBe('Use PostgreSQL for data storage'); }); it('should handle lore not found', async () => { mockDb.findLore.mockReturnValue(null); await expect( server.callTool('get_lore', { lore_id: 'nonexistent', }) ).rejects.toThrow('Lore not found'); }); }); describe('list_realms', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should list all realms', async () => { mockDb.listRealms = vi.fn().mockReturnValue([mockRealm]); mockDb.listLoresByRealm = vi.fn().mockReturnValue(mockLores); mockDb.getRealmLoreCount = vi.fn().mockReturnValue(2); const result = await server.callTool('list_realms', {}); expect(mockDb.listRealms).toHaveBeenCalled(); expect(result.realms).toHaveLength(1); expect(result.realms[0]).toMatchObject({ id: 'realm-123', name: 'test-realm', path: '/test/realm', }); }); }); describe('create_lore', () => { it('should create a new lore', async () => { const newLore: Lore = { id: 'lore-new', realmId: 'realm-123', content: 'New architectural decision', why: 'To improve performance', type: 'decree', provinces: ['api', 'frontend'], sigils: ['architecture', 'performance'], confidence: 95, origin: { type: 'manual', reference: 'mcp' }, status: 'living', createdAt: new Date(), updatedAt: new Date(), }; // Make realm's lastSeen different from createdAt to indicate it's not newly created const existingRealm = { ...mockRealm, lastSeen: new Date(Date.now() + 1000), // 1 second later }; mockDb.findRealmByPath = vi.fn().mockReturnValue(existingRealm); mockDb.createLore = vi.fn().mockReturnValue(newLore); const result = await server.callTool('create_lore', { realm_path: '/test/realm', content: 'New architectural decision', why: 'To improve performance', type: 'decree', provinces: ['api', 'frontend'], sigils: ['architecture', 'performance'], confidence: 95, }); expect(mockDb.findRealmByPath).toHaveBeenCalledWith('/test/realm'); expect(mockDb.createLore).toHaveBeenCalledWith({ realmId: 'realm-123', content: 'New architectural decision', why: 'To improve performance', type: 'decree', provinces: ['api', 'frontend'], sigils: ['architecture', 'performance'], confidence: 95, origin: { type: 'manual', reference: 'mcp' }, }); expect(result.lore).toMatchObject({ id: 'lore-new', content: 'New architectural decision', type: 'decree', }); expect(result.message).toBe('Lore created successfully'); }); it('should auto-create realm if not found', async () => { const newRealm: Realm = { id: 'realm-new', name: 'newrealm', path: '/nonexistent/newrealm', isMonorepo: false, provinces: [], lastSeen: new Date(), createdAt: new Date(), }; const newLore: Lore = { id: 'lore-new', realmId: 'realm-new', content: 'Test lore', type: 'decree', provinces: [], sigils: [], confidence: 80, origin: { type: 'manual', reference: 'mcp' }, status: 'living', createdAt: new Date(), updatedAt: new Date(), }; mockDb.findRealmByPath = vi.fn().mockReturnValue(null); mockDb.createRealm = vi.fn().mockReturnValue(newRealm); mockDb.createLore = vi.fn().mockReturnValue(newLore); const result = await server.callTool('create_lore', { realm_path: '/nonexistent/newrealm', content: 'Test lore', type: 'decree', }); expect(mockDb.createRealm).toHaveBeenCalledWith({ name: 'newrealm', path: '/nonexistent/newrealm', isMonorepo: false, provinces: [], }); expect(mockDb.createLore).toHaveBeenCalled(); expect(result.message).toBe('Realm initialized and lore created successfully'); expect(result.realm.created).toBe(true); }); }); describe('update_lore', () => { it('should update an existing lore', async () => { const existingLore = mockLores[0]!; const updatedLore: Lore = { id: existingLore.id, realmId: existingLore.realmId, content: 'Use PostgreSQL 15 for data storage', why: existingLore.why, type: existingLore.type, provinces: existingLore.provinces, sigils: ['database', 'postgresql', 'upgrade'], confidence: 95, origin: existingLore.origin, status: existingLore.status, createdAt: existingLore.createdAt, updatedAt: new Date(), }; mockDb.findLore = vi.fn().mockReturnValue(existingLore); mockDb.updateLore = vi.fn().mockReturnValue(updatedLore); mockDb.listRealms = vi.fn().mockReturnValue([mockRealm]); const result = await server.callTool('update_lore', { lore_id: 'lore-1', content: 'Use PostgreSQL 15 for data storage', confidence: 95, sigils: ['database', 'postgresql', 'upgrade'], }); expect(mockDb.findLore).toHaveBeenCalledWith('lore-1'); expect(mockDb.updateLore).toHaveBeenCalledWith('lore-1', { content: 'Use PostgreSQL 15 for data storage', confidence: 95, sigils: ['database', 'postgresql', 'upgrade'], why: undefined, type: undefined, provinces: undefined, status: undefined, origin: undefined, }); expect(result.lore.content).toBe('Use PostgreSQL 15 for data storage'); expect(result.lore.confidence).toBe(95); expect(result.message).toBe('Lore updated successfully'); }); it('should throw error if lore not found', async () => { mockDb.findLore = vi.fn().mockReturnValue(null); await expect( server.callTool('update_lore', { lore_id: 'nonexistent-lore', content: 'Updated content', }) ).rejects.toThrow('Lore not found with ID: nonexistent-lore'); }); it('should update lore status', async () => { const existingLore = mockLores[0]!; const archivedLore: Lore = { id: existingLore.id, realmId: existingLore.realmId, content: existingLore.content, why: existingLore.why, type: existingLore.type, provinces: existingLore.provinces, sigils: existingLore.sigils, confidence: existingLore.confidence, origin: existingLore.origin, status: 'archived', createdAt: existingLore.createdAt, updatedAt: new Date(), }; mockDb.findLore = vi.fn().mockReturnValue(existingLore); mockDb.updateLore = vi.fn().mockReturnValue(archivedLore); mockDb.listRealms = vi.fn().mockReturnValue([mockRealm]); const result = await server.callTool('update_lore', { lore_id: 'lore-1', status: 'archived', }); expect(result.lore.status).toBe('archived'); }); }); describe('delete_lore', () => { it('should delete a lore with confirmation', async () => { const loreToDelete = mockLores[0]!; mockDb.findLore = vi.fn().mockReturnValue(loreToDelete); mockDb.listRealms = vi.fn().mockReturnValue([mockRealm]); mockDb.deleteLore = vi.fn(); const result = await server.callTool('delete_lore', { lore_id: 'lore-1', confirm: true, }); expect(mockDb.findLore).toHaveBeenCalledWith('lore-1'); expect(mockDb.deleteLore).toHaveBeenCalledWith('lore-1'); expect(result.deleted).toBe(true); expect(result.lore_id).toBe('lore-1'); expect(result.message).toBe('Lore permanently deleted'); }); it('should throw error without confirmation', async () => { await expect( server.callTool('delete_lore', { lore_id: 'lore-1', confirm: false, }) ).rejects.toThrow('Deletion must be confirmed by setting confirm: true'); }); it('should throw error if lore not found', async () => { mockDb.findLore = vi.fn().mockReturnValue(null); await expect( server.callTool('delete_lore', { lore_id: 'nonexistent-lore', confirm: true, }) ).rejects.toThrow('Lore not found with ID: nonexistent-lore'); }); }); describe('archive_lore', () => { it('should archive an active lore', async () => { const activeLore = mockLores[0]!; const archivedLore: Lore = { id: activeLore.id, realmId: activeLore.realmId, content: activeLore.content, why: activeLore.why, type: activeLore.type, provinces: activeLore.provinces, sigils: activeLore.sigils, confidence: activeLore.confidence, origin: activeLore.origin, status: 'archived', createdAt: activeLore.createdAt, updatedAt: new Date(), }; mockDb.findLore = vi.fn() .mockReturnValueOnce(activeLore) // First call: verify exists .mockReturnValueOnce(archivedLore); // Second call: after archiving mockDb.softDeleteLore = vi.fn(); mockDb.listRealms = vi.fn().mockReturnValue([mockRealm]); const result = await server.callTool('archive_lore', { lore_id: 'lore-1', }); expect(mockDb.softDeleteLore).toHaveBeenCalledWith('lore-1'); expect(result.lore.status).toBe('archived'); expect(result.message).toBe('Lore archived successfully'); }); it('should throw error if lore already archived', async () => { const archivedLore = { ...mockLores[0]!, status: 'archived' }; mockDb.findLore = vi.fn().mockReturnValue(archivedLore); await expect( server.callTool('archive_lore', { lore_id: 'lore-1', }) ).rejects.toThrow('Lore is already archived'); }); it('should throw error if lore not found', async () => { mockDb.findLore = vi.fn().mockReturnValue(null); await expect( server.callTool('archive_lore', { lore_id: 'nonexistent-lore', }) ).rejects.toThrow('Lore not found with ID: nonexistent-lore'); }); }); describe('restore_lore', () => { it('should restore an archived lore', async () => { const archivedLore = { ...mockLores[0]!, status: 'archived' as const }; const restoredLore: Lore = { id: archivedLore.id, realmId: archivedLore.realmId, content: archivedLore.content, why: archivedLore.why, type: archivedLore.type, provinces: archivedLore.provinces, sigils: archivedLore.sigils, confidence: archivedLore.confidence, origin: archivedLore.origin, status: 'living', createdAt: archivedLore.createdAt, updatedAt: new Date(), }; mockDb.findLore = vi.fn() .mockReturnValueOnce(archivedLore) // First call: verify exists and archived .mockReturnValueOnce(restoredLore); // Second call: after restoring mockDb.restoreLore = vi.fn(); mockDb.listRealms = vi.fn().mockReturnValue([mockRealm]); const result = await server.callTool('restore_lore', { lore_id: 'lore-1', }); expect(mockDb.restoreLore).toHaveBeenCalledWith('lore-1'); expect(result.lore.status).toBe('living'); expect(result.message).toBe('Lore restored successfully'); }); it('should throw error if lore not archived', async () => { const activeLore = mockLores[0]!; mockDb.findLore = vi.fn().mockReturnValue(activeLore); await expect( server.callTool('restore_lore', { lore_id: 'lore-1', }) ).rejects.toThrow('Lore is not archived'); }); it('should throw error if lore not found', async () => { mockDb.findLore = vi.fn().mockReturnValue(null); await expect( server.callTool('restore_lore', { lore_id: 'nonexistent-lore', }) ).rejects.toThrow('Lore not found with ID: nonexistent-lore'); }); }); describe('relation tools', () => { const mockRelation = { fromLoreId: 'lore-1', toLoreId: 'lore-2', type: 'depends_on', strength: 1.0, metadata: null, createdAt: new Date(), }; describe('create_relation', () => { it('should create a relation between two lores', async () => { mockDb.findLore = vi.fn() .mockReturnValueOnce(mockLores[0]) // fromLore .mockReturnValueOnce(mockLores[1]); // toLore mockDb.createRelation = vi.fn().mockReturnValue(mockRelation); const result = await server.callTool('create_relation', { from_lore_id: 'lore-1', to_lore_id: 'lore-2', type: 'depends_on', strength: 1.0, }); expect(mockDb.createRelation).toHaveBeenCalledWith({ fromLoreId: 'lore-1', toLoreId: 'lore-2', type: 'depends_on', strength: 1.0, metadata: undefined, }); expect(result.relation.type).toBe('depends_on'); expect(result.message).toBe('Relation created successfully'); }); it('should throw error if source lore not found', async () => { mockDb.findLore = vi.fn().mockReturnValue(null); await expect( server.callTool('create_relation', { from_lore_id: 'nonexistent', to_lore_id: 'lore-2', type: 'depends_on', }) ).rejects.toThrow('Source lore not found with ID: nonexistent'); }); it('should throw error if lores are in different realms', async () => { const lore1 = { ...mockLores[0], realmId: 'realm-1' }; const lore2 = { ...mockLores[1], realmId: 'realm-2' }; mockDb.findLore = vi.fn() .mockReturnValueOnce(lore1) .mockReturnValueOnce(lore2); await expect( server.callTool('create_relation', { from_lore_id: 'lore-1', to_lore_id: 'lore-2', type: 'depends_on', }) ).rejects.toThrow('Cannot create relation between lores in different realms'); }); }); describe('delete_relation', () => { it('should delete a relation', async () => { mockDb.findLore = vi.fn() .mockReturnValueOnce(mockLores[0]) // fromLore .mockReturnValueOnce(mockLores[1]); // toLore mockDb.deleteRelation = vi.fn(); const result = await server.callTool('delete_relation', { from_lore_id: 'lore-1', to_lore_id: 'lore-2', type: 'depends_on', }); expect(mockDb.deleteRelation).toHaveBeenCalledWith('lore-1', 'lore-2', 'depends_on'); expect(result.deleted).toBe(true); expect(result.message).toBe('Relation deleted successfully'); }); }); describe('list_relations', () => { it('should list all relations for a lore', async () => { const relations = [ mockRelation, { fromLoreId: 'lore-2', toLoreId: 'lore-1', type: 'contradicts', strength: 0.8, metadata: null, createdAt: new Date(), }, ]; mockDb.findLore = vi.fn() .mockReturnValueOnce(mockLores[0]) // main lore .mockReturnValueOnce(mockLores[1]); // related lore mockDb.listRelationsByLore = vi.fn().mockReturnValue(relations); const result = await server.callTool('list_relations', { lore_id: 'lore-1', direction: 'both', }); expect(mockDb.listRelationsByLore).toHaveBeenCalledWith('lore-1'); expect(result.relations).toHaveLength(2); expect(result.relations[0].direction).toBe('outgoing'); expect(result.relations[1].direction).toBe('incoming'); }); it('should filter by direction', async () => { const relations = [ mockRelation, { fromLoreId: 'lore-2', toLoreId: 'lore-1', type: 'contradicts', strength: 0.8, metadata: null, createdAt: new Date(), }, ]; mockDb.findLore = vi.fn().mockReturnValue(mockLores[0]); mockDb.listRelationsByLore = vi.fn().mockReturnValue(relations); const result = await server.callTool('list_relations', { lore_id: 'lore-1', direction: 'from', }); expect(result.relations).toHaveLength(1); expect(result.relations[0].fromLoreId).toBe('lore-1'); }); }); }); describe('get_realm_stats', () => { it('should return comprehensive realm statistics', async () => { const lores = [ ...mockLores, { id: 'lore-3', realmId: 'realm-123', content: 'High confidence decision', type: 'decree' as const, provinces: ['api'], sigils: ['important', 'architecture'], confidence: 95, origin: { type: 'manual' as const, reference: 'cli' }, status: 'living' as const, createdAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), // 5 days ago updatedAt: new Date(), }, { id: 'lore-4', realmId: 'realm-123', content: 'Low confidence assumption', type: 'belief' as const, provinces: ['frontend'], sigils: ['uncertain', 'review'], confidence: 30, origin: { type: 'manual' as const, reference: 'cli' }, status: 'archived' as const, createdAt: new Date(Date.now() - 60 * 24 * 60 * 60 * 1000), // 60 days ago updatedAt: new Date(), }, ]; mockDb.findRealmByPath = vi.fn().mockReturnValue(mockRealm); mockDb.listLoresByRealm = vi.fn().mockReturnValue(lores); mockDb.listRelationsByLore = vi.fn().mockReturnValue([]); const result = await server.callTool('get_realm_stats', { realm_path: '/test/realm', }); expect(mockDb.findRealmByPath).toHaveBeenCalledWith('/test/realm'); expect(mockDb.listLoresByRealm).toHaveBeenCalledWith('realm-123'); expect(result.statistics.totalLores).toBe(4); expect(result.statistics.loresByType.decree).toBe(3); expect(result.statistics.loresByType.belief).toBe(1); expect(result.statistics.loresByStatus.living).toBe(3); expect(result.statistics.loresByStatus.archived).toBe(1); expect(result.statistics.highConfidenceLores).toBe(2); expect(result.statistics.lowConfidenceLores).toBe(1); expect(result.statistics.averageConfidence).toBeGreaterThan(0); expect(result.statistics.loreGrowth.lastWeek).toBe(1); expect(result.statistics.topSigils.length).toBeGreaterThanOrEqual(6); }); it('should throw error if realm not found', async () => { mockDb.findRealmByPath = vi.fn().mockReturnValue(null); await expect( server.callTool('get_realm_stats', { realm_path: '/nonexistent/realm', }) ).rejects.toThrow('Realm not found at path: /nonexistent/realm'); }); }); }); describe('Error Handling', () => { it('should handle unknown tool', async () => { await expect( server.callTool('unknown_tool', {}) ).rejects.toThrow('Tool not found: unknown_tool'); }); it('should handle database errors gracefully', async () => { mockDb.findRealmByPath = vi.fn().mockImplementation(() => { throw new Error('Database connection failed'); }); await expect( server.callTool('search_lores', { query: 'test', realm_path: '/test', }) ).rejects.toThrow('Database connection failed'); }); }); });