NucleicProtocol/CBOR: a deterministic Codable CBOR encoder/decoder (value-tree parse/serialize + Encoder/Decoder adapters) with type-flexible numeric decoding (so JSONValue's int/double probing round-trips) and sorted map keys. Plus length-prefixed framing (4-byte BE) with a FrameAccumulator that reassembles across arbitrary stream chunks and caps frame size. NucleicProtocol/Sync: the SYNC_PROTOCOL §5 message set — ClientMsg/HostMsg with tagged Codable, Hello/Welcome handshake, Subscribe/Verbosity, SessionSummary, SessionSnapshot, EventBatch, WireError, DeviceScope. HostMsg decodes unknown tags to .unknown for forward compatibility. 15 NucleicProtocol tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
92 lines
3.4 KiB
Swift
92 lines
3.4 KiB
Swift
import Foundation
|
|
import Testing
|
|
|
|
@testable import NucleicProtocol
|
|
|
|
@Suite struct CBORTests {
|
|
private func roundTrip<T: Codable & Equatable>(_ value: T) throws -> T {
|
|
let data = try CBOREncoder().encode(value)
|
|
return try CBORDecoder().decode(T.self, from: data)
|
|
}
|
|
|
|
@Test func primitivesRoundTrip() throws {
|
|
#expect(try roundTrip(true) == true)
|
|
#expect(try roundTrip("héllo • world") == "héllo • world")
|
|
#expect(try roundTrip(Int(42)) == 42)
|
|
#expect(try roundTrip(Int(-7)) == -7)
|
|
#expect(try roundTrip(UInt64.max) == UInt64.max)
|
|
#expect(try roundTrip(Double(3.14159)) == 3.14159)
|
|
let ints: [Int] = [1, 2, 3]
|
|
#expect(try roundTrip(ints) == ints)
|
|
let dict: [String: Int] = ["a": 1, "b": 2]
|
|
#expect(try roundTrip(dict) == dict)
|
|
}
|
|
|
|
@Test func optionalsAndNil() throws {
|
|
struct Box: Codable, Equatable { var a: Int?; var b: String? }
|
|
#expect(try roundTrip(Box(a: nil, b: "x")) == Box(a: nil, b: "x"))
|
|
#expect(try roundTrip(Box(a: 5, b: nil)) == Box(a: 5, b: nil))
|
|
}
|
|
|
|
@Test func jsonValueRoundTripsThroughCBOR() throws {
|
|
let v: JSONValue = [
|
|
"tool": "Bash",
|
|
"argv": ["git", "status"],
|
|
"nested": ["count": 3, "ok": true, "ratio": 0.5, "missing": nil],
|
|
]
|
|
#expect(try roundTrip(v) == v)
|
|
}
|
|
|
|
@Test func wholeNumbersDecodeAsIntegers() throws {
|
|
// JSONValue encodes whole numbers as ints; ensure they probe back as .number.
|
|
let v: JSONValue = ["exitCode": 0, "tokens": 12345]
|
|
let back = try roundTrip(v)
|
|
#expect(back["exitCode"]?.intValue == 0)
|
|
#expect(back["tokens"]?.intValue == 12345)
|
|
}
|
|
|
|
@Test func agentEventRoundTrips() throws {
|
|
let event = AgentEvent(
|
|
sessionID: SessionID(rawValue: "s1"),
|
|
seq: 99,
|
|
at: Date(timeIntervalSince1970: 1_700_000_000),
|
|
backend: .claudeCode,
|
|
nativeType: "assistant",
|
|
kind: .toolCallStarted(ToolCall(
|
|
toolCallID: "t1", name: "Bash",
|
|
input: ["command": "ls -la"], parentToolCallID: nil)))
|
|
let back = try roundTrip(event)
|
|
#expect(back == event)
|
|
}
|
|
|
|
@Test func mapKeyOrderIsDeterministic() throws {
|
|
// Same logical object, different dict literal order → identical bytes.
|
|
let a: JSONValue = ["z": 1, "a": 2, "m": 3]
|
|
let b: JSONValue = ["a": 2, "m": 3, "z": 1]
|
|
let ea = try CBOREncoder().encode(a)
|
|
let eb = try CBOREncoder().encode(b)
|
|
#expect(ea == eb)
|
|
}
|
|
|
|
@Test func framingReassemblesAcrossChunks() throws {
|
|
let payloads = [Data("alpha".utf8), Data("bravo-bravo".utf8), Data([], )]
|
|
var stream = Data()
|
|
for p in payloads { stream.append(WireFraming.frame(p)) }
|
|
|
|
let acc = FrameAccumulator()
|
|
var got: [Data] = []
|
|
// Feed one byte at a time to stress boundary handling.
|
|
for byte in stream {
|
|
got.append(contentsOf: try acc.push(Data([byte])))
|
|
}
|
|
#expect(got == payloads)
|
|
}
|
|
|
|
@Test func framingRejectsOversizeLength() throws {
|
|
let acc = FrameAccumulator(maxFrameSize: 8)
|
|
var bogus = Data([0x00, 0x00, 0x10, 0x00]) // claims 4096 bytes
|
|
bogus.append(Data(repeating: 0, count: 10))
|
|
#expect(throws: FrameAccumulator.FramingError.self) { try acc.push(bogus) }
|
|
}
|
|
}
|