eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1// Pure-logic tests for the local message store (web/static/store.js, #457).
2// The IndexedDB plumbing can't run in Node; these pin the decision logic the
3// plumbing and shell.js rely on: when a snapshot is trusted, how tombstones in
4// a fetched page translate to store writes, and which cached threads must be
5// pruned against a live thread list.
6import assert from "assert";
7import { SNAP_VERSION, snapshotUsable, partitionRows, staleThreadIds, backfillCursor, runBackfill } from "../web/static/store.js";
8
9function test(name, fn) {
10 try {
11 fn();
12 console.log(`PASS ${name}`);
13 } catch (e) {
14 console.error(`FAIL ${name}`);
15 console.error(e);
16 process.exit(1);
17 }
18}
19
20const goodSnap = () => ({
21 v: SNAP_VERSION,
22 ts: 1,
23 threads: { threads: [{ id: "t1" }] },
24 roster: { roster: [{ user_id: "u1" }] },
25});
26
27test("snapshotUsable accepts a well-formed snapshot (optional parts absent)", () => {
28 assert.strictEqual(snapshotUsable(goodSnap()), true);
29});
30
31test("snapshotUsable rejects null, wrong version, and missing core responses", () => {
32 assert.strictEqual(snapshotUsable(null), false);
33 assert.strictEqual(snapshotUsable({}), false);
34 assert.strictEqual(snapshotUsable({ ...goodSnap(), v: SNAP_VERSION + 1 }), false, "future format must not hydrate");
35 assert.strictEqual(snapshotUsable({ ...goodSnap(), threads: null }), false);
36 assert.strictEqual(snapshotUsable({ ...goodSnap(), threads: { threads: "nope" } }), false);
37 assert.strictEqual(snapshotUsable({ ...goodSnap(), roster: {} }), false);
38});
39
40test("snapshotUsable accepts empty-but-present lists (a brand-new space)", () => {
41 assert.strictEqual(snapshotUsable({ ...goodSnap(), threads: { threads: [] }, roster: { roster: [] } }), true);
42});
43
44test("partitionRows keeps every row and lists tombstoned targets to drop", () => {
45 const rows = [
46 { id: 10, blob: "aaa" },
47 { id: 11, blob: "", tombstone_of: 7 },
48 { id: 12, blob: "bbb" },
49 ];
50 const { puts, unsend } = partitionRows(rows);
51 assert.strictEqual(puts.length, 3, "tombstone rows are stored too — they keep the delete durable locally");
52 assert.deepStrictEqual(unsend, [7], "the unsent message's ciphertext must drop");
53});
54
55test("partitionRows tolerates junk: null rows, id-less rows, empty input", () => {
56 assert.deepStrictEqual(partitionRows(null), { puts: [], unsend: [] });
57 const { puts, unsend } = partitionRows([null, {}, { id: 0 }, { id: 5, blob: "x" }]);
58 assert.strictEqual(puts.length, 1);
59 assert.deepStrictEqual(unsend, []);
60});
61
62test("staleThreadIds prunes cached threads missing from the live list", () => {
63 const live = { threads: [{ id: "a" }, { id: "b" }] };
64 assert.deepStrictEqual(staleThreadIds(["a", "b", "gone"], live), ["gone"]);
65 assert.deepStrictEqual(staleThreadIds(["a", "b"], live), []);
66});
67
68test("staleThreadIds never prunes on a malformed live list (an error body must not wipe the cache)", () => {
69 assert.deepStrictEqual(staleThreadIds(["a", "b"], null), []);
70 assert.deepStrictEqual(staleThreadIds(["a", "b"], {}), []);
71 assert.deepStrictEqual(staleThreadIds(["a", "b"], { error: "boom" }), []);
72 // An EMPTY live list is well-formed — a member with no threads keeps no rows.
73 assert.deepStrictEqual(staleThreadIds(["a"], { threads: [] }), ["a"]);
74});
75
76test("backfillCursor: resumes from the persisted watermark, not from scratch", () => {
77 assert.strictEqual(backfillCursor({ oldest: 42, complete: false }, 900), 42, "interrupted walk continues at its mark");
78 assert.strictEqual(backfillCursor(null, 900), 900, "first walk starts at the oldest stored row");
79 assert.strictEqual(backfillCursor(null, 0), 0, "empty store → seed a recent page first");
80});
81
82test("backfillCursor: a completed thread is never re-walked", () => {
83 assert.strictEqual(backfillCursor({ oldest: 1, complete: true }, 900), -1);
84 assert.strictEqual(backfillCursor({ oldest: 0, complete: true }, 0), -1, "empty-thread completion holds too");
85});
86
87// runBackfill is async but runs here: in Node the IDB plumbing degrades to its
88// best-effort fallbacks (no mark, no stored rows, writes no-op), which is
89// exactly the fresh-walk shape the stall guard needs.
90await (async () => {
91 const name = "runBackfill: a page that fails to advance the cursor stops the walk (never spins)";
92 try {
93 // A buggy server ignores `before` and replays the same non-empty page
94 // forever. The walk's only termination used to be an empty page — this
95 // would loop for good; the monotonic-cursor guard must bail instead.
96 let calls = 0;
97 const page = [{ id: 500, created_at: 1, blob: "x" }, { id: 501, created_at: 2, blob: "y" }];
98 const r = await runBackfill("t1", async () => {
99 calls++;
100 assert.ok(calls < 10, "walk did not stop on a non-advancing page");
101 return page;
102 });
103 assert.deepStrictEqual(r, { complete: false }, "a stalled walk reports incomplete, not complete");
104 assert.strictEqual(calls, 2, "one seed page + one non-advancing page, then stop");
105 console.log(`PASS ${name}`);
106 } catch (e) {
107 console.error(`FAIL ${name}`);
108 console.error(e);
109 process.exit(1);
110 }
111})();
112
113console.log("\nALL PASSED");
114