/** * 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); }); }); });