Files
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

293 lines
11 KiB
TypeScript

/**
* Unit Tests for CoverageLearner
* ADR-023: Quality Feedback Loop System
*/
import { describe, it, expect, beforeEach, afterEach} from 'vitest';
import {
CoverageLearner,
createCoverageLearner,
} from '../../../src/feedback/coverage-learner.js';
import type { CoverageSession } from '../../../src/feedback/types.js';
describe('CoverageLearner', () => {
let learner: CoverageLearner;
beforeEach(() => {
learner = createCoverageLearner();
});
afterEach(() => {
// Reset state to prevent leaks between tests
});
function createSession(overrides: Partial<CoverageSession> = {}): CoverageSession {
return {
id: `session-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
agentId: 'test-agent',
targetPath: 'src/services/user.ts',
technique: 'gap-analysis',
beforeCoverage: { lines: 60, branches: 50, functions: 55 },
afterCoverage: { lines: 75, branches: 65, functions: 70 },
testsGenerated: 5,
testsPassed: 4,
gapsTargeted: [
{ id: 'gap-1', type: 'uncovered-branch', filePath: 'src/services/user.ts', startLine: 42, riskScore: 0.8, addressed: true },
{ id: 'gap-2', type: 'uncovered-function', filePath: 'src/services/user.ts', startLine: 100, riskScore: 0.5, addressed: true },
{ id: 'gap-3', type: 'uncovered-line', filePath: 'src/services/user.ts', startLine: 150, riskScore: 0.3, addressed: false },
],
durationMs: 60000,
startedAt: new Date(Date.now() - 60000),
completedAt: new Date(),
...overrides,
};
}
describe('learnFromSession', () => {
it('should learn from successful coverage session', async () => {
const session = createSession({
beforeCoverage: { lines: 50, branches: 40, functions: 45 },
afterCoverage: { lines: 75, branches: 65, functions: 70 }, // 25% improvement
});
const strategy = await learner.learnFromSession(session);
expect(strategy).not.toBeNull();
expect(strategy!.technique).toBe('gap-analysis');
expect(strategy!.avgImprovement).toBeGreaterThan(0);
expect(strategy!.successCount).toBe(1);
});
it('should not learn from low improvement session', async () => {
const session = createSession({
beforeCoverage: { lines: 70, branches: 65, functions: 72 },
afterCoverage: { lines: 71, branches: 66, functions: 73 }, // Only ~1% improvement
});
const strategy = await learner.learnFromSession(session);
expect(strategy).toBeNull();
});
it('should update existing strategy on repeated success', async () => {
const session1 = createSession({
beforeCoverage: { lines: 50, branches: 40, functions: 45 },
afterCoverage: { lines: 70, branches: 60, functions: 65 },
});
const session2 = createSession({
id: 'session-2',
beforeCoverage: { lines: 55, branches: 45, functions: 50 },
afterCoverage: { lines: 80, branches: 70, functions: 75 },
});
await learner.learnFromSession(session1);
const strategy = await learner.learnFromSession(session2);
expect(strategy).not.toBeNull();
expect(strategy!.successCount).toBe(2);
expect(strategy!.confidence).toBeGreaterThan(0.5);
});
});
describe('getRecommendedStrategy', () => {
it('should recommend best strategy for file pattern', async () => {
// Learn from multiple sessions
await learner.learnFromSession(createSession({
targetPath: 'src/services/auth.ts',
technique: 'gap-analysis',
beforeCoverage: { lines: 40, branches: 30, functions: 35 },
afterCoverage: { lines: 70, branches: 60, functions: 65 },
}));
await learner.learnFromSession(createSession({
id: 'session-branch',
targetPath: 'src/services/user.ts',
technique: 'branch-coverage',
beforeCoverage: { lines: 50, branches: 40, functions: 45 },
afterCoverage: { lines: 85, branches: 80, functions: 82 },
}));
const recommendation = learner.getRecommendedStrategy('src/services/order.ts');
expect(recommendation).not.toBeNull();
// Should recommend branch-coverage as it had better improvement
expect(recommendation!.technique).toBe('branch-coverage');
});
it('should return null when no strategies learned', () => {
const recommendation = learner.getRecommendedStrategy('src/unknown/file.ts');
expect(recommendation).toBeNull();
});
});
describe('getSessionStats', () => {
it('should return correct session statistics', async () => {
await learner.learnFromSession(createSession({
technique: 'gap-analysis',
beforeCoverage: { lines: 50, branches: 40, functions: 45 },
afterCoverage: { lines: 75, branches: 65, functions: 70 },
}));
await learner.learnFromSession(createSession({
id: 'session-2',
technique: 'branch-coverage',
beforeCoverage: { lines: 60, branches: 50, functions: 55 },
afterCoverage: { lines: 80, branches: 75, functions: 78 },
}));
await learner.learnFromSession(createSession({
id: 'session-3',
technique: 'gap-analysis',
beforeCoverage: { lines: 70, branches: 65, functions: 68 },
afterCoverage: { lines: 72, branches: 67, functions: 70 }, // Low improvement
}));
const stats = learner.getSessionStats();
expect(stats.totalSessions).toBe(3);
expect(stats.successfulSessions).toBe(2); // Only 2 above threshold
expect(stats.byTechnique.get('gap-analysis')!.count).toBe(2);
expect(stats.byTechnique.get('branch-coverage')!.count).toBe(1);
expect(stats.strategiesLearned).toBe(2);
});
it('should return zero stats for empty learner', () => {
const stats = learner.getSessionStats();
expect(stats.totalSessions).toBe(0);
expect(stats.successfulSessions).toBe(0);
expect(stats.avgImprovement).toBe(0);
});
});
describe('analyzeGapResolution', () => {
it('should analyze gap resolution effectiveness', async () => {
await learner.learnFromSession(createSession({
gapsTargeted: [
{ id: 'g1', type: 'uncovered-branch', filePath: 'file.ts', startLine: 10, riskScore: 0.8, addressed: true },
{ id: 'g2', type: 'uncovered-branch', filePath: 'file.ts', startLine: 20, riskScore: 0.7, addressed: true },
{ id: 'g3', type: 'uncovered-function', filePath: 'file.ts', startLine: 30, riskScore: 0.5, addressed: false },
],
beforeCoverage: { lines: 50, branches: 40, functions: 45 },
afterCoverage: { lines: 75, branches: 65, functions: 70 },
}));
await learner.learnFromSession(createSession({
id: 'session-2',
gapsTargeted: [
{ id: 'g4', type: 'uncovered-branch', filePath: 'file.ts', startLine: 40, riskScore: 0.6, addressed: true },
{ id: 'g5', type: 'uncovered-line', filePath: 'file.ts', startLine: 50, riskScore: 0.3, addressed: true },
],
beforeCoverage: { lines: 60, branches: 50, functions: 55 },
afterCoverage: { lines: 80, branches: 75, functions: 78 },
}));
const analysis = learner.analyzeGapResolution();
expect(analysis.totalGapsTargeted).toBe(5);
expect(analysis.gapsAddressed).toBe(4);
expect(analysis.resolutionRate).toBe(0.8);
expect(analysis.byGapType.get('uncovered-branch')!.targeted).toBe(3);
expect(analysis.byGapType.get('uncovered-branch')!.addressed).toBe(3);
});
it('should return zero analysis for empty learner', () => {
const analysis = learner.analyzeGapResolution();
expect(analysis.totalGapsTargeted).toBe(0);
expect(analysis.resolutionRate).toBe(0);
});
});
describe('getAllStrategies', () => {
it('should return all learned strategies', async () => {
await learner.learnFromSession(createSession({
technique: 'gap-analysis',
beforeCoverage: { lines: 50, branches: 40, functions: 45 },
afterCoverage: { lines: 75, branches: 65, functions: 70 },
}));
await learner.learnFromSession(createSession({
id: 'session-2',
technique: 'edge-case-generation',
targetPath: 'src/utils/parser.ts',
beforeCoverage: { lines: 60, branches: 50, functions: 55 },
afterCoverage: { lines: 85, branches: 80, functions: 82 },
}));
const strategies = learner.getAllStrategies();
expect(strategies.length).toBe(2);
expect(strategies.some(s => s.technique === 'gap-analysis')).toBe(true);
expect(strategies.some(s => s.technique === 'edge-case-generation')).toBe(true);
});
});
describe('export/import', () => {
it('should export and import sessions', async () => {
await learner.learnFromSession(createSession({
beforeCoverage: { lines: 50, branches: 40, functions: 45 },
afterCoverage: { lines: 75, branches: 65, functions: 70 },
}));
const exportedSessions = learner.exportSessions();
const exportedStrategies = learner.exportStrategies();
// Create new learner and import
const newLearner = createCoverageLearner();
newLearner.importSessions(exportedSessions);
newLearner.importStrategies(exportedStrategies);
const stats = newLearner.getSessionStats();
expect(stats.totalSessions).toBe(1);
expect(stats.strategiesLearned).toBe(1);
});
});
describe('clear', () => {
it('should clear all data', async () => {
await learner.learnFromSession(createSession({
beforeCoverage: { lines: 50, branches: 40, functions: 45 },
afterCoverage: { lines: 75, branches: 65, functions: 70 },
}));
learner.clear();
const stats = learner.getSessionStats();
expect(stats.totalSessions).toBe(0);
expect(stats.strategiesLearned).toBe(0);
});
});
describe('technique strategies', () => {
it('should generate descriptive strategy for each technique', async () => {
const techniques = [
'gap-analysis',
'branch-coverage',
'edge-case-generation',
'mutation-guided',
'risk-based',
] as const;
for (let i = 0; i < techniques.length; i++) {
await learner.learnFromSession(createSession({
id: `tech-session-${i}`,
technique: techniques[i],
targetPath: `src/test-${i}.ts`,
beforeCoverage: { lines: 50, branches: 40, functions: 45 },
afterCoverage: { lines: 80, branches: 70, functions: 75 },
}));
}
const strategies = learner.getAllStrategies();
expect(strategies.length).toBe(5);
for (const strategy of strategies) {
expect(strategy.description).toBeTruthy();
expect(strategy.description.length).toBeGreaterThan(20);
}
});
});
});