eleven / ocho Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Tests for web/static/search.js (#586) — the pure matchers and the
2// SearchIndex with an injected decrypt, so everything runs in Node.
3import assert from "assert";
4import { matchText, snippetFor, SearchIndex } from "../web/static/search.js";
5
6function test(name, fn) {
7 return Promise.resolve()
8 .then(fn)
9 .then(() => console.log(`PASS ${name}`))
10 .catch((e) => {
11 console.error(`FAIL ${name}`);
12 console.error(e);
13 process.exit(1);
14 });
15}
16
17await test("matchText: case-insensitive AND across terms", () => {
18 assert.ok(matchText("We ship on Friday", "ship friday"));
19 assert.ok(matchText("We ship on Friday", "SHIP"));
20 assert.ok(!matchText("We ship on Friday", "ship monday"));
21 assert.ok(!matchText("", "x"));
22 assert.ok(!matchText("hello", ""));
23 assert.ok(!matchText("hello", " "), "whitespace-only query matches nothing");
24});
25
26await test("matchText: unicode text matches plainly", () => {
27 assert.ok(matchText("Cámara está lista 🎉", "cámara lista"));
28});
29
30await test("snippetFor: short text passes through; long text windows around the hit", () => {
31 assert.strictEqual(snippetFor("short one", "one"), "short one");
32 const long = "aaa ".repeat(40) + "NEEDLE in the middle " + "bbb ".repeat(40);
33 const snip = snippetFor(long, "needle", 60);
34 assert.ok(snip.includes("NEEDLE"), "hit stays in the window");
35 assert.ok(snip.length <= 62, "bounded (+ellipses)");
36 assert.ok(snip.startsWith("…") && snip.endsWith("…"), "cut edges ellipsised");
37});
38
39const fakeDecrypt = async (row) => {
40 // Rows carry their "plaintext" in-blob for the test: "name|text" or "SKIP".
41 if (row.blob === "SKIP") return null;
42 const [name, text] = row.blob.split("|");
43 return { name, text, ts: row.created_at * 1000 };
44};
45
46await test("SearchIndex: incremental ensure — re-ensuring decrypts each row once", async () => {
47 let decrypts = 0;
48 const idx = new SearchIndex(async (r) => {
49 decrypts++;
50 return fakeDecrypt(r);
51 });
52 await idx.ensureThread("t1", [
53 { id: 1, created_at: 10, blob: "Ana|the launch plan" },
54 { id: 2, created_at: 11, blob: "Bo|lunch at noon" },
55 ]);
56 assert.strictEqual(decrypts, 2);
57 await idx.ensureThread("t1", [
58 { id: 1, created_at: 10, blob: "Ana|the launch plan" },
59 { id: 2, created_at: 11, blob: "Bo|lunch at noon" },
60 { id: 3, created_at: 12, blob: "Ana|launch is a go" },
61 ]);
62 assert.strictEqual(decrypts, 3, "already-seen rows skipped");
63 const hits = idx.query("launch");
64 assert.strictEqual(hits.length, 2);
65 assert.strictEqual(hits[0].id, 3, "newest first");
66});
67
68await test("SearchIndex: OLDER rows arriving later (the backfill pattern) still index", async () => {
69 // The recent window indexes first; a backfill walk then delivers strictly
70 // older pages. A high-water mark would silently drop them — the regression
71 // this test pins (found live by the #586 E2E drive).
72 const idx = new SearchIndex(fakeDecrypt);
73 await idx.ensureThread("t1", [{ id: 100, created_at: 100, blob: "Ana|recent chatter" }]);
74 await idx.ensureThread("t1", [
75 { id: 5, created_at: 5, blob: "Bo|the ancient treasure" },
76 { id: 6, created_at: 6, blob: "Ana|more history" },
77 ]);
78 const hits = idx.query("ancient treasure");
79 assert.strictEqual(hits.length, 1, "backfilled row is searchable");
80 assert.strictEqual(hits[0].id, 5);
81});
82
83await test("SearchIndex: tombstone rows evict their target from the index", async () => {
84 const idx = new SearchIndex(fakeDecrypt);
85 await idx.ensureThread("t1", [
86 { id: 1, created_at: 10, blob: "Ana|delete me later" },
87 { id: 2, created_at: 11, blob: "Bo|keep me" },
88 ]);
89 assert.strictEqual(idx.query("delete me").length, 1);
90 await idx.ensureThread("t1", [{ id: 3, created_at: 12, blob: "", tombstone_of: 1 }]);
91 assert.strictEqual(idx.query("delete me").length, 0, "unsent message stops matching");
92 assert.strictEqual(idx.query("keep me").length, 1);
93});
94
95await test("SearchIndex: undecryptable rows skipped; drops work; cross-thread query + limit", async () => {
96 const idx = new SearchIndex(fakeDecrypt);
97 await idx.ensureThread("a", [
98 { id: 5, created_at: 1, blob: "Ana|target in a" },
99 { id: 6, created_at: 2, blob: "SKIP" },
100 ]);
101 await idx.ensureThread("b", [{ id: 9, created_at: 3, blob: "Bo|target in b" }]);
102 let hits = idx.query("target");
103 assert.deepStrictEqual(hits.map((h) => h.tid), ["b", "a"], "newest id first across threads");
104 idx.dropMessage("b", 9);
105 assert.strictEqual(idx.query("target").length, 1, "live delete evicts");
106 idx.dropThread("a");
107 assert.strictEqual(idx.query("target").length, 0, "leaving a thread evicts it");
108 // limit honoured
109 const many = new SearchIndex(fakeDecrypt);
110 await many.ensureThread("m", Array.from({ length: 60 }, (_, i) => ({ id: i + 1, created_at: i, blob: `N|hit ${i}` })));
111 assert.strictEqual(many.query("hit", 50).length, 50);
112});
113
114console.log("\nALL PASSED");
115