import XCTest
import PhysarumEngine
@testable import Physarum
/// The link layer reads arbitrary byte chunks off the TCP stream; the framing decoder must
/// reassemble whole messages regardless of how the bytes are split across reads.
final class RemoteFramingTests: XCTestCase {
private var sample: [RemoteMessage] {
let p = PresetCatalog.loadAll()[0].params
var f = AudioFactors(); f[.moveSpeed] = 1.2; f[.exposure] = 0.7
return [.hello(.init(selectedID: "a", params: p, cycling: false)), .intent(.next), .params(p),
.audioFactors(AudioFactorFrame(f))]
}
func testEncodeThenDecodeStreamYieldsMessages() throws {
let msgs = sample
var stream = Data()
for m in msgs { stream.append(try RemoteFraming.encode(m)) }
var decoder = RemoteFraming.Decoder()
var out: [RemoteMessage] = []
// Feed the stream in two arbitrary chunks to prove buffering across reads.
out += try decoder.consume(stream.prefix(stream.count / 3))
out += try decoder.consume(stream.suffix(stream.count - stream.count / 3))
XCTAssertEqual(out, msgs)
}
func testByteByByteFeedReassembles() throws {
let msgs = sample
var stream = Data()
for m in msgs { stream.append(try RemoteFraming.encode(m)) }
var decoder = RemoteFraming.Decoder()
var out: [RemoteMessage] = []
// Worst-case fragmentation: one byte per read. Nothing emits until a full frame lands.
for byte in stream { out += try decoder.consume(Data([byte])) }
XCTAssertEqual(out, msgs)
}
// A frame with a valid length header but a non-decodable body must be skipped, not left in the
// buffer to be misread as the next length header — a single bad frame must not poison the stream.
func testMalformedFrameIsSkippedNotPoisoning() throws {
let p = PresetCatalog.loadAll()[0].params
var stream = Data()
stream.append(try RemoteFraming.encode(.intent(.next))) // valid
var badLen = UInt32(3).bigEndian // 3-byte body of garbage (not JSON)
stream.append(Data(bytes: &badLen, count: 4))
stream.append(Data([0xFF, 0xFE, 0xFD]))
stream.append(try RemoteFraming.encode(.params(p))) // valid
var decoder = RemoteFraming.Decoder()
let out = try decoder.consume(stream)
XCTAssertEqual(out, [.intent(.next), .params(p)]) // bad frame dropped, both valid frames recovered
}
func testZeroLengthFrameIsSkipped() throws {
var stream = Data()
stream.append(try RemoteFraming.encode(.intent(.previous)))
stream.append(Data([0, 0, 0, 0])) // zero-length frame
stream.append(try RemoteFraming.encode(.intent(.next)))
var decoder = RemoteFraming.Decoder()
let out = try decoder.consume(stream)
XCTAssertEqual(out, [.intent(.previous), .intent(.next)])
}
// A corrupt/oversized length prefix can't be resynced (we don't know where the next frame starts);
// the decoder throws so the link tears the connection down and reconnects.
func testOversizedLengthThrows() {
var huge = UInt32(64 * 1024 * 1024).bigEndian // 64 MB, far above the frame ceiling
let header = Data(bytes: &huge, count: 4)
var decoder = RemoteFraming.Decoder()
XCTAssertThrowsError(try decoder.consume(header))
}
}