| 1 | import XCTest |
| 2 | @testable import LChatCore |
| 3 | |
| 4 | // π€ The native half of the app-host catalogue contract (docs/native-apps.md): |
| 5 | // AppManifest must decode exactly what appCatalogReport() (web/static/apps.js) |
| 6 | // emits. The web side pins the report's shape in test/appcatalog.test.mjs; |
| 7 | // this file pins the decode of those same shapes, so drift on either side |
| 8 | // breaks a test. The literals here mirror that test's fixtures: a takeover |
| 9 | // app with commands (vault-like) and a chatOnly companion. |
| 10 | final class AppCatalogTests: XCTestCase { |
| 11 | func testDecodesReport() throws { |
| 12 | let report = """ |
| 13 | [ |
| 14 | {"name":"vault","title":"Vault","glyph":"π","places":["chat"], |
| 15 | "listed":true,"quiet":false,"member":false, |
| 16 | "companion":false,"chatOnly":false,"chatReadOnly":false, |
| 17 | "defaultView":"split","defaultCommand":"", |
| 18 | "commands":[{"name":"vault","desc":"Save something","args":"<text>","local":false}]}, |
| 19 | {"name":"habits","title":"Habit tracker","glyph":"β
","places":["you"], |
| 20 | "listed":false,"quiet":false,"member":true, |
| 21 | "companion":true,"chatOnly":true,"chatReadOnly":false, |
| 22 | "defaultView":"chat","defaultCommand":"habit", |
| 23 | "commands":[{"name":"habit","desc":"Log a habit","args":"","local":true}]} |
| 24 | ] |
| 25 | """.data(using: .utf8)! |
| 26 | let catalog = try XCTUnwrap(AppCatalog.decode(reportJSON: report)) |
| 27 | XCTAssertEqual(catalog.count, 2) |
| 28 | |
| 29 | let vault = catalog[0] |
| 30 | XCTAssertEqual(vault.name, "vault") |
| 31 | XCTAssertEqual(vault.title, "Vault") |
| 32 | XCTAssertEqual(vault.glyph, "π") |
| 33 | XCTAssertEqual(vault.places, ["chat"]) |
| 34 | XCTAssertTrue(vault.listed) |
| 35 | XCTAssertFalse(vault.member) |
| 36 | // No chatMessage β takeover: render owns the whole surface. |
| 37 | XCTAssertFalse(vault.companion) |
| 38 | XCTAssertEqual(vault.commands, |
| 39 | [AppManifest.Command(name: "vault", desc: "Save something", |
| 40 | args: "<text>", local: false)]) |
| 41 | |
| 42 | let habits = catalog[1] |
| 43 | XCTAssertTrue(habits.member) |
| 44 | XCTAssertTrue(habits.companion) |
| 45 | XCTAssertTrue(habits.chatOnly) |
| 46 | XCTAssertEqual(habits.defaultView, "chat") |
| 47 | XCTAssertEqual(habits.defaultCommand, "habit") |
| 48 | } |
| 49 | |
| 50 | // Undecodable input is "no report", never an empty app set β an empty |
| 51 | // catalogue would read as "this space has no apps" downstream. A malformed |
| 52 | // RECORD inside a valid report is dropped alone (per-record tolerance): |
| 53 | // one odd member-app manifest must not poison the whole catalogue. |
| 54 | func testUndecodableReportIsNil() { |
| 55 | XCTAssertNil(AppCatalog.decode(reportJSON: Data("not json".utf8))) |
| 56 | XCTAssertEqual(AppCatalog.decode(reportJSON: Data(#"[{"name":"x"}]"#.utf8)), []) |
| 57 | } |
| 58 | |
| 59 | // The record round-trips through Codable losslessly β what the per-origin |
| 60 | // Keychain park depends on. |
| 61 | func testRoundTrip() throws { |
| 62 | let manifest = AppManifest( |
| 63 | name: "board", title: "Whiteboard", glyph: "ποΈ", places: ["chat"], |
| 64 | listed: true, quiet: false, member: false, |
| 65 | companion: false, chatOnly: false, chatReadOnly: false, |
| 66 | defaultView: "split", defaultCommand: "", |
| 67 | commands: [.init(name: "board", desc: "Open the board", args: "", local: false)]) |
| 68 | let data = try JSONEncoder().encode([manifest]) |
| 69 | XCTAssertEqual(AppCatalog.decode(reportJSON: data), [manifest]) |
| 70 | } |
| 71 | } |
| 72 | |