import Foundation import Testing @testable import KnotchyCore struct WireProtocolTests { private let size = PixelSize(width: 1024, height: 640) private func decodeAll(_ bytes: Data, into decoder: inout AgentMessageDecoder) throws -> [AgentMessage] { decoder.append(bytes) var messages: [AgentMessage] = [] while let message = try decoder.next() { messages.append(message) } return messages } private func decodeAll(_ bytes: Data) throws -> [AgentMessage] { var decoder = AgentMessageDecoder() return try decodeAll(bytes, into: &decoder) } private func violation(_ bytes: Data) -> AgentMessageDecoder.ProtocolViolation? { do { _ = try decodeAll(bytes) return nil } catch { return error as? AgentMessageDecoder.ProtocolViolation } } private func raw(_ type: UInt8, _ payload: [UInt8]) -> Data { framed(type, Data(payload)) } @Test func matchesTheImageProtocolVersion() throws { let root = URL(filePath: #filePath).deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() let containerfile = try String(contentsOf: root.appending(path: "vm/Containerfile"), encoding: .utf8) let arg = containerfile.split(separator: "\n").first { $0.hasPrefix("ARG KNOTCHY_PROTOCOL=") } #expect(arg == "ARG KNOTCHY_PROTOCOL=\(WireProtocol.version)") } @Test func pageURLMustBeShortUTF8() throws { let hello = raw(MessageType.hello, [1, 0x04, 0x00, 0x02, 0x80]) #expect(try decodeAll(hello + raw(MessageType.pageURL, Array("https://a.test/ü".utf8))) == [.hello(version: 1, size: size), .pageURL("https://a.test/ü")]) #expect(violation(hello + raw(MessageType.pageURL, [0x68, 0xC3])) != nil) // cut-off UTF-8 #expect(violation(hello + raw(MessageType.pageURL, [])) != nil) let long = [UInt8](repeating: 0x61, count: WireProtocol.maxPageURL + 1) #expect(violation(hello + raw(MessageType.pageURL, long)) != nil) } @Test func helloAndVideoBytesAreAsSpecified() throws { #expect(AgentMessage.hello(version: 1, size: size).encoded() == Data([0x01, 0, 0, 0, 5, 1, 0x04, 0x00, 0x02, 0x80])) let hello = raw(MessageType.hello, [1, 0x04, 0x00, 0x02, 0x80]) let video = raw(MessageType.video, [0x04, 0x00, 0x02, 0x80, 0xAA]) #expect(try decodeAll(hello + video) == [.hello(version: 1, size: size), .video(size: size, accessUnit: Data([0xAA]))]) } @Test func roundTripsEveryAgentMessage() throws { let messages: [AgentMessage] = [ .hello(version: 1, size: size), .navigated, .progress(0), .progress(100), .video(size: size, accessUnit: Data([0, 0, 0, 1, 0x09, 0xF0])), .pageURL("https://example.com/a?b=c#d"), .unknown(type: 0x7F), ] let stream = messages.reduce(into: Data()) { $0.append($1.encoded()) } #expect(try decodeAll(stream) == messages) } @Test func decodesAcrossArbitraryChunkBoundaries() throws { let messages: [AgentMessage] = [ .hello(version: 1, size: size), .video(size: size, accessUnit: Data(repeating: 0xAB, count: 3000)), .progress(42), ] let stream = messages.reduce(into: Data()) { $0.append($1.encoded()) } var decoder = AgentMessageDecoder() var decoded: [AgentMessage] = [] for chunkStart in stride(from: 0, to: stream.count, by: 7) { decoded += try decodeAll(stream.subdata(in: chunkStart..