eleven / ocho Public

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

Plain git — no account needed to clone.

Download

Download this file

1// πŸ€– The app-host protocol's catalogue report (docs/native-apps.md): the
2// serialized shapes the NATIVE shells consume via window.lchatNative.
3// appCatalog() / commandItems(). These shapes are a cross-platform contract β€”
4// Swift decodes them β€” so this test pins them against the REAL framework
5// (defineApp normalization included), not a reconstruction. Widening the
6// report is additive; renaming or dropping a field here means updating the
7// native decoders in the same PR.
8import assert from "assert";
9import { defineApp, appCatalogReport, commandCatalog } from "../web/static/apps.js";
10// A real shipped app as a sanity row: the vault is the canonical TAKEOVER app
11// (groupFeedMessage, no chatMessage β€” docs/building-apps.md), so its record
12// pins the companion gate against the real thing.
13import "../web/static/apps/vault.js";
14
15// A synthetic companion app exercising every reported field.
16defineApp({
17 name: "cataloguetest",
18 title: "Catalogue Test",
19 glyph: "πŸ§ͺ",
20 places: ["you", "chat"],
21 quiet: true,
22 defaultView: "interface",
23 chatReadOnly: true,
24 initialState: () => ({ items: [] }),
25 events: { "cataloguetest.add": (s) => s },
26 chatMessage: () => null,
27 defaultCommand: "/ct-add",
28 commands: {
29 "/ct": { run: () => {}, desc: "Test the catalogue", args: "<thing>" },
30 "/ct-add": { run: () => {}, desc: "Add locally", args: "<thing>", local: true },
31 "/ct-bare": () => {}, // bare-handler form must normalize to empty desc/args
32 },
33});
34
35// A synthetic takeover app: no chatMessage, chat-place only.
36defineApp({
37 name: "takeovertest",
38 title: "Takeover Test",
39 glyph: "🎨",
40 places: ["chat"],
41 initialState: () => ({}),
42 events: {},
43 render: () => {},
44});
45
46const report = appCatalogReport();
47const byName = Object.fromEntries(report.map((a) => [a.name, a]));
48
49// The report is PLAIN JSON β€” the whole point is that evaluateJavaScript can
50// hand it to Swift losslessly. A function or undefined leaking in breaks that.
51assert.deepStrictEqual(JSON.parse(JSON.stringify(report)), report, "report must round-trip through JSON unchanged");
52
53// Every record carries the full field set, correctly typed.
54for (const a of report) {
55 assert.strictEqual(typeof a.name, "string");
56 assert.ok(a.name, "name is never empty");
57 assert.strictEqual(typeof a.title, "string");
58 assert.strictEqual(typeof a.glyph, "string");
59 assert.strictEqual(typeof a.blurb, "string");
60 assert.strictEqual(typeof a.requiresService, "string");
61 assert.strictEqual(typeof a.openEvent, "string");
62 assert.ok(Array.isArray(a.places));
63 for (const p of a.places) assert.ok(["you", "chat", "space"].includes(p), `place ${p} unknown`);
64 for (const k of ["listed", "quiet", "member", "surface", "companion", "chatOnly", "chatReadOnly"]) {
65 assert.strictEqual(typeof a[k], "boolean", `${a.name}.${k} is a boolean`);
66 }
67 assert.ok(["chat", "split", "interface"].includes(a.defaultView), `${a.name}.defaultView normalized`);
68 assert.strictEqual(typeof a.defaultCommand, "string");
69 assert.ok(Array.isArray(a.commands));
70 for (const c of a.commands) {
71 assert.ok(c.name.startsWith("/"), `${a.name} command ${c.name} keeps its slash`);
72 assert.strictEqual(typeof c.desc, "string");
73 assert.strictEqual(typeof c.args, "string");
74 assert.strictEqual(typeof c.local, "boolean");
75 }
76}
77
78// The companion record, field by field.
79const ct = byName.cataloguetest;
80assert.ok(ct, "synthetic companion app reported");
81assert.strictEqual(ct.title, "Catalogue Test");
82assert.strictEqual(ct.glyph, "πŸ§ͺ");
83assert.deepStrictEqual(ct.places, ["you", "chat"]);
84assert.strictEqual(ct.listed, true); // defaults on: places is non-empty
85assert.strictEqual(ct.quiet, true);
86assert.strictEqual(ct.member, false);
87assert.strictEqual(ct.companion, true);
88assert.strictEqual(ct.chatOnly, false);
89assert.strictEqual(ct.chatReadOnly, true);
90assert.strictEqual(ct.defaultView, "interface");
91assert.strictEqual(ct.defaultCommand, "/ct-add");
92assert.deepStrictEqual(
93 ct.commands.find((c) => c.name === "/ct"),
94 { name: "/ct", desc: "Test the catalogue", args: "<thing>", local: false },
95);
96assert.deepStrictEqual(
97 ct.commands.find((c) => c.name === "/ct-add"),
98 { name: "/ct-add", desc: "Add locally", args: "<thing>", local: true },
99);
100assert.deepStrictEqual(
101 ct.commands.find((c) => c.name === "/ct-bare"),
102 { name: "/ct-bare", desc: "", args: "", local: false },
103);
104
105// The takeover record: no chatMessage means no companion, and the unset
106// presentation fields land on their documented defaults.
107const to = byName.takeovertest;
108assert.ok(to, "synthetic takeover app reported");
109assert.strictEqual(to.companion, false);
110assert.strictEqual(to.chatOnly, false);
111assert.strictEqual(to.chatReadOnly, false);
112assert.strictEqual(to.defaultView, "split");
113assert.strictEqual(to.defaultCommand, "");
114
115// The real vault: a takeover app (groupFeedMessage only) β€” the companion gate
116// answered from shipped code, not this file's synthetics.
117const vault = byName.vault;
118assert.ok(vault, "vault registered and reported");
119assert.strictEqual(vault.companion, false, "vault is a takeover app");
120assert.ok(vault.title && vault.glyph, "vault carries identity");
121
122// commandItems() serves commandCatalog(threadId); with no thread context the
123// rows still come back JSON-plain with the typeahead fields native ranks on.
124const rows = commandCatalog();
125assert.deepStrictEqual(JSON.parse(JSON.stringify(rows)), rows, "command rows must round-trip through JSON");
126const ctRow = rows.find((r) => r.name === "/ct");
127assert.ok(ctRow, "global command listed in the catalog");
128assert.strictEqual(typeof ctRow.desc, "string");
129assert.strictEqual(ctRow.args, "<thing>");
130assert.ok(!rows.some((r) => r.name === "/ct-add"), "local commands stay out of the global catalog");
131const me = rows.find((r) => r.name === "/me");
132assert.ok(me && me.bot === false, "/me rides the floor, client-answered");
133
134console.log("appcatalog: all assertions passed");
135