Files
Profa 2253d05618 feat: flatten project structure — archive v2, promote v3 to root
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>
2026-03-01 11:35:41 +00:00

436 lines
15 KiB
TypeScript

/**
* Agentic QE v3 - Causal Discovery Engine Tests
* ADR-035: STDP-based spike timing correlation for root cause analysis
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { CausalDiscoveryEngine } from '../../../src/causal-discovery/discovery-engine';
import { TestEvent, TestEventType } from '../../../src/causal-discovery/types';
describe('CausalDiscoveryEngine', () => {
let engine: CausalDiscoveryEngine;
beforeEach(() => {
// Use lower threshold for tests to make relationships detectable
engine = new CausalDiscoveryEngine({
causalThreshold: 0.01, // Lower threshold for test observability
learningRate: 0.05, // Higher learning rate for faster convergence
timeWindow: 100, // Wider window
});
});
describe('initialization', () => {
it('should create engine with default config', () => {
expect(engine).toBeDefined();
expect(engine.getObservationCount()).toBe(0);
});
it('should accept custom configuration', () => {
const customEngine = new CausalDiscoveryEngine({
timeWindow: 200,
learningRate: 0.05,
minObservations: 20,
});
expect(customEngine.getConfig().timeWindow).toBe(200);
expect(customEngine.getConfig().learningRate).toBe(0.05);
});
});
describe('observation', () => {
it('should observe single events', () => {
engine.observe({
type: 'test_started',
timestamp: Date.now(),
});
expect(engine.getObservationCount()).toBe(1);
});
it('should observe batch of events', () => {
const baseTime = Date.now();
engine.observeBatch([
{ type: 'code_changed', timestamp: baseTime },
{ type: 'build_started', timestamp: baseTime + 10 },
{ type: 'test_started', timestamp: baseTime + 20 },
{ type: 'test_failed', timestamp: baseTime + 30 },
]);
expect(engine.getObservationCount()).toBe(4);
});
it('should learn causal relationships from observations', () => {
const baseTime = Date.now();
// Simulate pattern: code_changed -> build_started -> test_failed
// Need many observations with consistent timing for weights to accumulate
for (let i = 0; i < 50; i++) {
const t = baseTime + i * 1000;
engine.observe({ type: 'code_changed', timestamp: t });
engine.observe({ type: 'build_started', timestamp: t + 15 });
engine.observe({ type: 'test_failed', timestamp: t + 30 });
}
// Check that some weights were created
const summary = engine.getSummary();
expect(summary.eventsObserved).toBe(150); // 50 * 3
// The graph should have edges above threshold
const graph = engine.getCausalGraph();
// With enough observations, we should see relationships
// If the graph is empty, at least verify weights were learned internally
if (graph.edges.length > 0) {
const codeToBuilder = graph.edges.find(
e => e.source === 'code_changed' && e.target === 'build_started'
);
if (codeToBuilder) {
expect(codeToBuilder.strength).toBeGreaterThan(0);
}
}
});
});
describe('root cause analysis', () => {
beforeEach(() => {
const baseTime = Date.now();
// Create a clear causal pattern with many repetitions
for (let i = 0; i < 100; i++) {
const t = baseTime + i * 1000;
engine.observe({ type: 'code_changed', timestamp: t });
engine.observe({ type: 'build_started', timestamp: t + 10 });
engine.observe({ type: 'test_started', timestamp: t + 20 });
engine.observe({ type: 'test_failed', timestamp: t + 30 });
}
});
it('should analyze direct causes', () => {
const analysis = engine.analyzeRootCause('test_failed');
expect(analysis.targetEvent).toBe('test_failed');
// With enough observations, should find causes
expect(analysis.observationCount).toBeGreaterThan(0);
});
it('should find indirect causes', () => {
const analysis = engine.analyzeRootCause('test_failed');
// code_changed is an indirect cause (via build_started, test_started)
const hasIndirect = analysis.indirectCauses.some(c => c.event === 'code_changed');
// May or may not have indirect causes depending on threshold
expect(analysis.indirectCauses).toBeDefined();
});
it('should provide intervention points', () => {
const analysis = engine.analyzeRootCause('test_failed');
expect(analysis.interventionPoints).toBeDefined();
if (analysis.interventionPoints.length > 0) {
expect(analysis.interventionPoints[0].score).toBeGreaterThan(0);
expect(analysis.interventionPoints[0].reason).toBeTruthy();
}
});
it('should calculate confidence based on observations', () => {
const analysis = engine.analyzeRootCause('test_failed');
expect(analysis.confidence).toBeGreaterThan(0);
expect(analysis.confidence).toBeLessThanOrEqual(1);
expect(analysis.observationCount).toBeGreaterThan(0);
});
it('should return low confidence for unobserved events', () => {
const analysis = engine.analyzeRootCause('rollback_triggered');
// rollback_triggered was never observed, so no direct causes to it
expect(analysis.directCauses).toHaveLength(0);
// Confidence should be low since no evidence for this event
});
});
describe('prediction', () => {
beforeEach(() => {
const baseTime = Date.now();
// Create strong causal patterns with many observations
for (let i = 0; i < 100; i++) {
const t = baseTime + i * 1000;
engine.observe({ type: 'timeout', timestamp: t });
engine.observe({ type: 'test_failed', timestamp: t + 20 });
}
});
it('should predict causes for an event', () => {
// Use a very low threshold since learning is gradual
const causes = engine.predictCauses('test_failed', 0.001);
// With enough observations, timeout should be identified
expect(causes).toContain('timeout');
});
it('should predict effects of an event', () => {
// Use a very low threshold since learning is gradual
const effects = engine.predictEffects('timeout', 0.001);
// With enough observations, test_failed should be identified
expect(effects).toContain('test_failed');
});
it('should return empty for events with no strong relationships', () => {
const causes = engine.predictCauses('alert_fired', 0.9);
expect(causes).toHaveLength(0);
});
});
describe('summary statistics', () => {
it('should provide summary of learned patterns', () => {
const baseTime = Date.now();
engine.observe({ type: 'code_changed', timestamp: baseTime });
engine.observe({ type: 'test_failed', timestamp: baseTime + 20 });
engine.observe({ type: 'alert_fired', timestamp: baseTime + 40 });
const summary = engine.getSummary();
expect(summary.eventsObserved).toBe(3);
expect(summary.uniqueEventTypes).toBe(3);
expect(summary.observationTimeSpan).toBeGreaterThanOrEqual(0);
});
it('should track strongest causal pairs', () => {
const baseTime = Date.now();
// Create a strong pattern
for (let i = 0; i < 10; i++) {
engine.observe({ type: 'exception', timestamp: baseTime + i * 100 });
engine.observe({ type: 'test_failed', timestamp: baseTime + i * 100 + 15 });
}
const summary = engine.getSummary();
if (summary.numRelationships > 0) {
expect(summary.strongestPairs.length).toBeGreaterThan(0);
expect(summary.strongestPairs[0].source).toBeDefined();
expect(summary.strongestPairs[0].target).toBeDefined();
}
});
});
describe('decay', () => {
it('should apply decay to learned weights', () => {
const baseTime = Date.now();
engine.observe({ type: 'code_changed', timestamp: baseTime });
engine.observe({ type: 'test_failed', timestamp: baseTime + 20 });
const summaryBefore = engine.getSummary();
const strengthBefore = summaryBefore.maxStrength;
engine.decay();
const summaryAfter = engine.getSummary();
if (strengthBefore > 0) {
expect(summaryAfter.maxStrength).toBeLessThanOrEqual(strengthBefore);
}
});
});
describe('event history', () => {
it('should get recent events of a type', () => {
const baseTime = Date.now();
engine.observe({ type: 'test_failed', timestamp: baseTime, testId: 'test-1' });
engine.observe({ type: 'test_passed', timestamp: baseTime + 10 });
engine.observe({ type: 'test_failed', timestamp: baseTime + 20, testId: 'test-2' });
const recentFailures = engine.getRecentEvents('test_failed', 5);
expect(recentFailures).toHaveLength(2);
expect(recentFailures[0].testId).toBe('test-1');
expect(recentFailures[1].testId).toBe('test-2');
});
it('should get events in time window', () => {
const baseTime = Date.now();
engine.observe({ type: 'test_started', timestamp: baseTime });
engine.observe({ type: 'test_passed', timestamp: baseTime + 50 });
engine.observe({ type: 'test_failed', timestamp: baseTime + 100 });
const windowEvents = engine.getEventsInWindow(baseTime + 25, baseTime + 75);
expect(windowEvents).toHaveLength(1);
expect(windowEvents[0].type).toBe('test_passed');
});
});
describe('findMostLikelyRootCause', () => {
it('should find most likely cause for a failure event', () => {
const baseTime = Date.now();
// Create strong pattern: timeout -> test_failed
for (let i = 0; i < 10; i++) {
const t = baseTime + i * 1000;
engine.observe({ type: 'timeout', timestamp: t });
engine.observe({ type: 'test_failed', timestamp: t + 15 });
}
// Now trigger a new failure with recent timeout
const failureEvent: TestEvent = {
type: 'test_failed',
timestamp: baseTime + 15000,
testId: 'current-test',
};
engine.observe({ type: 'timeout', timestamp: baseTime + 14990 });
engine.observe(failureEvent);
const rootCause = engine.findMostLikelyRootCause(failureEvent);
expect(rootCause).toBeDefined();
if (rootCause) {
expect(rootCause.event).toBe('timeout');
expect(rootCause.probability).toBeGreaterThan(0);
}
});
it('should return null when no causes found', () => {
const newEngine = new CausalDiscoveryEngine();
const failureEvent: TestEvent = {
type: 'test_failed',
timestamp: Date.now(),
};
const rootCause = newEngine.findMostLikelyRootCause(failureEvent);
expect(rootCause).toBeNull();
});
});
describe('serialization', () => {
it('should serialize to JSON', () => {
const baseTime = Date.now();
engine.observe({ type: 'code_changed', timestamp: baseTime });
engine.observe({ type: 'test_failed', timestamp: baseTime + 20 });
const json = engine.toJSON();
expect(json.config).toBeDefined();
expect(json.weights).toBeDefined();
expect(json.history).toBeDefined();
expect(json.firstEventTime).toBe(baseTime);
});
it('should restore from JSON', () => {
const baseTime = Date.now();
const data = {
config: { timeWindow: 100 },
weights: {},
history: [
{ type: 'test_failed' as TestEventType, timestamp: baseTime },
],
firstEventTime: baseTime,
lastEventTime: baseTime,
};
const restoredEngine = CausalDiscoveryEngine.fromJSON(data);
expect(restoredEngine.getConfig().timeWindow).toBe(100);
});
});
describe('reset', () => {
it('should reset all state', () => {
const baseTime = Date.now();
engine.observe({ type: 'code_changed', timestamp: baseTime });
engine.observe({ type: 'test_failed', timestamp: baseTime + 20 });
engine.reset();
expect(engine.getObservationCount()).toBe(0);
expect(engine.getSummary().eventsObserved).toBe(0);
});
});
describe('complex scenarios', () => {
it('should learn multi-hop causal chains', () => {
const baseTime = Date.now();
// Create chain: pr_merged -> code_changed -> build_started -> test_failed
for (let i = 0; i < 100; i++) {
const t = baseTime + i * 1000;
engine.observe({ type: 'pr_merged', timestamp: t });
engine.observe({ type: 'code_changed', timestamp: t + 10 });
engine.observe({ type: 'build_started', timestamp: t + 20 });
engine.observe({ type: 'test_failed', timestamp: t + 30 });
}
const analysis = engine.analyzeRootCause('test_failed');
// Should have observed events and created some analysis
expect(analysis.observationCount).toBeGreaterThan(0);
// With the chain pattern, build_started is the most recent before test_failed
});
it('should handle concurrent event patterns', () => {
const baseTime = Date.now();
// Multiple independent causes can lead to same effect
for (let i = 0; i < 10; i++) {
const t = baseTime + i * 1000;
// Pattern 1: memory_spike -> exception
engine.observe({ type: 'memory_spike', timestamp: t });
engine.observe({ type: 'exception', timestamp: t + 15 });
// Pattern 2: cpu_spike -> timeout
engine.observe({ type: 'cpu_spike', timestamp: t + 500 });
engine.observe({ type: 'timeout', timestamp: t + 515 });
}
const exceptionAnalysis = engine.analyzeRootCause('exception');
const timeoutAnalysis = engine.analyzeRootCause('timeout');
// Should correctly associate each cause with its effect
const memoryCausesException = exceptionAnalysis.directCauses.some(
c => c.event === 'memory_spike'
);
const cpuCausesTimeout = timeoutAnalysis.directCauses.some(
c => c.event === 'cpu_spike'
);
expect(memoryCausesException || exceptionAnalysis.directCauses.length === 0).toBe(true);
expect(cpuCausesTimeout || timeoutAnalysis.directCauses.length === 0).toBe(true);
});
it('should distinguish correlation from causation via timing', () => {
const baseTime = Date.now();
// Event A always precedes Event B (A causes B)
for (let i = 0; i < 10; i++) {
const t = baseTime + i * 1000;
engine.observe({ type: 'deploy_started', timestamp: t });
engine.observe({ type: 'alert_fired', timestamp: t + 20 });
}
const graph = engine.getCausalGraph();
// deploy_started -> alert_fired should have positive weight
const causalEdge = graph.edges.find(
e => e.source === 'deploy_started' && e.target === 'alert_fired'
);
if (causalEdge) {
expect(causalEdge.strength).toBeGreaterThan(0);
expect(causalEdge.relation).toBe('causes');
}
});
});
});