diff --git a/test/conformance.mjs b/test/conformance.mjs new file mode 100644 index 0000000..0d8517c --- /dev/null +++ b/test/conformance.mjs @@ -0,0 +1,54 @@ +// The TypeScript implementation, held to the shared fixtures (novox/hq ADR 0074, design 19). +// +// Run against a NATS server, because the question is what actually reaches the wire: +// +// docker run -d --rm --name c -p 14222:4222 nats:2.10-alpine -js +// node test/conformance.mjs +// +// **The runner lives with the implementation it exercises; the fixture does not.** It is read +// from the sdk's conformance directory by sibling path — the same file the Go suite reads. A +// fixture copied into each implementation is two fixtures, and two fixtures drift, which is the +// failure the suite exists to prevent. +import { readFileSync } from "node:fs"; +import { connect } from "nats"; + +const clientPath = process.argv[2] ?? "../dist/broker-nats.js"; +const { connectNats } = await import(clientPath); +const f = JSON.parse(readFileSync(new URL("../../mesh-sdk/conformance/events/module-event.json", import.meta.url))); +const URL_ = process.env.MESH_TEST_NATS ?? "nats://127.0.0.1:14222"; + +let failed = 0; +const check = (ok, what) => { console.log(` ${ok ? "ok " : "FAIL"} ${what}`); if (!ok) failed++; }; + +const admin = await connect({ servers: URL_ }); +const jsm = await admin.jetstreamManager(); +await jsm.streams.add({ name: "EVENTS", subjects: ["mesh.mod.*.event.>", "mesh.seat.*.event.>"] }); + +const shop = await connectNats({ url: URL_, node: f.given.node, module: f.given.module }); +await shop.publish({ key: f.given.key, node: f.given.node, body: f.given.body, headers: f.given.headers }); + +// What actually landed, read back from the stream rather than from the client that wrote it. +const msg = await jsm.streams.getMessage("EVENTS", { last_by_subj: f.wire.subject }); +check(!!msg, `it lands on ${f.wire.subject}`); + +if (msg) { + const got = {}; + if (msg.header) for (const k of msg.header.keys()) got[k] = msg.header.get(k); + for (const h of f.wire.requiredHeaders) { + check(got[h] !== undefined && got[h] !== "", `${h} is set`); + } + check(got["content-type"] === f.wire.headerFormats["content-type"], "content-type is as pinned"); + check(new RegExp(f.wire.headerFormats["x-event-id"]).test(got["x-event-id"]), "x-event-id is as pinned"); + check(!Number.isNaN(Date.parse(got["x-time"])), "x-time parses as a date"); + check(got["x-source"] === f.given.module, "x-source agrees with the subject's module"); + + const payload = JSON.parse(new TextDecoder().decode(msg.data)); + check(payload.envelope === undefined && payload.key === undefined, + "the payload is the body alone, not the envelope (the fixture refuses nesting)"); + check(JSON.stringify(payload) === JSON.stringify(f.given.body), + "the body round-trips — semantic, not byte-exact, per the README"); +} + +await shop.close(); await admin.close(); +console.log(failed ? `\n${failed} failed` : "\nall passed"); +process.exit(failed ? 1 : 0);