/** * Traceability Checker Tests * Comprehensive test suite for requirements traceability system * * Test Coverage: * - ID extraction (consolidated) * - Link building * - Orphan detection * - Coverage analysis * - Matrix generation * - Reporting * - Validation * - Maintenance * * Target: 35-45 tests, >85% coverage */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { IDExtractor } from '../../../src/traceability/id-extractor.ts'; import { MatrixGenerator } from '../../../src/traceability/matrix-generator.ts'; import { TraceabilityChecker } from '../../../src/traceability/traceability-checker.ts'; import { FilesystemSandbox } from '../../../src/testing/mocks/filesystem-sandbox.ts'; describe('IDExtractor', () => { let extractor: IDExtractor; beforeEach(() => { extractor = new IDExtractor(); }); describe('extractFromLine', () => { it('should extract all ID types from lines', () => { const testCases = [ { line: '// Implements UC-001: Validate AI-Generated Content', expectedId: 'UC-001', expectedType: 'use-case' }, { line: '// NFR-PERF-001: Context loading <5s', expectedId: 'NFR-PERF-001', expectedType: 'nfr' }, { line: '// Covers: US-042', expectedId: 'US-042', expectedType: 'user-story' }, { line: '// Feature F-005: Requirements Traceability', expectedId: 'F-005', expectedType: 'feature' }, { line: '// AC-012: Must complete within 5 seconds', expectedId: 'AC-012', expectedType: 'acceptance-criteria' }, ]; for (const { line, expectedId, expectedType } of testCases) { const ids = extractor.extractFromLine(line, 1); expect(ids, `failed for: ${line}`).toHaveLength(1); expect(ids[0].id, `wrong id for: ${line}`).toBe(expectedId); expect(ids[0].type, `wrong type for: ${line}`).toBe(expectedType); expect(ids[0].lineNumber).toBe(1); } }); it('should extract multiple IDs from one line', () => { const line = '// @traceability UC-001, NFR-ACC-001, F-003'; const ids = extractor.extractFromLine(line); expect(ids).toHaveLength(3); expect(ids[0].id).toBe('UC-001'); expect(ids[1].id).toBe('NFR-ACC-001'); expect(ids[2].id).toBe('F-003'); }); it('should deduplicate IDs on same line', () => { const line = '// UC-001 and UC-001 again'; const ids = extractor.extractFromLine(line); expect(ids).toHaveLength(1); expect(ids[0].id).toBe('UC-001'); }); it('should extract context around ID', () => { const line = '// This function implements UC-001 for validation'; const ids = extractor.extractFromLine(line); expect(ids[0].context).toContain('UC-001'); expect(ids[0].context).toContain('implements'); }); it('should handle lines without IDs and test descriptions', () => { const noIdLine = '// Just a regular comment'; expect(extractor.extractFromLine(noIdLine)).toHaveLength(0); const testLine = ` it('should validate UC-001: AI Pattern Detection', () => {`; const ids = extractor.extractFromLine(testLine); expect(ids).toHaveLength(1); expect(ids[0].id).toBe('UC-001'); }); }); describe('extractFromContent', () => { it('should extract IDs from multiline content', () => { const content = ` /** * ValidationEngine - Implements UC-001 * Performance target: NFR-PERF-001 * Accuracy target: NFR-ACC-001 */ export class ValidationEngine { // Covers: F-001 } `.trim(); const ids = extractor.extractFromContent(content); expect(ids.length).toBeGreaterThanOrEqual(4); expect(ids.map(id => id.id)).toContain('UC-001'); expect(ids.map(id => id.id)).toContain('NFR-PERF-001'); expect(ids.map(id => id.id)).toContain('NFR-ACC-001'); expect(ids.map(id => id.id)).toContain('F-001'); }); it('should deduplicate IDs across lines', () => { const content = ` // UC-001 // UC-001 again // UC-001 third time `.trim(); const ids = extractor.extractFromContent(content); expect(ids).toHaveLength(1); expect(ids[0].id).toBe('UC-001'); }); it('should preserve line numbers', () => { const content = `Line 1 Line 2 // UC-001 on line 3 Line 4`; const ids = extractor.extractFromContent(content); expect(ids[0].lineNumber).toBe(3); }); it('should handle empty content and content with no IDs', () => { expect(extractor.extractFromContent('')).toHaveLength(0); const noIdContent = ` export class Foo { bar() { return 42; } } `.trim(); expect(extractor.extractFromContent(noIdContent)).toHaveLength(0); }); }); describe('extractFromFiles', () => { it('should extract IDs from multiple files in parallel', async () => { const files = new Map(); files.set('file1.ts', '// UC-001: First file'); files.set('file2.ts', '// UC-002: Second file\n// NFR-PERF-001: Performance'); files.set('file3.ts', '// F-003: Third file'); const results = await extractor.extractFromFiles(files); expect(results.size).toBe(3); expect(results.get('file1.ts')?.ids).toHaveLength(1); expect(results.get('file2.ts')?.ids).toHaveLength(2); expect(results.get('file3.ts')?.ids).toHaveLength(1); }); it('should track extraction time per file', async () => { const files = new Map(); files.set('file1.ts', '// UC-001'); const results = await extractor.extractFromFiles(files); const result = results.get('file1.ts')!; expect(result.extractionTime).toBeGreaterThan(0); }); it('should handle empty file map', async () => { const files = new Map(); const results = await extractor.extractFromFiles(files); expect(results.size).toBe(0); }); }); describe('isValidId', () => { it('should validate all ID types and formats', () => { const validCases = [ { id: 'UC-001', type: 'use-case' }, { id: 'UC-999', type: 'use-case' }, { id: 'NFR-PERF-001', type: 'nfr' }, { id: 'NFR-SECURITY-999', type: 'nfr' }, { id: 'US-001', type: 'user-story' }, { id: 'F-001', type: 'feature' }, ]; for (const { id, type } of validCases) { expect(extractor.isValidId(id), `should be valid: ${id} (${type})`).toBe(true); } const invalidCases = [ 'UC-1', // Too short 'UC-1234', // Too long 'NFR-P-001', // NFR category too short 'NFR-VERYLONGCATEGORY-001', // NFR category too long 'US-1', // US number too short 'F-1', // Feature number too short 'invalid', // No format match 'UC001', // Missing dash 'UC-', // Missing number ]; for (const id of invalidCases) { expect(extractor.isValidId(id), `should be invalid: ${id}`).toBe(false); } }); }); describe('parseId', () => { it('should parse all ID formats correctly', () => { const testCases = [ { id: 'UC-001', expected: { prefix: 'UC', number: '001' } }, { id: 'NFR-PERF-001', expected: { prefix: 'NFR', category: 'PERF', number: '001' } }, { id: 'US-042', expected: { prefix: 'US', number: '042' } }, { id: 'F-005', expected: { prefix: 'F', number: '005' } }, { id: 'INVALID', expected: null }, ]; for (const { id, expected } of testCases) { const parsed = extractor.parseId(id); expect(parsed, `parse failed for: ${id}`).toEqual(expected); } }); }); }); describe('MatrixGenerator', () => { let generator: MatrixGenerator; beforeEach(() => { generator = new MatrixGenerator(); }); describe('generateMatrix', () => { it('should generate matrix from links', () => { const requirements = ['UC-001', 'UC-002']; const codeFiles = ['src/engine.ts']; const testFiles = ['test/engine.test.ts']; const links = new Map([ ['UC-001', { code: ['src/engine.ts'], tests: ['test/engine.test.ts'] }], ['UC-002', { code: [], tests: [] }] ]); const matrix = generator.generateMatrix(requirements, codeFiles, testFiles, links); expect(matrix.requirements).toEqual(requirements); expect(matrix.code).toEqual(codeFiles); expect(matrix.tests).toEqual(testFiles); expect(matrix.links).toHaveLength(2); }); it('should mark linked and unlinked cells correctly', () => { const requirements = ['UC-001']; const codeFiles = ['src/engine.ts']; const testFiles = ['test/engine.test.ts']; // Test with links const withLinks = new Map([ ['UC-001', { code: ['src/engine.ts'], tests: ['test/engine.test.ts'] }] ]); const matrixLinked = generator.generateMatrix(requirements, codeFiles, testFiles, withLinks); const rowLinked = matrixLinked.links[0]; const codeCellLinked = rowLinked.find(cell => cell.itemType === 'code'); const testCellLinked = rowLinked.find(cell => cell.itemType === 'test'); expect(codeCellLinked?.linked).toBe(true); expect(testCellLinked?.linked).toBe(true); // Test without links const withoutLinks = new Map([['UC-001', { code: [], tests: [] }]]); const matrixUnlinked = generator.generateMatrix(requirements, codeFiles, testFiles, withoutLinks); const rowUnlinked = matrixUnlinked.links[0]; const codeCellUnlinked = rowUnlinked.find(cell => cell.itemType === 'code'); const testCellUnlinked = rowUnlinked.find(cell => cell.itemType === 'test'); expect(codeCellUnlinked?.linked).toBe(false); expect(testCellUnlinked?.linked).toBe(false); }); }); describe('export formats', () => { const createTestMatrix = (itemPath = 'src/engine.ts') => ({ requirements: ['UC-001'], code: [itemPath], tests: ['test/engine.test.ts'], links: [ [ { requirementId: 'UC-001', itemPath, itemType: 'code' as const, linked: true, verified: true, confidence: 1.0 }, { requirementId: 'UC-001', itemPath: 'test/engine.test.ts', itemType: 'test' as const, linked: true, verified: true, confidence: 1.0 } ] ] }); it('should export to CSV with proper escaping and optional columns', () => { const matrix = createTestMatrix(); const csv = generator.exportToCSV(matrix); expect(csv).toContain('Requirement'); expect(csv).toContain('UC-001'); expect(csv).toContain('src/engine.ts'); expect(csv).toContain('test/engine.test.ts'); // Test CSV escaping const matrixWithCommas = createTestMatrix('src/file,with,commas.ts'); const csvEscaped = generator.exportToCSV(matrixWithCommas); expect(csvEscaped).toContain('"src/file,with,commas.ts"'); // Test optional columns const csvWithVerification = generator.exportToCSV(matrix, { format: 'csv', includeVerification: true }); expect(csvWithVerification).toContain('Verified'); const csvWithConfidence = generator.exportToCSV(matrix, { format: 'csv', includeConfidence: true }); expect(csvWithConfidence).toContain('Confidence'); }); it('should export to Markdown with coverage indicators', () => { const matrix = createTestMatrix(); const md = generator.exportToMarkdown(matrix); expect(md).toContain('# Traceability Matrix'); expect(md).toContain('**UC-001**'); expect(md).toContain('src/engine.ts'); expect(md).toContain('test/engine.test.ts'); expect(md).toMatch(/[✅⚠️❌]/); // Test missing links const emptyMatrix = { requirements: ['UC-001'], code: [], tests: [], links: [[]] }; const mdEmpty = generator.exportToMarkdown(emptyMatrix); expect(mdEmpty).toContain('*NONE*'); }); it('should export to HTML with CSS and entity escaping', () => { const matrix = createTestMatrix(); const html = generator.exportToHTML(matrix); expect(html).toContain(''); expect(html).toContain('Traceability Matrix'); expect(html).toContain('UC-001'); expect(html).toContain('src/engine.ts'); expect(html).toContain('