Archived
147 lines
5.0 KiB
Swift
147 lines
5.0 KiB
Swift
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"])
|
|
}
|
|
}
|