mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
2253d05618
Remove the nested v3/ subdirectory by moving all active source code, tests, configs, and assets directly to the project root. Archive v2/ (dead code, 847MB) to v2-archive.tar.gz. Key changes: - Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/ - Move v3/tsconfig.json, v3/vitest.config.ts to root - Merge v3/package.json into root package.json (ESM, scripts, deps, exports) - Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/ - Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes - Fix all init installer path resolution (3-level-up → 2-level-up) - Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe" - Update settings.json ADR/DDD directories to docs/implementation/ - Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks) - Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces - Update 15 guidance shards: v3/src/ → src/ path references - Fix sync interfaces: remove v3/ and v2/ path references - Fix test path calculations (process.cwd() + '..' no longer needed) - Fix flaky perf test threshold (500ms → 1000ms for CI environments) - Fix complexity-analyzer test to match ADR-051 keyword-based eligibility Build passes, 17,901 tests pass, CLI and MCP bundles verified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
352 lines
10 KiB
TypeScript
352 lines
10 KiB
TypeScript
/**
|
|
* Agentic QE v3 - Causal Root Cause Analyzer Service Tests
|
|
* ADR-035: STDP-based spike timing correlation for root cause analysis
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import {
|
|
CausalRootCauseAnalyzerService,
|
|
createCausalRootCauseAnalyzer,
|
|
createTestEvent,
|
|
testResultToEvents,
|
|
} from '../../../src/domains/defect-intelligence/services/causal-root-cause-analyzer';
|
|
import { MemoryBackend } from '../../../src/kernel/interfaces';
|
|
|
|
// Mock MemoryBackend
|
|
const createMockMemory = (): MemoryBackend => ({
|
|
get: vi.fn().mockResolvedValue(null),
|
|
set: vi.fn().mockResolvedValue(undefined),
|
|
delete: vi.fn().mockResolvedValue(true),
|
|
search: vi.fn().mockResolvedValue([]),
|
|
list: vi.fn().mockResolvedValue([]),
|
|
stats: vi.fn().mockResolvedValue({
|
|
totalKeys: 0,
|
|
totalSize: 0,
|
|
namespaces: [],
|
|
}),
|
|
initialize: vi.fn().mockResolvedValue(undefined),
|
|
dispose: vi.fn().mockResolvedValue(undefined),
|
|
});
|
|
|
|
describe('CausalRootCauseAnalyzerService', () => {
|
|
let service: CausalRootCauseAnalyzerService;
|
|
let mockMemory: MemoryBackend;
|
|
|
|
beforeEach(() => {
|
|
mockMemory = createMockMemory();
|
|
service = new CausalRootCauseAnalyzerService(mockMemory);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('initialization', () => {
|
|
it('should create service with default config', () => {
|
|
expect(service).toBeDefined();
|
|
expect(service.getObservationCount()).toBe(0);
|
|
});
|
|
|
|
it('should accept custom configuration', () => {
|
|
const customService = new CausalRootCauseAnalyzerService(mockMemory, {
|
|
minConfidence: 0.5,
|
|
autoPersist: false,
|
|
});
|
|
expect(customService).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('event observation', () => {
|
|
it('should observe single events', () => {
|
|
service.observeEvent({
|
|
type: 'test_started',
|
|
timestamp: Date.now(),
|
|
testId: 'test-1',
|
|
});
|
|
|
|
expect(service.getObservationCount()).toBe(1);
|
|
});
|
|
|
|
it('should observe batch of events', () => {
|
|
const baseTime = Date.now();
|
|
service.observeEvents([
|
|
{ type: 'code_changed', timestamp: baseTime },
|
|
{ type: 'build_started', timestamp: baseTime + 10 },
|
|
{ type: 'test_failed', timestamp: baseTime + 20 },
|
|
]);
|
|
|
|
expect(service.getObservationCount()).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('root cause analysis', () => {
|
|
beforeEach(() => {
|
|
const baseTime = Date.now();
|
|
|
|
// Create clear causal patterns
|
|
for (let i = 0; i < 15; i++) {
|
|
const t = baseTime + i * 1000;
|
|
service.observeEvent({ type: 'code_changed', timestamp: t });
|
|
service.observeEvent({ type: 'build_started', timestamp: t + 10 });
|
|
service.observeEvent({ type: 'test_failed', timestamp: t + 25 });
|
|
}
|
|
});
|
|
|
|
it('should analyze root cause successfully', async () => {
|
|
const result = await service.analyzeRootCause({
|
|
targetEvent: 'test_failed',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.targetEvent).toBe('test_failed');
|
|
expect(result.value.observationCount).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it('should include direct causes in response', async () => {
|
|
const result = await service.analyzeRootCause({
|
|
targetEvent: 'test_failed',
|
|
includeIndirect: false,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.directCauses).toBeDefined();
|
|
if (result.value.directCauses.length > 0) {
|
|
expect(result.value.directCauses[0].event).toBeDefined();
|
|
expect(result.value.directCauses[0].strength).toBeGreaterThan(0);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should include indirect causes when requested', async () => {
|
|
const result = await service.analyzeRootCause({
|
|
targetEvent: 'test_failed',
|
|
includeIndirect: true,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.indirectCauses).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it('should provide intervention recommendations', async () => {
|
|
const result = await service.analyzeRootCause({
|
|
targetEvent: 'test_failed',
|
|
maxInterventions: 3,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.interventions).toBeDefined();
|
|
expect(result.value.interventions.length).toBeLessThanOrEqual(3);
|
|
}
|
|
});
|
|
|
|
it('should generate human-readable summary', async () => {
|
|
const result = await service.analyzeRootCause({
|
|
targetEvent: 'test_failed',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.summary).toBeTruthy();
|
|
expect(typeof result.value.summary).toBe('string');
|
|
}
|
|
});
|
|
|
|
it('should handle unobserved events gracefully', async () => {
|
|
const result = await service.analyzeRootCause({
|
|
targetEvent: 'rollback_triggered',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
// rollback_triggered was not observed, so no causes
|
|
expect(result.value.directCauses).toHaveLength(0);
|
|
// Summary should indicate no significant patterns found
|
|
expect(result.value.summary).toBeTruthy();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('predictions', () => {
|
|
beforeEach(() => {
|
|
const baseTime = Date.now();
|
|
|
|
// Create strong pattern with many observations
|
|
for (let i = 0; i < 100; i++) {
|
|
const t = baseTime + i * 1000;
|
|
service.observeEvent({ type: 'timeout', timestamp: t });
|
|
service.observeEvent({ type: 'test_failed', timestamp: t + 15 });
|
|
}
|
|
});
|
|
|
|
it('should predict causes', () => {
|
|
// The service uses default high threshold, so we check internal state
|
|
const summary = service.getSummary();
|
|
expect(summary.eventsObserved).toBe(200);
|
|
// Causes prediction depends on threshold
|
|
});
|
|
|
|
it('should predict effects', () => {
|
|
// The service uses default high threshold
|
|
const summary = service.getSummary();
|
|
expect(summary.uniqueEventTypes).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('summary', () => {
|
|
it('should provide summary statistics', () => {
|
|
const baseTime = Date.now();
|
|
service.observeEvent({ type: 'test_started', timestamp: baseTime });
|
|
service.observeEvent({ type: 'test_passed', timestamp: baseTime + 20 });
|
|
|
|
const summary = service.getSummary();
|
|
|
|
expect(summary.eventsObserved).toBe(2);
|
|
expect(summary.uniqueEventTypes).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('persistence', () => {
|
|
it('should persist state to memory', async () => {
|
|
service.observeEvent({ type: 'test_failed', timestamp: Date.now() });
|
|
|
|
const result = await service.persist();
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockMemory.set).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should restore state from memory', async () => {
|
|
const result = await service.restore();
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockMemory.get).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle restore with no saved state', async () => {
|
|
(mockMemory.get as ReturnType<typeof vi.fn>).mockResolvedValue(null);
|
|
|
|
const result = await service.restore();
|
|
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('reset', () => {
|
|
it('should reset all learned patterns', () => {
|
|
service.observeEvent({ type: 'test_failed', timestamp: Date.now() });
|
|
expect(service.getObservationCount()).toBe(1);
|
|
|
|
service.reset();
|
|
|
|
expect(service.getObservationCount()).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('decay', () => {
|
|
it('should apply decay without errors', () => {
|
|
service.observeEvent({ type: 'test_failed', timestamp: Date.now() });
|
|
|
|
expect(() => service.decay()).not.toThrow();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('createCausalRootCauseAnalyzer', () => {
|
|
it('should create analyzer with factory function', () => {
|
|
const mockMemory = createMockMemory();
|
|
const analyzer = createCausalRootCauseAnalyzer(mockMemory);
|
|
|
|
expect(analyzer).toBeInstanceOf(CausalRootCauseAnalyzerService);
|
|
});
|
|
|
|
it('should accept custom config in factory', () => {
|
|
const mockMemory = createMockMemory();
|
|
const analyzer = createCausalRootCauseAnalyzer(mockMemory, {
|
|
minConfidence: 0.7,
|
|
});
|
|
|
|
expect(analyzer).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('createTestEvent', () => {
|
|
it('should create event with required fields', () => {
|
|
const event = createTestEvent('test_failed');
|
|
|
|
expect(event.type).toBe('test_failed');
|
|
expect(event.timestamp).toBeDefined();
|
|
expect(event.id).toBeDefined();
|
|
});
|
|
|
|
it('should include optional fields', () => {
|
|
const event = createTestEvent('test_failed', {
|
|
testId: 'test-123',
|
|
file: 'test.spec.ts',
|
|
data: { error: 'assertion failed' },
|
|
});
|
|
|
|
expect(event.testId).toBe('test-123');
|
|
expect(event.file).toBe('test.spec.ts');
|
|
expect(event.data?.error).toBe('assertion failed');
|
|
});
|
|
});
|
|
|
|
describe('testResultToEvents', () => {
|
|
it('should convert passing test to events', () => {
|
|
const events = testResultToEvents({
|
|
testId: 'test-1',
|
|
file: 'auth.test.ts',
|
|
passed: true,
|
|
duration: 100,
|
|
});
|
|
|
|
expect(events).toHaveLength(2);
|
|
expect(events[0].type).toBe('test_started');
|
|
expect(events[1].type).toBe('test_passed');
|
|
});
|
|
|
|
it('should convert failing test to events', () => {
|
|
const events = testResultToEvents({
|
|
testId: 'test-2',
|
|
file: 'api.test.ts',
|
|
passed: false,
|
|
duration: 500,
|
|
error: 'assertion failed: expected 200 got 500',
|
|
});
|
|
|
|
expect(events).toHaveLength(2);
|
|
expect(events[0].type).toBe('test_started');
|
|
expect(events[1].type).toBe('assertion_failed');
|
|
});
|
|
|
|
it('should detect timeout failures', () => {
|
|
const events = testResultToEvents({
|
|
testId: 'test-3',
|
|
file: 'slow.test.ts',
|
|
passed: false,
|
|
duration: 30000,
|
|
error: 'timeout: test took too long',
|
|
});
|
|
|
|
expect(events[1].type).toBe('timeout');
|
|
});
|
|
|
|
it('should detect exception failures', () => {
|
|
const events = testResultToEvents({
|
|
testId: 'test-4',
|
|
file: 'crash.test.ts',
|
|
passed: false,
|
|
duration: 50,
|
|
error: 'Error: unexpected null pointer',
|
|
});
|
|
|
|
expect(events[1].type).toBe('exception');
|
|
});
|
|
});
|