Rebuild protocol runtime from scratch

Implement an aligned two-section frame format, a compiled prefix router, a minimal pending and active hook engine, and a header-first receive path that only decodes payloads on local delivery. Recreate the protocol-focused test suite and document the explicit framing deviation in src/protocol/PROTOCOL_CHANGES.md.
This commit is contained in:
Michael Mikovsky
2026-04-25 12:37:54 -06:00
parent 3d92b5cf0d
commit 080f55ddd3
16 changed files with 410 additions and 571 deletions
+4 -9
View File
@@ -2,7 +2,7 @@ use alloc::{borrow::ToOwned, string::String, vec, vec::Vec};
use crate::protocol::{
CallMessage, FaultMessage, FrameError, HookTarget, PacketHeader, PacketType, ProtocolFault,
ValidationError, decode_frame, encode_packet, validate_call, validate_header,
ValidationError, SECTION_ALIGN, decode_frame, encode_packet, validate_call, validate_header,
validate_procedure_id,
};
@@ -29,14 +29,12 @@ fn packet_framing_roundtrip_preserves_header_and_payload() {
};
let frame = encode_packet(&header, &call).expect("frame should encode");
assert_eq!(frame.as_ptr() as usize % SECTION_ALIGN, 0);
let parsed = decode_frame(&frame).expect("frame should decode");
assert_eq!(parsed.header(), &header);
assert_eq!(parsed.packet_type(), PacketType::Call);
assert_eq!(
parsed.deserialize_call().expect("call should deserialize"),
call
);
assert_eq!(parsed.deserialize_call().expect("call should deserialize"), call);
}
#[test]
@@ -101,8 +99,5 @@ fn truncated_frames_are_rejected() {
let frame = encode_packet(&header, &message).expect("frame should encode");
let truncated = &frame[..frame.len() - 1];
assert!(matches!(
decode_frame(truncated),
Err(FrameError::Truncated)
));
assert!(matches!(decode_frame(truncated), Err(FrameError::Truncated)));
}
+93
View File
@@ -155,3 +155,96 @@ fn invalid_hook_peer_emits_local_fault_event() {
other => panic!("expected fault event, got {other:?}"),
}
}
#[test]
fn hook_closes_only_after_both_sides_end() {
let mut endpoint = ProtocolEndpoint::new(
Vec::new(),
None,
vec![ChildRoute::registered(path(&["server"]))],
Vec::new(),
);
let hook_id = endpoint.allocate_hook_id();
endpoint
.make_call(
path(&["server"]),
None,
"example.service.v1.invoke",
Some(hook_id),
vec![1],
)
.expect("call should establish an active hook");
let host_key = crate::protocol::tree::HookKey::new(Vec::new(), hook_id);
assert!(endpoint.hooks.active(&host_key).is_some());
endpoint
.send_data(
path(&["server"]),
hook_id,
"example.service.v1.invoke",
vec![2],
true,
)
.expect("local end should succeed");
assert!(endpoint.hooks.active(&host_key).is_some());
let frame = encode_packet(
&PacketHeader {
packet_type: PacketType::Data,
src_path: path(&["server"]),
dst_path: Vec::new(),
dst_leaf: None,
hook_id: Some(hook_id),
},
&DataMessage {
procedure_id: "example.service.v1.invoke".to_owned(),
data: vec![3],
end_hook: true,
},
)
.expect("peer final data should encode");
endpoint
.receive(&Ingress::Child(path(&["server"])), frame)
.expect("peer final data should be handled");
assert!(endpoint.hooks.active(&host_key).is_none());
}
#[test]
fn pending_hook_fault_is_delivered_before_activation() {
let mut endpoint = ProtocolEndpoint::new(path(&["server"]), None, Vec::new(), Vec::new());
let header = PacketHeader {
packet_type: PacketType::Call,
src_path: path(&["client"]),
dst_path: path(&["server"]),
dst_leaf: None,
hook_id: None,
};
let call = crate::protocol::CallMessage {
procedure_id: crate::protocol::INTROSPECTION_PROCEDURE_ID.to_owned(),
data: Vec::new(),
response_hook: Some(crate::protocol::HookTarget {
hook_id: 11,
return_path: path(&["client"]),
}),
};
endpoint
.hooks
.insert_pending(crate::protocol::tree::PendingHook {
return_path: path(&["client"]),
hook_id: 11,
caller_src_path: path(&["client"]),
procedure_id: call.procedure_id.clone(),
dst_leaf: None,
})
.expect("pending hook should insert");
let outcome = endpoint
.handle_introspection(&header, Some(crate::protocol::tree::HookKey::new(path(&["client"]), 11)))
.expect("introspection should handle pending hook");
assert!(outcome.forward.is_some() || outcome.event.is_some());
}