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,71 @@
|
||||
import Foundation
|
||||
import WireProtocol
|
||||
|
||||
/// Deterministic RNG for the roundtrip property test (reproducible failures:
|
||||
/// the seed is baked into the test; change it only with a note in the test).
|
||||
struct SplitMix64: RandomNumberGenerator {
|
||||
private var state: UInt64
|
||||
|
||||
init(seed: UInt64) {
|
||||
state = seed
|
||||
}
|
||||
|
||||
mutating func next() -> UInt64 {
|
||||
state &+= 0x9E37_79B9_7F4A_7C15
|
||||
var z = state
|
||||
z = (z ^ (z >> 30)) &* 0xBF58_476D_1CE4_E5B9
|
||||
z = (z ^ (z >> 27)) &* 0x94D0_49BB_1331_11EB
|
||||
return z ^ (z >> 31)
|
||||
}
|
||||
}
|
||||
|
||||
enum MessageGen {
|
||||
/// Character pool that stresses the JSON escaper: quotes, backslashes,
|
||||
/// C0 control bytes (ESC / CR / TAB / ^C / NUL), CJK, emoji, plain ASCII.
|
||||
private static let stressPool: [Character] = [
|
||||
"a", "Z", "0", " ", "\"", "\\", "/", "{", "}",
|
||||
"\u{1B}", "\r", "\n", "\t", "\u{03}", "\u{00}", "\u{7F}",
|
||||
"中", "文", "終", "端", "🚀", "🧪", "é", "ß",
|
||||
]
|
||||
|
||||
static func randomString(using rng: inout some RandomNumberGenerator, maxLength: Int = 48) -> String {
|
||||
let length = Int.random(in: 0...maxLength, using: &rng)
|
||||
return String((0..<length).map { _ in stressPool.randomElement(using: &rng)! })
|
||||
}
|
||||
|
||||
static func randomAbsolutePath(using rng: inout some RandomNumberGenerator) -> String {
|
||||
"/" + randomString(using: &rng, maxLength: 24)
|
||||
}
|
||||
|
||||
/// Seeded UUID v4 (version nibble 4, variant 10xx) — passes SESSION_ID_RE.
|
||||
static func randomUUIDv4(using rng: inout some RandomNumberGenerator) -> UUID {
|
||||
var bytes = (0..<16).map { _ in UInt8.random(in: 0...255, using: &rng) }
|
||||
bytes[6] = (bytes[6] & 0x0F) | 0x40
|
||||
bytes[8] = (bytes[8] & 0x3F) | 0x80
|
||||
return UUID(uuid: (
|
||||
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
||||
bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]
|
||||
))
|
||||
}
|
||||
|
||||
static func randomClientMessage(using rng: inout some RandomNumberGenerator) -> ClientMessage {
|
||||
switch Int.random(in: 0...4, using: &rng) {
|
||||
case 0:
|
||||
let sessionId = Bool.random(using: &rng) ? randomUUIDv4(using: &rng) : nil
|
||||
let cwd = Bool.random(using: &rng) ? randomAbsolutePath(using: &rng) : nil
|
||||
return .attach(sessionId: sessionId, cwd: cwd)
|
||||
case 1:
|
||||
return .input(data: randomString(using: &rng))
|
||||
case 2:
|
||||
return .resize(
|
||||
cols: Int.random(in: 1...1000, using: &rng),
|
||||
rows: Int.random(in: 1...1000, using: &rng)
|
||||
)
|
||||
case 3:
|
||||
let mode = Bool.random(using: &rng) ? ApproveMode.allCases.randomElement(using: &rng) : nil
|
||||
return .approve(mode: mode)
|
||||
default:
|
||||
return .reject
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user