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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295 | /**
* Golden-vector checks for board's OWN layer: the storage namespace, the stroke
* codec and op vocabulary (app/ops), the gesture→op pipeline (app/tools), the
* projector (app/state), the live payload validators, the allow-list access
* layer (lib/{epoch,policy,wrapbox}) and the local board directory.
*
* Every scenario is a scripted replay from test/helpers/app-fakes.mjs — the
* SAME function that produced the fixtures against the TypeScript, so the
* generator and the checker drive identical call shapes and the fixtures stay
* replayable now that the TypeScript is gone.
*
* Run green in BOTH modes (`BOARD_TS=1` against the TypeScript, and by default
* against `test-dist/testlib.js`) before the port landed.
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { installDom, localStorageStub, mkCtx } from "./harness/dom.mjs";
import { mods, priv, readVector, json } from "./helpers/load.mjs";
import { installDetRandom, resetRandom, restoreRandom, hex } from "./helpers/det-random.mjs";
installDom();
const M = await mods();
const F = await import("./helpers/app-fakes.mjs");
const { Identity } = await import("ardegazu-id-kit");
test("the storage namespace: every localStorage key and IndexedDB name", () => {
const v = readVector("config");
assert.equal(M.APP_SALT, v.appSalt);
assert.equal(M.NS, v.ns);
// a changed string here orphans every existing user's identity seed, grants,
// board list, camera and block store
for (const [k, want] of v.keys) assert.equal(M.nsKey(k), want, `nsKey(${k})`);
for (const [k, want] of v.perBoardKeys) assert.equal(M.nsKey(`${k}:ROOMID`), want, `nsKey(${k}:ROOMID)`);
for (const [d, want] of v.dbs) assert.equal(M.nsDb(d), want, `nsDb(${d})`);
});
test("the fixed-point stroke codec and the op sanitisers are byte-identical", async () => {
assert.deepEqual(json(await F.opsScript(M)), readVector("ops"));
});
test("a gesture produces exactly the deployed log ops and live previews", async () => {
installDetRandom();
try {
assert.deepEqual(json(await F.toolsScript(M, priv, resetRandom)), readVector("tools"));
} finally {
restoreRandom();
}
});
test("the projector: z-order, per-field LWW, tombstones, the strict content gate", async () => {
assert.deepEqual(json(await F.projectorScript(M, priv)), readVector("projector"));
});
test("live payloads: the validators, the strict-mode seal boundary, idFields", async () => {
assert.deepEqual(json(await F.liveScript(M)), readVector("live"));
});
test("same-identity device sync accepts a board list only from our own key", async () => {
assert.deepEqual(json(await F.deviceSyncScript(M, Identity, localStorageStub)), readVector("devicesync"));
});
test("the epoch keyring: sealed ops byte-exact, keychain cascade, epoch isolation", async () => {
installDetRandom();
try {
assert.deepEqual(json(await F.epochScript(M, resetRandom, hex)), readVector("epoch"));
} finally {
restoreRandom();
}
});
test("X25519 identity, certs and sealed-box wraps are byte-exact", async () => {
installDetRandom();
try {
assert.deepEqual(json(await F.wrapboxScript(M, Identity, resetRandom, hex)), readVector("wrapbox"));
} finally {
restoreRandom();
}
});
test("the access fold is deterministic and order-independent", async () => {
assert.deepEqual(json(await F.policyScript(M, Identity)), readVector("policy"));
});
test("the TOFU trust store (and its null-prototype name maps)", async () => {
assert.deepEqual(json(await F.trustScript(M, localStorageStub)), readVector("trust"));
});
test("the board directory and the same-identity merge", async () => {
assert.deepEqual(json(await F.boardsScript(M, localStorageStub)), readVector("boards"));
});
test("board's mailbox outgoing queue is uncapped — no silent drop-oldest", async () => {
// The 2000-item drop-oldest (and its per-instance `queueCap` option) is
// RETIRED: the queue holds this user's own undeposited ops, and the suite's
// standing rule is bound WORK, never HISTORY — the cap silently discarded
// the EARLIEST strokes of a long offline session, and raising its threshold
// (board's one sanctioned drift from chat's mailbox) only deferred the loss.
// The "queuecap" fixture recorded the retired TypeScript's drop and went
// with it; this test is CLJS-only by nature — BOARD_TS=1 replays a
// historical record, not this contract.
localStorageStub.clear();
const notices = [];
const m = new M.MailboxSync({
log: F.fakeBoardLog(),
client: { async list() { return []; }, async deposit() {}, async fetch() { return null; } },
cipher: { async seal(b) { return b; }, async open(b) { return b; } },
maxMessageKb: 64,
keys: {
cursor: M.nsKey("mbx-cursor:R"),
queue: M.nsKey("mbx-queue:R"),
sent: M.nsKey("mbx-sent:R"),
retry: M.nsKey("mbx-retry:R"),
ident: M.nsKey("mbx-ident:R"),
},
hasIdentity: true,
onNotice: (msg) => notices.push(msg),
});
const push = priv(m, "push");
// well past both retired caps (chat's 500, board's 2000)
for (let i = 0; i < 2501; i++) push({ t: "entry", hash: `h${i}` });
const q = priv(m, "queue")();
assert.equal(q.length, 2501, "every op survives — nothing was dropped");
assert.equal(q[0].hash, "h0", "the EARLIEST op survives — drop-oldest is gone");
assert.equal(q[q.length - 1].hash, "h2500");
// …and the one remaining way to lose a queued item — localStorage refusing
// the write (quota, private mode) — is SURFACED through onNotice, once per
// session, never swallowed.
const realSetItem = localStorageStub.setItem;
try {
localStorageStub.setItem = () => { throw new Error("QuotaExceededError"); };
priv(m, "dropHead")({ t: "entry", hash: "h0" }); // synchronous persist path
assert.equal(notices.length, 1, "the failed persist must be surfaced");
priv(m, "dropHead")({ t: "entry", hash: "h1" });
assert.equal(notices.length, 1, "…and only once per session");
} finally {
localStorageStub.setItem = realSetItem;
}
});
test("cross-check: the op shapes rooms-kit's headless BoardClient speaks", () => {
// rooms-kit/src/ardegazu/rooms/board.cljs is a SECOND, independent
// implementation of this same app protocol (the bot fleet runs it). Its
// fixture drives its own simplified link-mode fold, so it is not replayable
// against BoardStore (its element ids are not 22-char base64url and it does no
// sanitisation) — what is comparable is the op vocabulary and the element key
// order, which must agree exactly.
const tools = readVector("tools");
const strokeAdd = JSON.parse(tools.pen.appended[0]);
assert.deepEqual(Object.keys(strokeAdd), ["t", "ts", "name", "el"]);
assert.deepEqual(Object.keys(strokeAdd.el), ["id", "k", "x", "y", "pts", "prs", "w", "c"]);
const noPrs = JSON.parse(tools.penNoPressure.appended[0]);
assert.deepEqual(Object.keys(noPrs.el), ["id", "k", "x", "y", "pts", "w", "c"]);
const textAdd = JSON.parse(tools.text.appended[0]);
assert.deepEqual(Object.keys(textAdd.el), ["id", "k", "x", "y", "text", "size", "c"]);
const del = JSON.parse(tools.eraser.appended[0]);
assert.deepEqual(Object.keys(del), ["t", "ts", "name", "ids"]);
const drawFrame = JSON.parse(tools.pen.broadcasts[0][1]);
assert.deepEqual(Object.keys(drawFrame), ["kind", "id", "seg", "w", "c"]);
const doneFrame = JSON.parse(tools.pen.broadcasts.at(-1)[1]);
assert.deepEqual(Object.keys(doneFrame), ["kind", "id", "seg", "w", "c", "done"]);
// live stroke segments are capped at 512 numbers, the same cadence
for (const [, frame] of tools.pen.broadcasts) assert.ok(JSON.parse(frame).seg.length <= 512);
});
test("every wire op and stored record wider than eight keys is accounted for", () => {
// the `#js {}` / `js-obj` eight-pair trap (dev/docs/CLJS.md): these are the
// shapes that route through a PersistentHashMap and emit in HASH order if
// built with a literal. test/source-hygiene.test.mjs forbids the construct
// outright; this test states the widths so the claim is checkable.
const ops = readVector("ops");
const widths = Object.fromEntries(
ops.sanitizeElement.filter((c) => c.keys).map((c) => [c.name, c.keys.length]),
);
assert.equal(widths["img-plain"], 11);
assert.equal(widths["shape-fill"], 10);
assert.equal(widths["stroke-pressure"], 8);
assert.equal(widths["note-plain"], 8);
const patch = ops.sanitizePatch.find((c) => c.name === "all-fields");
assert.equal(patch.keys.length, 13);
});
test("no goog-define reaches the app as the \"\" sentinel", () => {
// THE bug that cost chat's port its first browser boot: a goog-define cannot
// be nil, so "" is the "unset" sentinel — and every API whose own default is a
// nullish check (`some?`, `!= null`) is TRUE for "". id-kit's IdBridge does
// exactly that with `bridgeUrl`, so the sentinel reached `new URL("")`, threw
// "Invalid URL", and took the whole room boot with it. No vector test caught
// it; only the browser did. board.config runs every define through
// `or-default`, and this asserts the result.
const defines = {
APP_SALT: M.APP_SALT,
NS: M.NS,
RELAY_MULTIADDR: M.RELAY_MULTIADDR,
TURN_CREDS_URL: M.TURN_CREDS_URL,
DISCOVERY_TOPIC: M.DISCOVERY_TOPIC,
MAILBOX_URL: M.MAILBOX_URL,
MAILBOX_CREDS_URL: M.MAILBOX_CREDS_URL,
ID_BRIDGE_URL: M.ID_BRIDGE_URL,
};
for (const [k, v] of Object.entries(defines)) {
assert.notEqual(v, "", `${k} is the ""-sentinel — it must be a real value or null`);
assert.ok(v === null || (typeof v === "string" && v.length > 0), `${k} = ${JSON.stringify(v)}`);
}
// the four that must default to a real production value when unset
assert.equal(M.APP_SALT, "board.ardegazu.ro/v1");
assert.match(M.RELAY_MULTIADDR, /^\/(ip4|dns4)\//);
assert.match(M.TURN_CREDS_URL, /^https:\/\//);
assert.equal(M.DISCOVERY_TOPIC, "_peer-discovery._p2p._pubsub");
// and the three that must be null (nullish) when unset, never ""
assert.equal(M.MAILBOX_URL, null);
assert.equal(M.MAILBOX_CREDS_URL, null);
assert.equal(M.ID_BRIDGE_URL, null);
assert.equal(typeof M.APP_VERSION, "number");
});
test("camera math: round-trip, zoom anchoring, clamped restore", () => {
// Property assertions, not fixtures: the TypeScript is gone, so there is
// nothing left to extract from — but these hold for any correct camera and a
// sign error in the port would break every one of them. The camera is
// persisted per board (`<ns>:cam:<roomId>`), so a broken restore is very
// visible and completely silent.
const cam = new M.Camera();
cam.tx = 137;
cam.ty = -42;
cam.z = 2.5;
for (const [wx, wy] of [[0, 0], [10, -20], [1e5, 1e5], [-0.5, 0.25]]) {
const s = cam.worldToScreen(wx, wy);
const w = cam.screenToWorld(s.x, s.y);
assert.ok(Math.abs(w.x - wx) < 1e-9 && Math.abs(w.y - wy) < 1e-9, `round-trip ${wx},${wy}`);
}
assert.deepEqual(Object.keys(cam.worldToScreen(0, 0)), ["x", "y"]);
// zoomAt keeps the world point under the screen anchor fixed
const before = cam.screenToWorld(400, 300);
cam.zoomAt(400, 300, 1.7);
const after = cam.screenToWorld(400, 300);
assert.ok(Math.abs(after.x - before.x) < 1e-9 && Math.abs(after.y - before.y) < 1e-9, "zoom anchor");
assert.ok(cam.z > 2.5, "zoomed in");
// panBy moves the world under the viewport by -d/z
const p0 = cam.screenToWorld(0, 0);
cam.panBy(30, -15);
const p1 = cam.screenToWorld(0, 0);
assert.ok(Math.abs(p1.x - (p0.x - 30 / cam.z)) < 1e-9, "pan x");
assert.ok(Math.abs(p1.y - (p0.y + 15 / cam.z)) < 1e-9, "pan y");
// save/restore is a 3-key record, and restore clamps and rejects garbage
assert.deepEqual(Object.keys(cam.save()), ["tx", "ty", "z"]);
const c2 = new M.Camera();
c2.restore(JSON.parse(JSON.stringify(cam.save())));
assert.deepEqual(c2.save(), JSON.parse(JSON.stringify(cam.save())));
c2.restore({ z: 1e9 });
assert.equal(c2.z, 64, "zoom clamped up");
c2.restore({ z: 0 });
assert.equal(c2.z, 0.02, "zoom clamped down");
const keep = c2.save();
for (const junk of [null, undefined, 7, "x", { tx: "1", ty: Number.NaN, z: Number.POSITIVE_INFINITY }]) {
c2.restore(junk);
assert.deepEqual(c2.save(), keep, `restore(${JSON.stringify(junk) ?? String(junk)}) is a no-op`);
}
// visibleRect is the screen box mapped to world
const r = cam.visibleRect(800, 600);
assert.deepEqual(Object.keys(r), ["x0", "y0", "x1", "y1"]);
assert.deepEqual([r.x0, r.y0], [cam.screenToWorld(0, 0).x, cam.screenToWorld(0, 0).y]);
assert.deepEqual([r.x1, r.y1], [cam.screenToWorld(800, 600).x, cam.screenToWorld(800, 600).y]);
});
test("theme tokens and greedy text wrap", () => {
for (const mode of ["white", "black"]) {
const th = M.themeFor(mode);
assert.equal(th.mode, mode);
assert.equal(th.palette.length, 8);
assert.equal(th.notePastel.length, 8);
// token 0 is "ink" (auto per theme), 1..7 the palette, out of range = ink
assert.equal(M.resolveColor(0, th), th.ink);
assert.equal(M.resolveColor(1, th), th.palette[1]);
assert.equal(M.resolveColor(7, th), th.palette[7]);
assert.equal(M.resolveColor(8, th), th.ink);
assert.equal(M.resolveColor(-1, th), th.ink);
}
// the harness's measureText is 0.6 em/char (a stated constant, never a
// platform font metric — see test/harness/dom.mjs), so the wrap points below
// are a property of the greedy algorithm and not of this machine's fonts
const ctx = mkCtx();
const noWrap = M.layoutText(ctx, "one two three", 10, undefined);
assert.deepEqual([...noWrap.lines], ["one two three"]);
assert.equal(noWrap.h, 13);
const wrapped = M.layoutText(ctx, "one two three four", 10, 60);
assert.ok(wrapped.lines.length > 1, "wrapped");
assert.ok(wrapped.lines.every((l) => l.length * 6 <= 60 || !l.includes(" ")), "each line fits or is one word");
// blank paragraphs survive as empty lines (they are vertical space)
assert.deepEqual([...M.layoutText(ctx, "a\n\nb", 10, undefined).lines], ["a", "", "b"]);
});
|