/** * lib/audit-finding — ONE reader for the finding shapes the audit families emit. * * The reason this file exists: /support-report required `ruleId`, the dev packs * write `code`, so an audit-dev-frontend envelope carrying 45 findings parsed * to zero and a verdict dispute was refused as « seen: none » (support report * 1766f66848a9). The DRIFT leg below reads the packs' own type files: a pack * that introduces a FOURTH spelling turns red here, instead of going blind in * production. */ import { describe, it, expect } from 'vitest' import { readFileSync } from 'node:fs' import { join, relative } from 'node:path' import { FINDING_RULE_ID_KEYS, findingEvidenceOf, findingMessageOf, findingRuleIdOf, rawFindingsOf, } from '../audit-finding.js' import { SKILLS_ROOT, baSkillFile, listFiles } from './helpers/cli-options.js' describe('findingRuleIdOf', () => { it('reads the three spellings', () => { expect(findingRuleIdOf({ ruleId: 'XD-002' })).toBe('XD-002') expect(findingRuleIdOf({ code: 'DEV-UI-034' })).toBe('DEV-UI-034') expect(findingRuleIdOf({ rule: 'DEV-TEST-001' })).toBe('DEV-TEST-001') }) it('ruleId wins when several are present — the BA shape is the reference', () => { expect(findingRuleIdOf({ ruleId: 'A-001', code: 'B-002', rule: 'C-003' })).toBe('A-001') }) it('a non-string or blank identity is not an identity — it falls through, then null', () => { expect(findingRuleIdOf({ ruleId: 42, code: 'DEV-API-008' })).toBe('DEV-API-008') expect(findingRuleIdOf({ ruleId: ' ' })).toBeNull() expect(findingRuleIdOf({ severity: 'err' })).toBeNull() expect(findingRuleIdOf(null)).toBeNull() expect(findingRuleIdOf('DEV-UI-034')).toBeNull() expect(findingRuleIdOf(['DEV-UI-034'])).toBeNull() }) }) describe('findingMessageOf / findingEvidenceOf', () => { it('message, else solution, else detail, else label', () => { expect(findingMessageOf({ message: 'm', solution: 's' })).toBe('m') expect(findingMessageOf({ solution: 's', label: 'l' })).toBe('s') expect(findingMessageOf({ detail: 'd', label: 'l' })).toBe('d') expect(findingMessageOf({ label: 'DEV_UI_034_err' })).toBe('DEV_UI_034_err') }) it('an OBJECT detail (audit-dev-actions-alignment) is not a text', () => { expect(findingMessageOf({ detail: { serviceUrl: '/api/x' }, message: 'drift' })).toBe('drift') expect(findingMessageOf({ detail: { serviceUrl: '/api/x' } })).toBe('') }) it('an empty solution (the ok findings of the dev packs) falls to the label', () => { expect(findingMessageOf({ solution: '', label: 'DEV_UI_034_ok' })).toBe('DEV_UI_034_ok') }) it('evidence, else refs, stringified', () => { expect(findingEvidenceOf({ evidence: ['Memo', 'Contact'] })).toEqual(['Memo', 'Contact']) expect(findingEvidenceOf({ refs: ['UC-X-001#AC-01', 7] })).toEqual(['UC-X-001#AC-01', '7']) expect(findingEvidenceOf({ evidence: 'Memo' })).toEqual([]) expect(findingEvidenceOf({})).toEqual([]) }) }) describe('rawFindingsOf', () => { it('report.findings[] — the lib/output envelope', () => { expect(rawFindingsOf({ success: true, report: { findings: [{ code: 'A' }] } })).toEqual([{ code: 'A' }]) }) it('findings[] at the root — audit-dev-tests prints its report flat', () => { expect(rawFindingsOf({ success: false, errors: [], findings: [{ rule: 'DEV-TEST-001' }] })).toEqual([{ rule: 'DEV-TEST-001' }]) }) it('the envelope wins over a root array', () => { expect(rawFindingsOf({ report: { findings: [{ code: 'A' }] }, findings: [{ rule: 'B' }] })).toEqual([{ code: 'A' }]) }) it('report: null (a refusal) and non-audit outputs carry none', () => { expect(rawFindingsOf({ success: false, report: null, errors: ['bad spec'] })).toEqual([]) expect(rawFindingsOf({ report: { findings: 'none' } })).toEqual([]) expect(rawFindingsOf(null)).toEqual([]) expect(rawFindingsOf([])).toEqual([]) }) }) // ─── Drift: what the packs DECLARE ───────────────────────────────────────── // The declaration of a finding type: `export interface Finding {`, // `AlignmentFinding`, or the Zod `FindingSchema = z.object({`. const FINDING_DECL = /(?:export\s+interface\s+\w*Finding\s*\{|export\s+const\s+FindingSchema\s*=\s*z\.object\(\{)/ /** A top-level member of that declaration: two-space indent, then the name. */ const memberRe = (name: string): RegExp => new RegExp(`^ ${name}\\??:`, 'm') function findingBlock(source: string): string | null { const m = FINDING_DECL.exec(source) if (!m) return null const from = m.index + m[0].length const end = source.indexOf('\n}', from) return end < 0 ? null : source.slice(from, end) } const typeFiles = listFiles( join(SKILLS_ROOT, 'development'), (p) => /[\\/]audit-dev-[^\\/]+[\\/]cli[\\/][^\\/]+[\\/]types\.ts$/.test(p), ) const rel = (p: string): string => relative(SKILLS_ROOT, p).replace(/\\/g, '/') describe('every audit-dev-* finding type names its identity with a spelling the reader knows', () => { it('finds the packs (a drift test over zero files proves nothing)', () => { expect(typeFiles.length).toBeGreaterThanOrEqual(10) }) for (const file of typeFiles) { it(rel(file), () => { const block = findingBlock(readFileSync(file, 'utf8').replace(/\r\n/g, '\n')) expect(block, 'no Finding declaration found — teach FINDING_DECL the new shape').not.toBeNull() const spelled = FINDING_RULE_ID_KEYS.filter((k) => memberRe(k).test(block!)) expect( spelled, `the finding type declares none of ${FINDING_RULE_ID_KEYS.join(' / ')} — add the new spelling to lib/audit-finding`, ).not.toEqual([]) }) } it('the three spellings are all still in use (a dead key would be a lie in the table)', () => { const ba = readFileSync(baSkillFile('audit-run/cli/audit-ba/types.ts'), 'utf8') expect(ba).toMatch(/^ ruleId: string\r?$/m) const used = new Set(['ruleId']) for (const file of typeFiles) { const block = findingBlock(readFileSync(file, 'utf8').replace(/\r\n/g, '\n')) ?? '' for (const k of FINDING_RULE_ID_KEYS) if (memberRe(k).test(block)) used.add(k) } expect([...used].sort()).toEqual([...FINDING_RULE_ID_KEYS].sort()) }) })