import XCTest @testable import LChatCore // Presence wire contract: the "presence" hub frame and the roster's stamped // presence field decode as the server sends them (presence.go / PR #413), so // the native dots read the same states the web does. final class PresenceTests: XCTestCase { func testPresenceFrameDecodes() throws { let json = #"{"type":"presence","user_id":"u-alice","presence":"away"}"# let frame = try JSONDecoder().decode(ChatSocket.Frame.self, from: Data(json.utf8)) XCTAssertEqual(frame.type, "presence") XCTAssertEqual(frame.user_id, "u-alice") XCTAssertEqual(frame.presence, "away") XCTAssertNil(frame.thread_id) // presence is roster-wide, never thread-scoped } // Older frames (msg/typing/…) carry no presence fields — they must keep // decoding with the new optionals absent. func testFramesWithoutPresenceStillDecode() throws { let json = #"{"type":"msg","thread_id":"t1","id":7,"created_at":1700000000,"blob":"x"}"# let frame = try JSONDecoder().decode(ChatSocket.Frame.self, from: Data(json.utf8)) XCTAssertEqual(frame.type, "msg") XCTAssertNil(frame.user_id) XCTAssertNil(frame.presence) } // The pure filter rules mirror web/static/ui.js — same cases as ui.test.mjs, // so web and native can't drift on what "Online only" or a category means. func testOnlineFilterKeeps() { // People decide: online keeps, away/offline drop. XCTAssertTrue(onlineFilterKeeps([.init(agent: false, presence: "online")])) XCTAssertFalse(onlineFilterKeeps([.init(agent: false, presence: "away")])) XCTAssertFalse(onlineFilterKeeps([.init(agent: false, presence: "offline")])) // A chat with ONLY bots falls back to the bot's own status (sidecar health). XCTAssertTrue(onlineFilterKeeps([.init(agent: true, presence: "online")])) XCTAssertFalse(onlineFilterKeeps([.init(agent: true, presence: "offline")])) // In a mixed room only the people count — an always-on bot must not make // every room it sits in read "someone's online". XCTAssertFalse(onlineFilterKeeps([ .init(agent: true, presence: "online"), .init(agent: false, presence: "offline"), ])) XCTAssertTrue(onlineFilterKeeps([ .init(agent: true, presence: "offline"), .init(agent: false, presence: "online"), ])) // Notes/app threads (no other members) never match. XCTAssertFalse(onlineFilterKeeps([])) } func testChatCategory() { XCTAssertEqual(chatCategory(isApp: true, others: []), .apps) XCTAssertEqual(chatCategory(isApp: true, others: [.init(agent: true, presence: "online")]), .apps) XCTAssertEqual(chatCategory(isApp: false, others: [.init(agent: true, presence: "offline")]), .bots) // Any person in the room makes it a people chat, bots present or not. XCTAssertEqual(chatCategory(isApp: false, others: [ .init(agent: true, presence: "online"), .init(agent: false, presence: "offline"), ]), .people) XCTAssertEqual(chatCategory(isApp: false, others: []), .people) // your own Notes } // The inline filter chip's copy — one shared derivation, so iOS and Mac say // the same words for the same flags. func testChatFilterSummary() { XCTAssertNil(chatFilterSummary(onlineOnly: false, people: true, bots: true, apps: true)) XCTAssertEqual(chatFilterSummary(onlineOnly: true, people: true, bots: true, apps: true), "Online only") XCTAssertEqual(chatFilterSummary(onlineOnly: false, people: true, bots: true, apps: false), "Hiding apps") XCTAssertEqual(chatFilterSummary(onlineOnly: false, people: true, bots: false, apps: false), "Hiding bots & apps") XCTAssertEqual(chatFilterSummary(onlineOnly: false, people: false, bots: false, apps: false), "Hiding people, bots & apps") XCTAssertEqual(chatFilterSummary(onlineOnly: true, people: true, bots: false, apps: false), "Online only · hiding bots & apps") } func testPresenceText() { XCTAssertEqual(presenceText("online"), "Online") XCTAssertEqual(presenceText("away"), "Away") XCTAssertEqual(presenceText("offline"), "Offline") XCTAssertNil(presenceText("")) XCTAssertNil(presenceText(nil)) } func testRosterEntryCarriesPresence() throws { let json = #"{"user_id":"u1","label":"Alice","public_key":"pk","presence":"online"}"# let entry = try JSONDecoder().decode(RosterEntry.self, from: Data(json.utf8)) XCTAssertEqual(entry.presence, "online") // The field is omitempty on the server — absent must decode too. let legacy = #"{"user_id":"u2","label":"Bob","public_key":"pk2"}"# let bob = try JSONDecoder().decode(RosterEntry.self, from: Data(legacy.utf8)) XCTAssertNil(bob.presence) } }