feat(ios): W0 scaffold + day-1 spike + WireProtocol frozen contract + TestSupport doubles
T-iOS-1: ios/ XcodeGen project (iOS 17, Swift 6 strict concurrency, ATS per PLAN §5.2), 5 SPM package shells, CI skeleton T-iOS-2: Origin spike vs real server — URLSessionWebSocketTask custom Origin CONFIRMED (no Starscream); 16MiB replay + EMSGSIZE(40) errno correction written back to plan T-iOS-3: WireProtocol frozen contract, 59 tests, 100% line coverage, cross-impl vectors vs src/protocol.ts via tsx T-iOS-4: FakeTransport/FakeClock/FakeHTTPTransport doubles Verify: independent agent re-ran all acceptance — 6/6 PASS
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import Testing
|
||||
@testable import TestSupport
|
||||
|
||||
@Suite("FakeClock")
|
||||
struct FakeClockTests {
|
||||
private static let sleepDuration: Duration = .seconds(25)
|
||||
private static let partialAdvance: Duration = .seconds(10)
|
||||
private static let remainingAdvance: Duration = .seconds(15)
|
||||
|
||||
@Test("sleeper wakes only after the clock is manually advanced past its deadline")
|
||||
func sleeperWakesOnlyAfterAdvancePastDeadline() async throws {
|
||||
// Arrange
|
||||
let clock = FakeClock()
|
||||
let deadline = clock.now.advanced(by: Self.sleepDuration)
|
||||
let sleeper = Task {
|
||||
try await clock.sleep(until: deadline, tolerance: nil)
|
||||
}
|
||||
await clock.waitForSleepers(count: 1)
|
||||
|
||||
// Act: advance short of the deadline — the sleeper must stay asleep.
|
||||
clock.advance(by: Self.partialAdvance)
|
||||
#expect(clock.pendingSleeperCount == 1)
|
||||
|
||||
// Act: advance up to the deadline — the sleeper must wake.
|
||||
clock.advance(by: Self.remainingAdvance)
|
||||
try await sleeper.value
|
||||
|
||||
// Assert: zero real-time waiting, purely manual time.
|
||||
#expect(clock.pendingSleeperCount == 0)
|
||||
#expect(clock.now == FakeClock.Instant(offset: Self.sleepDuration))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import TestSupport
|
||||
|
||||
@Suite("FakeHTTPTransport")
|
||||
struct FakeHTTPTransportTests {
|
||||
private static let originValue = "http://192.168.1.5:3000"
|
||||
|
||||
@Test("replays queued responses per URL/method, records requests with headers, and fails loudly when unqueued")
|
||||
func replaysQueuedResponsesAndRecordsRequests() async throws {
|
||||
// Arrange
|
||||
let transport = FakeHTTPTransport()
|
||||
let listURL = try #require(URL(string: "http://192.168.1.5:3000/live-sessions"))
|
||||
let unqueuedURL = try #require(URL(string: "http://192.168.1.5:3000/other"))
|
||||
let body = Data("[]".utf8)
|
||||
await transport.queueSuccess(url: listURL, body: body)
|
||||
|
||||
var request = URLRequest(url: listURL)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue(Self.originValue, forHTTPHeaderField: "Origin")
|
||||
|
||||
// Act
|
||||
let (data, response) = try await transport.send(request)
|
||||
|
||||
// Assert: the queued response came back for that URL/method.
|
||||
#expect(data == body)
|
||||
#expect(response.statusCode == FakeHTTPTransport.defaultOKStatus)
|
||||
#expect(response.url == listURL)
|
||||
|
||||
// Assert: the request was recorded verbatim, headers included
|
||||
// (this is what lets APIClient tests assert Origin-iff-G, plan §3.4).
|
||||
let recorded = await transport.recordedRequests
|
||||
#expect(recorded.count == 1)
|
||||
#expect(recorded.first?.url == listURL)
|
||||
#expect(recorded.first?.value(forHTTPHeaderField: "Origin") == Self.originValue)
|
||||
|
||||
// Assert: an unqueued route throws an explicit, identifying error.
|
||||
await #expect(throws: FakeHTTPTransportError.noQueuedResponse(method: "GET", url: unqueuedURL)) {
|
||||
_ = try await transport.send(URLRequest(url: unqueuedURL))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
import WireProtocol
|
||||
@testable import TestSupport
|
||||
|
||||
@Suite("FakeTransport")
|
||||
struct FakeTransportTests {
|
||||
@Test("scripts connect failures, delivers injected frames, records sends and closes")
|
||||
func scriptsFailuresDeliversFramesAndRecordsTraffic() async throws {
|
||||
// Arrange
|
||||
let transport = FakeTransport()
|
||||
let baseURL = try #require(URL(string: "http://192.168.1.5:3000"))
|
||||
let endpoint = try #require(HostEndpoint(baseURL: baseURL))
|
||||
let attachFrame = #"{"type":"attach","sessionId":null}"#
|
||||
|
||||
// Act + Assert: a scripted failure makes the next connect throw it.
|
||||
await transport.scriptConnectFailure()
|
||||
await #expect(throws: FakeTransportError.scriptedConnectFailure) {
|
||||
_ = try await transport.connect(to: endpoint)
|
||||
}
|
||||
|
||||
// Act: connect for real, send one frame, inject one frame, close cleanly.
|
||||
let connection = try await transport.connect(to: endpoint)
|
||||
try await connection.send(attachFrame)
|
||||
await transport.emit(frame: "server-frame-1")
|
||||
await transport.finishFrames()
|
||||
|
||||
var received: [String] = []
|
||||
for try await frame in connection.frames {
|
||||
received.append(frame)
|
||||
}
|
||||
await connection.close()
|
||||
|
||||
// Assert: injected frames arrived in order and ended with a clean finish.
|
||||
#expect(received == ["server-frame-1"])
|
||||
// Assert: the double recorded everything the client did.
|
||||
#expect(await transport.sentFrames == [attachFrame])
|
||||
#expect(await transport.closeCallCount == 1)
|
||||
#expect(await transport.connectAttempts == [endpoint, endpoint])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user