import XCTest @testable import LChatCore // 🤖 Phase 5 (docs/native-apps.md): the raw material of an app-host // projection query — AppChatProjection.unprojected decodes exactly the app // events native has no built-in projection for, and nothing else. The // query's other half (window.lchatNative.projectEvent) lives in shell.js and // answers with the app's own chatMessage/groupFeedMessage line. final class ProjectionQueryTests: XCTestCase { private let key = try! GroupKey(rawKey: Data(repeating: 3, count: 32)) private func blob(_ json: String) -> String { try! key.encryptData(Data(json.utf8)) } func testNamespacedEventDecodes() { let json = #"{"type":"myhabits.checkin","habit":"run","name":"Ana","uid":"u-ana","ts":1700000000000}"# let ev = AppChatProjection.unprojected(blob(json), key: key) XCTAssertEqual(ev?.name, "Ana") XCTAssertEqual(ev?.uid, "u-ana") XCTAssertEqual(ev?.ts, 1_700_000_000_000) // The decrypted JSON rides verbatim — the host's projectEvent parses it. XCTAssertEqual(ev?.eventJSON, json) } func testEnvelopeFieldsAreOptional() { let ev = AppChatProjection.unprojected(blob(#"{"type":"vault.add"}"#), key: key) XCTAssertEqual(ev?.name, "") XCTAssertNil(ev?.uid) XCTAssertNil(ev?.ts) } func testChatInternalsStaySilent() { // chat.* internals (typing beats, checkpoints) never query. XCTAssertNil(AppChatProjection.unprojected(blob(#"{"type":"chat.typing","name":"Ana"}"#), key: key)) } func testUnnamespacedAndMalformedStaySilent() { XCTAssertNil(AppChatProjection.unprojected(blob(#"{"type":"chat","text":"hi"}"#), key: key)) XCTAssertNil(AppChatProjection.unprojected(blob(#"{"type":".oops"}"#), key: key)) XCTAssertNil(AppChatProjection.unprojected(blob(#"{"text":"no type"}"#), key: key)) XCTAssertNil(AppChatProjection.unprojected("not-a-blob", key: key)) } }