import Foundation import Testing @testable import Voice /// Pure coordinator tests for the server replacement worker loop semantics /// (Iteration 08 Item 2). These verify the latest-wins coalescing and /// drain-until-empty behavior that the ConversationView worker implements, /// without requiring a live SwiftUI view or real session disconnect. /// /// The production code uses `@State` in a SwiftUI view; this test models the /// same logic as a pure function to verify A→B→C semantics where C arrives /// while B is disconnecting. struct ServerReplacementCoordinatorTests { /// Models the pending-URL coalescing and worker-loop drain semantics. /// Returns the sequence of URLs the worker would process in order. private static func drainPending( initialActive: String, applies: [String], disconnectDurations: [Int] = [] ) -> [String] { var activeURL = initialActive var pending: String? = nil var processed: [String] = [] var isReplacing = false for url in applies { // Every accepted Apply writes pendingServerURL (latest wins). pending = url // Start the worker if not already active. guard !isReplacing else { continue } isReplacing = true // Worker loop: drain until no pending value remains. while let url = pending { pending = nil guard url != activeURL else { continue } // Simulate disconnect (no-op in pure test). activeURL = url processed.append(url) } isReplacing = false } return processed } @Test("single apply processes one URL") func singleApply() { let result = Self.drainPending( initialActive: "https://server-a", applies: ["https://server-b"] ) #expect(result == ["https://server-b"]) } @Test("rapid A to B to C coalesces to final C") func rapidABC() { // All three Applies arrive before the worker starts (isReplacing // is false for each, but the first one starts the worker which // drains all pending values). // In practice: Apply A sets pending=A, starts worker. Worker // consumes A. Apply B sets pending=B during disconnect. Worker // loops, consumes B. Apply C sets pending=C during B's disconnect. // Worker loops, consumes C. Final state: activeServerURL == C. let result = Self.drainPending( initialActive: "https://server-a", applies: ["https://server-b", "https://server-c", "https://server-d"] ) // The worker processes each URL in order as it drains. #expect(result == ["https://server-b", "https://server-c", "https://server-d"]) } @Test("reapplying same URL is skipped") func sameURLSkipped() { let result = Self.drainPending( initialActive: "https://server-a", applies: ["https://server-a"] ) #expect(result.isEmpty) } @Test("latest wins when multiple applies during replacement") func latestWinsDuringReplacement() { // Simulates: Apply B starts worker. During B's disconnect, // Apply C arrives (pending=C). Worker finishes B, loops, // sees pending=C, processes C. Final active = C. var activeURL = "https://server-a" var pending: String? = nil // Apply B pending = "https://server-b" // Worker starts, consumes B if let url = pending { pending = nil activeURL = url } // During B's disconnect, Apply C arrives pending = "https://server-c" // Worker loops: sees pending=C, processes it while let url = pending { pending = nil guard url != activeURL else { continue } activeURL = url } #expect(activeURL == "https://server-c") } @Test("A to B to C with C arriving during B disconnect ends on C") func abcWithCDuringBDisconnect() { var activeURL = "https://server-a" var pending: String? = nil var processed: [String] = [] // Apply B (A is the initial active URL) pending = "https://server-b" // Worker loop iteration 1: consume B while let url = pending { pending = nil guard url != activeURL else { continue } // Simulate disconnect of current session (A) activeURL = url processed.append(url) // During B's disconnect, Apply C arrives pending = "https://server-c" } // Worker loop iteration 2: consume C while let url = pending { pending = nil guard url != activeURL else { continue } activeURL = url processed.append(url) } #expect(activeURL == "https://server-c") #expect(processed == ["https://server-b", "https://server-c"]) } }