widgetdc-cortex / apps /backend /src /tests /knowledge /KnowledgeCompiler.test.ts
Kraft102's picture
Initial deployment - WidgeTDC Cortex Backend v2.1.0
529090e
/**
* ╔══════════════════════════════════════════════════════════════════════════════╗
* β•‘ 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({});
});
});