Files
proffesor-for-testing__agen…/tests/unit/domains/coverage-analysis/coverage-embedder.test.ts
Dragan Spiridonov 00e4d2cfa6 fix(tests): add afterEach cleanup to 104 test files missing it (#374 P2)
125 test files had beforeEach/beforeAll but no afterEach/afterAll,
leaking state between tests and causing order-dependent failures.

Added cleanup to 104 files (skipped 4 archived, 16 import-only):
- 2 critical DB handle leaks (witness-chain): added db.close()
- 7 high-risk service files: added vi.restoreAllMocks()
- 7 beforeAll files: added matching afterAll
- 88 medium/low risk: added afterEach with appropriate cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 12:51:47 +00:00

303 lines
11 KiB
TypeScript

/**
* Agentic QE v3 - Coverage Embedder Tests
*
* Tests for the coverage embedder that converts coverage data into
* dense vector embeddings for HNSW-based similarity search.
*/
import { describe, it, expect, beforeEach, afterEach} from 'vitest';
import {
CoverageEmbedder,
createCoverageEmbedder,
DEFAULT_EMBEDDER_CONFIG,
type CoverageQuery,
} from '../../../../src/domains/coverage-analysis/services/coverage-embedder';
import type { FileCoverage, CoverageGap } from '../../../../src/domains/coverage-analysis/interfaces';
describe('CoverageEmbedder', () => {
let embedder: CoverageEmbedder;
beforeEach(() => {
embedder = createCoverageEmbedder();
});
afterEach(() => {
// Reset state to prevent leaks between tests
});
describe('embedFileCoverage', () => {
it('should create embedding with correct dimensions', () => {
const coverage = createTestFileCoverage('src/test.ts', 80, 70, 90, 85);
const result = embedder.embedFileCoverage(coverage);
expect(result.vector).toHaveLength(DEFAULT_EMBEDDER_CONFIG.dimensions);
});
it('should create normalized embedding', () => {
const coverage = createTestFileCoverage('src/test.ts', 80, 70, 90, 85);
const result = embedder.embedFileCoverage(coverage);
// L2 normalized vector should have magnitude close to 1
const magnitude = Math.sqrt(
result.vector.reduce((sum, v) => sum + v * v, 0)
);
expect(magnitude).toBeCloseTo(1, 1);
});
it('should include accurate metadata', () => {
const coverage = createTestFileCoverage('src/test.ts', 80, 70, 90, 85);
coverage.uncoveredLines = [10, 11, 12, 13, 14];
coverage.uncoveredBranches = [20, 21];
const result = embedder.embedFileCoverage(coverage);
expect(result.metadata.filePath).toBe('src/test.ts');
expect(result.metadata.lineCoverage).toBeCloseTo(80, 1);
expect(result.metadata.branchCoverage).toBeCloseTo(70, 1);
expect(result.metadata.functionCoverage).toBeCloseTo(90, 1);
expect(result.metadata.statementCoverage).toBeCloseTo(85, 1);
expect(result.metadata.uncoveredLineCount).toBe(5);
expect(result.metadata.uncoveredBranchCount).toBe(2);
});
it('should calculate risk score based on coverage gaps', () => {
// Low coverage = high risk
const lowCoverage = createTestFileCoverage('src/low.ts', 30, 20, 40, 35);
const lowResult = embedder.embedFileCoverage(lowCoverage);
// High coverage = low risk
const highCoverage = createTestFileCoverage('src/high.ts', 95, 90, 98, 96);
const highResult = embedder.embedFileCoverage(highCoverage);
expect(lowResult.metadata.riskScore).toBeGreaterThan(highResult.metadata.riskScore);
});
it('should have higher confidence for files with more data', () => {
// File with all coverage types
const fullCoverage = createTestFileCoverage('src/full.ts', 80, 70, 90, 85);
fullCoverage.lines.total = 100;
fullCoverage.branches.total = 50;
fullCoverage.functions.total = 20;
fullCoverage.statements.total = 120;
const fullResult = embedder.embedFileCoverage(fullCoverage);
// File with minimal data
const minCoverage = createTestFileCoverage('src/min.ts', 80, 70, 90, 85);
minCoverage.lines.total = 0;
minCoverage.branches.total = 0;
minCoverage.functions.total = 0;
minCoverage.statements.total = 0;
const minResult = embedder.embedFileCoverage(minCoverage);
expect(fullResult.confidence).toBeGreaterThan(minResult.confidence);
});
it('should create similar embeddings for similar coverage', () => {
const coverage1 = createTestFileCoverage('src/a.ts', 75, 65, 80, 70);
const coverage2 = createTestFileCoverage('src/b.ts', 76, 66, 81, 71);
const coverage3 = createTestFileCoverage('src/c.ts', 20, 15, 25, 18);
const result1 = embedder.embedFileCoverage(coverage1);
const result2 = embedder.embedFileCoverage(coverage2);
const result3 = embedder.embedFileCoverage(coverage3);
// Coverage1 and coverage2 should be more similar than coverage1 and coverage3
const similarity12 = cosineSimilarity(result1.vector, result2.vector);
const similarity13 = cosineSimilarity(result1.vector, result3.vector);
expect(similarity12).toBeGreaterThan(similarity13);
});
});
describe('embedCoverageGap', () => {
it('should create embedding from coverage gap', () => {
const gap: CoverageGap = {
id: 'gap-123',
file: 'src/test.ts',
lines: [10, 11, 12, 13, 14],
branches: [20, 21],
riskScore: 0.7,
severity: 'high',
recommendation: 'Add tests',
};
const result = embedder.embedCoverageGap(gap);
expect(result.vector).toHaveLength(DEFAULT_EMBEDDER_CONFIG.dimensions);
expect(result.metadata.filePath).toBe('src/test.ts');
expect(result.metadata.uncoveredLineCount).toBe(5);
expect(result.metadata.uncoveredBranchCount).toBe(2);
expect(result.metadata.riskScore).toBe(0.7);
});
it('should encode gap severity in embedding', () => {
const criticalGap: CoverageGap = {
id: 'gap-critical',
file: 'src/critical.ts',
lines: Array.from({ length: 50 }, (_, i) => i + 1),
branches: Array.from({ length: 10 }, (_, i) => i + 1),
riskScore: 0.9,
severity: 'critical',
recommendation: 'Critical',
};
const lowGap: CoverageGap = {
id: 'gap-low',
file: 'src/low.ts',
lines: [1, 2],
branches: [],
riskScore: 0.2,
severity: 'low',
recommendation: 'Low priority',
};
const criticalResult = embedder.embedCoverageGap(criticalGap);
const lowResult = embedder.embedCoverageGap(lowGap);
// Embeddings should encode risk score differently
// The risk score is captured in the embedding
expect(criticalResult.metadata.riskScore).toBeGreaterThan(lowResult.metadata.riskScore);
// The embeddings may be similar due to normalization, but risk scores differ
expect(criticalResult.vector[0]).not.toBe(lowResult.vector[0]);
});
});
describe('embedQuery', () => {
it('should create query embedding from coverage criteria', () => {
const query: CoverageQuery = {
minLineCoverage: 60,
maxLineCoverage: 80,
minRiskScore: 0.5,
};
const result = embedder.embedQuery(query);
expect(result.vector).toHaveLength(DEFAULT_EMBEDDER_CONFIG.dimensions);
expect(result.confidence).toBeGreaterThan(0);
});
it('should create different embeddings for different queries', () => {
const lowCoverageQuery: CoverageQuery = {
maxLineCoverage: 40,
minRiskScore: 0.7,
};
const highCoverageQuery: CoverageQuery = {
minLineCoverage: 80,
maxRiskScore: 0.3,
};
const lowResult = embedder.embedQuery(lowCoverageQuery);
const highResult = embedder.embedQuery(highCoverageQuery);
// Queries should have different metadata
expect(lowResult.metadata.lineCoverage).not.toBe(highResult.metadata.lineCoverage);
// The first few dimensions should differ based on query parameters
expect(lowResult.vector[0]).not.toBe(highResult.vector[0]);
expect(lowResult.vector[4]).not.toBe(highResult.vector[4]);
});
});
describe('batchEmbed', () => {
it('should embed multiple files efficiently', () => {
const coverages = [
createTestFileCoverage('src/a.ts', 80, 70, 90, 85),
createTestFileCoverage('src/b.ts', 60, 50, 70, 65),
createTestFileCoverage('src/c.ts', 40, 30, 50, 45),
];
const results = embedder.batchEmbed(coverages);
expect(results).toHaveLength(3);
results.forEach((result) => {
expect(result.vector).toHaveLength(DEFAULT_EMBEDDER_CONFIG.dimensions);
expect(result.metadata).toBeDefined();
expect(result.confidence).toBeGreaterThan(0);
});
});
});
describe('DEFAULT_EMBEDDER_CONFIG', () => {
it('should have sensible default values', () => {
expect(DEFAULT_EMBEDDER_CONFIG.dimensions).toBe(384);
expect(DEFAULT_EMBEDDER_CONFIG.includePathFeatures).toBe(true);
expect(DEFAULT_EMBEDDER_CONFIG.includeTemporalFeatures).toBe(true);
expect(DEFAULT_EMBEDDER_CONFIG.normalization).toBe('l2');
});
});
describe('custom configuration', () => {
it('should respect custom dimensions', () => {
const customEmbedder = createCoverageEmbedder({ dimensions: 64 });
const coverage = createTestFileCoverage('src/test.ts', 80, 70, 90, 85);
const result = customEmbedder.embedFileCoverage(coverage);
expect(result.vector).toHaveLength(64);
});
it('should respect custom normalization', () => {
const minmaxEmbedder = createCoverageEmbedder({ normalization: 'minmax' });
const coverage = createTestFileCoverage('src/test.ts', 80, 70, 90, 85);
const result = minmaxEmbedder.embedFileCoverage(coverage);
// MinMax normalized values should be between 0 and 1
result.vector.forEach((v) => {
expect(v).toBeGreaterThanOrEqual(0);
expect(v).toBeLessThanOrEqual(1);
});
});
});
});
// ============================================================================
// Test Helpers
// ============================================================================
function createTestFileCoverage(
path: string,
lineCoverage: number,
branchCoverage: number,
functionCoverage: number,
statementCoverage: number
): FileCoverage {
const total = 100;
const linesCovered = Math.floor((lineCoverage / 100) * total);
const branchesCovered = Math.floor((branchCoverage / 100) * 50);
const functionsCovered = Math.floor((functionCoverage / 100) * 20);
const statementsCovered = Math.floor((statementCoverage / 100) * 120);
return {
path,
lines: { covered: linesCovered, total },
branches: { covered: branchesCovered, total: 50 },
functions: { covered: functionsCovered, total: 20 },
statements: { covered: statementsCovered, total: 120 },
uncoveredLines: Array.from({ length: total - linesCovered }, (_, i) => i + linesCovered + 1),
uncoveredBranches: Array.from(
{ length: 50 - branchesCovered },
(_, i) => i + branchesCovered + 1
),
};
}
function cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) throw new Error('Vector length mismatch');
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
return denominator === 0 ? 0 : dotProduct / denominator;
}