#!/usr/bin/env node /** * Conformance runner — Assertion Envelope v0.1 (SPEC.md §9). * * The test set is normative: an implementation that fails it does not conform, * whatever its documentation says. This runner exercises the PRODUCER profile * (schema + the one cross-field rule the schema cannot express) and the * documented CONSUMER tolerance cases. * * Layout: * producer/valid/*.json raw messages — MUST validate * producer/invalid/*.json {reason, message} — message MUST be rejected * consumer/*.json {expectation, message} — tolerance cases * * Exit codes: 0 conform, 1 non-conform. A refusal names its cause. */ import { readFileSync, readdirSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import Ajv2020 from 'ajv/dist/2020.js'; import addFormats from 'ajv-formats'; const here = dirname(fileURLToPath(import.meta.url)); const schema = JSON.parse(readFileSync(join(here, '../schema/assertion.schema.json'), 'utf8')); const ajv = new Ajv2020.default({ allErrors: true, strict: true }); addFormats.default(ajv); const validate = ajv.compile(schema); /** The rule JSON Schema cannot express (SPEC.md §4, field 10). */ function crossFieldErrors(msg) { const errs = []; if (typeof msg?.id === 'string' && typeof msg?.jurisdiction === 'string') { const prefix = msg.id.split('/')[0]; if (prefix !== msg.jurisdiction) errs.push(`jurisdiction "${msg.jurisdiction}" must equal the prefix of id ("${prefix}")`); } return errs; } const lire = (d) => { try { return readdirSync(join(here, d)).filter((f) => f.endsWith('.json')).sort(); } catch { return []; } }; let failures = 0; const fail = (m) => { failures += 1; console.error(` ✗ ${m}`); }; const ok = (m) => console.log(` ✓ ${m}`); console.log('Producer profile — valid messages MUST pass:'); for (const f of lire('producer/valid')) { const msg = JSON.parse(readFileSync(join(here, 'producer/valid', f), 'utf8')); const schemaOk = validate(msg); const cross = crossFieldErrors(msg); if (schemaOk && cross.length === 0) ok(f); else fail(`${f} rejected: ${schemaOk ? cross.join('; ') : ajv.errorsText(validate.errors)}`); } console.log('Producer profile — invalid messages MUST be rejected, for the declared reason:'); for (const f of lire('producer/invalid')) { const { reason, message } = JSON.parse(readFileSync(join(here, 'producer/invalid', f), 'utf8')); const schemaOk = validate(message); const cross = crossFieldErrors(message); if (!schemaOk || cross.length > 0) ok(`${f} (${reason})`); else fail(`${f} ACCEPTED although: ${reason}`); } console.log('Consumer profile — tolerance cases:'); for (const f of lire('consumer')) { const { expectation, message } = JSON.parse(readFileSync(join(here, 'consumer', f), 'utf8')); // v0.1 minimal behavioural check: the message parses, carries an identity and a // version, and nothing in this runner mutates it. Fuller consumer harnesses // (relay round-trips, append-only stores) belong to the implementations. if (typeof message?.envelope === 'string' && typeof message?.id === 'string') ok(`${f} (${expectation})`); else fail(`${f}: a consumer cannot even locate identity and version`); } if (failures > 0) { console.error(`\nNON-CONFORM: ${failures} failure(s).`); process.exit(1); } console.log('\nConform: every fixture behaved as declared.');