| 1 | import XCTest |
| 2 | @testable import LChatCore |
| 3 | |
| 4 | // 🤖 The notification-tap resolution ladder (NotificationRoute): parked key → |
| 5 | // snapshot scan → only-space. The silent-drop bug this pins: a stale or |
| 6 | // missing SharedThreadKeys park used to strand the tap on the chat list. |
| 7 | final class NotificationRouteTests: XCTestCase { |
| 8 | |
| 9 | private func inst(_ origin: String, archived: Bool = false) -> SavedInstance { |
| 10 | var i = SavedInstance(origin: origin, deviceToken: "t", screenName: "s", |
| 11 | userPub: "pub", userPriv: "priv") |
| 12 | i.archived = archived |
| 13 | return i |
| 14 | } |
| 15 | |
| 16 | func testParkedKeyWins() { |
| 17 | let a = inst("https://a.example"), b = inst("https://b.example") |
| 18 | let r = NotificationRoute.resolve( |
| 19 | threadID: "th1", instances: [a, b], |
| 20 | entryOrigin: { _ in "https://b.example" }, |
| 21 | snapshotHasThread: { _, _ in XCTFail("snapshot consulted despite park"); return false }) |
| 22 | guard case .parkedKey(let hit) = r else { return XCTFail("\(r)") } |
| 23 | XCTAssertEqual(hit.origin, b.origin) |
| 24 | } |
| 25 | |
| 26 | func testStaleParkFallsBackToSnapshot() { |
| 27 | // The park points at an origin that's no longer signed in (re-added |
| 28 | // space) — the snapshot scan must still find the thread's real home. |
| 29 | let a = inst("https://a.example"), b = inst("https://b.example") |
| 30 | let r = NotificationRoute.resolve( |
| 31 | threadID: "th1", instances: [a, b], |
| 32 | entryOrigin: { _ in "https://gone.example" }, |
| 33 | snapshotHasThread: { origin, thread in origin == "https://b.example" && thread == "th1" }) |
| 34 | guard case .snapshot(let hit) = r else { return XCTFail("\(r)") } |
| 35 | XCTAssertEqual(hit.origin, b.origin) |
| 36 | } |
| 37 | |
| 38 | func testMissingParkFallsBackToSnapshot() { |
| 39 | let a = inst("https://a.example"), b = inst("https://b.example") |
| 40 | let r = NotificationRoute.resolve( |
| 41 | threadID: "th1", instances: [a, b], |
| 42 | entryOrigin: { _ in nil }, |
| 43 | snapshotHasThread: { origin, _ in origin == "https://a.example" }) |
| 44 | guard case .snapshot(let hit) = r else { return XCTFail("\(r)") } |
| 45 | XCTAssertEqual(hit.origin, a.origin) |
| 46 | } |
| 47 | |
| 48 | func testSingleSpaceAlwaysResolves() { |
| 49 | // Nothing recorded anywhere (wiped keychain, brand-new thread): with |
| 50 | // one signed-in space the tap still routes there. |
| 51 | let a = inst("https://a.example") |
| 52 | let r = NotificationRoute.resolve( |
| 53 | threadID: "new-thread", instances: [a], |
| 54 | entryOrigin: { _ in nil }, |
| 55 | snapshotHasThread: { _, _ in false }) |
| 56 | guard case .onlySpace(let hit) = r else { return XCTFail("\(r)") } |
| 57 | XCTAssertEqual(hit.origin, a.origin) |
| 58 | } |
| 59 | |
| 60 | func testSingleActiveSpaceSkipsArchived() { |
| 61 | let live = inst("https://live.example") |
| 62 | let parked = inst("https://old.example", archived: true) |
| 63 | let r = NotificationRoute.resolve( |
| 64 | threadID: "th1", instances: [live, parked], |
| 65 | entryOrigin: { _ in nil }, |
| 66 | snapshotHasThread: { _, _ in false }) |
| 67 | guard case .onlySpace(let hit) = r else { return XCTFail("\(r)") } |
| 68 | XCTAssertEqual(hit.origin, live.origin) |
| 69 | } |
| 70 | |
| 71 | func testMultiSpaceUnknownThreadStaysUnresolved() { |
| 72 | let a = inst("https://a.example"), b = inst("https://b.example") |
| 73 | let r = NotificationRoute.resolve( |
| 74 | threadID: "th1", instances: [a, b], |
| 75 | entryOrigin: { _ in nil }, |
| 76 | snapshotHasThread: { _, _ in false }) |
| 77 | guard case .unresolved = r else { return XCTFail("\(r)") } |
| 78 | } |
| 79 | } |
| 80 | |