// Tests for the saved-spaces list helpers (star + independent sort), run in Node. // Same harness style as home/test/crypto.test.mjs. // node home/test/serverlist.test.mjs import assert from "node:assert/strict"; import { partitionServers, partitionArchived, spaceRestoreURL, setArchived, toggleStar, moveInGroup, orderServers, filterServers, fuzzyMatch, soleAdminHold, addTombstone, clearTombstone, tombstonedAt, mergeRemoved, pruneTombstones, } from "../web/static/serverlist.js"; let failures = 0; async function test(name, fn) { try { await fn(); console.log("PASS " + name); } catch (e) { console.log("FAIL " + name + " — " + (e && e.message)); failures++; } } const urls = (list) => list.map((s) => s.url); await test("space links request automatic restore and preserve their query", () => { assert.equal(spaceRestoreURL("https://chat.example.com"), "https://chat.example.com/?restore=1"); assert.equal(spaceRestoreURL("https://chat.example.com/?thread=7"), "https://chat.example.com/?thread=7&restore=1"); assert.equal(spaceRestoreURL("not a url"), "not a url"); }); await test("partitionServers splits starred from the rest, keeping array order", () => { const servers = [ { url: "a", starred: true }, { url: "b" }, { url: "c", starred: true }, { url: "d" }, ]; const { starred, others } = partitionServers(servers); assert.deepEqual(urls(starred), ["a", "c"]); assert.deepEqual(urls(others), ["b", "d"]); }); await test("partitionServers tolerates null/empty", () => { assert.deepEqual(partitionServers(null), { starred: [], others: [] }); assert.deepEqual(partitionServers([]), { starred: [], others: [] }); }); await test("toggleStar adds then removes the flag, and doesn't mutate the input", () => { const servers = [{ url: "a" }, { url: "b" }]; const on = toggleStar(servers, "a"); assert.equal(on.find((s) => s.url === "a").starred, true); assert.equal(servers[0].starred, undefined, "input array/entry must be untouched"); const off = toggleStar(on, "a"); assert.equal("starred" in off.find((s) => s.url === "a"), false, "unstarred entry carries no flag"); }); await test("starring sends the item to the END of the Starred list", () => { // x, y already starred; star 'b' → it lands after them (end of Starred). const servers = [ { url: "x", starred: true }, { url: "y", starred: true }, { url: "a" }, { url: "b" }, { url: "c" }, ]; const { starred, others } = partitionServers(toggleStar(servers, "b")); assert.deepEqual(urls(starred), ["x", "y", "b"], "b appended to Starred"); assert.deepEqual(urls(others), ["a", "c"], "others keep order minus b"); }); await test("unstarring sends the item to the TOP of the rest", () => { // x, y starred; unstar 'x' → it becomes the FIRST of the rest. const servers = [ { url: "x", starred: true }, { url: "y", starred: true }, { url: "a" }, { url: "b" }, ]; const { starred, others } = partitionServers(toggleStar(servers, "x")); assert.deepEqual(urls(starred), ["y"], "x left Starred"); assert.deepEqual(urls(others), ["x", "a", "b"], "x is now the top of the rest"); }); await test("toggleStar leaves unknown urls unchanged", () => { const servers = [{ url: "a", name: "A" }]; assert.deepEqual(toggleStar(servers, "zzz"), servers); }); await test("moveInGroup reorders within a group and never disturbs the other", () => { // starred: [a, c] others: [b, d] (interleaved in the master array) const servers = [ { url: "a", starred: true }, { url: "b" }, { url: "c", starred: true }, { url: "d" }, ]; // Move starred 'c' up: it swaps with 'a' (its nearest starred neighbour), // jumping over the non-starred 'b'. Others keep their order. const moved = moveInGroup(servers, "c", -1); assert.deepEqual(urls(partitionServers(moved).starred), ["c", "a"]); assert.deepEqual(urls(partitionServers(moved).others), ["b", "d"], "other list unchanged"); }); await test("moveInGroup down within the 'others' group skips over starred entries", () => { const servers = [ { url: "b" }, { url: "a", starred: true }, { url: "d" }, ]; const moved = moveInGroup(servers, "b", +1); // b swaps with d, over starred a assert.deepEqual(urls(partitionServers(moved).others), ["d", "b"]); assert.deepEqual(urls(partitionServers(moved).starred), ["a"]); }); await test("moveInGroup at a group boundary is a no-op", () => { const servers = [{ url: "a", starred: true }, { url: "b" }]; assert.deepEqual(moveInGroup(servers, "a", -1), servers, "top of its group can't move up"); assert.deepEqual(moveInGroup(servers, "b", +1), servers, "bottom of its group can't move down"); }); await test("orderServers reorders to match the given full order, preserving entries", () => { const servers = [ { url: "a", starred: true }, { url: "b" }, { url: "c", starred: true }, ]; // A drag result: starred list became [c, a], others [b]. const next = orderServers(servers, ["c", "a", "b"]); assert.deepEqual(urls(next), ["c", "a", "b"]); assert.equal(next[0].starred, true, "entry objects (with starred) are preserved"); assert.equal(servers[0].url, "a", "input is not mutated"); }); await test("orderServers ignores unknown urls and keeps unlisted entries at the end", () => { const servers = [{ url: "a" }, { url: "b" }, { url: "c" }]; const next = orderServers(servers, ["c", "zzz"]); // 'zzz' unknown; a,b unlisted assert.deepEqual(urls(next), ["c", "a", "b"]); }); await test("partitionArchived splits archived from active, keeping array order", () => { const servers = [ { url: "a", starred: true }, { url: "b", archived: true }, { url: "c" }, { url: "d", archived: true }, ]; const { active, archived } = partitionArchived(servers); assert.deepEqual(urls(active), ["a", "c"]); assert.deepEqual(urls(archived), ["b", "d"]); }); await test("partitionArchived tolerates null/empty", () => { assert.deepEqual(partitionArchived(null), { active: [], archived: [] }); assert.deepEqual(partitionArchived([]), { active: [], archived: [] }); }); await test("setArchived archives: sets the flag, drops starred, doesn't mutate input", () => { const servers = [{ url: "a", starred: true }, { url: "b" }]; const next = setArchived(servers, "a", true); const entry = next.find((s) => s.url === "a"); assert.equal(entry.archived, true); assert.equal("starred" in entry, false, "an archived space leaves the rail"); assert.equal(servers[0].starred, true, "input entry untouched"); assert.equal("archived" in servers[0], false, "input entry untouched"); }); await test("archiving sends the item to the TOP of the archived group, active kept in order", () => { // x active, y already archived; archive 'x' → it lands above y (top of archive). const servers = [ { url: "s", starred: true }, { url: "x" }, { url: "z" }, { url: "y", archived: true }, ]; const { active, archived } = partitionArchived(setArchived(servers, "x", true)); assert.deepEqual(urls(active), ["s", "z"], "active keeps order minus x"); assert.deepEqual(urls(archived), ["x", "y"], "x is the top of the archive"); }); await test("unarchiving sends the item to the BOTTOM of the active list", () => { const servers = [ { url: "s", starred: true }, { url: "a" }, { url: "z", archived: true }, { url: "y", archived: true }, ]; const next = setArchived(servers, "z", false); const entry = next.find((s) => s.url === "z"); assert.equal("archived" in entry, false, "unarchived entry carries no flag"); const { active, archived } = partitionArchived(next); assert.deepEqual(urls(active), ["s", "a", "z"], "z rejoins at the bottom, after kept spaces"); assert.deepEqual(urls(archived), ["y"], "y stays archived"); }); await test("setArchived leaves unknown urls unchanged", () => { const servers = [{ url: "a", name: "A" }]; assert.deepEqual(setArchived(servers, "zzz", true), servers); }); await test("fuzzyMatch is a case-insensitive subsequence match", () => { assert.equal(fuzzyMatch("fam", "family space"), true); assert.equal(fuzzyMatch("fly", "family"), true, "f-l-y appear in order"); assert.equal(fuzzyMatch("xyz", "family"), false); assert.equal(fuzzyMatch("", "anything"), true, "empty query matches"); }); await test("filterServers keeps fuzzy matches over name + host, order preserved", () => { const servers = [ { url: "https://family.example.com", name: "Family", starred: true }, { url: "https://work.example.com", name: "Work" }, { url: "https://a4f.11ch.at" }, ]; assert.deepEqual(urls(filterServers(servers, "fam")), ["https://family.example.com"]); assert.deepEqual(urls(filterServers(servers, "11ch")), ["https://a4f.11ch.at"], "matches the host"); assert.deepEqual(urls(filterServers(servers, "")), urls(servers), "empty query = everything"); assert.deepEqual(filterServers(servers, "zzzz"), [], "no matches"); }); await test("soleAdminHold engages only on an explicit sole-admin answer (fail-open)", () => { assert.equal(soleAdminHold({ role: "admin", sole_admin: true }), true, "the one positive case"); assert.equal(soleAdminHold({ role: "admin", sole_admin: false }), false, "other admins exist"); assert.equal(soleAdminHold({ role: "admin" }), false, "older build without the field"); assert.equal(soleAdminHold({ role: "member", sole_admin: true }), false, "field without the role"); assert.equal(soleAdminHold({ role: "admin", sole_admin: "true" }), false, "malformed answer"); assert.equal(soleAdminHold(null), false, "no answer at all"); }); await test("addTombstone upserts newest-per-url without mutating the input", () => { const removed = [{ url: "a", ts: 100 }]; const next = addTombstone(removed, "b", 200); assert.deepEqual( next.map((t) => t.url).sort(), ["a", "b"], ); assert.equal(removed.length, 1, "input untouched"); const bumped = addTombstone(next, "a", 300); assert.equal(tombstonedAt(bumped, "a"), 300, "re-removal keeps the newest ts"); assert.equal(bumped.filter((t) => t.url === "a").length, 1, "one tombstone per url"); }); await test("clearTombstone removes the url; an emptied list drops out entirely", () => { const removed = [{ url: "a", ts: 100 }, { url: "b", ts: 200 }]; assert.deepEqual(clearTombstone(removed, "a").map((t) => t.url), ["b"]); assert.equal(clearTombstone([{ url: "a", ts: 100 }], "a"), undefined, "empty list → undefined, so the blob key disappears"); }); await test("tombstonedAt returns the removal time, 0 when absent or malformed", () => { assert.equal(tombstonedAt([{ url: "a", ts: 42 }], "a"), 42); assert.equal(tombstonedAt([{ url: "a", ts: 42 }], "b"), 0); assert.equal(tombstonedAt(null, "a"), 0); assert.equal(tombstonedAt([{ url: "a", ts: "soon" }], "a"), 0, "malformed ts reads as never"); }); await test("mergeRemoved unions two lists, newest tombstone per url", () => { const merged = mergeRemoved( [{ url: "a", ts: 100 }, { url: "b", ts: 500 }], [{ url: "a", ts: 300 }, { url: "c", ts: 200 }], ); assert.equal(tombstonedAt(merged, "a"), 300, "newer side wins"); assert.equal(tombstonedAt(merged, "b"), 500); assert.equal(tombstonedAt(merged, "c"), 200); assert.equal(merged.length, 3); }); await test("pruneTombstones drops year-old entries and caps the list at the newest 200", () => { const now = 1_000_000_000_000; const year = 365 * 24 * 60 * 60 * 1000; const fresh = { url: "fresh", ts: now - 1000 }; const stale = { url: "stale", ts: now - year - 1 }; assert.deepEqual(pruneTombstones([fresh, stale], now), [fresh]); const many = Array.from({ length: 250 }, (_, i) => ({ url: "u" + i, ts: now - i })); const capped = pruneTombstones(many, now); assert.equal(capped.length, 200); assert.equal(tombstonedAt(capped, "u0"), now, "newest kept"); assert.equal(tombstonedAt(capped, "u249"), 0, "oldest dropped"); }); console.log(failures ? `\n${failures} FAILED` : "\nALL PASSED"); process.exit(failures ? 1 : 0);