import Foundation import Testing import WireProtocol // T-iOS-3 · MessageCodec.encode 与服务器 parseClientMessage 的逐键契约 + roundtrip // property + fuzz。向量出处:src/protocol.ts、src/server.ts:91-102、test/protocol.test.ts。 // MARK: - encode 形状(服务器视角逐键一致) @Test("encode(attach) 无 sessionId 时也显式携带 sessionId:null(src/protocol.ts:132-134)") func encodeAttachCarriesExplicitNullSessionId() { // Arrange / Act let frame = MessageCodec.encode(.attach(sessionId: nil, cwd: nil)) // Assert — 逐字节 + 服务器视角双重断言 #expect(frame == "{\"type\":\"attach\",\"sessionId\":null}") #expect(ServerViewParser.parse(frame) == .attach(sessionId: nil, cwd: nil)) } @Test("encode(attach) 带 UUID → 服务器收到小写 UUID 字符串") func encodeAttachSerializesLowercasedUUID() { // Arrange let uuid = UUID(uuidString: "F47AC10B-58CC-4372-A567-0E02B2C3D479")! // Act let frame = MessageCodec.encode(.attach(sessionId: uuid, cwd: nil)) // Assert #expect(frame == "{\"type\":\"attach\",\"sessionId\":\"f47ac10b-58cc-4372-a567-0e02b2c3d479\"}") #expect(ServerViewParser.parse(frame) == .attach(sessionId: "f47ac10b-58cc-4372-a567-0e02b2c3d479", cwd: nil)) } @Test("encode(attach) 带 cwd → cwd 键出现且为绝对路径原文;无 cwd 时键不出现") func encodeAttachCwdKeyAppearsOnlyWhenPresent() throws { // Arrange / Act let withCwd = MessageCodec.encode(.attach(sessionId: nil, cwd: "/Users/dev/proj")) let withoutCwd = MessageCodec.encode(.attach(sessionId: nil, cwd: nil)) // Assert #expect(ServerViewParser.parse(withCwd) == .attach(sessionId: nil, cwd: "/Users/dev/proj")) let withoutObj = try #require(jsonObject(withoutCwd)) #expect(withoutObj.index(forKey: "cwd") == nil) } @Test("encode(input) 原始键盘字节逐字节透传(Esc/^C/CR/Tab/Shift+Tab)") func encodeInputPassesRawKeyboardBytesVerbatim() { // Arrange — test/protocol.test.ts:79 的 verbatim 向量 let raw = "\u{1B}[A\u{03}\r\t\u{1B}[Z" // Act let frame = MessageCodec.encode(.input(data: raw)) // Assert #expect(ServerViewParser.parse(frame) == .input(data: raw)) } @Test("encode(input) 控制字节按 JSON.stringify 规则转义(\\r 短转义、ESC → \\u001b)") func encodeInputEscapesControlBytesLikeJSONStringify() { // Arrange / Act let frame = MessageCodec.encode(.input(data: "hi\r\u{1B}[A\"\\")) // Assert — 与 JS 客户端 JSON.stringify 输出逐字节一致 #expect(frame == "{\"type\":\"input\",\"data\":\"hi\\r\\u001b[A\\\"\\\\\"}") } @Test("encode(resize) cols/rows 为 JSON 整数,服务器按 [1,1000] 接受") func encodeResizeEmitsIntegers() { // Arrange / Act let frame = MessageCodec.encode(.resize(cols: 120, rows: 40)) // Assert #expect(frame == "{\"type\":\"resize\",\"cols\":120,\"rows\":40}") #expect(ServerViewParser.parse(frame) == .resize(cols: 120, rows: 40)) #expect(ServerViewParser.parse(MessageCodec.encode(.resize(cols: 1, rows: 1))) == .resize(cols: 1, rows: 1)) #expect(ServerViewParser.parse(MessageCodec.encode(.resize(cols: 1000, rows: 1000))) == .resize(cols: 1000, rows: 1000)) } @Test("encode(approve) 无 mode → 裸 {type:approve};encode(reject) → {type:reject}") func encodeApproveRejectBareFrames() { // Arrange / Act / Assert #expect(MessageCodec.encode(.approve(mode: nil)) == "{\"type\":\"approve\"}") #expect(MessageCodec.encode(.reject) == "{\"type\":\"reject\"}") #expect(ServerViewParser.parse("{\"type\":\"approve\"}") == .approve) #expect(ServerViewParser.parse("{\"type\":\"reject\"}") == .reject) } @Test("encode(approve.mode) 把 mode 放在顶层键(src/server.ts:94-102 读原始帧 obj['mode'])", arguments: ApproveMode.allCases) func encodeApproveModeIsTopLevelKey(mode: ApproveMode) throws { // Arrange / Act let frame = MessageCodec.encode(.approve(mode: mode)) // Assert — 服务器 parseClientMessage 接受该帧形状 #expect(ServerViewParser.parse(frame) == .approve) // …且 WS 接线层的 parseApproveMode 能从顶层恢复 mode #expect(ServerViewParser.topLevelMode(frame) == mode.rawValue) let obj = try #require(jsonObject(frame)) #expect(obj["mode"] as? String == mode.rawValue) } @Test("ApproveMode rawValue 与服务器 PERMISSION_MODES 白名单一致(src/server.ts:76)") func approveModeRawValuesMatchServerWhitelist() { // Arrange / Act let rawValues = Set(ApproveMode.allCases.map(\.rawValue)) // Assert #expect(rawValues == ServerViewParser.permissionModes) } // MARK: - roundtrip property(approve.mode 豁免:服务器刻意丢 mode,src/protocol.ts:77-79) @Test("roundtrip property:任意合法 ClientMessage encode→(服务器视角)decode 不变形") func roundtripPropertyHoldsForRandomClientMessages() { // Arrange — 固定种子保证可复现 var rng = SplitMix64(seed: 0xC0FF_EE00_0003) let iterations = 300 for _ in 0.. [String: Any]? { guard let data = text.data(using: .utf8) else { return nil } return (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] }