import { describe, expect, it } from 'vitest' import { AnnotationSource, AnnotationStatus, Affiliation, CombatReadiness, MediaStatus, MediaType, } from '../src/types' import { loadEnumSnapshot } from './fixtures/enum_spec_snapshot' // AZ-459 — Wire-contract enum compliance. // FT-P-04 / row 14, 18 — AnnotationStatus on the wire matches spec set // FT-P-05 / rows 15-17 — MediaStatus / Affiliation / CombatReadiness match // FT-P-06 / rows 18-19 — detection wire payload uses spec enum values // (e2e captures the actual outbound POST; this fast // test asserts the typed enum constants in src/types // agree with the snapshot, which IS the wire format // per src/api/client.ts JSON.stringify path) // FT-N-15 / rows 20-21 — MediaType magic-literal hygiene; static counterpart // lives in scripts/run-tests.sh // // Drift-failure semantics (AC-2): when the UI's enum value differs from the // spec snapshot, the test surfaces the divergence loudly. Today the UI drifts // on AnnotationStatus / MediaStatus / Affiliation. We use Vitest's it.fails() // for those documented drifts so the runner reports them as known-failing // (= today's PASS) and flips to FAIL the moment the production enum is fixed // (= tomorrow's signal to remove the wrapper). For verification_pending // enums (CombatReadiness, MediaType per the snapshot), we skip with a clear // QUARANTINE marker that names the resolution path. // // Black-box discipline (P9 / AC-2 of the task spec): comparing the spec // snapshot to typed src/types enum SHAPES is allowed because those enums // ARE the wire format per src/api/client.ts. It is NOT allowed to compare // the snapshot to itself (a tautology) or to derive expected values from // the UI rather than the contract. const snapshot = loadEnumSnapshot() interface DriftReport { enumName: string observed: Record expected: Record missingFromUi: string[] extraOnUi: string[] numericMismatches: Array<{ name: string; observed: number; expected: number }> } function compareEnum( enumName: string, uiEnum: Record, expected: Record, ): DriftReport { // Filter out reverse-mapped numeric keys that TypeScript synthesises for // numeric enums — keep only string-keyed entries with numeric values. const observed: Record = {} for (const [k, v] of Object.entries(uiEnum)) { if (typeof v === 'number') observed[k] = v } const obsKeys = new Set(Object.keys(observed)) const expKeys = new Set(Object.keys(expected)) const missingFromUi = [...expKeys].filter((k) => !obsKeys.has(k)) const extraOnUi = [...obsKeys].filter((k) => !expKeys.has(k)) const numericMismatches: Array<{ name: string; observed: number; expected: number }> = [] for (const k of obsKeys) { if (k in expected && observed[k] !== expected[k]) { numericMismatches.push({ name: k, observed: observed[k], expected: expected[k] }) } } return { enumName, observed, expected, missingFromUi, extraOnUi, numericMismatches } } function describeDrift(d: DriftReport): string { const parts: string[] = [`enum ${d.enumName} drift:`] if (d.missingFromUi.length) parts.push(`missing from UI: ${d.missingFromUi.join(', ')}`) if (d.extraOnUi.length) parts.push(`extra on UI: ${d.extraOnUi.join(', ')}`) for (const m of d.numericMismatches) parts.push(`${m.name} = ${m.observed} (UI) vs ${m.expected} (spec)`) return parts.join(' | ') } describe('AZ-459 / wire-contract enum compliance', () => { describe('FT-P-04 (rows 14, 18) — AnnotationStatus matches spec', () => { // Drift documented: src/types says Edited=1, spec says Edited=20. The // it.fails wrapper makes this test pass today (drift exists; assertion // throws as required by AC-2) and fails the day Step 4 lifts the drift // (assertion succeeds, alerting the author to remove the wrapper). it.fails('UI matches spec exactly (currently DRIFTED — Step 4 fix pending; see ui_drift_summary.AnnotationStatus)', () => { // Arrange const drift = compareEnum('AnnotationStatus', AnnotationStatus, snapshot.enums.AnnotationStatus.values) // Assert (will throw today because of documented drift) expect( drift.numericMismatches.length === 0 && drift.missingFromUi.length === 0 && drift.extraOnUi.length === 0, describeDrift(drift), ).toBe(true) }) // Regression detector: the drift IS what the snapshot's ui_drift_summary // says it is. If the UI's drift shape changes (e.g. someone renames Edited // to EditPending), this test fails so the snapshot's documentation can be // updated alongside the code. it('current UI drift matches the documented ui_drift_summary entry', () => { // Arrange const documentedDrift = (snapshot.ui_drift_summary as { AnnotationStatus?: { ui_values?: Record } }).AnnotationStatus?.ui_values ?? null // Assert expect(documentedDrift).not.toBeNull() const observed: Record = {} for (const [k, v] of Object.entries(AnnotationStatus)) { if (typeof v === 'number') observed[k] = v } expect(observed).toEqual(documentedDrift) }) }) describe('FT-P-05 (rows 15-17) — MediaStatus / Affiliation / CombatReadiness match spec', () => { it.fails('MediaStatus UI matches spec exactly (currently DRIFTED — Step 4 fix pending)', () => { // Arrange const drift = compareEnum('MediaStatus', MediaStatus, snapshot.enums.MediaStatus.values) // Assert (today drifted) expect( drift.numericMismatches.length === 0 && drift.missingFromUi.length === 0 && drift.extraOnUi.length === 0, describeDrift(drift), ).toBe(true) }) it.fails('Affiliation UI matches spec exactly (currently DRIFTED — Step 4 fix pending)', () => { // Arrange const drift = compareEnum('Affiliation', Affiliation, snapshot.enums.Affiliation.values) // Assert (today drifted) expect( drift.numericMismatches.length === 0 && drift.missingFromUi.length === 0 && drift.extraOnUi.length === 0, describeDrift(drift), ).toBe(true) }) // QUARANTINE — verification_pending: true per snapshot. Numeric values are // inferred sequentially; Step 4 .NET-service inspection must confirm // before this test ungates. Recorded as a CSV `Result: QUARANTINE` row in // scripts/run-tests.sh static profile (FT-P-05.CR partial). it.skip('CombatReadiness UI matches spec — QUARANTINE: snapshot.verification_pending=true; lifted by Step 4 .NET inspection', () => { // Arrange const drift = compareEnum('CombatReadiness', CombatReadiness, snapshot.enums.CombatReadiness.values) // Assert (held until snapshot.verification_pending flips to false) expect(drift.numericMismatches.length === 0, describeDrift(drift)).toBe(true) }) it('snapshot still flags CombatReadiness verification_pending (alerts when Step 4 lifts the flag)', () => { // Arrange + Assert expect(snapshot.enums.CombatReadiness.verification_pending).toBe(true) }) }) describe('FT-P-06 (rows 18, 19) — detection wire payload uses spec enum values', () => { // Affiliation and CombatReadiness ride on every detection POST body. // The fast-profile half asserts the typed enum constants ship the values // that the wire would carry; the e2e half (e2e/tests/wire_contract.e2e.ts) // captures the actual outbound POST. it('AnnotationSource matches spec exactly (no drift; control case)', () => { // Arrange const drift = compareEnum('AnnotationSource', AnnotationSource, snapshot.enums.AnnotationSource.values) // Assert expect(drift.numericMismatches, describeDrift(drift)).toEqual([]) expect(drift.missingFromUi).toEqual([]) expect(drift.extraOnUi).toEqual([]) }) it('the value sets shipped by the wire payload are members of the spec set (not just any number)', () => { // Arrange const annotationStatusSpecSet = new Set(Object.values(snapshot.enums.AnnotationStatus.values)) const annotationSourceSpecSet = new Set(Object.values(snapshot.enums.AnnotationSource.values)) const affiliationSpecSet = new Set(Object.values(snapshot.enums.Affiliation.values)) // Assert — every UI annotation-source value is in the spec set. for (const v of Object.values(AnnotationSource).filter((x): x is number => typeof x === 'number')) { expect(annotationSourceSpecSet.has(v)).toBe(true) } // For drifted enums, the test simply documents that the UI value is NOT // currently in the spec set. We do NOT assert membership (it would // tautologically pass today's drift); instead we document the // membership-set the test will gate on once Step 4 lifts the drift. // The drift-itself test above is the authoritative gate. void annotationStatusSpecSet void affiliationSpecSet }) }) describe('FT-N-15 (rows 20-21) — MediaType magic-literal hygiene (fast counterpart)', () => { // The static profile counterpart in scripts/run-tests.sh runs the regex // sweep across src/. The fast counterpart asserts the typed enum's // SHAPE: every member is a number (not a string), so `mediaType === '1'` // would be a type-level error — strict TS already catches it. We pin the // shape so a future refactor cannot flip the enum to string values without // a deliberate decision. it('MediaType members are numeric (typed enum, not magic strings)', () => { // Arrange const numericMembers = Object.values(MediaType).filter((v): v is number => typeof v === 'number') const stringMembers = Object.values(MediaType).filter((v): v is string => typeof v === 'string') // Assert — all values are numeric reverse-maps; only the string keys are textual. expect(numericMembers.length).toBeGreaterThan(0) // The string side of the reverse map equals the named members. expect(stringMembers.sort()).toEqual(['Image', 'None', 'Video']) }) it.skip('MediaType numeric assignment matches spec — QUARANTINE: snapshot.verification_pending=true (Step 4 .NET inspection pending)', () => { // Arrange const drift = compareEnum('MediaType', MediaType, snapshot.enums.MediaType.values) // Assert (held) expect(drift.numericMismatches, describeDrift(drift)).toEqual([]) }) it('snapshot still flags MediaType verification_pending (alerts when Step 4 lifts)', () => { // Arrange + Assert expect(snapshot.enums.MediaType.verification_pending).toBe(true) }) }) })