import XCTest @testable import LChatCore // 🤖 The joiner-side welcome landing (#1376): SpaceClaim marks a fresh // member/DM-invite join pending and creates the welcome app-thread, so // WelcomeLanding hands the thread to the first screen and the bundled flow // asks the joiner's name. These pin the flag semantics (UserDefaults) and the // parked-name store that bridges "name accepted" → "instance key arrived". @MainActor final class WelcomePendingTests: XCTestCase { // A unique origin per test so UserDefaults flags never leak across runs. private func freshInstance() -> SavedInstance { SavedInstance(origin: "https://test-\(UUID().uuidString).invalid", deviceToken: "tok", screenName: "", userPub: "pub", userPriv: "priv") } private func welcomeThread(id: String = "t1", hidden: Bool = false, key: String = "k") -> ChatThread { ChatThread(id: id, kind: "self", title: "Eleven", preview: "", lastAt: 0, unread: false, pinned: false, hidden: hidden, muted: nil, members: ["u1"], threadKeyB64: key, avatar: nil, isApp: true, appId: "welcome", glyph: nil, badge: nil) } func testMarkPendingArmsAndRearmsTheLanding() async { let inst = freshInstance() XCTAssertFalse(WelcomeLanding.isPending(inst)) // A previous life of this space (same origin) already landed once — // a FRESH join must land again, so markPending re-arms the flag. UserDefaults.standard.set(true, forKey: WelcomeLanding.flagKey(inst)) WelcomeLanding.markPending(inst) XCTAssertTrue(WelcomeLanding.isPending(inst)) XCTAssertFalse(UserDefaults.standard.bool(forKey: WelcomeLanding.flagKey(inst))) // The invalid origin means conversationFinished can't answer "done", // so the thread lands — and only ONCE per device (the landed flag). let landed = await WelcomeLanding.threadToLand(instance: inst, threads: [welcomeThread()]) XCTAssertEqual(landed?.id, "t1") let again = await WelcomeLanding.threadToLand(instance: inst, threads: [welcomeThread()]) XCTAssertNil(again, "the landing is once per device+instance") XCTAssertTrue(WelcomeLanding.isPending(inst), "pending survives the landing — reopening resumes the conversation") } func testThreadToLandSkipsHiddenAndKeylessThreadsAndNonWelcomeApps() async { let inst = freshInstance() let hidden = welcomeThread(hidden: true) let keyless = welcomeThread(key: "") var other = welcomeThread() other.appId = "todo" let noLanding = await WelcomeLanding.threadToLand(instance: inst, threads: [hidden, keyless, other]) XCTAssertNil(noLanding) // And crucially: a no-thread answer must NOT burn the landed flag — // the claim's thread can appear one refresh later. let landed = await WelcomeLanding.threadToLand(instance: inst, threads: [welcomeThread()]) XCTAssertEqual(landed?.id, "t1") } func testPendingLabelParksPeeksAndClears() { let id = "https://label-\(UUID().uuidString).invalid" XCTAssertNil(PendingLabel.peek(instanceID: id)) PendingLabel.park("Ada", instanceID: id) XCTAssertEqual(PendingLabel.peek(instanceID: id), "Ada") PendingLabel.park("Grace", instanceID: id) // a newer answer supersedes XCTAssertEqual(PendingLabel.peek(instanceID: id), "Grace") PendingLabel.clear(instanceID: id) XCTAssertNil(PendingLabel.peek(instanceID: id)) } func testPendingLabelTreatsEmptyAsNothingParked() { let id = "https://label-\(UUID().uuidString).invalid" PendingLabel.park("", instanceID: id) XCTAssertNil(PendingLabel.peek(instanceID: id), "an empty name must never publish as a blank label") PendingLabel.clear(instanceID: id) } } // 🤖 The admin-side twin: a space claimed through an admin-granting setup // link lands its admin in Eleven's SETUP conversation — whether the space was // created natively or on the web and handed over with "Open in the app". @MainActor final class SetupPendingTests: XCTestCase { private func freshInstance() -> SavedInstance { SavedInstance(origin: "https://setup-\(UUID().uuidString).invalid", deviceToken: "tok", screenName: "", userPub: "pub", userPriv: "priv") } private func setupThread(id: String = "s1", app: String = "setup") -> ChatThread { ChatThread(id: id, kind: "self", title: "Eleven", preview: "", lastAt: 0, unread: false, pinned: false, hidden: false, muted: nil, members: ["u1"], threadKeyB64: "k", avatar: nil, isApp: true, appId: app, glyph: nil, badge: nil) } func testSetupLandsOnceWhenTheClaimArmedIt() async { let inst = freshInstance() // Unarmed: an admin whose setup was done long ago (before the flow // recorded a finish marker) must never be dragged back into it. let unarmed = await SetupLanding.threadToLand(instance: inst, threads: [setupThread()]) XCTAssertNil(unarmed) SetupLanding.markPending(inst) XCTAssertTrue(SetupLanding.isPending(inst)) let landed = await SetupLanding.threadToLand(instance: inst, threads: [setupThread()]) XCTAssertEqual(landed?.id, "s1") let again = await SetupLanding.threadToLand(instance: inst, threads: [setupThread()]) XCTAssertNil(again, "the landing is once per device+instance") } func testSetupIgnoresOtherAppThreads() async { let inst = freshInstance() SetupLanding.markPending(inst) let none = await SetupLanding.threadToLand(instance: inst, threads: [setupThread(app: "welcome")]) XCTAssertNil(none) } }