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
80 lines
3.2 KiB
Swift
80 lines
3.2 KiB
Swift
import Foundation
|
||
import Testing
|
||
import WireProtocol
|
||
|
||
// T-iOS-3 · 冻结契约常量绊线:Tunables(§3.2.1 唯一取值表)与 WireConstants。
|
||
// 这些断言故意逐字重复取值表——改常量必须回 T-iOS-3 改契约并同步这里。
|
||
|
||
@Test("Tunables 取值与 §3.2.1 表逐项一致")
|
||
func tunablesMatchFrozenValueTable() {
|
||
#expect(Tunables.pingInterval == .seconds(25))
|
||
#expect(Tunables.pongMissLimit == 2)
|
||
#expect(Tunables.listPollInterval == .seconds(5)) // public/launcher.ts:30 REFRESH_MS
|
||
#expect(Tunables.telemetryStaleTtlMs == 30_000) // public/tabs.ts:45 STATUSLINE_TTL_MS
|
||
#expect(Tunables.digestFadeDelay == .seconds(8))
|
||
#expect(Tunables.titleMaxLength == 256)
|
||
#expect(Tunables.maxWSMessageBytes == 16 * 1024 * 1024)
|
||
}
|
||
|
||
@Test("maxWSMessageBytes ≥ 6 × 默认 SCROLLBACK_BYTES(2MiB)(JSON 转义最坏膨胀系数)")
|
||
func maxWSMessageBytesCoversWorstCaseReplayExpansion() {
|
||
// Arrange — src/config.ts:39 DEFAULT_SCROLLBACK_BYTES = 2MiB;\uXXXX 转义最坏 6×
|
||
let defaultScrollbackBytes = 2 * 1024 * 1024
|
||
let worstCaseEscapeFactor = 6
|
||
|
||
// Assert
|
||
#expect(Tunables.maxWSMessageBytes >= worstCaseEscapeFactor * defaultScrollbackBytes)
|
||
}
|
||
|
||
@Test("WireConstants:wsPath/soft-reset 前缀/spawn 失败码/resize 区间")
|
||
func wireConstantsMatchServer() {
|
||
#expect(WireConstants.wsPath == "/term") // src/config.ts:41 DEFAULT_WS_PATH
|
||
#expect(WireConstants.replaySoftResetPrefix == "\u{1B}[0m") // src/types.ts:167-170
|
||
#expect(WireConstants.spawnFailedExitCode == -1) // M4
|
||
#expect(WireConstants.resizeRange == 1...1000) // src/protocol.ts:113-115
|
||
}
|
||
|
||
@Test("TransportConnection:能力句柄原样保存;frames finish = 干净断线")
|
||
func transportConnectionHoldsCapabilityHandles() async throws {
|
||
// Arrange
|
||
actor Recorder {
|
||
var sentFrames: [String] = []
|
||
var isClosed = false
|
||
func recordSend(_ frame: String) { sentFrames.append(frame) }
|
||
func recordClose() { isClosed = true }
|
||
}
|
||
let recorder = Recorder()
|
||
let connection = TransportConnection(
|
||
frames: AsyncThrowingStream { continuation in
|
||
continuation.yield("{\"type\":\"output\",\"data\":\"x\"}")
|
||
continuation.finish()
|
||
},
|
||
send: { await recorder.recordSend($0) },
|
||
close: { await recorder.recordClose() }
|
||
)
|
||
|
||
// Act
|
||
var received: [String] = []
|
||
for try await frame in connection.frames {
|
||
received.append(frame)
|
||
}
|
||
try await connection.send("{\"type\":\"reject\"}")
|
||
await connection.close()
|
||
|
||
// Assert
|
||
#expect(received == ["{\"type\":\"output\",\"data\":\"x\"}"])
|
||
#expect(await recorder.sentFrames == ["{\"type\":\"reject\"}"])
|
||
#expect(await recorder.isClosed)
|
||
}
|
||
|
||
@Test("ClaudeStatus/GateKind rawValue 与服务器字面量一致")
|
||
func statusAndGateRawValuesMatchServer() {
|
||
#expect(ClaudeStatus.working.rawValue == "working")
|
||
#expect(ClaudeStatus.waiting.rawValue == "waiting")
|
||
#expect(ClaudeStatus.idle.rawValue == "idle")
|
||
#expect(ClaudeStatus.unknown.rawValue == "unknown")
|
||
#expect(ClaudeStatus.stuck.rawValue == "stuck")
|
||
#expect(GateKind.tool.rawValue == "tool")
|
||
#expect(GateKind.plan.rawValue == "plan")
|
||
}
|