~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atirc-ios-client

0
1
main
214 lines · 7.9 KB Raw History
//
//  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<String?, Never>) 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])
    }
}