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

505 lines
17 KiB
TypeScript

/**
* Agentic QE v3 - Causal Graph Tests
* ADR-035: STDP-based spike timing correlation for root cause analysis
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { CausalGraphImpl } from '../../../src/causal-discovery/causal-graph';
import { CausalEdge, TestEventType } from '../../../src/causal-discovery/types';
describe('CausalGraphImpl', () => {
const createEdge = (
source: TestEventType,
target: TestEventType,
strength: number = 0.5
): CausalEdge => ({
source,
target,
strength,
relation: 'causes',
observations: 10,
lastObserved: Date.now(),
});
describe('construction', () => {
it('should create an empty graph', () => {
const graph = new CausalGraphImpl([], []);
expect(graph.nodes).toHaveLength(0);
expect(graph.edges).toHaveLength(0);
});
it('should create a graph with nodes and edges', () => {
const nodes: TestEventType[] = ['code_changed', 'build_started', 'test_failed'];
const edges: CausalEdge[] = [
createEdge('code_changed', 'build_started'),
createEdge('build_started', 'test_failed'),
];
const graph = new CausalGraphImpl(nodes, edges);
expect(graph.nodes).toHaveLength(3);
expect(graph.edges).toHaveLength(2);
});
});
describe('edgesFrom', () => {
it('should return all edges originating from a node', () => {
const nodes: TestEventType[] = ['code_changed', 'build_started', 'test_started', 'test_failed'];
const edges: CausalEdge[] = [
createEdge('code_changed', 'build_started'),
createEdge('code_changed', 'test_started'),
createEdge('build_started', 'test_failed'),
];
const graph = new CausalGraphImpl(nodes, edges);
const fromCodeChanged = graph.edgesFrom('code_changed');
expect(fromCodeChanged).toHaveLength(2);
expect(fromCodeChanged.map(e => e.target)).toContain('build_started');
expect(fromCodeChanged.map(e => e.target)).toContain('test_started');
});
it('should return empty array for node with no outgoing edges', () => {
const nodes: TestEventType[] = ['code_changed', 'test_failed'];
const edges: CausalEdge[] = [createEdge('code_changed', 'test_failed')];
const graph = new CausalGraphImpl(nodes, edges);
const fromTestFailed = graph.edgesFrom('test_failed');
expect(fromTestFailed).toHaveLength(0);
});
});
describe('edgesTo', () => {
it('should return all edges pointing to a node', () => {
const nodes: TestEventType[] = ['code_changed', 'config_changed', 'test_failed'];
const edges: CausalEdge[] = [
createEdge('code_changed', 'test_failed', 0.7),
createEdge('config_changed', 'test_failed', 0.5),
];
const graph = new CausalGraphImpl(nodes, edges);
const toTestFailed = graph.edgesTo('test_failed');
expect(toTestFailed).toHaveLength(2);
expect(toTestFailed.map(e => e.source)).toContain('code_changed');
expect(toTestFailed.map(e => e.source)).toContain('config_changed');
});
});
describe('reachableFrom', () => {
it('should find all reachable nodes via BFS', () => {
const nodes: TestEventType[] = [
'code_changed',
'build_started',
'test_started',
'test_failed',
'alert_fired',
];
const edges: CausalEdge[] = [
createEdge('code_changed', 'build_started'),
createEdge('build_started', 'test_started'),
createEdge('test_started', 'test_failed'),
createEdge('test_failed', 'alert_fired'),
];
const graph = new CausalGraphImpl(nodes, edges);
const reachable = graph.reachableFrom('code_changed');
expect(reachable.size).toBe(5);
expect(reachable.has('code_changed')).toBe(true);
expect(reachable.has('build_started')).toBe(true);
expect(reachable.has('test_failed')).toBe(true);
expect(reachable.has('alert_fired')).toBe(true);
});
it('should handle isolated nodes', () => {
const nodes: TestEventType[] = ['code_changed', 'test_failed'];
const edges: CausalEdge[] = []; // No edges
const graph = new CausalGraphImpl(nodes, edges);
const reachable = graph.reachableFrom('code_changed');
expect(reachable.size).toBe(1);
expect(reachable.has('code_changed')).toBe(true);
});
});
describe('reachableTo', () => {
it('should find all nodes that can reach a target', () => {
const nodes: TestEventType[] = [
'code_changed',
'config_changed',
'build_started',
'test_failed',
];
const edges: CausalEdge[] = [
createEdge('code_changed', 'build_started'),
createEdge('config_changed', 'build_started'),
createEdge('build_started', 'test_failed'),
];
const graph = new CausalGraphImpl(nodes, edges);
const canReach = (graph as CausalGraphImpl).reachableTo('test_failed');
expect(canReach.size).toBe(4);
expect(canReach.has('code_changed')).toBe(true);
expect(canReach.has('config_changed')).toBe(true);
});
});
describe('transitiveClosure', () => {
it('should compute transitive closure', () => {
const nodes: TestEventType[] = ['code_changed', 'build_started', 'test_failed'];
const edges: CausalEdge[] = [
createEdge('code_changed', 'build_started', 0.8),
createEdge('build_started', 'test_failed', 0.6),
];
const graph = new CausalGraphImpl(nodes, edges);
const closed = graph.transitiveClosure();
// Should now have direct edge from code_changed to test_failed
const transitiveEdge = closed.edges.find(
e => e.source === 'code_changed' && e.target === 'test_failed'
);
expect(transitiveEdge).toBeDefined();
// Transitive strength should be min of path
expect(transitiveEdge!.strength).toBe(0.6);
});
it('should handle empty graph', () => {
const graph = new CausalGraphImpl([], []);
const closed = graph.transitiveClosure();
expect(closed.nodes).toHaveLength(0);
expect(closed.edges).toHaveLength(0);
});
it('should preserve direct edges', () => {
const nodes: TestEventType[] = ['code_changed', 'test_failed'];
const edges: CausalEdge[] = [createEdge('code_changed', 'test_failed', 0.9)];
const graph = new CausalGraphImpl(nodes, edges);
const closed = graph.transitiveClosure();
expect(closed.edges).toHaveLength(1);
expect(closed.edges[0].strength).toBe(0.9);
});
});
describe('findPaths', () => {
it('should find all paths between two nodes', () => {
const nodes: TestEventType[] = [
'code_changed',
'build_started',
'test_started',
'test_failed',
];
const edges: CausalEdge[] = [
createEdge('code_changed', 'build_started'),
createEdge('build_started', 'test_failed'),
createEdge('code_changed', 'test_started'),
createEdge('test_started', 'test_failed'),
];
const graph = new CausalGraphImpl(nodes, edges);
const paths = graph.findPaths('code_changed', 'test_failed');
expect(paths.length).toBe(2);
// Both paths should start at code_changed and end at test_failed
for (const path of paths) {
expect(path[0]).toBe('code_changed');
expect(path[path.length - 1]).toBe('test_failed');
}
});
it('should return empty for unreachable targets', () => {
const nodes: TestEventType[] = ['code_changed', 'test_failed'];
const edges: CausalEdge[] = []; // No connection
const graph = new CausalGraphImpl(nodes, edges);
const paths = graph.findPaths('code_changed', 'test_failed');
expect(paths).toHaveLength(0);
});
it('should handle cycles without infinite loops', () => {
const nodes: TestEventType[] = ['test_started', 'test_failed', 'alert_fired'];
const edges: CausalEdge[] = [
createEdge('test_started', 'test_failed'),
createEdge('test_failed', 'alert_fired'),
createEdge('alert_fired', 'test_started'), // Cycle back
];
const graph = new CausalGraphImpl(nodes, edges);
const paths = graph.findPaths('test_started', 'alert_fired');
// Should find path without getting stuck in cycle
expect(paths.length).toBeGreaterThan(0);
expect(paths[0]).toContain('test_started');
expect(paths[0]).toContain('alert_fired');
});
});
describe('getPathStrength', () => {
it('should calculate path strength as product of edges', () => {
const nodes: TestEventType[] = ['code_changed', 'build_started', 'test_failed'];
const edges: CausalEdge[] = [
createEdge('code_changed', 'build_started', 0.8),
createEdge('build_started', 'test_failed', 0.5),
];
const graph = new CausalGraphImpl(nodes, edges);
const strength = graph.getPathStrength(['code_changed', 'build_started', 'test_failed']);
expect(strength).toBeCloseTo(0.4, 5); // 0.8 * 0.5
});
it('should return 0 for broken paths', () => {
const nodes: TestEventType[] = ['code_changed', 'test_failed'];
const edges: CausalEdge[] = []; // No edges
const graph = new CausalGraphImpl(nodes, edges);
const strength = graph.getPathStrength(['code_changed', 'test_failed']);
expect(strength).toBe(0);
});
});
describe('stronglyConnectedComponents', () => {
it('should find SCCs using Tarjan algorithm', () => {
const nodes: TestEventType[] = [
'test_started',
'test_failed',
'alert_fired',
'code_changed',
];
const edges: CausalEdge[] = [
// Cycle: test_started -> test_failed -> alert_fired -> test_started
createEdge('test_started', 'test_failed'),
createEdge('test_failed', 'alert_fired'),
createEdge('alert_fired', 'test_started'),
// Isolated node
createEdge('code_changed', 'test_started'),
];
const graph = new CausalGraphImpl(nodes, edges);
const sccs = graph.stronglyConnectedComponents();
// Should have 2 SCCs: the cycle (3 nodes) and the isolated node
expect(sccs).toHaveLength(2);
// One SCC should have 3 nodes (the cycle)
const largeScc = sccs.find(scc => scc.length === 3);
expect(largeScc).toBeDefined();
expect(largeScc).toContain('test_started');
expect(largeScc).toContain('test_failed');
expect(largeScc).toContain('alert_fired');
});
it('should handle DAG (no cycles)', () => {
const nodes: TestEventType[] = ['code_changed', 'build_started', 'test_failed'];
const edges: CausalEdge[] = [
createEdge('code_changed', 'build_started'),
createEdge('build_started', 'test_failed'),
];
const graph = new CausalGraphImpl(nodes, edges);
const sccs = graph.stronglyConnectedComponents();
// Each node is its own SCC
expect(sccs).toHaveLength(3);
sccs.forEach(scc => expect(scc).toHaveLength(1));
});
});
describe('hasCycles', () => {
it('should detect cycles', () => {
const nodes: TestEventType[] = ['test_started', 'test_failed', 'alert_fired'];
const edges: CausalEdge[] = [
createEdge('test_started', 'test_failed'),
createEdge('test_failed', 'alert_fired'),
createEdge('alert_fired', 'test_started'),
];
const graph = new CausalGraphImpl(nodes, edges);
expect(graph.hasCycles()).toBe(true);
});
it('should detect self-loops', () => {
const nodes: TestEventType[] = ['test_flaky'];
const edges: CausalEdge[] = [createEdge('test_flaky', 'test_flaky')];
const graph = new CausalGraphImpl(nodes, edges);
expect(graph.hasCycles()).toBe(true);
});
it('should return false for acyclic graphs', () => {
const nodes: TestEventType[] = ['code_changed', 'test_failed'];
const edges: CausalEdge[] = [createEdge('code_changed', 'test_failed')];
const graph = new CausalGraphImpl(nodes, edges);
expect(graph.hasCycles()).toBe(false);
});
});
describe('getFeedbackLoops', () => {
it('should return all feedback loops', () => {
const nodes: TestEventType[] = [
'test_started',
'test_failed',
'test_flaky',
'code_changed',
];
const edges: CausalEdge[] = [
createEdge('test_started', 'test_failed'),
createEdge('test_failed', 'test_started'), // Loop 1
createEdge('test_flaky', 'test_flaky'), // Self-loop
createEdge('code_changed', 'test_started'),
];
const graph = new CausalGraphImpl(nodes, edges);
const loops = graph.getFeedbackLoops();
expect(loops.length).toBe(2);
});
});
describe('degree analysis', () => {
it('should find high out-degree nodes', () => {
const nodes: TestEventType[] = [
'code_changed',
'build_started',
'test_started',
'test_failed',
'alert_fired',
];
const edges: CausalEdge[] = [
createEdge('code_changed', 'build_started'),
createEdge('code_changed', 'test_started'),
createEdge('code_changed', 'test_failed'),
createEdge('build_started', 'test_started'),
];
const graph = new CausalGraphImpl(nodes, edges);
const highOutDegree = graph.getHighOutDegreeNodes(3);
expect(highOutDegree[0].node).toBe('code_changed');
expect(highOutDegree[0].outDegree).toBe(3);
});
it('should find high in-degree nodes', () => {
const nodes: TestEventType[] = [
'code_changed',
'config_changed',
'timeout',
'test_failed',
];
const edges: CausalEdge[] = [
createEdge('code_changed', 'test_failed'),
createEdge('config_changed', 'test_failed'),
createEdge('timeout', 'test_failed'),
];
const graph = new CausalGraphImpl(nodes, edges);
const highInDegree = graph.getHighInDegreeNodes(3);
expect(highInDegree[0].node).toBe('test_failed');
expect(highInDegree[0].inDegree).toBe(3);
});
});
describe('intervention points', () => {
it('should find optimal intervention points', () => {
const nodes: TestEventType[] = [
'code_changed',
'build_started',
'test_started',
'test_failed',
];
const edges: CausalEdge[] = [
createEdge('code_changed', 'build_started'),
createEdge('code_changed', 'test_started'),
createEdge('build_started', 'test_failed'),
createEdge('test_started', 'test_failed'),
];
const graph = new CausalGraphImpl(nodes, edges);
const interventions = graph.findInterventionPoints('test_failed', 3);
// code_changed is a bottleneck - intervening there prevents test_failed
expect(interventions.length).toBeGreaterThan(0);
});
});
describe('subgraph extraction', () => {
it('should extract subgraph to target', () => {
const nodes: TestEventType[] = [
'code_changed',
'config_changed',
'build_started',
'test_failed',
'alert_fired',
];
const edges: CausalEdge[] = [
createEdge('code_changed', 'build_started'),
createEdge('build_started', 'test_failed'),
createEdge('config_changed', 'test_failed'),
createEdge('test_failed', 'alert_fired'),
];
const graph = new CausalGraphImpl(nodes, edges);
const subgraph = graph.getSubgraphTo('test_failed');
// Should include nodes that can reach test_failed
expect(subgraph.nodes).toContain('code_changed');
expect(subgraph.nodes).toContain('build_started');
expect(subgraph.nodes).toContain('config_changed');
expect(subgraph.nodes).toContain('test_failed');
// alert_fired cannot reach test_failed (it's downstream)
expect(subgraph.nodes).not.toContain('alert_fired');
});
it('should extract subgraph from source', () => {
const nodes: TestEventType[] = [
'pr_opened',
'code_changed',
'build_started',
'test_failed',
];
const edges: CausalEdge[] = [
createEdge('pr_opened', 'code_changed'),
createEdge('code_changed', 'build_started'),
createEdge('build_started', 'test_failed'),
];
const graph = new CausalGraphImpl(nodes, edges);
const subgraph = graph.getSubgraphFrom('code_changed');
expect(subgraph.nodes).toContain('code_changed');
expect(subgraph.nodes).toContain('build_started');
expect(subgraph.nodes).toContain('test_failed');
expect(subgraph.nodes).not.toContain('pr_opened'); // Upstream, not reachable from
});
});
describe('statistics', () => {
it('should compute graph statistics', () => {
const nodes: TestEventType[] = ['code_changed', 'build_started', 'test_failed'];
const edges: CausalEdge[] = [
createEdge('code_changed', 'build_started'),
createEdge('build_started', 'test_failed'),
];
const graph = new CausalGraphImpl(nodes, edges);
const stats = graph.getStats();
expect(stats.nodes).toBe(3);
expect(stats.edges).toBe(2);
expect(stats.density).toBeCloseTo(2 / 6, 2); // 2 edges out of 6 possible
expect(stats.hasCycles).toBe(false);
});
});
});