@crosslink/conformance is the referee: it runs one
language-neutral corpus against any implementation and reports byte-exact
differences instead of “looks right.”
The adapter contract
An implementation proves compliance by implementing four operations:errorCode is what makes negative fixtures portable — every SDK maps its own
exception type to the same small set of wire error codes, so a Swift
CrosslinkProtocolError.invalidJSON and a TypeScript CrosslinkError with code
invalid_message are asserted against the same expected string.
The corpus
Positive and negative fixtures live in separate files and are combined at test time:packages/protocol/fixtures/messages-v1.json— one entry per message kind (req,res,err,chunk-binary,end,event,sub,cancel,ping), each with the input object, its exact canonical JSON string, and the exact frame bytes as hex.packages/conformance/fixtures/invalid-v1.json— malformed inputs (truncated JSON, an unsupported protocol version, an unknown message type, a request missing its id) paired with the error code every implementation must raise.
npm run gen:fixtures -w @crosslink/protocol — never hand-edited. Negative
fixtures are curated by hand, since they encode intentionally malformed input.
Running the report
canonicalJson(obj) matches
exactly, encodeFrame(obj) matches the expected hex bytes, and round-tripping
through decodeMessage then re-encoding reproduces the same canonical string.
Each negative fixture must throw, and the thrown error’s errorCode must equal
the fixture’s expected code; an adapter that silently accepts malformed input
is reported as a failure with actual: "accepted".
report.passed counts every check that succeeded (cases.length * 3 + invalid.length on a fully-passing adapter), so a partial pass is visible at a
glance without diffing.
Wiring it into a new SDK
The TypeScript reference lives inpackages/protocol, and the pattern each
native SDK follows is the same one shown in Native protocol
SDKs: implement the four adapter operations, load both
fixture files, and call runConformance from that language’s own test runner.
The Rust crate does it in sdks/rust/tests/conformance.rs, driven by
cargo test; Kotlin and Swift do the equivalent in their own test targets.
What conformance does not cover
Conformance establishes wire compatibility only — that two implementations serialize and parse the same bytes for the same logical message. It does not replace:- Handshake tamper tests — CLX1’s signature and transcript-binding properties (see Encryption)
- Transport end-to-end tests — actually opening a WebSocket/WebRTC connection and completing a session
- Fuzzing — conformance fixtures are curated, not randomly generated
- Application capability tests — permission gating happens above the wire protocol (see Capabilities and RPC)