Files
proffesor-for-testing__agen…/tests/unit/feedback/coverage-learner.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

289 lines
11 KiB
TypeScript

/**
* Unit Tests for CoverageLearner
* ADR-023: Quality Feedback Loop System
*/
import { describe, it, expect, beforeEach } 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();
});
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);
}
});
});
});