Files
proffesor-for-testing__agen…/tests/unit/learning/causal-verifier.test.ts
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

354 lines
11 KiB
TypeScript

/**
* Unit Tests for CausalVerifier
* ADR-052 Phase 3 Action A3.3: Integrate CausalEngine with Causal Discovery
*/
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import type { IWasmLoader } from '../../../src/integrations/coherence/types.js';
import {
CausalVerifier,
createCausalVerifier,
createUninitializedCausalVerifier,
} from '../../../src/learning/causal-verifier.js';
// ============================================================================
// Mock WASM Loader
// ============================================================================
/**
* Create a mock WASM loader for testing
*/
function createMockWasmLoader(): IWasmLoader {
// Create a proper constructor function
const MockCausalEngine = function (this: any) {
// Raw WASM engine methods (called by wrapper)
this.computeCausalEffect = vi.fn().mockReturnValue({ effect: 0.75 });
this.findConfounders = vi.fn().mockReturnValue([]);
// Wrapper methods (not used by CausalAdapter)
this.set_data = vi.fn();
this.add_confounder = vi.fn();
this.compute_causal_effect = vi.fn().mockReturnValue(0.75);
this.detect_spurious_correlation = vi.fn().mockReturnValue(false);
this.get_confounders = vi.fn().mockReturnValue([]);
this.clear = vi.fn();
};
const mockModule = {
CausalEngine: MockCausalEngine as any,
};
return {
load: vi.fn().mockResolvedValue(mockModule),
isLoaded: vi.fn().mockReturnValue(true),
isAvailable: vi.fn().mockResolvedValue(true),
getVersion: vi.fn().mockReturnValue('1.0.0-test'),
getState: vi.fn().mockReturnValue('loaded' as const),
reset: vi.fn(),
};
}
// ============================================================================
// Tests
// ============================================================================
describe('CausalVerifier', () => {
let wasmLoader: IWasmLoader;
beforeEach(() => {
wasmLoader = createMockWasmLoader();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('Initialization', () => {
it('should create and initialize a verifier', async () => {
const verifier = await createCausalVerifier(wasmLoader);
expect(verifier.isInitialized()).toBe(true);
expect(wasmLoader.load).toHaveBeenCalled();
});
it('should create uninitialized verifier', () => {
const verifier = createUninitializedCausalVerifier(wasmLoader);
expect(verifier.isInitialized()).toBe(false);
});
it('should initialize manually', async () => {
const verifier = createUninitializedCausalVerifier(wasmLoader);
expect(verifier.isInitialized()).toBe(false);
await verifier.initialize();
expect(verifier.isInitialized()).toBe(true);
expect(wasmLoader.load).toHaveBeenCalled();
});
it('should handle initialization errors gracefully', async () => {
const failingLoader: IWasmLoader = {
...wasmLoader,
isAvailable: vi.fn().mockResolvedValue(false),
};
const verifier = createUninitializedCausalVerifier(failingLoader);
await expect(verifier.initialize()).rejects.toThrow(
'WASM module is not available'
);
});
it('should skip re-initialization if already initialized', async () => {
const verifier = await createCausalVerifier(wasmLoader);
const loadCallCount = (wasmLoader.load as any).mock.calls.length;
// Try to initialize again
await verifier.initialize();
// Should not call load again
expect((wasmLoader.load as any).mock.calls.length).toBe(loadCallCount);
});
});
describe('Causal Link Verification', () => {
it('should verify a causal link', async () => {
const verifier = await createCausalVerifier(wasmLoader);
// Need at least 30 samples for verification
const sampleSize = 50;
const causeValues = Array.from({ length: sampleSize }, (_, i) => i * 2);
const effectValues = Array.from({ length: sampleSize }, (_, i) => i * 0.01);
const result = await verifier.verifyCausalLink(
'test_count',
'bug_detection_rate',
causeValues,
effectValues
);
expect(result).toBeDefined();
expect(result.cause).toBe('test_count');
expect(result.effect).toBe('bug_detection_rate');
expect(result.isSpurious).toBe(false);
expect(result.direction).toBe('forward');
expect(result.confidence).toBeGreaterThan(0);
expect(result.effectStrength).toBeGreaterThan(0);
expect(result.explanation).toBeDefined();
expect(result.durationMs).toBeGreaterThanOrEqual(0);
});
it('should reject small sample sizes', async () => {
const verifier = await createCausalVerifier(wasmLoader);
await expect(
verifier.verifyCausalLink(
'cause',
'effect',
[1, 2, 3],
[4, 5, 6],
{ minSampleSize: 30 }
)
).rejects.toThrow('Sample size 3 is below minimum 30');
});
it('should reject mismatched array lengths', async () => {
const verifier = await createCausalVerifier(wasmLoader);
const sampleSize = 50;
const causeValues = Array.from({ length: sampleSize }, (_, i) => i);
const effectValues = Array.from({ length: 10 }, (_, i) => i);
await expect(
verifier.verifyCausalLink(
'cause',
'effect',
causeValues,
effectValues
)
).rejects.toThrow('Cause and effect arrays must have same length');
});
it('should throw if not initialized', async () => {
const verifier = createUninitializedCausalVerifier(wasmLoader);
await expect(
verifier.verifyCausalLink(
'cause',
'effect',
Array(30).fill(1),
Array(30).fill(1)
)
).rejects.toThrow('CausalVerifier not initialized');
});
});
describe('Pattern Causality Verification', () => {
it('should verify pattern causality', async () => {
const verifier = await createCausalVerifier(wasmLoader);
const result = await verifier.verifyPatternCausality(
'pattern-tdd-unit-tests',
'success',
{
patternApplications: Array(50).fill(1),
outcomes: Array(50).fill(1),
}
);
expect(result).toBeDefined();
expect(result.cause).toBe('pattern:pattern-tdd-unit-tests');
expect(result.effect).toBe('outcome:success');
});
it('should include confounders in pattern verification', async () => {
const verifier = await createCausalVerifier(wasmLoader);
const result = await verifier.verifyPatternCausality(
'pattern-integration-tests',
'success',
{
patternApplications: Array(50).fill(1),
outcomes: Array(50).fill(1),
confounders: {
code_quality: Array(50).fill(0.8),
team_experience: Array(50).fill(0.9),
},
}
);
expect(result).toBeDefined();
});
});
describe('Causal Edge Verification', () => {
it('should verify a causal edge from STDP graph', async () => {
const verifier = await createCausalVerifier(wasmLoader);
const result = await verifier.verifyCausalEdge(
'test_failed',
'build_failed',
{
sourceOccurrences: Array(50)
.fill(0)
.map((_, i) => (i % 3 === 0 ? 1 : 0)),
targetOccurrences: Array(50)
.fill(0)
.map((_, i) => (i % 3 === 0 ? 1 : 0)),
}
);
expect(result).toBeDefined();
expect(result.cause).toBe('test_failed');
expect(result.effect).toBe('build_failed');
});
});
describe('Batch Verification', () => {
it('should verify multiple links in batch', async () => {
const verifier = await createCausalVerifier(wasmLoader);
const links = [
{
cause: 'test_count',
effect: 'coverage',
causeValues: Array(50).fill(1),
effectValues: Array(50).fill(1),
},
{
cause: 'coverage',
effect: 'bug_detection',
causeValues: Array(50).fill(1),
effectValues: Array(50).fill(1),
},
];
const results = await verifier.verifyBatch(links);
expect(results).toHaveLength(2);
expect(results[0].cause).toBe('test_count');
expect(results[1].cause).toBe('coverage');
});
});
describe('Resource Management', () => {
it('should clear engine state', async () => {
const verifier = await createCausalVerifier(wasmLoader);
verifier.clear();
// Should still be initialized but cleared
expect(verifier.isInitialized()).toBe(true);
});
it('should dispose of resources', async () => {
const verifier = await createCausalVerifier(wasmLoader);
verifier.dispose();
expect(verifier.isInitialized()).toBe(false);
// Should throw after disposal
await expect(
verifier.verifyCausalLink(
'cause',
'effect',
Array(30).fill(1),
Array(30).fill(1)
)
).rejects.toThrow('CausalVerifier not initialized');
});
});
describe('Spurious Correlation Detection', () => {
it('should detect spurious correlations', async () => {
// Create a mock loader with spurious correlation
const MockCausalEngineSpurious = function (this: any) {
// Raw WASM engine methods (called by wrapper)
this.computeCausalEffect = vi.fn().mockReturnValue({ effect: 0.05 }); // Weak effect
this.findConfounders = vi.fn().mockReturnValue(['summer_season']); // Has confounders!
// Wrapper methods (not used by CausalAdapter)
this.set_data = vi.fn();
this.add_confounder = vi.fn();
this.compute_causal_effect = vi.fn().mockReturnValue(0.05);
this.detect_spurious_correlation = vi.fn().mockReturnValue(true);
this.get_confounders = vi.fn().mockReturnValue(['summer_season']);
this.clear = vi.fn();
};
const mockModule = {
CausalEngine: MockCausalEngineSpurious as any,
};
const mockLoader: IWasmLoader = {
load: vi.fn().mockResolvedValue(mockModule),
isLoaded: vi.fn().mockReturnValue(true),
isAvailable: vi.fn().mockResolvedValue(true),
getVersion: vi.fn().mockReturnValue('1.0.0-test'),
getState: vi.fn().mockReturnValue('loaded' as const),
reset: vi.fn(),
};
const verifier = await createCausalVerifier(mockLoader);
const result = await verifier.verifyCausalLink(
'ice_cream_sales',
'drowning_deaths',
Array(50).fill(1),
Array(50).fill(1)
);
// When confounders are found, it's marked as confounded (not spurious)
// But it's still not a true causal relationship
expect(result.confounders).toEqual(['summer_season']);
expect(result.isSpurious).toBe(false); // Confounded, not spurious
// Direction should be bidirectional when effect strength is low with confounders
expect(['bidirectional', 'none']).toContain(result.direction);
});
});
});