// ๐Ÿค– Tests for the app-schema engine (web/static/schema-engine.js) and the // todo/remind schema files. Three layers of proof: // 1. The schema fold reproduces the shared event vectors (the cross-platform // spec native also tests against). // 2. The schema fold is EXACTLY the hand-written reducer: randomized event // sequences through both must converge to deep-equal state. // 3. Views/actions: partition, ordering, search, templates, and action steps // (emit / schedule / unschedule / parse) do what the schema says. // Plus a drift gate: the schema copies bundled into the native package // (ios/LChatCore/Sources/LChatCore/Schemas/) must be byte-identical to the // canonical files in web/static/apps/schemas/. import assert from "assert"; import { readFileSync } from "fs"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import { foldSchemaEvent, initialState, partitionState, listModel, rowModel, detailModel, chatProjection, runAction, tickerRules, evalTemplate, relTime, } from "../web/static/schema-engine.js"; import { todoVectors, remindVectors } from "./event-vectors/index.js"; import { app as todoApp } from "../web/static/apps/todo.js"; import { app as remindApp, parseReminder } from "../web/static/apps/remind.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const schemaDir = join(__dirname, "..", "web", "static", "apps", "schemas"); const todoSchema = JSON.parse(readFileSync(join(schemaDir, "todo.json"), "utf8")); const remindSchema = JSON.parse(readFileSync(join(schemaDir, "remind.json"), "utf8")); let passed = 0; async function ok(name, fn) { await fn(); passed++; console.log(" โœ“ " + name); } // ---- 1. the schema fold reproduces the shared event vectors --------------- function runVectors(schema, vectors) { for (const v of vectors) { if (!v.event && !v.event_sequence) continue; // doc-only vectors (grace constants) let state = v.initial_state || initialState(schema); const events = v.event_sequence || [v.event]; for (const e of events) state = foldSchemaEvent(schema, state, e); const expected = v.expected_final_state || v.expected_state_update; assert.deepStrictEqual(state, expected, `vector ${v.name}`); } } console.log("schema fold vs event vectors:"); await ok("todo vectors", () => runVectors(todoSchema, todoVectors.vectors)); await ok("remind vectors", () => runVectors(remindSchema, remindVectors.vectors)); // ---- 2. the app's registered fold IS the schema fold ------------------------ // todo.js/remind.js now WIRE their events map from the schema (schemaEvents), // so this fuzz is no longer two independent implementations racing โ€” it pins // the defineApp wiring: every schema event type registered, folded through the // engine, malformed events no-op'd identically. (It caught real drift back // when the reducers were hand-written; keep it โ€” it's the wiring's regression // net, and it goes back to twin-hunting if an app ever hand-writes a fold.) function mulberry32(seed) { let a = seed; return () => { a |= 0; a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function fuzzParity(schema, app, makeEvent, rounds = 400) { const rnd = mulberry32(11); let schemaState = initialState(schema); let appState = app.initialState(); for (let i = 0; i < rounds; i++) { const e = makeEvent(rnd, i); schemaState = foldSchemaEvent(schema, schemaState, e); appState = app.reduce(appState, e); assert.deepStrictEqual(schemaState, appState, `diverged at event ${i}: ${JSON.stringify(e)}`); } } console.log("schema fold vs hand-written reducer (fuzz):"); await ok("todo parity", () => { fuzzParity(todoSchema, todoApp, (rnd, i) => { const id = "t" + Math.floor(rnd() * 12); const pick = rnd(); if (pick < 0.4) { // Some adds are malformed (no text) and must no-op identically. return { type: "todo.add", id, text: rnd() < 0.85 ? "task " + i : "", name: rnd() < 0.5 ? "Ana" : undefined, ts: 1700000000000 + i }; } if (pick < 0.6) return { type: "todo.done", id, ts: rnd() < 0.8 ? 1700000000000 + i : undefined }; if (pick < 0.75) return { type: "todo.reopen", id }; if (pick < 0.85) return { type: "todo.remove", id }; if (pick < 0.95) { // Moves anchor to another item, the start (""), or a missing anchor. const roll = rnd(); const after = roll < 0.5 ? "t" + Math.floor(rnd() * 12) : roll < 0.75 ? "" : "gone"; return { type: "todo.move", id, after }; } return { type: "unknown.event", id }; }); }); await ok("remind parity", () => { fuzzParity(remindSchema, remindApp, (rnd, i) => { const id = "r" + Math.floor(rnd() * 12); const pick = rnd(); if (pick < 0.5) { return { type: "remind.add", id: rnd() < 0.9 ? id : "", text: rnd() < 0.9 ? "thing " + i : "", when: rnd() < 0.9 ? 1700000000000 + Math.floor(rnd() * 1e7) : 0, ts: rnd() < 0.8 ? 1700000000000 + i : undefined, sid: rnd() < 0.7 ? "sch-" + i : undefined, }; } if (pick < 0.75) return { type: "remind.fire", id, ts: rnd() < 0.8 ? 1700000000000 + i : undefined }; return { type: "remind.done", id }; }); }); // ---- chat projections match the app's chatMessage hook --------------------- console.log("chat projections:"); await ok("todo.add projects, others silent", () => { const e = { type: "todo.add", id: "a", text: "buy milk" }; assert.deepStrictEqual(chatProjection(todoSchema, e), todoApp.chatMessage(e)); assert.strictEqual(chatProjection(todoSchema, { type: "todo.add", id: "a", text: "" }), null); assert.strictEqual(chatProjection(todoSchema, { type: "todo.done", id: "a" }), null); }); await ok("remind.fire personal vs group wording", () => { const e = { type: "remind.fire", id: "r", notice: { title: "โฐ Reminder", body: "call mum" } }; assert.strictEqual(chatProjection(remindSchema, e), remindApp.chatMessage(e)); assert.strictEqual(chatProjection(remindSchema, e, { group: true }), remindApp.chatMessage(e, { group: true })); }); await ok("remind.add action line", () => { const e = { type: "remind.add", id: "r", text: "call mum", when: 1893499200000 }; assert.deepStrictEqual(chatProjection(remindSchema, e), remindApp.chatMessage(e)); }); // ---- 3. views: partition, ordering, search, templates ---------------------- console.log("list model:"); const NOW = 1700000100000; await ok("todo partition + section order", () => { let s = initialState(todoSchema); for (const [id, text] of [["a", "one"], ["b", "two"], ["c", "three"]]) { s = foldSchemaEvent(todoSchema, s, { type: "todo.add", id, text, name: "Ana" }); } // b: archived (done long ago); c: in grace (done 200ms ago). s = foldSchemaEvent(todoSchema, s, { type: "todo.done", id: "b", ts: NOW - 60000 }); s = foldSchemaEvent(todoSchema, s, { type: "todo.done", id: "c", ts: NOW - 200 }); const m = listModel(todoSchema, s, NOW); assert.strictEqual(m.summary, "1 left"); const open = m.sections.find((x) => x.id === "open"); // Newest-first, in place: c (grace, still visible) then a. assert.deepStrictEqual(open.rows.map((r) => r.item.id), ["c", "a"]); assert.deepStrictEqual(open.rows.map((r) => r.grace), [true, false]); const done = m.sections.find((x) => x.id === "completed"); assert.strictEqual(done.title, "Completed (1)"); assert.strictEqual(done.collapsed, true); assert.deepStrictEqual(done.rows.map((r) => r.item.id), ["b"]); }); await ok("todo reorder: move fold op + the section's reorder surface", () => { let s = initialState(todoSchema); for (const [id, text] of [["a", "one"], ["b", "two"], ["c", "three"]]) { s = foldSchemaEvent(todoSchema, s, { type: "todo.add", id, text, name: "Ana" }); } // Visual (newest-first): c, b, a. Drag c below b = c now follows a in the // array โ†’ array a, c, b โ†’ visual b, c, a. s = foldSchemaEvent(todoSchema, s, { type: "todo.move", id: "c", after: "a" }); assert.deepStrictEqual(s.items.map((i) => i.id), ["a", "c", "b"]); const m = listModel(todoSchema, s, NOW); const open = m.sections.find((x) => x.id === "open"); assert.deepStrictEqual(open.rows.map((r) => r.item.id), ["b", "c", "a"]); // The open section is reorderable (newest-first); Completed is not. assert.strictEqual(open.reorder, "move"); assert.strictEqual(open.descending, true); assert.strictEqual(m.sections.find((x) => x.id === "completed").reorder, ""); // Searching suppresses reorder โ€” a filtered list has no meaningful drop. const filtered = listModel(todoSchema, s, NOW, { query: "t" }); assert.strictEqual(filtered.sections.find((x) => x.id === "open").reorder, ""); // Anchor to the start, and an unknown anchor no-ops. s = foldSchemaEvent(todoSchema, s, { type: "todo.move", id: "b", after: "" }); assert.deepStrictEqual(s.items.map((i) => i.id), ["b", "a", "c"]); s = foldSchemaEvent(todoSchema, s, { type: "todo.move", id: "a", after: "gone" }); assert.deepStrictEqual(s.items.map((i) => i.id), ["b", "a", "c"]); }); await ok("todo summary cases", () => { const empty = listModel(todoSchema, initialState(todoSchema), NOW); assert.strictEqual(empty.summary, "No to-dos yet โ€” add one above"); let s = foldSchemaEvent(todoSchema, initialState(todoSchema), { type: "todo.add", id: "a", text: "x" }); s = foldSchemaEvent(todoSchema, s, { type: "todo.done", id: "a", ts: NOW - 60000 }); assert.strictEqual(listModel(todoSchema, s, NOW).summary, "all done ๐ŸŽ‰"); }); await ok("undefined state reads as the empty state (virgin thread)", () => { // Regression: the host only seeds state on the first fold, so a never-used // app thread hands the renderer `undefined` โ€” the engine's entry points must // treat that as empty, not throw ("undefined is not an object" on the // Interface view of a fresh To-do/Reminders thread). for (const schema of [todoSchema, remindSchema]) { assert.deepStrictEqual(listModel(schema, undefined, NOW), listModel(schema, initialState(schema), NOW)); assert.deepStrictEqual(partitionState(schema, undefined, NOW), { active: [], grace: [], archived: [] }); assert.deepStrictEqual(tickerRules(schema, undefined, NOW, new Set()), []); } }); await ok("search filters rows across sections", () => { let s = initialState(todoSchema); s = foldSchemaEvent(todoSchema, s, { type: "todo.add", id: "a", text: "buy milk" }); s = foldSchemaEvent(todoSchema, s, { type: "todo.add", id: "b", text: "call mum" }); const m = listModel(todoSchema, s, NOW, { query: "milk" }); assert.deepStrictEqual(m.sections[0].rows.map((r) => r.item.id), ["a"]); }); await ok("remind sections: upcoming asc, settling, previous desc + empty lines", () => { let s = initialState(remindSchema); s = foldSchemaEvent(remindSchema, s, { type: "remind.add", id: "late", text: "late", when: NOW + 7200000, ts: 1, sid: "s1" }); s = foldSchemaEvent(remindSchema, s, { type: "remind.add", id: "soon", text: "soon", when: NOW + 60000, ts: 2, sid: "s2" }); s = foldSchemaEvent(remindSchema, s, { type: "remind.add", id: "old", text: "old", when: NOW - 90000, ts: 3, sid: "s3" }); s = foldSchemaEvent(remindSchema, s, { type: "remind.fire", id: "old", ts: NOW - 60000 }); // archived (grace 6s) const m = listModel(remindSchema, s, NOW); assert.deepStrictEqual(m.sections.find((x) => x.id === "upcoming").rows.map((r) => r.item.id), ["soon", "late"]); assert.strictEqual(m.sections.find((x) => x.id === "previous").title, "Previous reminders (1)"); assert.strictEqual(m.empty, ""); // All dismissed โ†’ the archived-aware empty line. let s2 = foldSchemaEvent(remindSchema, s, { type: "remind.done", id: "late" }); s2 = foldSchemaEvent(remindSchema, s2, { type: "remind.done", id: "soon" }); assert.strictEqual(listModel(remindSchema, s2, NOW).empty, "No upcoming reminders."); assert.strictEqual(listModel(remindSchema, initialState(remindSchema), NOW).empty, "No reminders yet. Add one above, or type /remind in any chat."); }); await ok("remind row model: reltime, due accent, abs note past a day", () => { const upcoming = { id: "a", text: "call", when: NOW + 2 * 86400000, ts: 1, sid: "s", fired: false, firedAt: 0 }; let r = rowModel(remindSchema, upcoming, NOW); assert.strictEqual(r.title, "call"); assert.strictEqual(r.subtitle, "in 2d"); assert.strictEqual(r.accent, false); assert.notStrictEqual(r.note, ""); // absolute time shown when >1d out const due = { ...upcoming, when: NOW - 120000 }; r = rowModel(remindSchema, due, NOW); assert.strictEqual(r.subtitle, "2m ago"); assert.strictEqual(r.accent, true); assert.strictEqual(r.note, ""); const fired = { ...upcoming, fired: true, firedAt: NOW - 300000 }; r = rowModel(remindSchema, fired, NOW); assert.strictEqual(r.subtitle, "Reminded ยท 5m ago"); assert.strictEqual(r.strike, true); }); await ok("todo detail model", () => { const d = detailModel(todoSchema, { id: "a", text: "buy milk", done: false, doneAt: 0, by: "Ana" }, NOW); assert.strictEqual(d.title, "buy milk"); const defs = d.sections.find((x) => x.kind === "definitions"); assert.deepStrictEqual(defs.rows, [ { term: "Status", value: "To do" }, { term: "Added by", value: "Ana" }, ]); const buttons = d.sections.find((x) => x.kind === "buttons"); assert.deepStrictEqual(buttons.items.map((b) => b.label), ["Mark done", "Remove"]); }); await ok("relTime shapes", () => { assert.strictEqual(relTime(NOW + 20000, NOW), "now"); assert.strictEqual(relTime(NOW + 45 * 60000, NOW), "in 45m"); assert.strictEqual(relTime(NOW - 3 * 3600000, NOW), "3h ago"); assert.strictEqual(relTime(NOW + 3 * 86400000, NOW), "in 3d"); }); // ---- actions ---------------------------------------------------------------- console.log("actions:"); function fakeIO(overrides = {}) { const io = { emitted: [], scheduled: [], unscheduled: [], hints: [], nextId: "id-1", emit(e) { io.emitted.push(e); return Promise.resolve(true); }, schedule(e, at) { io.scheduled.push({ event: e, at }); return Promise.resolve("sid-9"); }, unschedule(sid) { io.unscheduled.push(sid); return Promise.resolve(true); }, newId: () => io.nextId, parse: (kind, text) => (kind === "reminder" ? parseReminder(text, NOW) : null), hint: (t) => io.hints.push(t), ...overrides, }; return io; } await ok("todo add / toggle / remove", async () => { const io = fakeIO(); assert.strictEqual(await runAction(todoSchema, "add", { input: "buy milk" }, io), true); assert.deepStrictEqual(io.emitted, [{ type: "todo.add", id: "id-1", text: "buy milk" }]); assert.strictEqual(await runAction(todoSchema, "add", { input: "" }, io), false); io.emitted.length = 0; await runAction(todoSchema, "toggle", { item: { id: "a", done: false } }, io); await runAction(todoSchema, "toggle", { item: { id: "a", done: true } }, io); await runAction(todoSchema, "remove", { item: { id: "a", done: false } }, io); assert.deepStrictEqual(io.emitted.map((e) => e.type), ["todo.done", "todo.reopen", "todo.remove"]); }); await ok("remind add: parse โ†’ schedule โ†’ emit shares one id and records the sid", async () => { const io = fakeIO(); assert.strictEqual(await runAction(remindSchema, "add", { input: "call mum in 2h" }, io), true); assert.strictEqual(io.scheduled.length, 1); const fire = io.scheduled[0].event; assert.strictEqual(fire.type, "remind.fire"); assert.deepStrictEqual(fire.notice, { title: "โฐ Reminder", body: "call mum" }); assert.strictEqual(io.scheduled[0].at, NOW + 2 * 3600000); assert.deepStrictEqual(io.emitted, [ { type: "remind.add", id: "id-1", text: "call mum", when: NOW + 2 * 3600000, sid: "sid-9" }, ]); assert.strictEqual(fire.id, "id-1"); // the scheduled fire and the add share the minted id }); await ok("remind add: unparseable input hints and emits nothing", async () => { const io = fakeIO(); assert.strictEqual(await runAction(remindSchema, "add", { input: "call mum" }, io), false); assert.strictEqual(io.emitted.length, 0); assert.strictEqual(io.scheduled.length, 0); assert.strictEqual(io.hints.length, 1); }); await ok("remind add: failed schedule records an empty sid", async () => { const io = fakeIO({ schedule: () => Promise.resolve(null) }); await runAction(remindSchema, "add", { input: "call mum in 2h" }, io); assert.strictEqual(io.emitted[0].sid, ""); }); await ok("remind dismiss: unschedules only unfired items with a sid", async () => { let io = fakeIO(); await runAction(remindSchema, "dismiss", { item: { id: "a", fired: false, sid: "sch-1" } }, io); assert.deepStrictEqual(io.unscheduled, ["sch-1"]); assert.deepStrictEqual(io.emitted, [{ type: "remind.done", id: "a" }]); io = fakeIO(); await runAction(remindSchema, "dismiss", { item: { id: "a", fired: true, sid: "sch-1" } }, io); assert.deepStrictEqual(io.unscheduled, []); io = fakeIO(); await runAction(remindSchema, "dismiss", { item: { id: "a", fired: false, sid: "" } }, io); assert.deepStrictEqual(io.unscheduled, []); }); await ok("ticker: sid-less due reminders client-fire once", () => { let s = initialState(remindSchema); s = foldSchemaEvent(remindSchema, s, { type: "remind.add", id: "a", text: "due", when: NOW - 1000, ts: 1, sid: "" }); s = foldSchemaEvent(remindSchema, s, { type: "remind.add", id: "b", text: "scheduled", when: NOW - 1000, ts: 2, sid: "sch" }); s = foldSchemaEvent(remindSchema, s, { type: "remind.add", id: "c", text: "future", when: NOW + 1000, ts: 3, sid: "" }); const fired = new Set(); const due = tickerRules(remindSchema, s, NOW, fired); assert.deepStrictEqual(due, [{ type: "remind.fire", id: "a", notice: { title: "โฐ Reminder", body: "due" } }]); assert.deepStrictEqual(tickerRules(remindSchema, s, NOW, fired), []); // once = once }); // ---- native schema copies must not drift ------------------------------------ console.log("native bundle drift gate:"); await ok("ios schema copies are byte-identical to canonical", () => { const nativeDir = join(__dirname, "..", "ios", "LChatCore", "Sources", "LChatCore", "Schemas"); for (const name of ["todo.json", "remind.json"]) { const canonical = readFileSync(join(schemaDir, name), "utf8"); const bundled = readFileSync(join(nativeDir, name), "utf8"); assert.strictEqual(bundled, canonical, `${name}: run cp web/static/apps/schemas/${name} ios/LChatCore/Sources/LChatCore/Schemas/${name}`); } }); await ok("ios vector fixture copies are byte-identical to canonical", () => { const fixtureDir = join(__dirname, "..", "ios", "LChatCore", "Tests", "LChatCoreTests", "Fixtures"); for (const name of ["todo", "remind"]) { const canonical = readFileSync(join(__dirname, "event-vectors", `${name}.json`), "utf8"); const bundled = readFileSync(join(fixtureDir, `schema-vectors-${name}.json`), "utf8"); assert.strictEqual(bundled, canonical, `run cp test/event-vectors/${name}.json ios/LChatCore/Tests/LChatCoreTests/Fixtures/schema-vectors-${name}.json`); } }); console.log(`\nschema-engine: ${passed} checks passed`);