Files
zeekayandHanzo Dev acb9f38d95 zap: make the rename shippable, and give the golden fixture its generator
The rename to `zapwire` was inert: `cargo publish` refused the manifest
outright with

    all dependencies must have a version requirement specified when
    publishing. dependency `zapc` does not specify a version

so nothing could leave the workspace, and crates.io still resolved
`hanzo-zap` to THIS repo via 0.2.1 — the collision the rename exists to
end. The build-dependency now carries both a path and a version: the
path is what the workspace builds against, the version is what a
published `zapwire` resolves from crates.io. `zapc` gains the
`repository` link it was missing, since a crate whose repository field
points at the wrong source is precisely how the `hanzo-zap` confusion
started (0.2.1 declares `repository = hanzoai/zap` from a zap-proto/spec
checkout).

crosswire.rs told the reader to regenerate its golden hex with
`tests/gen_golden.go` — a file that does not exist, leaving hand-editing
hex as the only option the comment explicitly forbids. `interop/golden`
is that generator, and it emits the finished Rust `const` lines rather
than bare hex so there is no transcription step to get wrong. Its output
reproduces the committed fixture byte-for-byte.

`go vet ./interop` -> `go vet ./...`, so the new package is covered by
the same gate rather than needing its own line.

make test: 27 lib + 10 crosswire + 2 live-Go interop, green, exit 0.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 23:18:52 -07:00

149 lines
4.8 KiB
Rust

//! Cross-wire conformance against the canonical Go runtime.
//!
//! Every constant below is the VERBATIM hex output of
//! `github.com/zap-proto/[email protected]`'s `rpc.BuildRequest` /
//! `rpc.BuildResponse` for the stated inputs. Regenerate with
//! `go run ./interop/golden`, which prints these exact `const` lines —
//! paste them and never hand-edit. If Go changes the envelope, these
//! fail here; if Rust
//! changes it, these fail here too. That is the point.
use zapwire::rpc::{self, Call, NO_TARGET, STATUS_NOT_FOUND, STATUS_OK};
use zapwire::{Message, HEADER_SIZE};
/// rpc.Call{Method: 7, PromiseID: 1, Target: NO_TARGET, Cap: "cap-token", Payload: "hello-zap"}
const GO_REQ: &str = "5a415000020000c8100000003e000000070000000100000000000000100000000900000011000000090000006361702d746f6b656e68656c6c6f2d7a6170";
/// rpc.Call{Method: 2, PromiseID: 9, Target: 1, Cap: nil, Payload: nil}
const GO_REQ_EMPTY: &str =
"5a415000020000c8100000002c00000002000000090000000100000000000000000000000000000000000000";
/// rpc.BuildResponse(STATUS_OK, 1, "world")
const GO_RESP: &str =
"5a415000020000c81000000029000000c800000001000000000000000800000005000000776f726c64";
/// rpc.BuildResponse(STATUS_NOT_FOUND, 42, nil)
const GO_RESP_EMPTY: &str =
"5a415000020000c81000000024000000940100002a000000000000000000000000000000";
fn unhex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
// ── the read direction: Go builds, Rust parses ──────────────────────
#[test]
fn parses_go_request() {
let call = rpc::parse_request(&unhex(GO_REQ)).expect("parse go request");
assert_eq!(call.method, 7);
assert_eq!(call.promise_id, 1);
assert_eq!(call.target, NO_TARGET);
assert_eq!(call.cap, b"cap-token");
assert_eq!(call.payload, b"hello-zap");
}
#[test]
fn parses_go_request_empty() {
let call = rpc::parse_request(&unhex(GO_REQ_EMPTY)).expect("parse go request");
assert_eq!(call.method, 2);
assert_eq!(call.promise_id, 9);
assert_eq!(call.target, 1);
assert!(call.cap.is_empty());
assert!(call.payload.is_empty());
}
#[test]
fn parses_go_response() {
let resp = rpc::parse_response(&unhex(GO_RESP)).expect("parse go response");
assert_eq!(resp.status, STATUS_OK);
assert_eq!(resp.promise_id, 1);
assert_eq!(resp.body, b"world");
}
#[test]
fn parses_go_response_empty() {
let resp = rpc::parse_response(&unhex(GO_RESP_EMPTY)).expect("parse go response");
assert_eq!(resp.status, STATUS_NOT_FOUND);
assert_eq!(resp.promise_id, 42);
assert!(resp.body.is_empty());
}
// ── the write direction: Rust builds, bytes MUST equal Go's ─────────
#[test]
fn builds_go_identical_request() {
let got = rpc::build_request(&Call {
method: 7,
promise_id: 1,
target: NO_TARGET,
cap: b"cap-token".to_vec(),
payload: b"hello-zap".to_vec(),
});
assert_eq!(hex(&got), GO_REQ);
}
#[test]
fn builds_go_identical_request_empty() {
let got = rpc::build_request(&Call {
method: 2,
promise_id: 9,
target: 1,
cap: Vec::new(),
payload: Vec::new(),
});
assert_eq!(hex(&got), GO_REQ_EMPTY);
}
#[test]
fn builds_go_identical_response() {
assert_eq!(hex(&rpc::build_response(STATUS_OK, 1, b"world")), GO_RESP);
}
#[test]
fn builds_go_identical_response_empty() {
assert_eq!(
hex(&rpc::build_response(STATUS_NOT_FOUND, 42, b"")),
GO_RESP_EMPTY
);
}
// ── header-level facts the envelope depends on ─────────────────────
#[test]
fn go_envelope_is_version2_router_tagged() {
let bytes = unhex(GO_REQ);
let m = Message::parse(&bytes).expect("v2 header must parse");
assert_eq!(m.version(), 2, "the rpc envelope is a Version2 header");
assert_eq!(
m.flags() >> 8,
rpc::MSG_TYPE_ROUTER_BASE,
"router msgType rides the high byte of flags"
);
assert_eq!(
m.root().offset(),
HEADER_SIZE,
"the root object starts immediately after the header"
);
}
/// The stream case: a Go RPC envelope arriving on a socket must be
/// readable frame-by-frame, header first. Before v2 headers were
/// accepted this failed at the very first byte pair — `InvalidVersion(2)`
/// — which is why the Rust side could not talk to `zap-proto/go` at all.
#[test]
fn reads_a_go_envelope_off_a_stream() {
let bytes = unhex(GO_REQ);
let mut cur = std::io::Cursor::new(&bytes);
let (hdr, body) = zapwire::read_frame(&mut cur).expect("must read the Go rpc envelope");
assert_eq!(hdr.version, 2);
assert_eq!(hdr.size as usize, bytes.len());
assert_eq!(body.len(), bytes.len() - HEADER_SIZE);
}