// // AT_IRCTests.swift // AT-IRCTests // // Created by John on 10/30/26. // import Testing import UIKit import AtircGo @testable import AT_IRC @MainActor struct AT_IRCTests { @Test func nickNormalizationMatchesWeeChat() { #expect(NickColor.normalizedNick("Alice") == "alice") #expect(NickColor.normalizedNick("nick_123") == "nick") #expect(NickColor.normalizedNick("nick|away") == "nick") #expect(NickColor.normalizedNick("nick[away") == "nick") #expect(NickColor.normalizedNick("|nick|away") == "|nick") // A stop character before any real character is kept. #expect(NickColor.normalizedNick("_nick") == "_nick") } @Test func nickHashMatchesWeeChat() { #expect(NickColor.hash("alice") == 182_724_062_590) #expect(NickColor.hash("nick") == 5_838_659_604) #expect(NickColor.hash("bob") == 178_239_285) } @Test func paletteIndexMatchesWeeChat() { #expect(NickColor.paletteIndex(for: "alice") == 13) #expect(NickColor.paletteIndex(for: "nick") == 15) #expect(NickColor.paletteIndex(for: "bob") == 0) // Case and stop-character suffixes do not change the color. #expect(NickColor.paletteIndex(for: "ALICE") == NickColor.paletteIndex(for: "alice")) #expect(NickColor.paletteIndex(for: "alice_away") == NickColor.paletteIndex(for: "alice")) } @Test func colorCacheIsBounded() { for index in 0..<(NickColor.cacheLimit + 100) { _ = NickColor.color(for: "cache-test-\(index)") } #expect(NickColor.cachedNickCount == NickColor.cacheLimit) } @Test func composerClearsProgrammatically() { let textView = UITextView() textView.text = "already sent" ComposerTextView.apply("", to: textView) #expect(textView.text.isEmpty) } @Test func goRuntimeIsEmbedded() throws { #expect(GoBridge.runtimeVersion.hasPrefix("go1.")) #expect(GoBridge.echo(" Swift ") == "hello, Swift from Go") #expect(GoBridge.sha256Hex(Data("abc".utf8)) == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad") #expect(try GoBridge.parseCommand("PRIVMSG #swift :hello") == "PRIVMSG") #expect(throws: (any Error).self) { try GoBridge.parseCommand(" ") } } @Test func goStructAndMethodsBridge() { let counter = GoBridge.makeCounter() #expect(counter?.increment() == 1) #expect(counter?.count == 1) } @Test func goGoroutineRuns() async throws { GoBridge.recordAfter("ping") for _ in 0..<100 { if GoBridge.lastRecorded() == "ping" { break } try await Task.sleep(for: .milliseconds(10)) } #expect(GoBridge.lastRecorded() == "ping") } @Test func goCallsBackIntoSwift() async { let received = await withCheckedContinuation { (continuation: CheckedContinuation) in let callback = GoCallback { message in continuation.resume(returning: message) } GoBridge.emitLater(callback, message: "pong") } #expect(received == "pong") } @Test func composerIgnoresUnchangedText() { let textView = UITextView() textView.text = "draft" textView.selectedRange = NSRange(location: 2, length: 0) ComposerTextView.apply("draft", to: textView) #expect(textView.text == "draft") #expect(textView.selectedRange.location == 2) } @Test func askQuestionDecodesServerShape() throws { let json = """ {"id":"q1","account":"did:plc:acct","askedBy":"did:plc:agent", "askedHandle":"agent.owner.bsky.social","question":"Which color?", "options":["red","blue"],"context":"release branch","channel":"#owner", "createdAt":1760000000,"status":"pending"} """ let ask = try JSONDecoder().decode(AskQuestion.self, from: Data(json.utf8)) #expect(ask.id == "q1") #expect(ask.askedHandle == "agent.owner.bsky.social") #expect(ask.options == ["red", "blue"]) #expect(ask.isPending) #expect(ask.date?.timeIntervalSince1970 == 1_760_000_000) #expect(ask.asker == "agent.owner.bsky.social") } @Test func askQuestionsResponseCarriesModeAndDecisions() throws { let json = """ {"mode":"autonomous","questions":[ {"id":"q2","question":"Proceed?","status":"decided", "decision":"ship it","decidedBy":"agent", "adr":"https://example.com/adr/1"}]} """ let response = try JSONDecoder().decode(AskQuestionsResponse.self, from: Data(json.utf8)) #expect(response.mode == "autonomous") #expect(response.questions.first?.status == "decided") #expect(response.questions.first?.decision == "ship it") #expect(response.questions.first?.adr == "https://example.com/adr/1") } @Test func newPendingAskIDsSkipsAlreadyNotified() { let asks = [ AskQuestion(id: "a", question: "A?", status: "pending"), AskQuestion(id: "b", question: "B?", status: "answered"), AskQuestion(id: "c", question: "C?", status: "pending"), ] #expect(ChatStore.newPendingAskIDs(in: asks, notified: ["a"]) == ["c"]) #expect(ChatStore.newPendingAskIDs(in: asks, notified: ["a", "c"]).isEmpty) } @Test func presenceIdentitySplitsNickAndHost() { let member = AtircMember.from(presenceIdentity: "wasm@wasm.bobvmbuilder.bsky.social") #expect(member?.id == "wasm@wasm.bobvmbuilder.bsky.social") #expect(member?.nick == "wasm") #expect(member?.identity == "wasm.bobvmbuilder.bsky.social") #expect(AtircMember.from(presenceIdentity: "plain")?.nick == "plain") #expect(AtircMember.from(presenceIdentity: " ") == nil) } @Test func timelineInterleavesNoticesByDate() { let messages = [ AtircMessage( seq: 1, channel: "#c", did: nil, handle: nil, nick: "a", text: "one", ts: "1970-01-01T00:16:40.000Z", source: nil, identity: nil ), AtircMessage( seq: 3, channel: "#c", did: nil, handle: nil, nick: "a", text: "three", ts: "1970-01-01T00:18:20.000Z", source: nil, identity: nil ), ] let notice = ChatNotice( channel: "#c", nick: "joe", identity: "joe.host", kind: .join, date: Date(timeIntervalSince1970: 1_060) ) let rows = ChatStore.orderedTimeline(messages, notices: [notice]) #expect(rows.map(\.id) == ["seq-1", "notice-\(notice.id.uuidString)", "seq-3"]) } @Test func presenceJoinAndLeaveRenderInTimeline() async throws { let store = ChatStore() store.selectedChannel = "#test_presence" store.handlePresenceForTest([ "event": "join", "channel": "#test_presence", "identity": "joe@joe.example", "nick": "joe", ]) try await Task.sleep(for: .milliseconds(400)) #expect(store.members.map(\.nick) == ["joe"]) var notices = store.timeline.compactMap { row -> ChatNotice? in if case .notice(let notice) = row { return notice } return nil } #expect(notices.map(\.nick) == ["joe"]) #expect(notices.first?.kind == .join) store.handlePresenceForTest([ "event": "leave", "channel": "#test_presence", "identity": "joe@joe.example", "nick": "joe", "reason": "Subscriber disconnected", ]) try await Task.sleep(for: .milliseconds(400)) #expect(store.members.isEmpty) notices = store.timeline.compactMap { row -> ChatNotice? in if case .notice(let notice) = row { return notice } return nil } #expect(notices.map(\.kind) == [.join, .leave]) } }