import { describe, expect, it } from 'vitest' import { computeCoverage, detectGaps, qualityScore, toReport, formatTrace } from '../src/index.js' import { overall, validateReport } from '@extant2000/evidence-record' import type { TraceInput } from '../src/index.js' const req = (id: string, parent?: string | null) => ({ id, parent_id: parent ?? null, req_number: `R-${id}`, title: `Requirement ${id}`, }) describe('computeCoverage', () => { it('returns an all-zero shape for no requirements', () => { const c = computeCoverage({ requirements: [] }) expect(c.total_requirements).toBe(0) expect(c.pct_verified).toBe(0) }) it('counts a requirement traced by junction OR by verification FK, without double counting', () => { const input: TraceInput = { requirements: [req('a'), req('b')], requirementVerifications: [{ requirement_id: 'a' }], verificationItems: [{ id: 'v1', requirement_id: 'a', status: 'Passed' }], } // 'a' is linked by both routes but must count once. expect(computeCoverage(input).traced_to_test).toBe(1) }) it('treats a requirement with zero verifications as NOT verified', () => { // Guards the vacuous-every() trap: [].every(...) is true, which would // report a requirement with no evidence at all as fully verified. const input: TraceInput = { requirements: [req('a')], verificationItems: [] } expect(computeCoverage(input).verified).toBe(0) }) it('requires EVERY linked verification to pass or be waived', () => { const mixed: TraceInput = { requirements: [req('a')], verificationItems: [ { id: 'v1', requirement_id: 'a', status: 'Passed' }, { id: 'v2', requirement_id: 'a', status: 'Failed' }, ], } expect(computeCoverage(mixed).verified).toBe(0) const allGood: TraceInput = { requirements: [req('a')], verificationItems: [ { id: 'v1', requirement_id: 'a', status: 'Passed' }, { id: 'v2', requirement_id: 'a', status: 'Waived' }, ], } expect(computeCoverage(allGood).verified).toBe(1) }) it('computes percentages against the requirement total', () => { const input: TraceInput = { requirements: [req('a', 'root'), req('b'), req('c'), req('d')], } expect(computeCoverage(input).pct_parent).toBe(25) }) }) describe('detectGaps', () => { it('reports nothing for an empty requirement set', () => { expect(detectGaps({ requirements: [] })).toEqual({ gaps: [], total: 0 }) }) it('flags an untested, unrisked, parentless requirement three ways', () => { const { gaps } = detectGaps({ requirements: [req('a')] }) expect(gaps.map(g => g.gap_type).sort()).toEqual(['no_risk', 'no_test', 'orphaned']) }) it('rates a failed verification critical and everything else lower', () => { const { gaps } = detectGaps({ requirements: [req('a', 'root')], requirementVerifications: [{ requirement_id: 'a' }], requirementRisks: [{ requirement_id: 'a' }], verificationItems: [{ id: 'v1', requirement_id: 'a', status: 'Failed', method: 'Test' }], }) expect(gaps).toHaveLength(1) expect(gaps[0]!.gap_type).toBe('failed_verification') expect(gaps[0]!.severity).toBe('critical') }) it('flags suspect links', () => { const { gaps } = detectGaps({ requirements: [req('a', 'root')], requirementVerifications: [{ requirement_id: 'a' }], requirementRisks: [{ requirement_id: 'a' }], suspectLinks: [{ requirement_id: 'a' }], }) expect(gaps.map(g => g.gap_type)).toEqual(['suspect']) }) it('falls back to a short id when req_number is absent', () => { const { gaps } = detectGaps({ requirements: [{ id: 'abcdef1234567890' }] }) expect(gaps[0]!.display_id).toBe('abcdef12') }) }) describe('qualityScore', () => { it('applies the documented 0.3/0.3/0.2/0.2 weighting', () => { const s = qualityScore({ pct_test: 100, pct_verified: 100, pct_risk: 0, pct_parent: 0 }) expect(s.score).toBe(60) expect(s.grade).toBe('C') }) it('grades the boundaries inclusively', () => { const at = (n: number) => qualityScore({ pct_test: n, pct_verified: n, pct_risk: n, pct_parent: n, }).grade expect(at(90)).toBe('A') expect(at(75)).toBe('B') expect(at(60)).toBe('C') expect(at(40)).toBe('D') expect(at(39)).toBe('F') }) it('accepts custom weights for a different accreditation regime', () => { const s = qualityScore( { pct_test: 0, pct_verified: 100, pct_risk: 0, pct_parent: 0 }, { traced: 0, verified: 1, risk: 0, parent: 0 }, ) expect(s.score).toBe(100) expect(s.grade).toBe('A') }) it('round-trips from computeCoverage output', () => { const coverage = computeCoverage({ requirements: [req('a', 'root')], requirementVerifications: [{ requirement_id: 'a' }], requirementRisks: [{ requirement_id: 'a' }], verificationItems: [{ id: 'v1', requirement_id: 'a', status: 'Passed' }], }) expect(qualityScore(coverage).score).toBe(100) }) }) describe('report — the score has to lead back to the rows', () => { const input = { requirements: [ { id: 'r1-uuid-aaaa', req_number: 'SYS-001', title: 'Deorbit within 25 years', parent_id: 'p1' }, { id: 'r2-uuid-bbbb', req_number: 'SYS-002', title: 'Survive launch loads' }, ], verificationItems: [ { id: 'v1-uuid-cccc', requirement_id: 'r1-uuid-aaaa', status: 'Failed', method: 'Test' }, ], } it('cites the requirement row behind every gap', () => { // The gap this closes: "quality 50/100, grade D" with no route back to // the requirements that produced it. const text = formatTrace(input, { requirementTable: 'mission_requirements', evidence: 'full' }) expect(text).toContain('mission_requirements#r2-uuid-bbbb') expect(text).toContain('SYS-002') }) it('cites the verification row behind a failed verification', () => { const text = formatTrace(input, { verificationTable: 'mission_verification_items', evidence: 'full' }) expect(text).toContain('mission_verification_items#v1-uuid-cccc') }) it('breaks the composite score into checkable measurements', () => { const r = toReport(input) const traced = r.findings.find(f => f.id === 'score.traced')! expect(traced.evidence[0]!.source).toMatchObject({ kind: 'measurement', samples: 2 }) expect(formatTrace(input)).toContain('composite quality') }) it('treats an empty requirement set as not-assessed, not as a score of zero', () => { // computeCoverage correctly returns zeros, but 0% next to grade F reads // as "traced nothing" when the truth is "loaded nothing". const r = toReport({ requirements: [] }) expect(overall(r)).toBe('not-assessed') const text = formatTrace({ requirements: [] }) expect(text).toContain('NOT ASSESSED') expect(text).toContain('nothing was traced because nothing was read') expect(text).not.toContain('grade F') }) it('separates absent evidence from evidence of failure', () => { const r = toReport(input) const failed = r.findings.find(f => f.summary.includes('Failed'))! expect(failed.determination).toBe('fail') const noTest = r.findings.find(f => f.summary.includes('No verification item'))! expect(noTest.determination).toBe('not-assessed') }) it('every conclusion carries a citation', () => { expect(validateReport(toReport(input))).toEqual([]) }) })