game4 / client / test / frames.test.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// 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)}`);
  }
});

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/game4.git