UNPKG

aiwg

Version:

Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.

910 lines (743 loc) 31.7 kB
/** * 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<string, string>(); 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<string, string>(); 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<string, string>(); 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('<!DOCTYPE html>'); expect(html).toContain('<title>Traceability Matrix</title>'); expect(html).toContain('UC-001'); expect(html).toContain('src/engine.ts'); expect(html).toContain('<style>'); expect(html).toContain('coverage-high'); // Test HTML entity escaping const matrixWithScript = createTestMatrix('src/<script>.ts'); const htmlEscaped = generator.exportToHTML(matrixWithScript); expect(htmlEscaped).toContain('&lt;script&gt;'); expect(htmlEscaped).not.toContain('<script>'); }); it('should export to Excel (TSV)', () => { const matrix = createTestMatrix(); const tsv = generator.exportToExcel(matrix); expect(tsv).toContain('\t'); expect(tsv).not.toContain(','); }); }); }); describe('TraceabilityChecker', () => { let sandbox: FilesystemSandbox; let checker: TraceabilityChecker; beforeEach(async () => { sandbox = new FilesystemSandbox(); await sandbox.initialize(); checker = new TraceabilityChecker(sandbox.getPath()); }); afterEach(async () => { await sandbox.cleanup(); }); describe('scanRequirements', () => { it('should scan requirements directory and extract all ID types', async () => { await sandbox.createDirectory('.aiwg/requirements'); await sandbox.writeFile( '.aiwg/requirements/use-cases.md', `# UC-001: Validate AI-Generated Content **Priority**: P0 Validate content against AI patterns. ` ); await sandbox.writeFile( '.aiwg/requirements/nfrs.md', `# NFR-PERF-001: Context Loading Performance **Priority**: P0 Context loading must complete in <5s for 95% of requests. ` ); const requirements = await checker.scanRequirements(); expect(requirements.size).toBeGreaterThan(0); expect(requirements.has('UC-001')).toBe(true); expect(requirements.has('NFR-PERF-001')).toBe(true); const uc001 = requirements.get('UC-001')!; expect(uc001.type).toBe('use-case'); expect(uc001.priority).toBe('P0'); const nfr = requirements.get('NFR-PERF-001')!; expect(nfr.type).toBe('nfr'); }); it('should process YAML files', async () => { await sandbox.createDirectory('.aiwg/requirements'); await sandbox.writeFile( '.aiwg/requirements/requirements.yaml', `requirements: - id: UC-002 title: Deploy Agent priority: P0 ` ); const requirements = await checker.scanRequirements(); expect(requirements.has('UC-002')).toBe(true); }); it('should handle missing requirements directory', async () => { const requirements = await checker.scanRequirements(); expect(requirements.size).toBe(0); }); }); describe('scanCode', () => { it('should scan code directory and track line numbers', async () => { await sandbox.createDirectory('src'); await sandbox.writeFile( 'src/engine.ts', `Line 1 Line 2 // UC-001 Line 4 /** * ValidationEngine - Implements UC-001 * NFR-PERF-001: Context loading <5s */ export class ValidationEngine { // Implementation } ` ); const codeRefs = await checker.scanCode(); expect(codeRefs.size).toBeGreaterThan(0); const engineRef = Array.from(codeRefs.values())[0]; expect(engineRef.requirementIds).toContain('UC-001'); expect(engineRef.requirementIds).toContain('NFR-PERF-001'); expect(engineRef.lineNumbers[0]).toBe(3); }); it('should ignore test files and handle missing directory', async () => { await sandbox.createDirectory('src'); await sandbox.writeFile('src/engine.test.ts', '// UC-001'); const codeRefs = await checker.scanCode(); expect(codeRefs.size).toBe(0); // Clean up and test missing directory await sandbox.cleanup(); await sandbox.initialize(); const checker2 = new TraceabilityChecker(sandbox.getPath()); const codeRefs2 = await checker2.scanCode(); expect(codeRefs2.size).toBe(0); }); }); describe('scanTests', () => { it('should scan test directory and extract test names', async () => { await sandbox.createDirectory('test'); await sandbox.writeFile( 'test/engine.test.ts', `describe('UC-001: AI Pattern Detection', () => { it('should detect banned phrases (NFR-ACC-001)', () => { // Test implementation }); }); it('should validate UC-001', () => {}); it('should handle errors', () => {}); ` ); const testRefs = await checker.scanTests(); expect(testRefs.size).toBeGreaterThan(0); const engineTest = Array.from(testRefs.values())[0]; expect(engineTest.requirementIds).toContain('UC-001'); expect(engineTest.requirementIds).toContain('NFR-ACC-001'); expect(engineTest.testNames).toContain('should validate UC-001'); expect(engineTest.testNames).toContain('should handle errors'); }); it('should handle missing test directory', async () => { const testRefs = await checker.scanTests(); expect(testRefs.size).toBe(0); }); }); describe('scanAll', () => { it('should scan all directories in parallel', async () => { // Create test structure await sandbox.createDirectory('.aiwg/requirements'); await sandbox.createDirectory('src'); await sandbox.createDirectory('test'); await sandbox.writeFile('.aiwg/requirements/uc.md', '# UC-001'); await sandbox.writeFile('src/engine.ts', '// UC-001'); await sandbox.writeFile('test/engine.test.ts', '// UC-001'); const result = await checker.scanAll(); expect(result.requirements.size).toBeGreaterThan(0); expect(result.code.size).toBeGreaterThan(0); expect(result.tests.size).toBeGreaterThan(0); expect(result.scanTime).toBeGreaterThan(0); }); it('should complete scan in <1min for 1000 files (NFR-TRACE-001)', async () => { // This is a performance test - skip in normal runs // To enable, set environment variable: RUN_PERF_TESTS=true if (!process.env.RUN_PERF_TESTS) { return; } // Create 1000 small files await sandbox.createDirectory('src'); for (let i = 0; i < 1000; i++) { await sandbox.writeFile(`src/file${i}.ts`, `// UC-${String(i % 100).padStart(3, '0')}`); } const startTime = performance.now(); await checker.scanAll(); const scanTime = performance.now() - startTime; // Should complete in <60s (60000ms) expect(scanTime).toBeLessThan(60000); }); }); describe('buildTraceabilityLinks', () => { it('should build links and calculate coverage metrics', async () => { await sandbox.createDirectory('.aiwg/requirements'); await sandbox.createDirectory('src'); await sandbox.createDirectory('test'); await sandbox.writeFile('.aiwg/requirements/uc.md', '# UC-001\n**Priority**: P0'); await sandbox.writeFile('src/engine.ts', '// Implements UC-001'); await sandbox.writeFile('test/engine.test.ts', '// Tests UC-001'); await checker.scanAll(); const links = await checker.buildTraceabilityLinks(); expect(links.has('UC-001')).toBe(true); const link = links.get('UC-001')!; expect(link.linkedItems.length).toBeGreaterThan(0); expect(link.coverage.hasCode).toBe(true); expect(link.coverage.hasTests).toBe(true); expect(link.coverage.completeness).toBeGreaterThan(0); }); it('should handle requirements without code', async () => { await sandbox.createDirectory('.aiwg/requirements'); await sandbox.writeFile('.aiwg/requirements/uc.md', '# UC-999'); await checker.scanAll(); const links = await checker.buildTraceabilityLinks(); const link = links.get('UC-999')!; expect(link.coverage.hasCode).toBe(false); expect(link.coverage.hasTests).toBe(false); }); }); describe('detectOrphans', () => { it('should detect orphaned requirements, code, and tests with severity', async () => { await sandbox.createDirectory('.aiwg/requirements'); await sandbox.writeFile('.aiwg/requirements/p0.md', '# UC-001\n**Priority**: P0'); await sandbox.writeFile('.aiwg/requirements/p1.md', '# UC-002\n**Priority**: P1'); await sandbox.writeFile('.aiwg/requirements/p2.md', '# UC-003\n**Priority**: P2'); await sandbox.createDirectory('src'); await sandbox.writeFile('src/orphan.ts', 'export class Orphan {}'); await sandbox.createDirectory('test'); await sandbox.writeFile('test/orphan.test.ts', "it('orphaned test', () => {});"); await checker.scanAll(); const orphans = await checker.detectOrphans(); expect(orphans.orphanedRequirements).toContain('UC-001'); expect(orphans.orphanedCode.length).toBeGreaterThan(0); expect(orphans.orphanedTests.length).toBeGreaterThan(0); expect(orphans.severity.get('UC-001')).toBe('critical'); expect(orphans.severity.get('UC-002')).toBe('warning'); expect(orphans.severity.get('UC-003')).toBe('info'); }); }); describe('calculateCoverage', () => { it('should calculate overall, by-type, and by-priority coverage', async () => { await sandbox.createDirectory('.aiwg/requirements'); await sandbox.createDirectory('src'); await sandbox.createDirectory('test'); await sandbox.writeFile('.aiwg/requirements/uc1.md', '# UC-001'); await sandbox.writeFile('.aiwg/requirements/uc2.md', '# UC-002'); await sandbox.writeFile('.aiwg/requirements/nfr.md', '# NFR-PERF-001'); await sandbox.writeFile('.aiwg/requirements/p0.md', '# UC-003\n**Priority**: P0'); await sandbox.writeFile('src/engine.ts', '// UC-001\n// UC-003'); await sandbox.writeFile('test/engine.test.ts', '// UC-001\n// UC-003'); await checker.scanAll(); const coverage = await checker.calculateCoverage(); // 2 out of 4 requirements traced = 50% expect(coverage.percentage).toBe(50); expect(coverage.byType.has('use-case')).toBe(true); expect(coverage.byType.has('nfr')).toBe(true); expect(coverage.byPriority.has('P0')).toBe(true); expect(coverage.byPriority.get('P0')).toBe(100); }); it('should identify gaps', async () => { await sandbox.createDirectory('.aiwg/requirements'); await sandbox.writeFile('.aiwg/requirements/uc.md', '# UC-001'); await checker.scanAll(); const coverage = await checker.calculateCoverage(); expect(coverage.gaps.requirementsWithoutCode).toContain('UC-001'); expect(coverage.gaps.requirementsWithoutTests).toContain('UC-001'); }); }); describe('generateMatrix', () => { it('should generate traceability matrix', async () => { await sandbox.createDirectory('.aiwg/requirements'); await sandbox.createDirectory('src'); await sandbox.createDirectory('test'); await sandbox.writeFile('.aiwg/requirements/uc.md', '# UC-001'); await sandbox.writeFile('src/engine.ts', '// UC-001'); await sandbox.writeFile('test/engine.test.ts', '// UC-001'); await checker.scanAll(); const matrix = await checker.generateMatrix(); expect(matrix.requirements).toContain('UC-001'); expect(matrix.links.length).toBeGreaterThan(0); }); it('should generate matrix in <30s for 1000 requirements (NFR-TRACE-05)', async () => { // Performance test - skip in normal runs if (!process.env.RUN_PERF_TESTS) { return; } // Create 1000 requirements await sandbox.createDirectory('.aiwg/requirements'); for (let i = 0; i < 1000; i++) { await sandbox.writeFile( `.aiwg/requirements/uc${i}.md`, `# UC-${String(i).padStart(3, '0')}` ); } await checker.scanAll(); const startTime = performance.now(); await checker.generateMatrix(); const genTime = performance.now() - startTime; // Should complete in <30s (30000ms) expect(genTime).toBeLessThan(30000); }); }); describe('exportMatrix', () => { beforeEach(async () => { await sandbox.createDirectory('.aiwg/requirements'); await sandbox.createDirectory('src'); await sandbox.createDirectory('test'); await sandbox.writeFile('.aiwg/requirements/uc.md', '# UC-001'); await sandbox.writeFile('src/engine.ts', '// UC-001'); await sandbox.writeFile('test/engine.test.ts', '// UC-001'); await checker.scanAll(); }); it('should export matrix to all formats', async () => { const formats = [ { format: 'csv' as const, expectedContent: 'UC-001' }, { format: 'markdown' as const, expectedContent: '# Traceability Matrix' }, { format: 'html' as const, expectedContent: '<!DOCTYPE html>' }, { format: 'excel' as const, expectedContent: '\t' }, ]; for (const { format, expectedContent } of formats) { const filePath = await checker.exportMatrix(format); const exists = await sandbox.fileExists(filePath.replace(sandbox.getPath() + '/', '')); expect(exists, `file should exist for: ${format}`).toBe(true); const content = await sandbox.readFile(filePath.replace(sandbox.getPath() + '/', '')); expect(content, `wrong content for: ${format}`).toContain(expectedContent); } }); }); describe('generateReport', () => { it('should generate comprehensive report', async () => { await sandbox.createDirectory('.aiwg/requirements'); await sandbox.createDirectory('src'); await sandbox.writeFile('.aiwg/requirements/uc1.md', '# UC-001'); await sandbox.writeFile('.aiwg/requirements/uc2.md', '# UC-002'); await sandbox.writeFile('src/engine.ts', '// UC-001'); await checker.scanAll(); const report = await checker.generateReport(); expect(report.totalRequirements).toBe(2); expect(report.orphanedRequirements.length).toBeGreaterThan(0); expect(report.gapsByRequirement.size).toBeGreaterThan(0); }); }); describe('exportReport', () => { beforeEach(async () => { await sandbox.createDirectory('.aiwg/requirements'); await sandbox.writeFile('.aiwg/requirements/uc.md', '# UC-001'); await checker.scanAll(); }); it('should export report to all formats', async () => { const formats = [ { format: 'markdown' as const }, { format: 'json' as const }, { format: 'html' as const }, ]; for (const { format } of formats) { const filePath = await checker.exportReport(format); const exists = await sandbox.fileExists(filePath.replace(sandbox.getPath() + '/', '')); expect(exists, `file should exist for: ${format}`).toBe(true); if (format === 'json') { const content = await sandbox.readFile(filePath.replace(sandbox.getPath() + '/', '')); const json = JSON.parse(content); expect(json.totalRequirements).toBeGreaterThan(0); } } }); }); describe('validateTraceability', () => { it('should validate against threshold', async () => { await sandbox.createDirectory('.aiwg/requirements'); await sandbox.createDirectory('src'); await sandbox.createDirectory('test'); await sandbox.writeFile('.aiwg/requirements/uc.md', '# UC-001'); await sandbox.writeFile('src/engine.ts', '// UC-001'); await sandbox.writeFile('test/engine.test.ts', '// UC-001'); await checker.scanAll(); const result = await checker.validateTraceability(0.8); // 80% threshold expect(result.passed).toBe(true); expect(result.coverage).toBe(100); }); it('should fail when below threshold', async () => { await sandbox.createDirectory('.aiwg/requirements'); await sandbox.writeFile('.aiwg/requirements/uc.md', '# UC-001'); await checker.scanAll(); const result = await checker.validateTraceability(0.5); expect(result.passed).toBe(false); expect(result.issues.length).toBeGreaterThan(0); }); }); describe('checkConstructionGate', () => { it('should pass when all P0 requirements traced', async () => { await sandbox.createDirectory('.aiwg/requirements'); await sandbox.createDirectory('src'); await sandbox.createDirectory('test'); await sandbox.writeFile('.aiwg/requirements/p0.md', '# UC-001\n**Priority**: P0'); await sandbox.writeFile('src/engine.ts', '// UC-001'); await sandbox.writeFile('test/engine.test.ts', '// UC-001'); await checker.scanAll(); const result = await checker.checkConstructionGate(); expect(result.passed).toBe(true); expect(result.p0Coverage).toBe(100); }); it('should fail when P0 requirements not traced and warn for low P1 coverage', async () => { await sandbox.createDirectory('.aiwg/requirements'); await sandbox.writeFile('.aiwg/requirements/p0.md', '# UC-001\n**Priority**: P0'); await sandbox.writeFile('.aiwg/requirements/p1.md', '# UC-002\n**Priority**: P1'); await checker.scanAll(); const result = await checker.checkConstructionGate(); expect(result.passed).toBe(false); expect(result.issues.length).toBeGreaterThan(0); expect(result.warnings.length).toBeGreaterThan(0); }); }); describe('link management', () => { it('should add, remove, and update links', async () => { await sandbox.createDirectory('.aiwg/requirements'); await sandbox.createDirectory('src'); await sandbox.writeFile('.aiwg/requirements/uc.md', '# UC-001'); await sandbox.writeFile('src/engine.ts', '// UC-001'); await checker.scanAll(); // Add link await checker.addLink('UC-001', { type: 'code', path: 'src/new-file.ts', verified: true, confidence: 1.0 }); await checker.addLink('UC-001', { type: 'test', path: 'test/new-test.test.ts', verified: true, confidence: 1.0 }); let links = await checker.buildTraceabilityLinks(); let link = links.get('UC-001')!; expect(link.linkedItems.some(item => item.path === 'src/new-file.ts')).toBe(true); expect(link.linkedItems.some(item => item.path === 'test/new-test.test.ts')).toBe(true); // Remove link await checker.removeLink('UC-001', sandbox.getPath() + '/src/engine.ts'); links = await checker.buildTraceabilityLinks(); link = links.get('UC-001')!; expect(link.linkedItems.some(item => item.path.includes('engine.ts'))).toBe(false); // Update link const oldPath = sandbox.getPath() + '/src/new-file.ts'; const newPath = sandbox.getPath() + '/src/updated-file.ts'; await checker.updateLink('UC-001', oldPath, { type: 'code', path: newPath, verified: true, confidence: 0.9 }); links = await checker.buildTraceabilityLinks(); link = links.get('UC-001')!; expect(link.linkedItems.some(item => item.path === newPath)).toBe(true); }); it('should throw error for non-existent requirement', async () => { await checker.scanAll(); await expect( checker.addLink('UC-999', { type: 'code', path: 'src/file.ts', verified: true, confidence: 1.0 }) ).rejects.toThrow(); }); }); });