Spaces:
Paused
Paused
| /** | |
| * ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| * β 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<string, number> = {}; | |
| 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<string, number> = {}; | |
| 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({}); | |
| }); | |
| }); | |