// Golden-vector tests: wire frames must serialize byte-identically to the TS
// implementation's (fixtures recorded off the real hostTick/sendSnap/
// broadcastRoster code paths while the TS was canon). These are also the
// frames peer-kit's LampionClient parses, so a key-order or omitted-field
// regression here desyncs the bot fleet as well as the deployed clients.
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 lvl = (l) => lib.mkLevel(l.stairs, l.rots, l.auto);
// Each builder is called with the values the fixture recorded, so a key-order
// or omitted-field regression shows up as a byte diff.
const built = {
"ro-idle": (f) => lib.mkRo(f.host, f.ps, false),
"ro-live": (f) => lib.mkRo(f.host, f.ps, true),
hi: (f) => lib.mkHi(f.nick, f.score),
in: (f) => lib.mkIn(f.a),
fu: () => lib.mkFu(),
st: (f) => lib.mkSt(f.round, f.tickMs, f.ticks, f.order, lvl(f.level), f.starts),
"tk-bare": (f) => lib.mkTk(f.n, null, null, null),
"tk-mv": (f) => lib.mkTk(f.n, f.mv, null, null),
"tk-rt-auto": (f) => lib.mkTk(f.n, null, f.rt, null),
"tk-rt-player": (f) => lib.mkTk(f.n, null, f.rt, null),
"tk-fp": (f) => lib.mkTk(f.n, null, null, f.fp),
"tk-mv-rt-fp": (f) => lib.mkTk(f.n, f.mv, f.rt, f.fp),
ov: (f) => lib.mkOv(f.w, f.tops, f.scores),
"ov-draw": (f) => lib.mkOv(f.w, f.tops, f.scores),
ma: (f) => lib.mkMa(f.w, f.scores),
lo: () => lib.mkLo(),
sn: (f) => lib.mkSn(f.round, f.tickMs, f.ticks, f.n, f.order, lvl(f.level), f.rots, f.ps),
};
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].frame)), byName[name].json);
});
}
// The trap CLJS.md warns about, pinned here as well as in the byte vector:
// "sn" carries NINE pairs, one past js-obj's ordered threshold.
test("sn key order is the wire order, all nine fields", () => {
const f = lib.mkSn(1, 125, 720, 7, ["a"], lib.mkLevel([[0]], [], [[0, 1]]), [3], [[0, 0, 1]]);
assert.deepEqual(Object.keys(f), [
"t", "round", "tickMs", "ticks", "n", "order", "level", "rots", "ps",
]);
assert.equal(
JSON.stringify(f),
'{"t":"sn","round":1,"tickMs":125,"ticks":720,"n":7,"order":["a"],' +
'"level":{"stairs":[[0]],"rots":[],"auto":[[0,1]]},"rots":[3],"ps":[[0,0,1]]}',
);
});
test("st key order is the wire order", () => {
assert.deepEqual(Object.keys(lib.mkSt(1, 125, 720, [], lib.mkLevel([], [], []), [])), [
"t", "round", "tickMs", "ticks", "order", "level", "starts",
]);
});
test("tk omits mv, rt and fp when empty, exactly as the TS spreads did", () => {
assert.equal(JSON.stringify(lib.mkTk(4, [], [], null)), '{"t":"tk","n":4}');
assert.equal(JSON.stringify(lib.mkTk(4, null, null, null)), '{"t":"tk","n":4}');
// ...and carries them, in wire order t,n,mv,rt,fp, when present
assert.equal(
JSON.stringify(lib.mkTk(4, [[0, 1, 2]], [[1, -1, 0]], [[1, 2, 1]])),
'{"t":"tk","n":4,"mv":[[0,1,2]],"rt":[[1,-1,0]],"fp":[[1,2,1]]}',
);
// an fp of an empty roster is still SENT (the TS tested `fp ?` truthiness,
// not its length) — only mv/rt were length-gated
assert.equal(JSON.stringify(lib.mkTk(16, [], [], [])), '{"t":"tk","n":16,"fp":[]}');
});
test("ro omits live unless the sender is host mid-round", () => {
assert.ok(!JSON.stringify(lib.mkRo("h", [], false)).includes("live"));
assert.equal(JSON.stringify(lib.mkRo("h", [], true)), '{"t":"ro","host":"h","ps":[],"live":1}');
});
test("ov carries a null winner for a shared night", () => {
assert.equal(JSON.stringify(lib.mkOv(null, [0, 0], [])), '{"t":"ov","w":null,"tops":[0,0],"scores":[]}');
});
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)}`);
}
});