// Golden-vector tests: wire frames must serialize byte-identically to the TS
// implementation's (fixtures recorded off the real hostStartBell / hostResolveBell
// / sendSnap / broadcastRoster code paths while the TS was canon).
//
// The key ORDER is the whole point of these, not just the field set: the ten-key
// "sn" frame is past `js-obj`'s eight-pair limit, and a hash-ordered "sn" would
// be structurally valid and would desync every deployed TS client and every bot.
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import * as lib from "../test-dist/testlib.js";
const vectors = JSON.parse(readFileSync(new URL("./vectors/frames.json", import.meta.url), "utf8"));
const byName = Object.fromEntries(vectors.map((v) => [v.name, v]));
const f = (name) => byName[name].frame;
// Each builder is called with the values the fixture recorded, so a key-order
// or renamed-field regression shows up as a byte diff.
const built = {
hi: () => lib.mkHi(f("hi").nick, f("hi").wins),
"ro-solo": () => lib.mkRo(f("ro-solo").host, f("ro-solo").ps),
ro: () => lib.mkRo(f("ro").host, f("ro").ps),
st: () => lib.mkSt(f("st").seance, f("st").order, f("st").candles),
be: () => lib.mkBe(f("be").bell, f("be").durMs, f("be").medium),
hm: () => lib.mkHm(f("hm").not),
pk: () => lib.mkPk(f("pk").r),
pc: () => lib.mkPc(f("pc").n),
rv: () => lib.mkRv(f("rv").bell, f("rv").haunt, f("rv").rooms, f("rv").candles),
ov: () => lib.mkOv(f("ov").w, f("ov").bells, f("ov").ps),
"ov-nowinner": () => lib.mkOv(f("ov-nowinner").w, f("ov-nowinner").bells, f("ov-nowinner").ps),
lo: () => lib.mkLo(),
fu: () => lib.mkFu(),
sn: () => {
const v = f("sn");
return lib.mkSn(v.seance, v.bell, v.walk, v.remainMs, v.medium, v.order, v.candles, v.rooms, v.haunt);
},
};
test("every recorded frame type has a builder under test", () => {
assert.deepEqual(Object.keys(built).sort(), vectors.map((v) => v.name).sort());
});
for (const [name, make] of Object.entries(built)) {
test(`frame ${name} serializes byte-identically to TS`, () => {
assert.equal(JSON.stringify(make()), byName[name].json);
});
}
test("every protocol frame type of the séance is covered", () => {
const types = new Set(vectors.map((v) => v.frame.t));
assert.deepEqual(
[...types].sort(),
["be", "fu", "hi", "hm", "lo", "ov", "pc", "pk", "ro", "rv", "sn", "st"],
);
});
// The eight-pair trap, stated as an assertion rather than a comment: "sn" must
// come out in wire order regardless of how the builder is compiled.
test("sn keeps its literal ten-key order (the js-obj hash-order trap)", () => {
const sn = lib.mkSn(7, 3, 1, 4200, 2, ["a", "b"], [3, 1], [0, 2], -1);
assert.deepEqual(Object.keys(sn), [
"t", "seance", "bell", "walk", "remainMs", "medium", "order", "candles", "rooms", "haunt",
]);
assert.equal(
JSON.stringify(sn),
'{"t":"sn","seance":7,"bell":3,"walk":1,"remainMs":4200,"medium":2,"order":["a","b"],"candles":[3,1],"rooms":[0,2],"haunt":-1}',
);
});
// peer-kit's SeanceClient (the bot fleet's independent implementation) parses
// these exact field names; a rename on either side silently stops the bots.
test("the fields peer-kit's SeanceClient reads are present and named as it expects", () => {
assert.deepEqual(Object.keys(lib.mkSt(1, ["a"], 3)), ["t", "seance", "order", "candles"]);
assert.deepEqual(Object.keys(lib.mkBe(1, 25000, 0)), ["t", "bell", "durMs", "medium"]);
assert.deepEqual(Object.keys(lib.mkHm(2)), ["t", "not"]);
assert.deepEqual(Object.keys(lib.mkRv(1, 0, [0, 1], [3, 2])), ["t", "bell", "haunt", "rooms", "candles"]);
assert.deepEqual(Object.keys(lib.mkOv("a", 4, [])), ["t", "w", "bells", "ps"]);
assert.deepEqual(Object.keys(lib.mkPk(2)), ["t", "r"]);
assert.deepEqual(Object.keys(lib.mkHi("zed", 1)), ["t", "nick", "wins"]);
});
test("isGameFrame accepts frames, rejects junk", () => {
for (const v of vectors) assert.equal(lib.isGameFrame(v.frame), true, v.name);
for (const junk of [null, undefined, 42, "hi", {}, { t: 7 }]) {
assert.equal(Boolean(lib.isGameFrame(junk)), false, `junk: ${JSON.stringify(junk)}`);
}
});