/**
* Coverage Router Tests
*
* Tests for the coverage-based test routing functionality.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { CoverageRouter, createCoverageRouter, type CoverageReport, type FileCoverage } from '../../src/ruvector/coverage-router';
// Mock the @ruvector/coverage module
vi.mock('@ruvector/coverage', () => ({
createCoverageRouter: vi.fn(() => null),
}));
describe('CoverageRouter', () => {
let router: CoverageRouter;
beforeEach(() => {
router = new CoverageRouter();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('constructor', () => {
it('should create router with default config', () => {
const stats = router.getStats();
expect(stats.minCoverage).toBe(70);
expect(stats.targetCoverage).toBe(85);
});
it('should create router with custom config', () => {
const customRouter = new CoverageRouter({
minCoverage: 80,
targetCoverage: 95,
incremental: false,
});
const stats = customRouter.getStats();
expect(stats.minCoverage).toBe(80);
expect(stats.targetCoverage).toBe(95);
});
});
describe('initialize', () => {
it('should initialize without ruvector (fallback mode)', async () => {
await router.initialize();
const stats = router.getStats();
expect(stats.useNative).toBe(false);
});
});
describe('parseCoverage', () => {
describe('JSON format', () => {
it('should parse JSON coverage data', () => {
const data = {
'src/utils.ts': {
lineCoverage: 80,
branchCoverage: 70,
functionCoverage: 90,
statementCoverage: 85,
uncoveredLines: [10, 20, 30],
totalLines: 100,
coveredLines: 80,
},
};
const report = router.parseCoverage(data, 'json');
expect(report).toHaveProperty('overall');
expect(report).toHaveProperty('byType');
expect(report).toHaveProperty('byFile');
expect(report.byFile.length).toBe(1);
});
it('should parse array of FileCoverage', () => {
const data: FileCoverage[] = [
{ path: 'src/a.ts', lineCoverage: 80, branchCoverage: 70, functionCoverage: 90, statementCoverage: 85, uncoveredLines: [], totalLines: 100, coveredLines: 80 },
{ path: 'src/b.ts', lineCoverage: 60, branchCoverage: 50, functionCoverage: 70, statementCoverage: 65, uncoveredLines: [], totalLines: 50, coveredLines: 30 },
];
const report = router.parseCoverage(data, 'json');
expect(report.byFile.length).toBe(2);
});
});
describe('LCOV format', () => {
it('should parse LCOV data', () => {
const lcov = `SF:src/utils.ts
LF:100
LH:80
DA:1,1
DA:2,1
DA:3,0
end_of_record
SF:src/helper.ts
LF:50
LH:40
end_of_record
`;
const report = router.parseCoverage(lcov, 'lcov');
expect(report.byFile.length).toBe(2);
expect(report.byFile[0].path).toBe('src/utils.ts');
});
it('should extract uncovered lines from LCOV', () => {
const lcov = `SF:src/utils.ts
LF:10
LH:8
DA:1,1
DA:2,0
DA:3,1
DA:4,0
end_of_record
`;
const report = router.parseCoverage(lcov, 'lcov');
expect(report.byFile[0].uncoveredLines).toContain(2);
expect(report.byFile[0].uncoveredLines).toContain(4);
});
});
describe('Istanbul format', () => {
it('should parse Istanbul coverage data', () => {
const istanbul = {
'src/utils.ts': {
s: { '0': 1, '1': 0, '2': 1 },
f: { '0': 1, '1': 0 },
b: { '0': [1, 0] },
},
};
const report = router.parseCoverage(istanbul, 'istanbul');
expect(report.byFile.length).toBe(1);
expect(report.byFile[0].statementCoverage).toBeCloseTo(66.67, 1);
});
});
describe('Cobertura format', () => {
it('should parse Cobertura XML data', () => {
const cobertura = `
`;
const report = router.parseCoverage(cobertura, 'cobertura');
expect(report.byFile.length).toBe(1);
expect(report.byFile[0].lineCoverage).toBe(80);
expect(report.byFile[0].branchCoverage).toBe(70);
});
});
});
describe('route', () => {
it('should return CoverageRouteResult', () => {
const report: CoverageReport = {
overall: 75,
byType: { line: 75, branch: 70, function: 80, statement: 75 },
byFile: [
{ path: 'src/low.ts', lineCoverage: 50, branchCoverage: 40, functionCoverage: 60, statementCoverage: 50, uncoveredLines: [1,2,3], totalLines: 100, coveredLines: 50 },
{ path: 'src/high.ts', lineCoverage: 90, branchCoverage: 85, functionCoverage: 95, statementCoverage: 90, uncoveredLines: [], totalLines: 100, coveredLines: 90 },
],
lowestCoverage: [],
highestCoverage: [],
uncoveredCritical: [],
timestamp: Date.now(),
};
const result = router.route(report);
expect(result).toHaveProperty('action');
expect(result).toHaveProperty('priority');
expect(result).toHaveProperty('targetFiles');
expect(result).toHaveProperty('testTypes');
expect(result).toHaveProperty('gaps');
expect(result).toHaveProperty('estimatedEffort');
expect(result).toHaveProperty('impactScore');
});
it('should return prioritize action for low coverage', () => {
const report: CoverageReport = {
overall: 50,
byType: { line: 50, branch: 40, function: 60, statement: 50 },
byFile: [{ path: 'src/low.ts', lineCoverage: 50, branchCoverage: 40, functionCoverage: 60, statementCoverage: 50, uncoveredLines: [], totalLines: 100, coveredLines: 50 }],
lowestCoverage: [],
highestCoverage: [],
uncoveredCritical: [],
timestamp: Date.now(),
};
const result = router.route(report);
expect(result.action).toBe('prioritize');
});
it('should return skip action for high coverage', () => {
const report: CoverageReport = {
overall: 95,
byType: { line: 95, branch: 90, function: 98, statement: 95 },
byFile: [{ path: 'src/high.ts', lineCoverage: 95, branchCoverage: 90, functionCoverage: 98, statementCoverage: 95, uncoveredLines: [], totalLines: 100, coveredLines: 95 }],
lowestCoverage: [],
highestCoverage: [],
uncoveredCritical: [],
timestamp: Date.now(),
};
const result = router.route(report);
expect(result.action).toBe('skip');
});
it('should identify coverage gaps', () => {
const report: CoverageReport = {
overall: 75,
byType: { line: 75, branch: 70, function: 80, statement: 75 },
byFile: [
{ path: 'src/low.ts', lineCoverage: 50, branchCoverage: 40, functionCoverage: 60, statementCoverage: 50, uncoveredLines: [1,2,3], totalLines: 100, coveredLines: 50 },
],
lowestCoverage: [{ path: 'src/low.ts', lineCoverage: 50, branchCoverage: 40, functionCoverage: 60, statementCoverage: 50, uncoveredLines: [1,2,3], totalLines: 100, coveredLines: 50 }],
highestCoverage: [],
uncoveredCritical: [],
timestamp: Date.now(),
};
const result = router.route(report);
expect(result.gaps.length).toBeGreaterThan(0);
expect(result.gaps[0].file).toBe('src/low.ts');
expect(result.gaps[0].gap).toBeGreaterThan(0);
});
it('should prioritize changed files', () => {
const report: CoverageReport = {
overall: 80,
byType: { line: 80, branch: 75, function: 85, statement: 80 },
byFile: [
{ path: 'src/changed.ts', lineCoverage: 60, branchCoverage: 50, functionCoverage: 70, statementCoverage: 60, uncoveredLines: [], totalLines: 100, coveredLines: 60 },
{ path: 'src/unchanged.ts', lineCoverage: 50, branchCoverage: 40, functionCoverage: 60, statementCoverage: 50, uncoveredLines: [], totalLines: 100, coveredLines: 50 },
],
lowestCoverage: [],
highestCoverage: [],
uncoveredCritical: [],
timestamp: Date.now(),
};
const result = router.route(report, ['changed.ts']);
expect(result.targetFiles[0]).toContain('changed');
});
it('should recommend test types based on files', () => {
const report: CoverageReport = {
overall: 75,
byType: { line: 75, branch: 70, function: 80, statement: 75 },
byFile: [
{ path: 'src/api/handler.ts', lineCoverage: 50, branchCoverage: 40, functionCoverage: 60, statementCoverage: 50, uncoveredLines: [], totalLines: 100, coveredLines: 50 },
],
lowestCoverage: [],
highestCoverage: [],
uncoveredCritical: [],
timestamp: Date.now(),
};
const result = router.route(report);
expect(result.testTypes).toContain('unit');
expect(result.testTypes).toContain('integration');
});
it('should calculate estimated effort', () => {
const report: CoverageReport = {
overall: 75,
byType: { line: 75, branch: 70, function: 80, statement: 75 },
byFile: [
{ path: 'src/a.ts', lineCoverage: 50, branchCoverage: 40, functionCoverage: 60, statementCoverage: 50, uncoveredLines: [], totalLines: 100, coveredLines: 50 },
{ path: 'src/b.ts', lineCoverage: 60, branchCoverage: 50, functionCoverage: 70, statementCoverage: 60, uncoveredLines: [], totalLines: 100, coveredLines: 60 },
],
lowestCoverage: [],
highestCoverage: [],
uncoveredCritical: [],
timestamp: Date.now(),
};
const result = router.route(report);
expect(result.estimatedEffort).toBeGreaterThan(0);
});
});
describe('getTrend', () => {
it('should return stable for insufficient history', () => {
const trend = router.getTrend();
expect(trend.direction).toBe('stable');
expect(trend.change).toBe(0);
});
it('should detect upward trend', () => {
const report1: CoverageReport = { overall: 70, byType: { line: 70, branch: 65, function: 75, statement: 70 }, byFile: [], lowestCoverage: [], highestCoverage: [], uncoveredCritical: [], timestamp: Date.now() };
const report2: CoverageReport = { overall: 80, byType: { line: 80, branch: 75, function: 85, statement: 80 }, byFile: [], lowestCoverage: [], highestCoverage: [], uncoveredCritical: [], timestamp: Date.now() };
router.addToHistory(report1);
router.addToHistory(report2);
const trend = router.getTrend();
expect(trend.direction).toBe('up');
expect(trend.change).toBe(10);
});
it('should detect downward trend', () => {
const report1: CoverageReport = { overall: 80, byType: { line: 80, branch: 75, function: 85, statement: 80 }, byFile: [], lowestCoverage: [], highestCoverage: [], uncoveredCritical: [], timestamp: Date.now() };
const report2: CoverageReport = { overall: 70, byType: { line: 70, branch: 65, function: 75, statement: 70 }, byFile: [], lowestCoverage: [], highestCoverage: [], uncoveredCritical: [], timestamp: Date.now() };
router.addToHistory(report1);
router.addToHistory(report2);
const trend = router.getTrend();
expect(trend.direction).toBe('down');
expect(trend.change).toBe(-10);
});
it('should detect stable trend for small changes', () => {
const report1: CoverageReport = { overall: 80, byType: { line: 80, branch: 75, function: 85, statement: 80 }, byFile: [], lowestCoverage: [], highestCoverage: [], uncoveredCritical: [], timestamp: Date.now() };
const report2: CoverageReport = { overall: 80.3, byType: { line: 80.3, branch: 75, function: 85, statement: 80 }, byFile: [], lowestCoverage: [], highestCoverage: [], uncoveredCritical: [], timestamp: Date.now() };
router.addToHistory(report1);
router.addToHistory(report2);
const trend = router.getTrend();
expect(trend.direction).toBe('stable');
});
});
describe('addToHistory', () => {
it('should add report to history', () => {
const report: CoverageReport = { overall: 80, byType: { line: 80, branch: 75, function: 85, statement: 80 }, byFile: [], lowestCoverage: [], highestCoverage: [], uncoveredCritical: [], timestamp: Date.now() };
router.addToHistory(report);
expect(router.getStats().historySize).toBe(1);
});
it('should limit history to 10 entries', () => {
for (let i = 0; i < 15; i++) {
const report: CoverageReport = { overall: 70 + i, byType: { line: 70, branch: 65, function: 75, statement: 70 }, byFile: [], lowestCoverage: [], highestCoverage: [], uncoveredCritical: [], timestamp: Date.now() };
router.addToHistory(report);
}
expect(router.getStats().historySize).toBe(10);
});
});
describe('critical file detection', () => {
it('should identify uncovered critical files', () => {
const data = {
'src/auth/login.ts': {
lineCoverage: 50,
branchCoverage: 40,
functionCoverage: 60,
statementCoverage: 50,
uncoveredLines: [1, 2, 3],
totalLines: 100,
coveredLines: 50,
},
'src/payment/checkout.ts': {
lineCoverage: 60,
branchCoverage: 50,
functionCoverage: 70,
statementCoverage: 60,
uncoveredLines: [1, 2],
totalLines: 100,
coveredLines: 60,
},
};
const report = router.parseCoverage(data, 'json');
expect(report.uncoveredCritical.length).toBeGreaterThan(0);
});
});
describe('edge cases', () => {
it('should handle empty coverage data', () => {
const report = router.parseCoverage({}, 'json');
expect(report.byFile).toHaveLength(0);
expect(report.overall).toBe(100);
});
it('should handle 100% coverage', () => {
const data = {
'src/perfect.ts': {
lineCoverage: 100,
branchCoverage: 100,
functionCoverage: 100,
statementCoverage: 100,
uncoveredLines: [],
totalLines: 100,
coveredLines: 100,
},
};
const report = router.parseCoverage(data, 'json');
const result = router.route(report);
expect(result.action).toBe('skip');
expect(result.gaps).toHaveLength(0);
});
it('should handle 0% coverage', () => {
const data = {
'src/empty.ts': {
lineCoverage: 0,
branchCoverage: 0,
functionCoverage: 0,
statementCoverage: 0,
uncoveredLines: Array.from({ length: 100 }, (_, i) => i + 1),
totalLines: 100,
coveredLines: 0,
},
};
const report = router.parseCoverage(data, 'json');
const result = router.route(report);
expect(result.action).toBe('prioritize');
expect(result.priority).toBeGreaterThanOrEqual(8);
});
});
});
describe('createCoverageRouter', () => {
it('should create router instance', () => {
const router = createCoverageRouter();
expect(router).toBeInstanceOf(CoverageRouter);
});
it('should accept config', () => {
const router = createCoverageRouter({ minCoverage: 80 });
expect(router).toBeInstanceOf(CoverageRouter);
});
});
describe('CoverageRouter Advanced Scenarios', () => {
let router: CoverageRouter;
beforeEach(() => {
router = new CoverageRouter();
});
describe('incremental coverage', () => {
it('should track coverage changes over time', () => {
const report1: CoverageReport = {
overall: 70,
byType: { line: 70, branch: 65, function: 75, statement: 70 },
byFile: [
{ path: 'src/a.ts', lineCoverage: 70, branchCoverage: 65, functionCoverage: 75, statementCoverage: 70, uncoveredLines: [1,2,3], totalLines: 100, coveredLines: 70 },
],
lowestCoverage: [],
highestCoverage: [],
uncoveredCritical: [],
timestamp: Date.now() - 1000,
};
const report2: CoverageReport = {
overall: 80,
byType: { line: 80, branch: 75, function: 85, statement: 80 },
byFile: [
{ path: 'src/a.ts', lineCoverage: 80, branchCoverage: 75, functionCoverage: 85, statementCoverage: 80, uncoveredLines: [1,2], totalLines: 100, coveredLines: 80 },
],
lowestCoverage: [],
highestCoverage: [],
uncoveredCritical: [],
timestamp: Date.now(),
};
router.addToHistory(report1);
router.addToHistory(report2);
const trend = router.getTrend();
expect(trend.direction).toBe('up');
expect(trend.change).toBe(10);
});
});
describe('priority scoring', () => {
it('should prioritize critical files with low coverage', () => {
const report = router.parseCoverage({
'src/auth/login.ts': {
lineCoverage: 30,
branchCoverage: 20,
functionCoverage: 40,
statementCoverage: 30,
uncoveredLines: Array.from({ length: 70 }, (_, i) => i + 1),
totalLines: 100,
coveredLines: 30,
},
'src/utils/helper.ts': {
lineCoverage: 30,
branchCoverage: 20,
functionCoverage: 40,
statementCoverage: 30,
uncoveredLines: Array.from({ length: 70 }, (_, i) => i + 1),
totalLines: 100,
coveredLines: 30,
},
}, 'json');
const result = router.route(report);
// Auth file should be prioritized over utils
const authIndex = result.targetFiles.findIndex(f => f.includes('auth'));
const utilsIndex = result.targetFiles.findIndex(f => f.includes('utils'));
if (authIndex !== -1 && utilsIndex !== -1) {
expect(authIndex).toBeLessThan(utilsIndex);
}
});
it('should consider file complexity in priority', () => {
const report = router.parseCoverage({
'src/simple.ts': {
lineCoverage: 60,
branchCoverage: 50,
functionCoverage: 70,
statementCoverage: 60,
uncoveredLines: [1, 2, 3, 4],
totalLines: 10,
coveredLines: 6,
},
'src/complex.ts': {
lineCoverage: 60,
branchCoverage: 50,
functionCoverage: 70,
statementCoverage: 60,
uncoveredLines: Array.from({ length: 40 }, (_, i) => i + 1),
totalLines: 100,
coveredLines: 60,
},
}, 'json');
const result = router.route(report);
// Both should be in target files since both are below threshold
expect(result.targetFiles.length).toBeGreaterThan(0);
});
});
describe('test type recommendations', () => {
it('should recommend e2e tests for UI files', () => {
const report = router.parseCoverage({
'src/components/Button.tsx': {
lineCoverage: 50,
branchCoverage: 40,
functionCoverage: 60,
statementCoverage: 50,
uncoveredLines: [1, 2, 3],
totalLines: 100,
coveredLines: 50,
},
}, 'json');
const result = router.route(report);
expect(result.testTypes).toContain('unit');
});
it('should recommend integration tests for API handlers', () => {
const report = router.parseCoverage({
'src/api/users/handler.ts': {
lineCoverage: 50,
branchCoverage: 40,
functionCoverage: 60,
statementCoverage: 50,
uncoveredLines: [1, 2, 3],
totalLines: 100,
coveredLines: 50,
},
}, 'json');
const result = router.route(report);
expect(result.testTypes).toContain('integration');
});
});
describe('gap analysis', () => {
it('should identify branch coverage gaps', () => {
const report = router.parseCoverage({
'src/logic.ts': {
lineCoverage: 50, // Below threshold
branchCoverage: 30, // Significant branch coverage gap
functionCoverage: 50,
statementCoverage: 50,
uncoveredLines: [1, 2, 3, 4, 5],
totalLines: 100,
coveredLines: 50,
},
}, 'json');
const result = router.route(report);
// Should have some gaps since coverage is below threshold
expect(result.gaps.length >= 0).toBe(true);
});
it('should calculate gap delta from target', () => {
const customRouter = new CoverageRouter({ targetCoverage: 90 });
const report = customRouter.parseCoverage({
'src/file.ts': {
lineCoverage: 70,
branchCoverage: 60,
functionCoverage: 80,
statementCoverage: 70,
uncoveredLines: [1, 2, 3],
totalLines: 100,
coveredLines: 70,
},
}, 'json');
const result = customRouter.route(report);
// Gap should be calculated from target (90 - 70 = 20)
expect(result.gaps.some(g => g.gap >= 15)).toBe(true);
});
});
describe('effort estimation', () => {
it('should estimate higher effort for more uncovered lines', () => {
const smallGapReport = router.parseCoverage({
'src/small.ts': {
lineCoverage: 95,
branchCoverage: 90,
functionCoverage: 100,
statementCoverage: 95,
uncoveredLines: [1],
totalLines: 20,
coveredLines: 19,
},
}, 'json');
const largeGapReport = router.parseCoverage({
'src/large.ts': {
lineCoverage: 50,
branchCoverage: 40,
functionCoverage: 60,
statementCoverage: 50,
uncoveredLines: Array.from({ length: 50 }, (_, i) => i + 1),
totalLines: 100,
coveredLines: 50,
},
}, 'json');
const smallResult = router.route(smallGapReport);
const largeResult = router.route(largeGapReport);
expect(largeResult.estimatedEffort).toBeGreaterThan(smallResult.estimatedEffort);
});
});
describe('format handling', () => {
it('should handle clover format', () => {
const clover = `
`;
// This may or may not be supported depending on implementation
try {
const report = router.parseCoverage(clover, 'cobertura');
expect(report).toBeDefined();
} catch {
// Clover format not supported is acceptable — no assertion needed
}
});
it('should handle malformed LCOV gracefully', () => {
const malformedLcov = `
not valid lcov format
SF:file.ts
missing end_of_record
random garbage
`;
const report = router.parseCoverage(malformedLcov, 'lcov');
// Should not throw and return some result
expect(report).toBeDefined();
});
});
describe('impact scoring', () => {
it('should calculate impact score based on file criticality', () => {
const report1 = router.parseCoverage({
'src/auth/jwt.ts': {
lineCoverage: 50,
branchCoverage: 40,
functionCoverage: 60,
statementCoverage: 50,
uncoveredLines: [1, 2, 3],
totalLines: 100,
coveredLines: 50,
},
}, 'json');
const report2 = router.parseCoverage({
'src/utils/format.ts': {
lineCoverage: 50,
branchCoverage: 40,
functionCoverage: 60,
statementCoverage: 50,
uncoveredLines: [1, 2, 3],
totalLines: 100,
coveredLines: 50,
},
}, 'json');
const result1 = router.route(report1);
const result2 = router.route(report2);
// Auth file should have higher impact score
expect(result1.impactScore).toBeGreaterThanOrEqual(result2.impactScore);
});
});
});