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