eleven / messenger Public

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

Plain git — no account needed to clone.

Download

Download this file

1// πŸ€– Tests for the app-schema engine (web/static/schema-engine.js) and the
2// todo/remind schema files. Three layers of proof:
3// 1. The schema fold reproduces the shared event vectors (the cross-platform
4// spec native also tests against).
5// 2. The schema fold is EXACTLY the hand-written reducer: randomized event
6// sequences through both must converge to deep-equal state.
7// 3. Views/actions: partition, ordering, search, templates, and action steps
8// (emit / schedule / unschedule / parse) do what the schema says.
9// Plus a drift gate: the schema copies bundled into the native package
10// (ios/LChatCore/Sources/LChatCore/Schemas/) must be byte-identical to the
11// canonical files in web/static/apps/schemas/.
12import assert from "assert";
13import { readFileSync } from "fs";
14import { fileURLToPath } from "url";
15import { dirname, join } from "path";
16import {
17 foldSchemaEvent,
18 initialState,
19 partitionState,
20 listModel,
21 rowModel,
22 detailModel,
23 chatProjection,
24 runAction,
25 tickerRules,
26 evalTemplate,
27 relTime,
28} from "../web/static/schema-engine.js";
29import { todoVectors, remindVectors } from "./event-vectors/index.js";
30import { app as todoApp } from "../web/static/apps/todo.js";
31import { app as remindApp, parseReminder } from "../web/static/apps/remind.js";
32
33const __dirname = dirname(fileURLToPath(import.meta.url));
34const schemaDir = join(__dirname, "..", "web", "static", "apps", "schemas");
35const todoSchema = JSON.parse(readFileSync(join(schemaDir, "todo.json"), "utf8"));
36const remindSchema = JSON.parse(readFileSync(join(schemaDir, "remind.json"), "utf8"));
37
38let passed = 0;
39async function ok(name, fn) {
40 await fn();
41 passed++;
42 console.log(" βœ“ " + name);
43}
44
45// ---- 1. the schema fold reproduces the shared event vectors ---------------
46
47function runVectors(schema, vectors) {
48 for (const v of vectors) {
49 if (!v.event && !v.event_sequence) continue; // doc-only vectors (grace constants)
50 let state = v.initial_state || initialState(schema);
51 const events = v.event_sequence || [v.event];
52 for (const e of events) state = foldSchemaEvent(schema, state, e);
53 const expected = v.expected_final_state || v.expected_state_update;
54 assert.deepStrictEqual(state, expected, `vector ${v.name}`);
55 }
56}
57
58console.log("schema fold vs event vectors:");
59await ok("todo vectors", () => runVectors(todoSchema, todoVectors.vectors));
60await ok("remind vectors", () => runVectors(remindSchema, remindVectors.vectors));
61
62// ---- 2. the app's registered fold IS the schema fold ------------------------
63// todo.js/remind.js now WIRE their events map from the schema (schemaEvents),
64// so this fuzz is no longer two independent implementations racing β€” it pins
65// the defineApp wiring: every schema event type registered, folded through the
66// engine, malformed events no-op'd identically. (It caught real drift back
67// when the reducers were hand-written; keep it β€” it's the wiring's regression
68// net, and it goes back to twin-hunting if an app ever hand-writes a fold.)
69
70function mulberry32(seed) {
71 let a = seed;
72 return () => {
73 a |= 0;
74 a = (a + 0x6d2b79f5) | 0;
75 let t = Math.imul(a ^ (a >>> 15), 1 | a);
76 t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
77 return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
78 };
79}
80
81function fuzzParity(schema, app, makeEvent, rounds = 400) {
82 const rnd = mulberry32(11);
83 let schemaState = initialState(schema);
84 let appState = app.initialState();
85 for (let i = 0; i < rounds; i++) {
86 const e = makeEvent(rnd, i);
87 schemaState = foldSchemaEvent(schema, schemaState, e);
88 appState = app.reduce(appState, e);
89 assert.deepStrictEqual(schemaState, appState, `diverged at event ${i}: ${JSON.stringify(e)}`);
90 }
91}
92
93console.log("schema fold vs hand-written reducer (fuzz):");
94await ok("todo parity", () => {
95 fuzzParity(todoSchema, todoApp, (rnd, i) => {
96 const id = "t" + Math.floor(rnd() * 12);
97 const pick = rnd();
98 if (pick < 0.4) {
99 // Some adds are malformed (no text) and must no-op identically.
100 return { type: "todo.add", id, text: rnd() < 0.85 ? "task " + i : "", name: rnd() < 0.5 ? "Ana" : undefined, ts: 1700000000000 + i };
101 }
102 if (pick < 0.6) return { type: "todo.done", id, ts: rnd() < 0.8 ? 1700000000000 + i : undefined };
103 if (pick < 0.75) return { type: "todo.reopen", id };
104 if (pick < 0.85) return { type: "todo.remove", id };
105 if (pick < 0.95) {
106 // Moves anchor to another item, the start (""), or a missing anchor.
107 const roll = rnd();
108 const after = roll < 0.5 ? "t" + Math.floor(rnd() * 12) : roll < 0.75 ? "" : "gone";
109 return { type: "todo.move", id, after };
110 }
111 return { type: "unknown.event", id };
112 });
113});
114await ok("remind parity", () => {
115 fuzzParity(remindSchema, remindApp, (rnd, i) => {
116 const id = "r" + Math.floor(rnd() * 12);
117 const pick = rnd();
118 if (pick < 0.5) {
119 return {
120 type: "remind.add",
121 id: rnd() < 0.9 ? id : "",
122 text: rnd() < 0.9 ? "thing " + i : "",
123 when: rnd() < 0.9 ? 1700000000000 + Math.floor(rnd() * 1e7) : 0,
124 ts: rnd() < 0.8 ? 1700000000000 + i : undefined,
125 sid: rnd() < 0.7 ? "sch-" + i : undefined,
126 };
127 }
128 if (pick < 0.75) return { type: "remind.fire", id, ts: rnd() < 0.8 ? 1700000000000 + i : undefined };
129 return { type: "remind.done", id };
130 });
131});
132
133// ---- chat projections match the app's chatMessage hook ---------------------
134
135console.log("chat projections:");
136await ok("todo.add projects, others silent", () => {
137 const e = { type: "todo.add", id: "a", text: "buy milk" };
138 assert.deepStrictEqual(chatProjection(todoSchema, e), todoApp.chatMessage(e));
139 assert.strictEqual(chatProjection(todoSchema, { type: "todo.add", id: "a", text: "" }), null);
140 assert.strictEqual(chatProjection(todoSchema, { type: "todo.done", id: "a" }), null);
141});
142await ok("remind.fire personal vs group wording", () => {
143 const e = { type: "remind.fire", id: "r", notice: { title: "⏰ Reminder", body: "call mum" } };
144 assert.strictEqual(chatProjection(remindSchema, e), remindApp.chatMessage(e));
145 assert.strictEqual(chatProjection(remindSchema, e, { group: true }), remindApp.chatMessage(e, { group: true }));
146});
147await ok("remind.add action line", () => {
148 const e = { type: "remind.add", id: "r", text: "call mum", when: 1893499200000 };
149 assert.deepStrictEqual(chatProjection(remindSchema, e), remindApp.chatMessage(e));
150});
151
152// ---- 3. views: partition, ordering, search, templates ----------------------
153
154console.log("list model:");
155const NOW = 1700000100000;
156
157await ok("todo partition + section order", () => {
158 let s = initialState(todoSchema);
159 for (const [id, text] of [["a", "one"], ["b", "two"], ["c", "three"]]) {
160 s = foldSchemaEvent(todoSchema, s, { type: "todo.add", id, text, name: "Ana" });
161 }
162 // b: archived (done long ago); c: in grace (done 200ms ago).
163 s = foldSchemaEvent(todoSchema, s, { type: "todo.done", id: "b", ts: NOW - 60000 });
164 s = foldSchemaEvent(todoSchema, s, { type: "todo.done", id: "c", ts: NOW - 200 });
165 const m = listModel(todoSchema, s, NOW);
166 assert.strictEqual(m.summary, "1 left");
167 const open = m.sections.find((x) => x.id === "open");
168 // Newest-first, in place: c (grace, still visible) then a.
169 assert.deepStrictEqual(open.rows.map((r) => r.item.id), ["c", "a"]);
170 assert.deepStrictEqual(open.rows.map((r) => r.grace), [true, false]);
171 const done = m.sections.find((x) => x.id === "completed");
172 assert.strictEqual(done.title, "Completed (1)");
173 assert.strictEqual(done.collapsed, true);
174 assert.deepStrictEqual(done.rows.map((r) => r.item.id), ["b"]);
175});
176
177await ok("todo reorder: move fold op + the section's reorder surface", () => {
178 let s = initialState(todoSchema);
179 for (const [id, text] of [["a", "one"], ["b", "two"], ["c", "three"]]) {
180 s = foldSchemaEvent(todoSchema, s, { type: "todo.add", id, text, name: "Ana" });
181 }
182 // Visual (newest-first): c, b, a. Drag c below b = c now follows a in the
183 // array β†’ array a, c, b β†’ visual b, c, a.
184 s = foldSchemaEvent(todoSchema, s, { type: "todo.move", id: "c", after: "a" });
185 assert.deepStrictEqual(s.items.map((i) => i.id), ["a", "c", "b"]);
186 const m = listModel(todoSchema, s, NOW);
187 const open = m.sections.find((x) => x.id === "open");
188 assert.deepStrictEqual(open.rows.map((r) => r.item.id), ["b", "c", "a"]);
189 // The open section is reorderable (newest-first); Completed is not.
190 assert.strictEqual(open.reorder, "move");
191 assert.strictEqual(open.descending, true);
192 assert.strictEqual(m.sections.find((x) => x.id === "completed").reorder, "");
193 // Searching suppresses reorder β€” a filtered list has no meaningful drop.
194 const filtered = listModel(todoSchema, s, NOW, { query: "t" });
195 assert.strictEqual(filtered.sections.find((x) => x.id === "open").reorder, "");
196 // Anchor to the start, and an unknown anchor no-ops.
197 s = foldSchemaEvent(todoSchema, s, { type: "todo.move", id: "b", after: "" });
198 assert.deepStrictEqual(s.items.map((i) => i.id), ["b", "a", "c"]);
199 s = foldSchemaEvent(todoSchema, s, { type: "todo.move", id: "a", after: "gone" });
200 assert.deepStrictEqual(s.items.map((i) => i.id), ["b", "a", "c"]);
201});
202
203await ok("todo summary cases", () => {
204 const empty = listModel(todoSchema, initialState(todoSchema), NOW);
205 assert.strictEqual(empty.summary, "No to-dos yet β€” add one above");
206 let s = foldSchemaEvent(todoSchema, initialState(todoSchema), { type: "todo.add", id: "a", text: "x" });
207 s = foldSchemaEvent(todoSchema, s, { type: "todo.done", id: "a", ts: NOW - 60000 });
208 assert.strictEqual(listModel(todoSchema, s, NOW).summary, "all done πŸŽ‰");
209});
210
211await ok("undefined state reads as the empty state (virgin thread)", () => {
212 // Regression: the host only seeds state on the first fold, so a never-used
213 // app thread hands the renderer `undefined` β€” the engine's entry points must
214 // treat that as empty, not throw ("undefined is not an object" on the
215 // Interface view of a fresh To-do/Reminders thread).
216 for (const schema of [todoSchema, remindSchema]) {
217 assert.deepStrictEqual(listModel(schema, undefined, NOW), listModel(schema, initialState(schema), NOW));
218 assert.deepStrictEqual(partitionState(schema, undefined, NOW), { active: [], grace: [], archived: [] });
219 assert.deepStrictEqual(tickerRules(schema, undefined, NOW, new Set()), []);
220 }
221});
222
223await ok("search filters rows across sections", () => {
224 let s = initialState(todoSchema);
225 s = foldSchemaEvent(todoSchema, s, { type: "todo.add", id: "a", text: "buy milk" });
226 s = foldSchemaEvent(todoSchema, s, { type: "todo.add", id: "b", text: "call mum" });
227 const m = listModel(todoSchema, s, NOW, { query: "milk" });
228 assert.deepStrictEqual(m.sections[0].rows.map((r) => r.item.id), ["a"]);
229});
230
231await ok("remind sections: upcoming asc, settling, previous desc + empty lines", () => {
232 let s = initialState(remindSchema);
233 s = foldSchemaEvent(remindSchema, s, { type: "remind.add", id: "late", text: "late", when: NOW + 7200000, ts: 1, sid: "s1" });
234 s = foldSchemaEvent(remindSchema, s, { type: "remind.add", id: "soon", text: "soon", when: NOW + 60000, ts: 2, sid: "s2" });
235 s = foldSchemaEvent(remindSchema, s, { type: "remind.add", id: "old", text: "old", when: NOW - 90000, ts: 3, sid: "s3" });
236 s = foldSchemaEvent(remindSchema, s, { type: "remind.fire", id: "old", ts: NOW - 60000 }); // archived (grace 6s)
237 const m = listModel(remindSchema, s, NOW);
238 assert.deepStrictEqual(m.sections.find((x) => x.id === "upcoming").rows.map((r) => r.item.id), ["soon", "late"]);
239 assert.strictEqual(m.sections.find((x) => x.id === "previous").title, "Previous reminders (1)");
240 assert.strictEqual(m.empty, "");
241 // All dismissed β†’ the archived-aware empty line.
242 let s2 = foldSchemaEvent(remindSchema, s, { type: "remind.done", id: "late" });
243 s2 = foldSchemaEvent(remindSchema, s2, { type: "remind.done", id: "soon" });
244 assert.strictEqual(listModel(remindSchema, s2, NOW).empty, "No upcoming reminders.");
245 assert.strictEqual(listModel(remindSchema, initialState(remindSchema), NOW).empty, "No reminders yet. Add one above, or type /remind in any chat.");
246});
247
248await ok("remind row model: reltime, due accent, abs note past a day", () => {
249 const upcoming = { id: "a", text: "call", when: NOW + 2 * 86400000, ts: 1, sid: "s", fired: false, firedAt: 0 };
250 let r = rowModel(remindSchema, upcoming, NOW);
251 assert.strictEqual(r.title, "call");
252 assert.strictEqual(r.subtitle, "in 2d");
253 assert.strictEqual(r.accent, false);
254 assert.notStrictEqual(r.note, ""); // absolute time shown when >1d out
255 const due = { ...upcoming, when: NOW - 120000 };
256 r = rowModel(remindSchema, due, NOW);
257 assert.strictEqual(r.subtitle, "2m ago");
258 assert.strictEqual(r.accent, true);
259 assert.strictEqual(r.note, "");
260 const fired = { ...upcoming, fired: true, firedAt: NOW - 300000 };
261 r = rowModel(remindSchema, fired, NOW);
262 assert.strictEqual(r.subtitle, "Reminded Β· 5m ago");
263 assert.strictEqual(r.strike, true);
264});
265
266await ok("todo detail model", () => {
267 const d = detailModel(todoSchema, { id: "a", text: "buy milk", done: false, doneAt: 0, by: "Ana" }, NOW);
268 assert.strictEqual(d.title, "buy milk");
269 const defs = d.sections.find((x) => x.kind === "definitions");
270 assert.deepStrictEqual(defs.rows, [
271 { term: "Status", value: "To do" },
272 { term: "Added by", value: "Ana" },
273 ]);
274 const buttons = d.sections.find((x) => x.kind === "buttons");
275 assert.deepStrictEqual(buttons.items.map((b) => b.label), ["Mark done", "Remove"]);
276});
277
278await ok("relTime shapes", () => {
279 assert.strictEqual(relTime(NOW + 20000, NOW), "now");
280 assert.strictEqual(relTime(NOW + 45 * 60000, NOW), "in 45m");
281 assert.strictEqual(relTime(NOW - 3 * 3600000, NOW), "3h ago");
282 assert.strictEqual(relTime(NOW + 3 * 86400000, NOW), "in 3d");
283});
284
285// ---- actions ----------------------------------------------------------------
286
287console.log("actions:");
288
289function fakeIO(overrides = {}) {
290 const io = {
291 emitted: [],
292 scheduled: [],
293 unscheduled: [],
294 hints: [],
295 nextId: "id-1",
296 emit(e) {
297 io.emitted.push(e);
298 return Promise.resolve(true);
299 },
300 schedule(e, at) {
301 io.scheduled.push({ event: e, at });
302 return Promise.resolve("sid-9");
303 },
304 unschedule(sid) {
305 io.unscheduled.push(sid);
306 return Promise.resolve(true);
307 },
308 newId: () => io.nextId,
309 parse: (kind, text) => (kind === "reminder" ? parseReminder(text, NOW) : null),
310 hint: (t) => io.hints.push(t),
311 ...overrides,
312 };
313 return io;
314}
315
316await ok("todo add / toggle / remove", async () => {
317 const io = fakeIO();
318 assert.strictEqual(await runAction(todoSchema, "add", { input: "buy milk" }, io), true);
319 assert.deepStrictEqual(io.emitted, [{ type: "todo.add", id: "id-1", text: "buy milk" }]);
320 assert.strictEqual(await runAction(todoSchema, "add", { input: "" }, io), false);
321 io.emitted.length = 0;
322 await runAction(todoSchema, "toggle", { item: { id: "a", done: false } }, io);
323 await runAction(todoSchema, "toggle", { item: { id: "a", done: true } }, io);
324 await runAction(todoSchema, "remove", { item: { id: "a", done: false } }, io);
325 assert.deepStrictEqual(io.emitted.map((e) => e.type), ["todo.done", "todo.reopen", "todo.remove"]);
326});
327
328await ok("remind add: parse β†’ schedule β†’ emit shares one id and records the sid", async () => {
329 const io = fakeIO();
330 assert.strictEqual(await runAction(remindSchema, "add", { input: "call mum in 2h" }, io), true);
331 assert.strictEqual(io.scheduled.length, 1);
332 const fire = io.scheduled[0].event;
333 assert.strictEqual(fire.type, "remind.fire");
334 assert.deepStrictEqual(fire.notice, { title: "⏰ Reminder", body: "call mum" });
335 assert.strictEqual(io.scheduled[0].at, NOW + 2 * 3600000);
336 assert.deepStrictEqual(io.emitted, [
337 { type: "remind.add", id: "id-1", text: "call mum", when: NOW + 2 * 3600000, sid: "sid-9" },
338 ]);
339 assert.strictEqual(fire.id, "id-1"); // the scheduled fire and the add share the minted id
340});
341
342await ok("remind add: unparseable input hints and emits nothing", async () => {
343 const io = fakeIO();
344 assert.strictEqual(await runAction(remindSchema, "add", { input: "call mum" }, io), false);
345 assert.strictEqual(io.emitted.length, 0);
346 assert.strictEqual(io.scheduled.length, 0);
347 assert.strictEqual(io.hints.length, 1);
348});
349
350await ok("remind add: failed schedule records an empty sid", async () => {
351 const io = fakeIO({ schedule: () => Promise.resolve(null) });
352 await runAction(remindSchema, "add", { input: "call mum in 2h" }, io);
353 assert.strictEqual(io.emitted[0].sid, "");
354});
355
356await ok("remind dismiss: unschedules only unfired items with a sid", async () => {
357 let io = fakeIO();
358 await runAction(remindSchema, "dismiss", { item: { id: "a", fired: false, sid: "sch-1" } }, io);
359 assert.deepStrictEqual(io.unscheduled, ["sch-1"]);
360 assert.deepStrictEqual(io.emitted, [{ type: "remind.done", id: "a" }]);
361 io = fakeIO();
362 await runAction(remindSchema, "dismiss", { item: { id: "a", fired: true, sid: "sch-1" } }, io);
363 assert.deepStrictEqual(io.unscheduled, []);
364 io = fakeIO();
365 await runAction(remindSchema, "dismiss", { item: { id: "a", fired: false, sid: "" } }, io);
366 assert.deepStrictEqual(io.unscheduled, []);
367});
368
369await ok("ticker: sid-less due reminders client-fire once", () => {
370 let s = initialState(remindSchema);
371 s = foldSchemaEvent(remindSchema, s, { type: "remind.add", id: "a", text: "due", when: NOW - 1000, ts: 1, sid: "" });
372 s = foldSchemaEvent(remindSchema, s, { type: "remind.add", id: "b", text: "scheduled", when: NOW - 1000, ts: 2, sid: "sch" });
373 s = foldSchemaEvent(remindSchema, s, { type: "remind.add", id: "c", text: "future", when: NOW + 1000, ts: 3, sid: "" });
374 const fired = new Set();
375 const due = tickerRules(remindSchema, s, NOW, fired);
376 assert.deepStrictEqual(due, [{ type: "remind.fire", id: "a", notice: { title: "⏰ Reminder", body: "due" } }]);
377 assert.deepStrictEqual(tickerRules(remindSchema, s, NOW, fired), []); // once = once
378});
379
380// ---- native schema copies must not drift ------------------------------------
381
382console.log("native bundle drift gate:");
383await ok("ios schema copies are byte-identical to canonical", () => {
384 const nativeDir = join(__dirname, "..", "ios", "LChatCore", "Sources", "LChatCore", "Schemas");
385 for (const name of ["todo.json", "remind.json"]) {
386 const canonical = readFileSync(join(schemaDir, name), "utf8");
387 const bundled = readFileSync(join(nativeDir, name), "utf8");
388 assert.strictEqual(bundled, canonical, `${name}: run cp web/static/apps/schemas/${name} ios/LChatCore/Sources/LChatCore/Schemas/${name}`);
389 }
390});
391
392await ok("ios vector fixture copies are byte-identical to canonical", () => {
393 const fixtureDir = join(__dirname, "..", "ios", "LChatCore", "Tests", "LChatCoreTests", "Fixtures");
394 for (const name of ["todo", "remind"]) {
395 const canonical = readFileSync(join(__dirname, "event-vectors", `${name}.json`), "utf8");
396 const bundled = readFileSync(join(fixtureDir, `schema-vectors-${name}.json`), "utf8");
397 assert.strictEqual(bundled, canonical, `run cp test/event-vectors/${name}.json ios/LChatCore/Tests/LChatCoreTests/Fixtures/schema-vectors-${name}.json`);
398 }
399});
400
401console.log(`\nschema-engine: ${passed} checks passed`);
402