import XCTest @testable import LChatCore // The unread rule shared by refresh() and applyReadFrame(), and the ReadState // storage behind it. The watermark half must match the web's seeding in // shell.js loadThreads (same server field, same comparison); the manual-unread // flag is native-only, deliberately device-local (the server mark is // forward-only, so "unread again" can't ride the watermark itself). final class ReadSyncTests: XCTestCase { func testIsUnread() { // Someone else's message past the watermark lights the dot… XCTAssertTrue(ThreadStore.isUnread(lastAt: 100, lastMine: false, seen: 50, manualUnreadAt: 0)) // …a watermark covering it (read on ANY device) clears it… XCTAssertFalse(ThreadStore.isUnread(lastAt: 100, lastMine: false, seen: 100, manualUnreadAt: 0)) XCTAssertFalse(ThreadStore.isUnread(lastAt: 100, lastMine: false, seen: 150, manualUnreadAt: 0)) // …and your own last message never counts as unread, watermark or not. XCTAssertFalse(ThreadStore.isUnread(lastAt: 100, lastMine: true, seen: 0, manualUnreadAt: 0)) // Mark-as-unread: the flag on exactly the newest message wins over a // covering watermark… XCTAssertTrue(ThreadStore.isUnread(lastAt: 100, lastMine: false, seen: 100, manualUnreadAt: 100)) // …retires itself once a newer message arrives (the watermark rule // takes over)… XCTAssertTrue(ThreadStore.isUnread(lastAt: 120, lastMine: false, seen: 100, manualUnreadAt: 100)) XCTAssertFalse(ThreadStore.isUnread(lastAt: 120, lastMine: false, seen: 120, manualUnreadAt: 100)) // …and never overrides the own-message rule. XCTAssertFalse(ThreadStore.isUnread(lastAt: 100, lastMine: true, seen: 100, manualUnreadAt: 100)) // An EMPTY thread (no messages, so lastAt 0) is never unread: 0 also // means "no manual flag", and the bare `manualUnreadAt == lastAt` // matched 0 == 0 — every empty thread relit its dot on every refresh // (the App Studio phantom dot, caught by the debug event window). XCTAssertFalse(ThreadStore.isUnread(lastAt: 0, lastMine: false, seen: 0, manualUnreadAt: 0)) } func testManualUnreadStorage() { let inst = "test-\(UUID().uuidString)" XCTAssertEqual(ReadState.manualUnreadAt(instance: inst, thread: "t"), 0, "unset reads as 0") ReadState.setManualUnread(instance: inst, thread: "t", at: 42) XCTAssertEqual(ReadState.manualUnreadAt(instance: inst, thread: "t"), 42) ReadState.setManualUnread(instance: inst, thread: "t", at: 0) XCTAssertEqual(ReadState.manualUnreadAt(instance: inst, thread: "t"), 0, "0 clears the flag") UserDefaults.standard.removeObject(forKey: "lchat.unread|\(inst)|t") } }