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.isValidSessionId(SESSION_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("isValidSessionId:variant=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 / isAbsoluteCwd(src/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 字节原样进 data(serialize 向量, 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}", // output:data 缺失 / 错型 "{\"type\":\"output\"}", "{\"type\":\"output\",\"data\":42}", // exit:code 缺失 / 字符串 / 非整数 "{\"type\":\"exit\"}", "{\"type\":\"exit\",\"code\":\"0\"}", "{\"type\":\"exit\",\"code\":1.5}", // status:status 缺失 / 未知值 / 错型(白名单) "{\"type\":\"status\"}", "{\"type\":\"status\",\"status\":\"sleeping\"}", "{\"type\":\"status\",\"status\":42}", // telemetry:telemetry 键缺失 / 非对象 / 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) }