From 03bc88ae9741535ab1d252c1e083264d85bdccf1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 28 Oct 2026 12:01:42 +0100 Subject: [PATCH] connector-contract-v3: pin the carriage's service id, operation and message shapes against the committed frame and the wire The golden carriage frame is read and re-encoded by the kit's own carriage functions, and both directions' frames are asserted field by field on the CultNet wire, so a renamed service id or operation fails. Co-Authored-By: Claude Opus 5.5 --- crates/model-connector/src/envelope.rs | 60 ++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/crates/model-connector/src/envelope.rs b/crates/model-connector/src/envelope.rs index 9b9cd45..7ea243d 100644 --- a/crates/model-connector/src/envelope.rs +++ b/crates/model-connector/src/envelope.rs @@ -911,4 +911,64 @@ mod tests { Err(ServiceError::Encoding) ); } + + fn golden_frame() -> Vec { + std::fs::read( + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/wire_v3/carriage_invocation.frame"), + ) + .expect("carriage_invocation.frame is committed") + } + + #[test] + fn the_committed_golden_frame_is_what_the_kit_reads_and_writes() { + let frame = golden_frame(); + let envelope = decode_invocation_frame(&frame).unwrap(); + assert_eq!(encode_invocation_frame(&envelope).unwrap(), frame); + } + + fn top_level(frame: &[u8]) -> std::collections::BTreeMap { + let rmpv::Value::Map(entries) = rmpv::decode::read_value(&mut &frame[..]).unwrap() else { + panic!("a CultNet frame is a map"); + }; + entries + .into_iter() + .map(|(key, value)| (key.as_str().unwrap().to_string(), value)) + .collect() + } + + #[test] + fn the_frames_name_the_connector_service_and_its_one_operation_on_the_wire() { + let envelope = sealed_for_carriage(); + let text = |map: &std::collections::BTreeMap, key: &str| { + map[key].as_str().unwrap().to_string() + }; + let request = top_level(&encode_invocation_frame(&envelope).unwrap()); + assert_eq!( + text(&request, "schemaVersion"), + "cultnet.operation_request.v0" + ); + assert_eq!(text(&request, "serviceId"), "gamecult.model.connector"); + assert_eq!(text(&request, "operation"), "invoke"); + assert_eq!( + text(&request, "payloadSchema"), + "gamecult.model.transport_envelope.v3" + ); + assert_eq!(text(&request, "payloadEncoding"), "messagepack-base64"); + assert_eq!(text(&request, "messageId"), envelope.request_id); + let response = top_level(&encode_result_frame(&envelope).unwrap()); + assert_eq!( + text(&response, "schemaVersion"), + "cultnet.operation_response.v0" + ); + assert_eq!(text(&response, "serviceId"), "gamecult.model.connector"); + assert_eq!(text(&response, "operation"), "invoke"); + assert_eq!(text(&response, "status"), "ok"); + assert_eq!( + text(&response, "payloadSchema"), + "gamecult.model.transport_envelope.v3" + ); + assert_eq!(text(&response, "payloadEncoding"), "messagepack-base64"); + assert_eq!(text(&response, "messageId"), envelope.request_id); + } }