mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
00e4d2cfa6
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>
303 lines
11 KiB
TypeScript
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;
|
|
}
|