T-iOS-5: pure reconnect reducer (1s→30s cap, mirrors terminal-session.ts) + 25s PingScheduler, 16 tests
T-iOS-6: gate epoch tracker (rising-edge semantics, canDecide guard) + AwayDigest reducer, 25 tests
T-iOS-7: HostRegistry with SecItemShim keychain seam, 30 tests, 88.1% own-src coverage
T-iOS-8: APIClient (Origin iff-G invariant) + two-step pairing probe, 45 tests, 98.1% coverage
Contract ruling: probe returns Result<HostEndpoint,_> (Host{id,name} built by pairing VM) —
resolves frozen-contract contradiction reported via BLOCKED protocol; adds Tunables.pairingProbeTimeout(10s)
Verified: 178 tests green across 5 packages; coverage gates pass; zero Owns violations
81 lines
3.3 KiB
Swift
81 lines
3.3 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.pairingProbeTimeout == .seconds(10)) // §3.4 契约裁定 2026-07-04
|
||
#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")
|
||
}
|