eleven / messenger Public

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

Plain git — no account needed to clone.

Download

Download this file

1import XCTest
2@testable import LChatCore
3
4// 🤖 The registry (docs/native-apps.md Layer 1): the bundled seed must load
5// and answer every question the collapsed lookup tables used to — titles,
6// glyphs, blurbs, picker lists, the install read-filter, and surface
7// presentation — and live reports must merge over it. The seed itself is
8// drift-gated against the web framework by test/appcatalog-seed.test.mjs;
9// these tests pin the Swift reads of it.
10@MainActor
11final class AppRegistryTests: XCTestCase {
12 // Distinct origins per test: registries are per-origin process singletons.
13 private func fresh(_ tag: String) -> AppRegistry {
14 AppRegistry.shared(origin: "https://\(tag).registry-tests.example")
15 }
16
17 func testSeedLoadsBuiltins() {
18 let r = fresh("seed")
19 XCTAssertEqual(r.title("todo"), "To-do")
20 XCTAssertEqual(r.glyph("vault"), "🔒")
21 XCTAssertEqual(r.title("connect"), "Integrations")
22 XCTAssertEqual(r.blurb("board"), "Sketch on a whiteboard")
23 // The doc's first-class-citizen guarantee: unknown apps still answer.
24 XCTAssertEqual(r.title("mystery-app"), "Mystery-App")
25 XCTAssertEqual(r.glyph("mystery-app"), "▦")
26 XCTAssertNil(r.blurb("mystery-app"))
27 }
28
29 func testPickerLists() {
30 let r = fresh("pickers")
31 let personal = r.personalApps.map(\.name)
32 let group = r.groupApps.map(\.name)
33 // The web's personal catalogue (places ∋ "you", listed) — todo yes,
34 // board no (board is chat-only on the web too).
35 XCTAssertTrue(personal.contains("todo"))
36 XCTAssertTrue(personal.contains("bookmarks"))
37 XCTAssertFalse(personal.contains("board"))
38 XCTAssertFalse(personal.contains("admin")) // places: [] — offered by the admin row, not the catalogue
39 // The group list gains the apps the old 4-entry catalog was missing —
40 // the "board can't be added to a group" fix.
41 XCTAssertTrue(group.contains("board"))
42 XCTAssertTrue(group.contains("poll"))
43 XCTAssertTrue(group.contains("vault"))
44 XCTAssertFalse(group.contains("connect")) // personal-only
45 }
46
47 func testKnownFiltersInstallsByPlace() {
48 let r = fresh("known")
49 let items = [
50 GroupApps.Item(id: "board", by: "Ana", ts: 1),
51 GroupApps.Item(id: "evil-app", by: "Mallory", ts: 2),
52 GroupApps.Item(id: "connect", by: "Ana", ts: 3), // real app, but not chat-capable
53 ]
54 XCTAssertEqual(r.known(items).map(\.id), ["board"])
55 let space = [
56 SpaceApps.Item(id: "gif", by: "Ana", ts: 1, uid: "u1"),
57 SpaceApps.Item(id: "todo", by: "Ana", ts: 2, uid: "u1"), // not space-capable
58 ]
59 XCTAssertEqual(r.knownSpace(space).map(\.id), ["gif"])
60 }
61
62 func testPresentation() {
63 let r = fresh("presentation")
64 XCTAssertEqual(r.presentation("setup"), .flow)
65 XCTAssertEqual(r.presentation("welcome"), .flow)
66 XCTAssertEqual(r.presentation("todo"), .schema(companion: true))
67 XCTAssertEqual(r.presentation("remind"), .schema(companion: true))
68 // Admin: a companion whose first view is its console (the Mac Chat|View
69 // / iOS opens-as-the-app fix); its Chat view is the read-only update
70 // stream, hence chatReadOnly.
71 XCTAssertEqual(r.presentation("admin"),
72 .companion(defaultView: "interface", chatOnly: false, chatReadOnly: true))
73 // Takeover is the universal fallback: vault, and any unknown app.
74 XCTAssertEqual(r.presentation("vault"), .takeover)
75 XCTAssertEqual(r.presentation("mystery-app"), .takeover)
76 }
77
78 func testLiveReportMergesOverSeed() {
79 let r = fresh("live-report")
80 XCTAssertFalse(r.hasLiveReport)
81 let report = """
82 [{"name":"habits","title":"Habit tracker","glyph":"✅","blurb":"Track a habit",
83 "places":["you"],"listed":true,"quiet":false,"member":true,
84 "companion":true,"chatOnly":false,"chatReadOnly":false,
85 "defaultView":"chat","defaultCommand":"","commands":[]},
86 {"name":"todo","title":"Tasks","glyph":"✓","blurb":"","places":["you","chat"],
87 "listed":true,"quiet":false,"member":false,"companion":true,"chatOnly":false,
88 "chatReadOnly":false,"defaultView":"split","defaultCommand":"/add","commands":[]}]
89 """.data(using: .utf8)!
90 r.update(reportJSON: report)
91 XCTAssertTrue(r.hasLiveReport)
92 // The member app registered and is personal-pickable.
93 XCTAssertEqual(r.title("habits"), "Habit tracker")
94 XCTAssertTrue(r.personalApps.map(\.name).contains("habits"))
95 XCTAssertEqual(r.presentation("habits"),
96 .companion(defaultView: "chat", chatOnly: false, chatReadOnly: false))
97 // The live report wins per app (a renamed todo renames here)…
98 XCTAssertEqual(r.title("todo"), "Tasks")
99 // …but the seed still floors every built-in the report didn't carry.
100 XCTAssertEqual(r.title("vault"), "Vault")
101 // An empty/undecodable report changes nothing.
102 r.update(reportJSON: Data("null".utf8))
103 XCTAssertEqual(r.title("habits"), "Habit tracker")
104 }
105
106 // Service-gated apps (manifest.requiresService — GitHub, DNSimple…) hide
107 // from the pickers until the space's connected set names their service:
108 // the web's matchingApps rule (newchat.js), so admin-native and member-web
109 // show the same list.
110 func testServiceGatedAppsHideUntilConnected() {
111 let r = fresh("services")
112 XCTAssertFalse(r.personalApps.map(\.name).contains("github"))
113 XCTAssertFalse(r.personalApps.map(\.name).contains("dnsimple"))
114 XCTAssertTrue(r.personalApps.map(\.name).contains("todo")) // ungated
115 r.updateConnected(["github"])
116 XCTAssertTrue(r.personalApps.map(\.name).contains("github"))
117 XCTAssertFalse(r.personalApps.map(\.name).contains("dnsimple"))
118 }
119
120 // Decoration flows from the apps' own manifests (symbol/mark in each JS
121 // spec → the report → the registry → AppIdentity): nothing hand-kept
122 // natively. The seed alone must dress the built-ins.
123 func testDecorationComesFromManifests() {
124 _ = fresh("decoration") // init pushes the seed's decoration
125 XCTAssertEqual(AppIdentity.symbol(for: "todo"), "checkmark.square")
126 XCTAssertEqual(AppIdentity.symbol(for: "admin"), "wrench.adjustable")
127 XCTAssertNil(AppIdentity.symbol(for: "github")) // brand mark, not a symbol
128 XCTAssertNil(AppIdentity.symbol(for: "some-member-app")) // emoji is content
129 }
130}
131