// The late-joiner snapshot, both directions. The host packs its whole loop
// rack into "sn"; the newcomer rebuilds it, dropping loops whose owner has
// already left (the host's own always stay) and keeping its local mutes,
// because loop ids survive the swap. Fixtures recorded off the real
// sendSnap/applySnap while the TS was canon.
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 v = JSON.parse(readFileSync(new URL("./vectors/snapshot.json", import.meta.url), "utf8"));
const plain = (x) => JSON.parse(JSON.stringify(x));
for (const c of v.sendSnap) {
test(`sendSnap ${c.name}: byte-identical to TS`, () => {
const f = c.frame;
assert.equal(JSON.stringify(lib.mkSn(f.bpm, f.org, f.loops)), c.json);
});
}
test("sendSnap carries the transport, so a newcomer lands on the same bar grid", () => {
const c = v.sendSnap.find((e) => e.name === "three-loops");
assert.equal(c.frame.bpm, 132);
assert.equal(c.frame.org, 123456.5); // fractional origins ride intact
assert.equal(c.frame.loops.length, 3);
});
for (const c of v.applySnap) {
test(`applySnap ${c.name}`, () => {
if (c.from !== c.host) {
// not from the host: nothing is adopted at all
assert.equal(c.bpm, 100);
assert.deepEqual(c.loopsAfter.map((l) => l[0]), ["stale.1"]);
assert.deepEqual(c.resyncPings, []);
return;
}
const room = new Set(c.peers);
assert.deepEqual(plain(lib.snapLoops(c.frameLoops, c.from, room)), c.loopsAfter);
// the transport is clamped on the way in (the fixture sent bpm 5000)
assert.equal(lib.clampBpm(5000), c.bpm);
assert.equal(c.origin, 777.25);
// and a fresh clock estimate is requested at 0 / 350 / 900 ms
assert.deepEqual(c.resyncPings, [0, 350, 900]);
});
}
test("a snapshot loop is kept only for a present owner — or for the host itself", () => {
const room = new Set(["B"]);
assert.equal(lib.keepSnapLoop("B", "H", room), true);
assert.equal(lib.keepSnapLoop("C", "H", room), false);
assert.equal(lib.keepSnapLoop("H", "H", new Set()), true, "the host's own always ride along");
});
test("local mutes survive the swap, because loop ids do", () => {
for (const c of v.applySnap) {
assert.ok(c.mutedAfter.includes("BBBBBB.1"), c.name);
assert.ok(c.mutedAfter.includes("gone.9"), `${c.name}: a mute for a vanished loop is harmless`);
}
});
test("the per-player quota applies while adopting a snapshot too", () => {
const many = Array.from({ length: 5 }, (_, k) => [`B.${k}`, "B", 0, 2, [[k, 60, 90, 1]]]);
const kept = plain(lib.snapLoops(many, "H", new Set(["B"])));
assert.deepEqual(kept.map((l) => l[0]), ["B.2", "B.3", "B.4"]);
});
test("snapshot instruments are normalised, bar counts are NOT (unlike lp)", () => {
const odd = [["x.1", "B", 9, 3, []]];
const kept = plain(lib.snapLoops(odd, "H", new Set(["B"])));
assert.equal(kept[0][2], 4, "instrument 9 becomes the audience code");
assert.equal(kept[0][3], 3, "a 3-bar length rides through verbatim, as the TS did");
});