/** * ╔══════════════════════════════════════════════════════════════════════════════╗ * ║ KNOWLEDGE COMPILER - ENHANCED TEST SUITE ║ * ╠══════════════════════════════════════════════════════════════════════════════╣ * ║ Tests for the KnowledgeCompiler auto-compilation and insight generation ║ * ╚══════════════════════════════════════════════════════════════════════════════╝ */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; // Mock dependencies vi.mock('../HyperLog.js', () => ({ hyperLog: { exportForAnalysis: () => ({ summary: { HEALING_ATTEMPT: 5, HEALING_SUCCESS: 4, HEALING_FAILED: 1, } }), getRecentEvents: () => [ { id: '1', eventType: 'HEALING_SUCCESS', timestamp: Date.now(), data: {} }, { id: '2', eventType: 'ERROR_UNHANDLED', timestamp: Date.now(), data: {} }, ], }, HyperLog: class MockHyperLog { }, })); vi.mock('../SelfHealingAdapter.js', () => ({ selfHealing: { getSystemStatus: () => ({ overallHealth: 'HEALTHY', services: [ { name: 'neo4j', status: 'healthy' }, { name: 'postgres', status: 'healthy' }, ], }), getPredictiveAlerts: () => [], }, SelfHealingAdapter: class MockSelfHealing { }, })); vi.mock('../../adapters/Neo4jAdapter.js', () => ({ neo4jAdapter: { executeQuery: async () => [{ nodes: 1000, relationships: 5000 }], }, })); // ═══════════════════════════════════════════════════════════════════════════════ // AUTO-COMPILATION TESTS // ═══════════════════════════════════════════════════════════════════════════════ describe('KnowledgeCompiler - Auto-Compilation', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('should start auto-compilation at specified interval', () => { const mockCompile = vi.fn(); let interval: NodeJS.Timeout | null = null; const startAutoCompilation = (intervalMs: number) => { interval = setInterval(mockCompile, intervalMs); }; startAutoCompilation(60000); expect(interval).toBeTruthy(); // Advance time by 60 seconds vi.advanceTimersByTime(60000); expect(mockCompile).toHaveBeenCalledTimes(1); // Advance another 60 seconds vi.advanceTimersByTime(60000); expect(mockCompile).toHaveBeenCalledTimes(2); clearInterval(interval!); }); it('should not start if already running', () => { let isRunning = false; let startCount = 0; const startAutoCompilation = () => { if (isRunning) return; isRunning = true; startCount++; }; startAutoCompilation(); startAutoCompilation(); startAutoCompilation(); expect(startCount).toBe(1); }); it('should stop auto-compilation cleanly', () => { const mockCompile = vi.fn(); let interval: NodeJS.Timeout | null = null; const start = () => { interval = setInterval(mockCompile, 1000); }; const stop = () => { if (interval) { clearInterval(interval); interval = null; } }; start(); vi.advanceTimersByTime(3000); expect(mockCompile).toHaveBeenCalledTimes(3); stop(); vi.advanceTimersByTime(3000); expect(mockCompile).toHaveBeenCalledTimes(3); // No additional calls }); }); // ═══════════════════════════════════════════════════════════════════════════════ // HEALTH STATUS TESTS // ═══════════════════════════════════════════════════════════════════════════════ describe('KnowledgeCompiler - Health Status', () => { it('should calculate health score correctly', () => { const calculateScore = (status: string, failures: number) => { let score = 100; if (status === 'DEGRADED') score = 70; if (status === 'CRITICAL') score = 30; score = Math.max(0, score - failures * 5); return score; }; expect(calculateScore('HEALTHY', 0)).toBe(100); expect(calculateScore('DEGRADED', 0)).toBe(70); expect(calculateScore('CRITICAL', 0)).toBe(30); expect(calculateScore('HEALTHY', 5)).toBe(75); expect(calculateScore('HEALTHY', 20)).toBe(0); // Clamped at 0 }); it('should track healing success rate', () => { const healingStats = { attempts: 10, successes: 8, failures: 2, }; const successRate = healingStats.attempts > 0 ? Math.round((healingStats.successes / healingStats.attempts) * 100) : 100; expect(successRate).toBe(80); }); it('should identify unhealthy services', () => { const services = [ { name: 'neo4j', status: 'healthy' }, { name: 'postgres', status: 'unhealthy' }, { name: 'redis', status: 'healthy' }, ]; const unhealthy = services.filter(s => s.status === 'unhealthy'); expect(unhealthy).toHaveLength(1); expect(unhealthy[0].name).toBe('postgres'); }); }); // ═══════════════════════════════════════════════════════════════════════════════ // INSIGHT GENERATION TESTS // ═══════════════════════════════════════════════════════════════════════════════ describe('KnowledgeCompiler - Insight Generation', () => { it('should detect error patterns', () => { const events = [ { type: 'ERROR_DATABASE', count: 10 }, { type: 'ERROR_NETWORK', count: 3 }, { type: 'HEALING_SUCCESS', count: 5 }, ]; const errorEvents = events.filter(e => e.type.includes('ERROR')); const repeatedErrors = errorEvents.filter(e => e.count >= 5); expect(errorEvents).toHaveLength(2); expect(repeatedErrors).toHaveLength(1); expect(repeatedErrors[0].type).toBe('ERROR_DATABASE'); }); it('should detect usage spikes', () => { const events = [ { type: 'API_CALL', count: 100 }, { type: 'DB_QUERY', count: 50 }, { type: 'CACHE_HIT', count: 10 }, ]; const avgCount = events.reduce((sum, e) => sum + e.count, 0) / events.length; const spikes = events.filter(e => e.count > avgCount * 1.5); expect(avgCount).toBeCloseTo(53.33, 1); expect(spikes).toHaveLength(1); expect(spikes[0].type).toBe('API_CALL'); }); it('should generate recommendations based on health', () => { const generateRecommendations = (health: string, errors: number) => { const recs = []; if (health === 'DEGRADED') { recs.push({ priority: 'high', action: 'Investigate degraded services' }); } if (errors > 20) { recs.push({ priority: 'high', action: 'Review error logs' }); } return recs; }; expect(generateRecommendations('HEALTHY', 5)).toHaveLength(0); expect(generateRecommendations('DEGRADED', 5)).toHaveLength(1); expect(generateRecommendations('DEGRADED', 25)).toHaveLength(2); }); }); // ═══════════════════════════════════════════════════════════════════════════════ // ACTIVITY SUMMARY TESTS // ═══════════════════════════════════════════════════════════════════════════════ describe('KnowledgeCompiler - Activity Summary', () => { it('should count events in last 24h', () => { const now = Date.now(); const oneDayAgo = now - 24 * 60 * 60 * 1000; const events = [ { timestamp: now - 1000 }, // Recent { timestamp: now - 60000 }, // Recent { timestamp: oneDayAgo - 1000 }, // Old { timestamp: oneDayAgo - 60000 }, // Old ]; const last24h = events.filter(e => e.timestamp > oneDayAgo); expect(last24h).toHaveLength(2); }); it('should aggregate top event types', () => { const events = [ { type: 'API_CALL' }, { type: 'API_CALL' }, { type: 'API_CALL' }, { type: 'DB_QUERY' }, { type: 'DB_QUERY' }, { type: 'ERROR' }, ]; const counts: Record = {}; for (const e of events) { counts[e.type] = (counts[e.type] || 0) + 1; } const sorted = Object.entries(counts) .sort((a, b) => b[1] - a[1]) .map(([type, count]) => ({ type, count })); expect(sorted[0]).toEqual({ type: 'API_CALL', count: 3 }); expect(sorted[1]).toEqual({ type: 'DB_QUERY', count: 2 }); expect(sorted[2]).toEqual({ type: 'ERROR', count: 1 }); }); }); // ═══════════════════════════════════════════════════════════════════════════════ // GRAPH STATS TESTS // ═══════════════════════════════════════════════════════════════════════════════ describe('KnowledgeCompiler - Graph Statistics', () => { it('should handle Neo4j Integer objects', () => { // Neo4j returns special Integer objects const mockNeo4jInt = { low: 1000, high: 0 }; const extractValue = (val: any) => { if (typeof val === 'object' && val !== null && 'low' in val) { return val.low; } return Number(val || 0); }; expect(extractValue(mockNeo4jInt)).toBe(1000); expect(extractValue(500)).toBe(500); expect(extractValue(null)).toBe(0); }); it('should aggregate nodes by label', () => { const queryResult = [ { label: 'Entity', count: 500 }, { label: 'Document', count: 300 }, { label: 'Concept', count: 200 }, ]; const nodesByLabel: Record = {}; for (const row of queryResult) { nodesByLabel[row.label] = row.count; } expect(nodesByLabel['Entity']).toBe(500); expect(nodesByLabel['Document']).toBe(300); expect(Object.keys(nodesByLabel)).toHaveLength(3); }); it('should handle graph query failures gracefully', async () => { const compileGraphStats = async () => { try { throw new Error('Neo4j connection failed'); } catch (error) { return { totalNodes: 0, totalRelationships: 0, nodesByLabel: {}, recentChanges: { added: 0, modified: 0, deleted: 0 }, }; } }; const stats = await compileGraphStats(); expect(stats.totalNodes).toBe(0); expect(stats.nodesByLabel).toEqual({}); }); });