// Pure-logic tests for the local message store (web/static/store.js, #457). // The IndexedDB plumbing can't run in Node; these pin the decision logic the // plumbing and shell.js rely on: when a snapshot is trusted, how tombstones in // a fetched page translate to store writes, and which cached threads must be // pruned against a live thread list. import assert from "assert"; import { SNAP_VERSION, snapshotUsable, partitionRows, staleThreadIds, backfillCursor, runBackfill } from "../web/static/store.js"; function test(name, fn) { try { fn(); console.log(`PASS ${name}`); } catch (e) { console.error(`FAIL ${name}`); console.error(e); process.exit(1); } } const goodSnap = () => ({ v: SNAP_VERSION, ts: 1, threads: { threads: [{ id: "t1" }] }, roster: { roster: [{ user_id: "u1" }] }, }); test("snapshotUsable accepts a well-formed snapshot (optional parts absent)", () => { assert.strictEqual(snapshotUsable(goodSnap()), true); }); test("snapshotUsable rejects null, wrong version, and missing core responses", () => { assert.strictEqual(snapshotUsable(null), false); assert.strictEqual(snapshotUsable({}), false); assert.strictEqual(snapshotUsable({ ...goodSnap(), v: SNAP_VERSION + 1 }), false, "future format must not hydrate"); assert.strictEqual(snapshotUsable({ ...goodSnap(), threads: null }), false); assert.strictEqual(snapshotUsable({ ...goodSnap(), threads: { threads: "nope" } }), false); assert.strictEqual(snapshotUsable({ ...goodSnap(), roster: {} }), false); }); test("snapshotUsable accepts empty-but-present lists (a brand-new space)", () => { assert.strictEqual(snapshotUsable({ ...goodSnap(), threads: { threads: [] }, roster: { roster: [] } }), true); }); test("partitionRows keeps every row and lists tombstoned targets to drop", () => { const rows = [ { id: 10, blob: "aaa" }, { id: 11, blob: "", tombstone_of: 7 }, { id: 12, blob: "bbb" }, ]; const { puts, unsend } = partitionRows(rows); assert.strictEqual(puts.length, 3, "tombstone rows are stored too — they keep the delete durable locally"); assert.deepStrictEqual(unsend, [7], "the unsent message's ciphertext must drop"); }); test("partitionRows tolerates junk: null rows, id-less rows, empty input", () => { assert.deepStrictEqual(partitionRows(null), { puts: [], unsend: [] }); const { puts, unsend } = partitionRows([null, {}, { id: 0 }, { id: 5, blob: "x" }]); assert.strictEqual(puts.length, 1); assert.deepStrictEqual(unsend, []); }); test("staleThreadIds prunes cached threads missing from the live list", () => { const live = { threads: [{ id: "a" }, { id: "b" }] }; assert.deepStrictEqual(staleThreadIds(["a", "b", "gone"], live), ["gone"]); assert.deepStrictEqual(staleThreadIds(["a", "b"], live), []); }); test("staleThreadIds never prunes on a malformed live list (an error body must not wipe the cache)", () => { assert.deepStrictEqual(staleThreadIds(["a", "b"], null), []); assert.deepStrictEqual(staleThreadIds(["a", "b"], {}), []); assert.deepStrictEqual(staleThreadIds(["a", "b"], { error: "boom" }), []); // An EMPTY live list is well-formed — a member with no threads keeps no rows. assert.deepStrictEqual(staleThreadIds(["a"], { threads: [] }), ["a"]); }); test("backfillCursor: resumes from the persisted watermark, not from scratch", () => { assert.strictEqual(backfillCursor({ oldest: 42, complete: false }, 900), 42, "interrupted walk continues at its mark"); assert.strictEqual(backfillCursor(null, 900), 900, "first walk starts at the oldest stored row"); assert.strictEqual(backfillCursor(null, 0), 0, "empty store → seed a recent page first"); }); test("backfillCursor: a completed thread is never re-walked", () => { assert.strictEqual(backfillCursor({ oldest: 1, complete: true }, 900), -1); assert.strictEqual(backfillCursor({ oldest: 0, complete: true }, 0), -1, "empty-thread completion holds too"); }); // runBackfill is async but runs here: in Node the IDB plumbing degrades to its // best-effort fallbacks (no mark, no stored rows, writes no-op), which is // exactly the fresh-walk shape the stall guard needs. await (async () => { const name = "runBackfill: a page that fails to advance the cursor stops the walk (never spins)"; try { // A buggy server ignores `before` and replays the same non-empty page // forever. The walk's only termination used to be an empty page — this // would loop for good; the monotonic-cursor guard must bail instead. let calls = 0; const page = [{ id: 500, created_at: 1, blob: "x" }, { id: 501, created_at: 2, blob: "y" }]; const r = await runBackfill("t1", async () => { calls++; assert.ok(calls < 10, "walk did not stop on a non-advancing page"); return page; }); assert.deepStrictEqual(r, { complete: false }, "a stalled walk reports incomplete, not complete"); assert.strictEqual(calls, 2, "one seed page + one non-advancing page, then stop"); console.log(`PASS ${name}`); } catch (e) { console.error(`FAIL ${name}`); console.error(e); process.exit(1); } })(); console.log("\nALL PASSED");