Files
hope-voice-api/ios/Tests/ServerReplacementCoordinatorTests.swift
2026-10-04 07:23:49 -04:00

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"])
}
}