import XCTest @testable import LChatCore // πŸ€– The registry (docs/native-apps.md Layer 1): the bundled seed must load // and answer every question the collapsed lookup tables used to β€” titles, // glyphs, blurbs, picker lists, the install read-filter, and surface // presentation β€” and live reports must merge over it. The seed itself is // drift-gated against the web framework by test/appcatalog-seed.test.mjs; // these tests pin the Swift reads of it. @MainActor final class AppRegistryTests: XCTestCase { // Distinct origins per test: registries are per-origin process singletons. private func fresh(_ tag: String) -> AppRegistry { AppRegistry.shared(origin: "https://\(tag).registry-tests.example") } func testSeedLoadsBuiltins() { let r = fresh("seed") XCTAssertEqual(r.title("todo"), "To-do") XCTAssertEqual(r.glyph("vault"), "πŸ”’") XCTAssertEqual(r.title("connect"), "Integrations") XCTAssertEqual(r.blurb("board"), "Sketch on a whiteboard") // The doc's first-class-citizen guarantee: unknown apps still answer. XCTAssertEqual(r.title("mystery-app"), "Mystery-App") XCTAssertEqual(r.glyph("mystery-app"), "β–¦") XCTAssertNil(r.blurb("mystery-app")) } func testPickerLists() { let r = fresh("pickers") let personal = r.personalApps.map(\.name) let group = r.groupApps.map(\.name) // The web's personal catalogue (places βˆ‹ "you", listed) β€” todo yes, // board no (board is chat-only on the web too). XCTAssertTrue(personal.contains("todo")) XCTAssertTrue(personal.contains("bookmarks")) XCTAssertFalse(personal.contains("board")) XCTAssertFalse(personal.contains("admin")) // places: [] β€” offered by the admin row, not the catalogue // The group list gains the apps the old 4-entry catalog was missing β€” // the "board can't be added to a group" fix. XCTAssertTrue(group.contains("board")) XCTAssertTrue(group.contains("poll")) XCTAssertTrue(group.contains("vault")) XCTAssertFalse(group.contains("connect")) // personal-only } func testKnownFiltersInstallsByPlace() { let r = fresh("known") let items = [ GroupApps.Item(id: "board", by: "Ana", ts: 1), GroupApps.Item(id: "evil-app", by: "Mallory", ts: 2), GroupApps.Item(id: "connect", by: "Ana", ts: 3), // real app, but not chat-capable ] XCTAssertEqual(r.known(items).map(\.id), ["board"]) let space = [ SpaceApps.Item(id: "gif", by: "Ana", ts: 1, uid: "u1"), SpaceApps.Item(id: "todo", by: "Ana", ts: 2, uid: "u1"), // not space-capable ] XCTAssertEqual(r.knownSpace(space).map(\.id), ["gif"]) } func testPresentation() { let r = fresh("presentation") XCTAssertEqual(r.presentation("setup"), .flow) XCTAssertEqual(r.presentation("welcome"), .flow) XCTAssertEqual(r.presentation("todo"), .schema(companion: true)) XCTAssertEqual(r.presentation("remind"), .schema(companion: true)) // Admin: a companion whose first view is its console (the Mac Chat|View // / iOS opens-as-the-app fix); its Chat view is the read-only update // stream, hence chatReadOnly. XCTAssertEqual(r.presentation("admin"), .companion(defaultView: "interface", chatOnly: false, chatReadOnly: true)) // Takeover is the universal fallback: vault, and any unknown app. XCTAssertEqual(r.presentation("vault"), .takeover) XCTAssertEqual(r.presentation("mystery-app"), .takeover) } func testLiveReportMergesOverSeed() { let r = fresh("live-report") XCTAssertFalse(r.hasLiveReport) let report = """ [{"name":"habits","title":"Habit tracker","glyph":"βœ…","blurb":"Track a habit", "places":["you"],"listed":true,"quiet":false,"member":true, "companion":true,"chatOnly":false,"chatReadOnly":false, "defaultView":"chat","defaultCommand":"","commands":[]}, {"name":"todo","title":"Tasks","glyph":"βœ“","blurb":"","places":["you","chat"], "listed":true,"quiet":false,"member":false,"companion":true,"chatOnly":false, "chatReadOnly":false,"defaultView":"split","defaultCommand":"/add","commands":[]}] """.data(using: .utf8)! r.update(reportJSON: report) XCTAssertTrue(r.hasLiveReport) // The member app registered and is personal-pickable. XCTAssertEqual(r.title("habits"), "Habit tracker") XCTAssertTrue(r.personalApps.map(\.name).contains("habits")) XCTAssertEqual(r.presentation("habits"), .companion(defaultView: "chat", chatOnly: false, chatReadOnly: false)) // The live report wins per app (a renamed todo renames here)… XCTAssertEqual(r.title("todo"), "Tasks") // …but the seed still floors every built-in the report didn't carry. XCTAssertEqual(r.title("vault"), "Vault") // An empty/undecodable report changes nothing. r.update(reportJSON: Data("null".utf8)) XCTAssertEqual(r.title("habits"), "Habit tracker") } // Service-gated apps (manifest.requiresService β€” GitHub, DNSimple…) hide // from the pickers until the space's connected set names their service: // the web's matchingApps rule (newchat.js), so admin-native and member-web // show the same list. func testServiceGatedAppsHideUntilConnected() { let r = fresh("services") XCTAssertFalse(r.personalApps.map(\.name).contains("github")) XCTAssertFalse(r.personalApps.map(\.name).contains("dnsimple")) XCTAssertTrue(r.personalApps.map(\.name).contains("todo")) // ungated r.updateConnected(["github"]) XCTAssertTrue(r.personalApps.map(\.name).contains("github")) XCTAssertFalse(r.personalApps.map(\.name).contains("dnsimple")) } // Decoration flows from the apps' own manifests (symbol/mark in each JS // spec β†’ the report β†’ the registry β†’ AppIdentity): nothing hand-kept // natively. The seed alone must dress the built-ins. func testDecorationComesFromManifests() { _ = fresh("decoration") // init pushes the seed's decoration XCTAssertEqual(AppIdentity.symbol(for: "todo"), "checkmark.square") XCTAssertEqual(AppIdentity.symbol(for: "admin"), "wrench.adjustable") XCTAssertNil(AppIdentity.symbol(for: "github")) // brand mark, not a symbol XCTAssertNil(AppIdentity.symbol(for: "some-member-app")) // emoji is content } }