import { describe, expect, it } from 'vitest' import { overall, validateReport } from '@extant2000/evidence-record' import { ContractDriftError, fieldReport, coverageReport, formatFields, formatCoverage, assertFields, assertShape, coverage, expectFields, type Parser, } from '../src/index.js' /** Minimal zod-shaped parser so the library stays dependency-free. */ function requireKeys(keys: string[]): Parser> { return { safeParse(data: unknown) { if (data == null || typeof data !== 'object' || Array.isArray(data)) { return { success: false as const, error: { issues: [{ path: [], message: 'expected an object' }] } } } const obj = data as Record const issues = keys.filter(k => !(k in obj)).map(k => ({ path: [k], message: 'required' })) return issues.length ? { success: false as const, error: { issues } } : { success: true as const, data: obj } }, } } describe('assertShape', () => { it('returns the parsed data when the shape holds', () => { expect(assertShape(requireKeys(['id']), { id: 1 }, 'GET /x')).toEqual({ id: 1 }) }) it('throws naming the context and the offending path', () => { try { assertShape(requireKeys(['id', 'name']), { id: 1 }, 'GET /missions') expect.unreachable('should have thrown') } catch (e) { expect(e).toBeInstanceOf(ContractDriftError) const err = e as ContractDriftError expect(err.context).toBe('GET /missions') expect(err.message).toContain('name: required') } }) it('catches null where a list was expected — the empty-table failure', () => { expect(() => assertShape(requireKeys([]), null, 'GET /rows')).toThrow(ContractDriftError) }) it('is NOT a framework HTTP error', () => { // A framework-specific HTTP error would tie the library to one framework. try { assertShape(requireKeys(['a']), {}, 'ctx') } catch (e) { expect(e).toBeInstanceOf(Error) expect((e as { statusCode?: number }).statusCode).toBeUndefined() } }) }) describe('expectFields — the silently-ignored request parameter', () => { // The motivating case: Stripe ignores an unknown `expand` path // rather than rejecting it, so a stale expand returns a VALID invoice that // simply lacks the field. A response-shape validator passes this. const basilInvoice = { id: 'in_1', latest_invoice: { id: 'inv_1', confirmation_secret: { client_secret: 'cs_x' } }, } const preBasilInvoice = { id: 'in_1', latest_invoice: { id: 'inv_1', payment_intent: { client_secret: 'pi_x' } }, } it('detects the field that was requested but never came back', () => { const r = expectFields(basilInvoice, ['latest_invoice.payment_intent']) expect(r.ok).toBe(false) expect(r.missing).toEqual(['latest_invoice.payment_intent']) }) it('passes when the requested field is present', () => { expect(expectFields(preBasilInvoice, ['latest_invoice.payment_intent']).ok).toBe(true) }) it('a schema validator would NOT catch this — the object is valid', () => { // Both shapes are perfectly well-formed invoices. Only asking "did what I // requested come back" distinguishes them. expect(assertShape(requireKeys(['id', 'latest_invoice']), basilInvoice, 'stripe')).toBeTruthy() expect(expectFields(basilInvoice, ['latest_invoice.payment_intent']).ok).toBe(false) }) it('handles a missing intermediate segment without throwing', () => { expect(expectFields({}, ['a.b.c']).missing).toEqual(['a.b.c']) }) it('treats an explicit null as PRESENT — absent and null are different', () => { // null means the provider answered "no value"; undefined means it never // acknowledged the request. Conflating them hides the bug this catches. expect(expectFields({ a: null }, ['a']).ok).toBe(true) }) it('reports partial results, not just a boolean', () => { const r = expectFields(basilInvoice, [ 'latest_invoice.confirmation_secret', 'latest_invoice.payment_intent', ]) expect(r.present).toEqual(['latest_invoice.confirmation_secret']) expect(r.missing).toEqual(['latest_invoice.payment_intent']) }) }) describe('assertFields', () => { it('surfaces the why-note so the failure explains itself', () => { try { assertFields({}, [{ path: 'latest_invoice.payment_intent', why: 'needed to mount Elements' }], 'stripe.subscriptions.create') expect.unreachable('should have thrown') } catch (e) { expect((e as ContractDriftError).message).toContain('needed to mount Elements') } }) it('does not throw when everything requested came back', () => { expect(() => assertFields({ a: 1 }, ['a'], 'ctx')).not.toThrow() }) }) describe('coverage', () => { it('reports which items are uncovered, not just a percentage', () => { const r = coverage(['a', 'b', 'c', 'd'], ['a', 'b'], 75) expect(r.pct).toBe(50) expect(r.uncovered).toEqual(['c', 'd']) expect(r.belowFloor).toBe(true) }) it('passes at exactly the floor', () => { expect(coverage(['a', 'b'], ['a'], 50).belowFloor).toBe(false) }) it('an empty surface is 0%, never a flattering 100', () => { // Nothing to check is not full coverage. const r = coverage([], [], 0) expect(r.pct).toBe(0) expect(r.covered).toBe(0) }) it('ignores checked items that are not part of the surface', () => { expect(coverage(['a'], ['a', 'ghost'], 100).pct).toBe(100) }) }) describe('report — schema pass and assertion fail are different verdicts', () => { it('keeps a valid-object pass separate from a missing-field failure', () => { // The Basil failure: a perfectly valid invoice missing the field that // was asked for. One verdict cannot carry both facts. const r = expectFields({ id: 'in_1', latest_invoice: {} }, [ { path: 'latest_invoice.confirmation_secret', why: 'Elements cannot mount without it' }, 'id', ]) expect(r.ok).toBe(false) const rep = fieldReport(r, { context: 'stripe.subscriptions.create', expectations: [ { path: 'latest_invoice.confirmation_secret', why: 'Elements cannot mount without it' }, ] }) expect(overall(rep)).toBe('fail') const text = formatFields(r, { context: 'stripe.subscriptions.create', expectations: [{ path: 'latest_invoice.confirmation_secret', why: 'Elements cannot mount without it' }], evidence: 'full', }) expect(text).toContain('latest_invoice.confirmation_secret') expect(text).toContain('Elements cannot mount without it') expect(text).toContain('stripe.subscriptions.create') }) it('treats an empty surface as not-assessed, not as 0% coverage', () => { const rep = coverageReport(coverage([], [])) expect(overall(rep)).toBe('not-assessed') expect(formatCoverage(coverage([], []))).toContain('This is not 0% coverage') }) it('names the uncovered endpoints, not just the percentage', () => { const c = coverage(['/a', '/b', '/c'], ['/a'], 80) const text = formatCoverage(c, 80) expect(text).toContain('/b') expect(text).toContain('/c') expect(text).toContain('Below the ratchet floor') }) it('every conclusion carries a citation', () => { const r = expectFields({ id: 'x' }, ['id', 'missing_one']) expect(validateReport(fieldReport(r))).toEqual([]) expect(validateReport(coverageReport(coverage(['/a'], ['/a'])))).toEqual([]) }) }) describe('Parser accepts a Zod 4 schema', () => { it('accepts PropertyKey paths, including symbols', () => { // Zod 4 types issue paths as PropertyKey[]. Narrowing to string|number // made a Zod 4 schema unassignable, and where typecheck gates the // pipeline that mismatch blocks a deploy. const sym = Symbol('secret') const schema = { safeParse: () => ({ success: false as const, error: { issues: [{ path: ['a', 1, sym], message: 'bad' }] }, }), } expect(() => assertShape(schema, {}, 'ctx')).toThrow(ContractDriftError) try { assertShape(schema, {}, 'ctx') } catch (e) { // `.join()` alone throws on a symbol; this must render, not crash. expect((e as ContractDriftError).message).toContain('Symbol(secret)') expect((e as ContractDriftError).message).toContain('a.1.') } }) it('still renders a plain string path unchanged', () => { const schema = { safeParse: () => ({ success: false as const, error: { issues: [{ path: ['latest_invoice', 'id'], message: 'required' }] }, }), } try { assertShape(schema, {}, 'ctx') } catch (e) { expect((e as ContractDriftError).message).toContain('latest_invoice.id: required') } }) })