Files
web-terminal/ios/Packages/WireProtocol/Tests/WireProtocolTests/ServerVectorTests.swift
Yaojia Wang cbaa08daba 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
2026-07-04 21:19:30 +02:00

260 lines
12 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import Testing
import WireProtocol
// T-iOS-3 · test/protocol.test.ts + decodeServer
// TS/Swift T-iOS-16 CI
private let validUUID = "f47ac10b-58cc-4372-a567-0e02b2c3d479"
// MARK: - Validation.isValidSessionIdSESSION_ID_RE , test/protocol.test.ts:19-49
@Test("isValidSessionId合法 UUID v4 通过")
func sessionIdAcceptsValidUUIDv4() {
#expect(Validation.isValidSessionId(validUUID))
}
@Test("isValidSessionId大写十六进制也通过服务器正则 /i")
func sessionIdAcceptsUppercaseHex() {
#expect(Validation.isValidSessionId("F47AC10B-58CC-4372-A567-0E02B2C3D479"))
}
@Test("isValidSessionIdvariant=8 的 UUID v4 通过")
func sessionIdAcceptsVariant8() {
#expect(Validation.isValidSessionId("550e8400-e29b-41d4-8716-446655440000"))
}
@Test("isValidSessionId非 UUID / 空串 / v1 / 错误 variant 全部拒绝M7",
arguments: [
"abc123",
"",
"550e8400-e29b-11d4-a716-446655440000", // v1=1
"f47ac10b-58cc-4372-c567-0e02b2c3d479", // variant 'c' 8/9/a/b
"f47ac10b58cc4372a5670e02b2c3d479", //
"f47ac10b-58cc-4372-a567-0e02b2c3d47", //
"g47ac10b-58cc-4372-a567-0e02b2c3d479", //
])
func sessionIdRejectsInvalid(candidate: String) {
#expect(!Validation.isValidSessionId(candidate))
}
@Test("isValidSessionId 与测试侧服务器正则镜像逐向量同判(防移植走样)")
func sessionIdAgreesWithServerRegexMirror() {
// Arrange
let candidates = [
validUUID, "F47AC10B-58CC-4372-A567-0E02B2C3D479",
"550e8400-e29b-41d4-8716-446655440000", "abc123", "",
"550e8400-e29b-11d4-a716-446655440000",
"f47ac10b-58cc-4372-c567-0e02b2c3d479",
]
for candidate in candidates {
// Act / Assert
#expect(Validation.isValidSessionId(candidate)
== ServerViewParser.isServerValidSessionId(candidate),
"分歧向量: \(candidate)")
}
}
// MARK: - isValidResize / isAbsoluteCwdsrc/protocol.ts:113-115,142-149
@Test("isValidResize边界 1/1000 通过0/1001 拒绝")
func resizeValidationBoundaries() {
#expect(Validation.isValidResize(cols: 1, rows: 1))
#expect(Validation.isValidResize(cols: 1000, rows: 1000))
#expect(Validation.isValidResize(cols: 120, rows: 40))
#expect(!Validation.isValidResize(cols: 0, rows: 40))
#expect(!Validation.isValidResize(cols: 1001, rows: 40))
#expect(!Validation.isValidResize(cols: 80, rows: 0))
#expect(!Validation.isValidResize(cols: 80, rows: 1001))
}
@Test("isAbsoluteCwd'/a' 通过;'a' 与空串拒绝")
func absoluteCwdValidation() {
#expect(Validation.isAbsoluteCwd("/a"))
#expect(!Validation.isAbsoluteCwd("a"))
#expect(!Validation.isAbsoluteCwd(""))
}
// MARK: - parseClientMessage transcribed from test/protocol.test.ts
@Test("服务器视角镜像合法客户端帧向量全部接受test/protocol.test.ts:53-124")
func serverMirrorAcceptsValidClientVectors() {
#expect(ServerViewParser.parse("{\"type\":\"attach\",\"sessionId\":null}")
== .attach(sessionId: nil, cwd: nil))
#expect(ServerViewParser.parse("{\"type\":\"attach\",\"sessionId\":\"\(validUUID)\"}")
== .attach(sessionId: validUUID, cwd: nil))
#expect(ServerViewParser.parse("{\"type\":\"input\",\"data\":\"ls -la\\r\"}")
== .input(data: "ls -la\r"))
#expect(ServerViewParser.parse("{\"type\":\"resize\",\"cols\":1,\"rows\":1}")
== .resize(cols: 1, rows: 1))
#expect(ServerViewParser.parse("{\"type\":\"resize\",\"cols\":1000,\"rows\":1000}")
== .resize(cols: 1000, rows: 1000))
#expect(ServerViewParser.parse("{\"type\":\"approve\"}") == .approve)
#expect(ServerViewParser.parse("{\"type\":\"reject\"}") == .reject)
// blur test/protocol.test.ts:120-123
#expect(ServerViewParser.parse("{\"type\":\"blur\"}") == nil)
// test/protocol.test.ts:284
#expect(ServerViewParser.parse("{\"type\":\"resize\",\"cols\":80,\"rows\":40,\"extra\":\"ignored\"}")
== .resize(cols: 80, rows: 40))
}
@Test("服务器视角镜像非法客户端帧向量全部拒绝test/protocol.test.ts:128-266",
arguments: [
"not json at all", "", "null", "[]", "42",
"{\"type\":\"ping\"}", "{\"data\":\"hello\"}", "{\"type\":42}",
"{\"type\":\"resize\",\"cols\":0,\"rows\":40}",
"{\"type\":\"resize\",\"cols\":1001,\"rows\":40}",
"{\"type\":\"resize\",\"cols\":80,\"rows\":0}",
"{\"type\":\"resize\",\"cols\":80,\"rows\":1001}",
"{\"type\":\"resize\",\"cols\":80.5,\"rows\":40}",
"{\"type\":\"resize\",\"cols\":80,\"rows\":24.9}",
"{\"type\":\"resize\",\"cols\":\"80\",\"rows\":40}",
"{\"type\":\"resize\",\"rows\":40}",
"{\"type\":\"resize\",\"cols\":80}",
"{\"type\":\"input\",\"data\":42}",
"{\"type\":\"input\",\"data\":null}",
"{\"type\":\"input\"}",
"{\"type\":\"input\",\"data\":{}}",
"{\"type\":\"attach\",\"sessionId\":\"abc123\"}",
"{\"type\":\"attach\",\"sessionId\":\"\"}",
"{\"type\":\"attach\",\"sessionId\":42}",
"{\"type\":\"attach\"}",
"{\"type\":\"attach\",\"sessionId\":null,\"cwd\":\"relative\"}",
"{\"type\":\"attach\",\"sessionId\":null,\"cwd\":42}",
])
func serverMirrorRejectsInvalidClientVectors(frame: String) {
#expect(ServerViewParser.parse(frame) == nil)
}
// MARK: - decodeServer 5 case
@Test("decodeServer(attached):合法 UUID → .attached大写 UUID 同样接受")
func decodeAttachedFrames() {
// Arrange
let expected = UUID(uuidString: validUUID)!
// Act / Assert
#expect(MessageCodec.decodeServer("{\"type\":\"attached\",\"sessionId\":\"\(validUUID)\"}")
== .attached(sessionId: expected))
#expect(MessageCodec.decodeServer(
"{\"type\":\"attached\",\"sessionId\":\"F47AC10B-58CC-4372-A567-0E02B2C3D479\"}")
== .attached(sessionId: expected))
}
@Test("decodeServer(output)ANSI 字节原样进 dataserialize 向量, test/protocol.test.ts:303-307")
func decodeOutputFrame() {
#expect(MessageCodec.decodeServer("{\"type\":\"output\",\"data\":\"\\u001b[1;32mHello\\u001b[0m\"}")
== .output(data: "\u{1B}[1;32mHello\u{1B}[0m"))
}
@Test("decodeServer(exit):仅 code / code+reason 两形状(-1 = spawn 失败)")
func decodeExitFrames() {
#expect(MessageCodec.decodeServer("{\"type\":\"exit\",\"code\":0}")
== .exit(code: 0, reason: nil))
#expect(MessageCodec.decodeServer(
"{\"type\":\"exit\",\"code\":-1,\"reason\":\"spawn failed: /bin/badshell\"}")
== .exit(code: WireConstants.spawnFailedExitCode, reason: "spawn failed: /bin/badshell"))
}
@Test("decodeServer(status)5 个 ClaudeStatus 值全解;缺省 detail/pending/gate 取安全默认",
arguments: ["working", "waiting", "idle", "unknown", "stuck"])
func decodeStatusAllClaudeStatuses(raw: String) throws {
// Arrange / Act
let message = MessageCodec.decodeServer("{\"type\":\"status\",\"status\":\"\(raw)\"}")
// Assert
let expectedStatus = try #require(ClaudeStatus(rawValue: raw))
#expect(message == .status(expectedStatus, detail: nil, pending: false, gate: nil))
}
@Test("decodeServer(status)pending/gate/detail 全携带tool 与 plan 两种 gate")
func decodeStatusWithGate() {
#expect(MessageCodec.decodeServer(
"{\"type\":\"status\",\"status\":\"waiting\",\"detail\":\"Bash\",\"pending\":true,\"gate\":\"tool\"}")
== .status(.waiting, detail: "Bash", pending: true, gate: .tool))
#expect(MessageCodec.decodeServer(
"{\"type\":\"status\",\"status\":\"waiting\",\"pending\":true,\"gate\":\"plan\"}")
== .status(.waiting, detail: nil, pending: true, gate: .plan))
}
@Test("decodeServer(status):未知 gate 值按缺席容忍(保留 pending 信号,不丢帧)")
func decodeStatusToleratesUnknownGate() {
#expect(MessageCodec.decodeServer(
"{\"type\":\"status\",\"status\":\"waiting\",\"pending\":true,\"gate\":\"alien\"}")
== .status(.waiting, detail: nil, pending: true, gate: nil))
// pending bool false
#expect(MessageCodec.decodeServer(
"{\"type\":\"status\",\"status\":\"working\",\"pending\":\"yes\"}")
== .status(.working, detail: nil, pending: false, gate: nil))
}
@Test("decodeServer(telemetry)全字段样本逐字段解出src/types.ts:406-416 镜像)")
func decodeTelemetryFullSample() throws {
// Arrange
let frame = """
{"type":"telemetry","telemetry":{"contextUsedPct":42.5,"costUsd":1.23,\
"linesAdded":10,"linesRemoved":2,"model":"Fable 5","effort":"high",\
"pr":{"number":7,"url":"https://github.com/x/y/pull/7","reviewState":"APPROVED"},\
"rate":{"fiveHourPct":12.5,"sevenDayPct":33},"at":1751600000000}}
"""
// Act
let message = try #require(MessageCodec.decodeServer(frame))
// Assert
let expected = StatusTelemetry(
contextUsedPct: 42.5, costUsd: 1.23, linesAdded: 10, linesRemoved: 2,
model: "Fable 5", effort: "high",
pr: PrInfo(number: 7, url: "https://github.com/x/y/pull/7", reviewState: "APPROVED"),
rate: RateInfo(fiveHourPct: 12.5, sevenDayPct: 33),
at: 1_751_600_000_000
)
#expect(message == .telemetry(expected))
}
@Test("decodeServer(telemetry):全可选字段缺省仍可解(只有 at")
func decodeTelemetryMinimalSample() {
#expect(MessageCodec.decodeServer("{\"type\":\"telemetry\",\"telemetry\":{\"at\":123}}")
== .telemetry(StatusTelemetry(at: 123)))
}
@Test("decodeServer(telemetry):可选字段类型错误按缺席容忍(帧保留)")
func decodeTelemetryToleratesWrongTypedOptionalField() {
#expect(MessageCodec.decodeServer(
"{\"type\":\"telemetry\",\"telemetry\":{\"at\":5,\"costUsd\":\"lots\",\"pr\":42}}")
== .telemetry(StatusTelemetry(at: 5)))
}
// MARK: - decodeServer nil throw
@Test("decodeServer非法帧全表 → nil坏 JSON/未知 type/字段缺失或错型/at 缺失)",
arguments: [
"not json at all", "", "null", "[]", "42", "true",
"{\"type\":\"ping\"}", "{\"data\":\"hello\"}", "{\"type\":42}",
// attached UUID / v1 / / M7
"{\"type\":\"attached\",\"sessionId\":\"abc123\"}",
"{\"type\":\"attached\",\"sessionId\":\"550e8400-e29b-11d4-a716-446655440000\"}",
"{\"type\":\"attached\"}",
"{\"type\":\"attached\",\"sessionId\":42}",
// outputdata /
"{\"type\":\"output\"}",
"{\"type\":\"output\",\"data\":42}",
// exitcode / /
"{\"type\":\"exit\"}",
"{\"type\":\"exit\",\"code\":\"0\"}",
"{\"type\":\"exit\",\"code\":1.5}",
// statusstatus / /
"{\"type\":\"status\"}",
"{\"type\":\"status\",\"status\":\"sleeping\"}",
"{\"type\":\"status\",\"status\":42}",
// telemetrytelemetry / / at / at
"{\"type\":\"telemetry\"}",
"{\"type\":\"telemetry\",\"telemetry\":\"x\"}",
"{\"type\":\"telemetry\",\"telemetry\":{\"costUsd\":1}}",
"{\"type\":\"telemetry\",\"telemetry\":{\"at\":\"now\"}}",
])
func decodeServerReturnsNilForMalformedFrames(frame: String) {
#expect(MessageCodec.decodeServer(frame) == nil)
}