/**
* Conformance vector VERIFIER — checks the fixtures against THIS build of
* lib/messages.js.
*
* Two jobs, and the second is the one people forget:
*
* 1. Guard the fixtures. If lib/messages.js changes the wire format, this fails
* loudly instead of the C++ port silently being checked against a protocol
* the JS side no longer speaks. Regenerate deliberately, do not "fix" by
* regenerating reflexively — a diff here means peers on the old version can
* no longer talk to peers on the new one, which is a compatibility decision,
* not a test failure.
*
* 2. Document the contract a port must satisfy. Every check below is a check
* the native implementation has to pass too:
* DECODE — fixture bytes decode to exactly the recorded value
* ENCODE — re-encoding that value reproduces the fixture bytes exactly
* (skipped for `truncated` cases, where a modern encoder
* legitimately appends the trailing field a pre-field peer
* omitted — the decode expectation is the point of those)
*
* Run: node test/conformance/verify.mjs
*/
import { readFileSync } from 'fs'
import { fileURLToPath } from 'url'
import { dirname, join } from 'path'
import * as messages from '../../lib/messages.js'
const __dir = dirname(fileURLToPath(import.meta.url))
const vectors = JSON.parse(readFileSync(join(__dir, 'vectors.json'), 'utf8'))
const pkg = JSON.parse(readFileSync(join(__dir, '../../package.json'), 'utf8'))
function encode (codec, value) {
const state = { start: 0, end: 0, buffer: null }
codec.preencode(state, value)
state.buffer = Buffer.alloc(state.end)
state.start = 0
codec.encode(state, value)
return state.buffer
}
function decode (codec, buf) {
const state = { start: 0, end: buf.length, buffer: Buffer.from(buf) }
return codec.decode(state)
}
function normalise (v) {
if (Buffer.isBuffer(v)) return { $buf: v.toString('hex') }
if (Array.isArray(v)) return v.map(normalise)
if (v && typeof v === 'object') {
const o = {}
for (const k of Object.keys(v).sort()) o[k] = normalise(v[k])
return o
}
return v
}
// The decoded form carries {$buf:hex} for buffers; turn it back into something
// the encoder accepts, so ENCODE is a real round trip and not a tautology.
function denormalise (v) {
if (v && typeof v === 'object' && typeof v.$buf === 'string') return Buffer.from(v.$buf, 'hex')
if (Array.isArray(v)) return v.map(denormalise)
if (v && typeof v === 'object') {
const o = {}
for (const k of Object.keys(v)) o[k] = denormalise(v[k])
return o
}
return v
}
let pass = 0
let fail = 0
const failures = []
if (vectors.overlayVersion !== pkg.version) {
console.log(` note fixtures generated at ${vectors.overlayVersion}, package is ${pkg.version}`)
}
for (const t of vectors.cases) {
const codec = messages[t.message]
const where = `${t.message} / ${t.label}`
if (!codec) {
fail++; failures.push(`${where}: message codec no longer exists`)
continue
}
const bytes = Buffer.from(t.bytes, 'hex')
// DECODE
let got
try {
got = normalise(decode(codec, bytes))
} catch (e) {
fail++; failures.push(`${where}: decode threw — ${e.message}`)
continue
}
if (JSON.stringify(got) !== JSON.stringify(t.decoded)) {
fail++
failures.push(`${where}: decode mismatch\n want ${JSON.stringify(t.decoded)}\n got ${JSON.stringify(got)}`)
continue
}
// ENCODE (byte-exact), except where the fixture models a pre-field peer.
if (!t.truncated) {
let re
try {
re = encode(codec, denormalise(t.decoded))
} catch (e) {
fail++; failures.push(`${where}: re-encode threw — ${e.message}`)
continue
}
if (re.toString('hex') !== t.bytes) {
fail++
failures.push(`${where}: encode mismatch\n want ${t.bytes}\n got ${re.toString('hex')}`)
continue
}
}
pass++
}
const covered = new Set(vectors.cases.map(c => c.message))
const all = Object.keys(messages)
const uncovered = all.filter(m => !covered.has(m))
console.log(`hyper-overlay conformance — ${vectors.protocol} @ ${vectors.overlayVersion}`)
console.log(` ${pass}/${vectors.cases.length} cases pass`)
console.log(` ${covered.size}/${all.length} message types covered`)
if (uncovered.length) console.log(` UNCOVERED: ${uncovered.join(', ')}`)
for (const f of failures) console.log(` FAIL ${f}`)
// An uncovered message is a hole a port can fall through unnoticed, so it fails
// the run rather than printing a warning nobody reads.
const ok = fail === 0 && uncovered.length === 0
console.log(ok ? 'PASS' : 'FAIL')
process.exit(ok ? 0 : 1)