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>
293 lines
11 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|
|
});
|