Split protocol and leaf surfaces into crates

Move the protocol runtime into unshell-protocol and remote shell leaf code into unshell-leaves so endpoint and TUI roles can compile independently without circular dependencies.
This commit is contained in:
Michael Mikovsky
2026-04-26 12:39:06 -06:00
parent 74f08333ae
commit d4100d0604
41 changed files with 435 additions and 195 deletions
+106
View File
@@ -0,0 +1,106 @@
use alloc::{borrow::ToOwned, format, string::String, vec, vec::Vec};
use core::convert::Infallible;
use rkyv::{Archive, Deserialize, Serialize};
use crate::protocol::tree::{
Call, CallLeaf, ChildRoute, EndpointOutcome, Ingress, LeafRuntime, ProtocolEndpoint,
decode_call_input, encode_call_reply,
};
use crate::protocol::{PacketType, decode_frame};
use crate::{Leaf, procedures};
fn path(parts: &[&str]) -> Vec<String> {
parts.iter().map(|part| (*part).to_owned()).collect()
}
#[derive(Leaf)]
#[leaf(id = "org.example.v1.echo")]
struct EchoLeaf {
prefix: String,
}
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
struct EchoRequest {
text: String,
}
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
struct EchoResponse {
text: String,
}
#[procedures(error = Infallible)]
impl EchoLeaf {
#[call]
fn echo(&mut self, request: Call<EchoRequest>) -> EchoResponse {
EchoResponse {
text: format!("{}{}", self.prefix, request.input.text),
}
}
}
impl CallLeaf for EchoLeaf {
type Error = Infallible;
}
#[test]
fn leaf_runtime_dispatches_generated_call_procedure() {
let endpoint = ProtocolEndpoint::new(
path(&["agent"]),
Some(Vec::new()),
Vec::new(),
vec![EchoLeaf::protocol_leaf_spec()],
);
let mut runtime = LeafRuntime::new(
endpoint,
EchoLeaf {
prefix: String::from("echo: "),
},
);
let mut controller = ProtocolEndpoint::new(
Vec::new(),
None,
vec![ChildRoute {
path: path(&["agent"]),
registered: true,
}],
Vec::new(),
);
let hook_id = controller.allocate_hook_id();
let controller_outcome = controller
.send_call(
path(&["agent"]),
Some(EchoLeaf::protocol_leaf_name()),
EchoLeaf::protocol_procedure_id("echo").expect("generated suffix should resolve"),
Some(hook_id),
encode_call_reply(&EchoRequest {
text: String::from("hello"),
})
.expect("request should encode"),
)
.expect("call should encode");
let EndpointOutcome::Forward { frame, .. } = controller_outcome else {
panic!("controller should forward call to child");
};
let outcome = runtime
.receive(&Ingress::Parent, frame)
.expect("runtime should handle call");
let [response_frame] = outcome.frames.as_slice() else {
panic!("expected one response frame");
};
let parsed = decode_frame(response_frame.as_slice()).expect("response frame should decode");
assert_eq!(parsed.packet_type(), PacketType::Data);
let response = decode_call_input::<EchoResponse>(
parsed
.deserialize_data()
.expect("data payload should deserialize")
.data
.as_slice(),
)
.expect("typed response should decode");
assert_eq!(response.text, "echo: hello");
}
@@ -0,0 +1,4 @@
mod call;
mod procedure;
mod protocol;
mod tree;
@@ -0,0 +1,280 @@
use alloc::{borrow::ToOwned, collections::BTreeMap, format, string::String, vec, vec::Vec};
use core::convert::Infallible;
use crate::protocol::tree::{
Call, ChildRoute, Endpoint, EndpointOutcome, HookKey, Ingress, OutgoingData, Procedure,
ProcedureEffect, ProcedureRuntime, ProcedureStore, ProtocolEndpoint, encode_call_reply,
};
use crate::protocol::{PacketType, decode_frame};
use crate::{Leaf, Procedure};
fn path(parts: &[&str]) -> Vec<String> {
parts.iter().map(|part| (*part).to_owned()).collect()
}
#[derive(Default, Leaf)]
#[leaf(id = "org.example.v1.stream")]
struct StreamLeaf {
sessions: BTreeMap<HookKey, ProcedureOpen>,
}
impl ProcedureStore<ProcedureOpen> for StreamLeaf {
fn procedure_sessions(&mut self) -> &mut BTreeMap<HookKey, ProcedureOpen> {
&mut self.sessions
}
}
#[derive(Debug, Clone, PartialEq, Eq, Procedure)]
#[procedure(leaf = StreamLeaf, name = "open")]
struct ProcedureOpen {
prefix: String,
}
impl Procedure<StreamLeaf> for ProcedureOpen {
type Error = Infallible;
type Input = String;
fn open(_leaf: &mut StreamLeaf, call: Call<Self::Input>) -> Result<Self, Self::Error> {
Ok(Self { prefix: call.input })
}
fn on_data(
_leaf: &mut StreamLeaf,
session: &mut Self,
data: crate::protocol::tree::IncomingData,
) -> Result<ProcedureEffect, Self::Error> {
Ok(ProcedureEffect {
outgoing: vec![OutgoingData {
dst_path: data.hook_key.return_path,
hook_id: data.hook_key.hook_id,
procedure_id: ProcedureOpen::protocol_procedure_id(),
data: format!(
"{}{}",
session.prefix,
String::from_utf8_lossy(&data.message.data)
)
.into_bytes(),
end_hook: data.message.end_hook,
}],
close_session: data.message.end_hook,
})
}
}
#[test]
fn procedure_runtime_routes_data_to_stored_session() {
let endpoint = ProtocolEndpoint::new(
path(&["agent"]),
Some(Vec::new()),
Vec::new(),
vec![crate::protocol::tree::LeafSpec {
name: StreamLeaf::protocol_leaf_name(),
procedures: vec![ProcedureOpen::protocol_procedure_id()],
}],
);
let mut runtime =
ProcedureRuntime::<StreamLeaf, ProcedureOpen>::new(endpoint, StreamLeaf::default());
let mut controller = ProtocolEndpoint::new(
Vec::new(),
None,
vec![ChildRoute {
path: path(&["agent"]),
registered: true,
}],
Vec::new(),
);
let hook_id = controller.allocate_hook_id();
let open = controller
.send_call(
path(&["agent"]),
Some(StreamLeaf::protocol_leaf_name()),
ProcedureOpen::protocol_procedure_id(),
Some(hook_id),
encode_call_reply(&String::from("prefix:")).expect("procedure input should encode"),
)
.expect("open call should encode");
let EndpointOutcome::Forward {
frame: open_frame, ..
} = open
else {
panic!("controller should forward opening call");
};
runtime
.receive(&Ingress::Parent, open_frame)
.expect("runtime should open a session");
let data = controller
.send_data(
path(&["agent"]),
hook_id,
ProcedureOpen::protocol_procedure_id(),
b"hello".to_vec(),
true,
)
.expect("data should encode");
let EndpointOutcome::Forward {
frame: data_frame, ..
} = data
else {
panic!("controller should forward data frame");
};
let outcome = runtime
.receive(&Ingress::Parent, data_frame)
.expect("runtime should route data to session");
let [response_frame] = outcome.frames.as_slice() else {
panic!("expected one response frame");
};
let parsed = decode_frame(response_frame.as_slice()).expect("response frame should decode");
assert_eq!(parsed.packet_type(), PacketType::Data);
let message = parsed.deserialize_data().expect("data should deserialize");
assert!(message.end_hook);
assert_eq!(String::from_utf8_lossy(&message.data), "prefix:hello");
let forwarded = controller
.receive(&Ingress::Child(path(&["agent"])), response_frame.clone())
.expect("controller should receive session response");
assert!(matches!(forwarded, EndpointOutcome::Local(_)));
assert!(runtime.leaf_mut().procedure_sessions().is_empty());
}
#[derive(Default, Leaf)]
#[leaf(id = "org.example.v1.duplex")]
struct DuplexLeaf {
sessions: BTreeMap<HookKey, DuplexProcedure>,
}
impl ProcedureStore<DuplexProcedure> for DuplexLeaf {
fn procedure_sessions(&mut self) -> &mut BTreeMap<HookKey, DuplexProcedure> {
&mut self.sessions
}
}
#[derive(Debug, Clone, PartialEq, Eq, Procedure)]
#[procedure(leaf = DuplexLeaf, name = "open")]
struct DuplexProcedure {
saw_peer_close: bool,
}
impl Procedure<DuplexLeaf> for DuplexProcedure {
type Error = Infallible;
type Input = ();
fn open(_leaf: &mut DuplexLeaf, _call: Call<Self::Input>) -> Result<Self, Self::Error> {
Ok(Self {
saw_peer_close: false,
})
}
fn on_data(
_leaf: &mut DuplexLeaf,
session: &mut Self,
data: crate::protocol::tree::IncomingData,
) -> Result<ProcedureEffect, Self::Error> {
if data.message.data == b"local-end" {
return Ok(ProcedureEffect::outgoing(vec![OutgoingData {
dst_path: data.hook_key.return_path,
hook_id: data.hook_key.hook_id,
procedure_id: DuplexProcedure::protocol_procedure_id(),
data: Vec::new(),
end_hook: true,
}]));
}
if data.message.end_hook {
session.saw_peer_close = true;
return Ok(ProcedureEffect::close(Vec::new()));
}
Ok(ProcedureEffect::default())
}
}
#[test]
fn procedure_runtime_keeps_session_after_local_end_until_explicit_close() {
let endpoint = ProtocolEndpoint::new(
path(&["agent"]),
Some(Vec::new()),
Vec::new(),
vec![crate::protocol::tree::LeafSpec {
name: DuplexLeaf::protocol_leaf_name(),
procedures: vec![DuplexProcedure::protocol_procedure_id()],
}],
);
let mut runtime =
ProcedureRuntime::<DuplexLeaf, DuplexProcedure>::new(endpoint, DuplexLeaf::default());
let mut controller = ProtocolEndpoint::new(
Vec::new(),
None,
vec![ChildRoute {
path: path(&["agent"]),
registered: true,
}],
Vec::new(),
);
let hook_id = controller.allocate_hook_id();
let open = controller
.send_call(
path(&["agent"]),
Some(DuplexLeaf::protocol_leaf_name()),
DuplexProcedure::protocol_procedure_id(),
Some(hook_id),
encode_call_reply(&()).expect("unit call should encode"),
)
.expect("open call should encode");
let EndpointOutcome::Forward {
frame: open_frame, ..
} = open
else {
panic!("controller should forward opening call");
};
runtime
.receive(&Ingress::Parent, open_frame)
.expect("runtime should open duplex session");
let local_end = controller
.send_data(
path(&["agent"]),
hook_id,
DuplexProcedure::protocol_procedure_id(),
b"local-end".to_vec(),
false,
)
.expect("local end trigger should encode");
let EndpointOutcome::Forward {
frame: local_end_frame,
..
} = local_end
else {
panic!("controller should forward local end trigger");
};
let outcome = runtime
.receive(&Ingress::Parent, local_end_frame)
.expect("runtime should emit a local end packet");
assert_eq!(outcome.frames.len(), 1);
assert_eq!(runtime.leaf_mut().procedure_sessions().len(), 1);
let peer_end = encode_call_reply(&()).expect("unit value is just a placeholder");
let peer_end = crate::protocol::encode_packet(
&crate::protocol::PacketHeader {
packet_type: PacketType::Data,
src_path: Vec::new(),
dst_path: path(&["agent"]),
dst_leaf: None,
hook_id: Some(hook_id),
},
&crate::protocol::DataMessage {
procedure_id: DuplexProcedure::protocol_procedure_id(),
data: peer_end,
end_hook: true,
},
)
.expect("peer end frame should encode");
let peer_end_outcome = runtime
.receive(&Ingress::Parent, peer_end)
.expect("runtime should accept peer end after local end");
assert!(peer_end_outcome.frames.is_empty());
assert!(runtime.leaf_mut().procedure_sessions().is_empty());
}
@@ -0,0 +1,109 @@
use alloc::{borrow::ToOwned, string::String, vec, vec::Vec};
use crate::protocol::{
CallMessage, FaultMessage, FrameError, HookTarget, PacketHeader, PacketType, ProtocolFault,
SECTION_ALIGN, ValidationError, decode_frame, encode_packet, validate_call, validate_header,
validate_procedure_id,
};
fn path(parts: &[&str]) -> Vec<String> {
parts.iter().map(|part| (*part).to_owned()).collect()
}
#[test]
fn packet_framing_roundtrip_preserves_header_and_payload() {
let header = PacketHeader {
packet_type: PacketType::Call,
src_path: path(&["root", "caller"]),
dst_path: path(&["root", "callee"]),
dst_leaf: Some("service".to_owned()),
hook_id: None,
};
let call = CallMessage {
procedure_id: "example.service.v1.invoke".to_owned(),
data: vec![1, 2, 3, 4],
response_hook: Some(HookTarget {
hook_id: 7,
return_path: path(&["root", "caller"]),
}),
};
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
);
}
#[test]
fn header_and_call_validation_reject_invalid_combinations() {
let invalid_header = PacketHeader {
packet_type: PacketType::Data,
src_path: path(&["peer"]),
dst_path: path(&["host"]),
dst_leaf: Some("service".to_owned()),
hook_id: None,
};
assert_eq!(
validate_header(&invalid_header),
Err(ValidationError::HeaderInvariant(
"Data and Fault packets must not carry dst_leaf"
))
);
let header = PacketHeader {
packet_type: PacketType::Call,
src_path: path(&["caller"]),
dst_path: path(&["callee"]),
dst_leaf: Some("service".to_owned()),
hook_id: None,
};
let invalid_call = CallMessage {
procedure_id: "example.service.v1.invoke".to_owned(),
data: Vec::new(),
response_hook: Some(HookTarget {
hook_id: 5,
return_path: path(&["elsewhere"]),
}),
};
assert_eq!(
validate_call(&header, &invalid_call),
Err(ValidationError::CallInvariant(
"response_hook.return_path must equal header.src_path"
))
);
}
#[test]
fn procedure_validation_accepts_introspection_and_non_empty_opaque_ids() {
assert_eq!(validate_procedure_id(""), Ok(()));
assert_eq!(validate_procedure_id("example.service.v01.invoke"), Ok(()));
assert_eq!(validate_procedure_id("contains spaces"), Ok(()));
}
#[test]
fn truncated_frames_are_rejected() {
let header = PacketHeader {
packet_type: PacketType::Fault,
src_path: path(&["src"]),
dst_path: path(&["dst"]),
dst_leaf: None,
hook_id: Some(9),
};
let message = FaultMessage {
fault: ProtocolFault::INTERNAL_ERROR,
};
let frame = encode_packet(&header, &message).expect("frame should encode");
let truncated = &frame[..frame.len() - 1];
assert!(matches!(
decode_frame(truncated),
Err(FrameError::Truncated)
));
}
+369
View File
@@ -0,0 +1,369 @@
use alloc::{borrow::ToOwned, string::String, vec, vec::Vec};
use crate::protocol::tree::{
ChildRoute, DefaultRouteProvider, Endpoint, EndpointOutcome, Ingress, LeafNode, LeafSpec,
LocalEvent, ProtocolEndpoint, RouteDecision, RouteProvider, TreeNode,
};
use crate::protocol::{
DataMessage, EndpointIntrospection, FaultMessage, PacketHeader, PacketType, ProtocolFault,
deserialize_archived_bytes, encode_packet,
};
fn path(parts: &[&str]) -> Vec<String> {
parts.iter().map(|part| (*part).to_owned()).collect()
}
#[test]
fn tree_node_paths_flatten_explicitly() {
let tree = TreeNode::Root {
children: vec![TreeNode::Endpoint {
segment: "branch".to_owned(),
leaves: vec![LeafNode {
name: "service".to_owned(),
procedures: vec!["example.service.v1.invoke".to_owned()],
}],
children: vec![TreeNode::Endpoint {
segment: "leaf".to_owned(),
leaves: Vec::new(),
children: Vec::new(),
}],
}],
};
assert_eq!(
tree.paths(),
vec![
Vec::<String>::new(),
path(&["branch"]),
path(&["branch", "leaf"])
]
);
}
#[test]
fn longest_prefix_routing_prefers_most_specific_child() {
let provider = DefaultRouteProvider;
let child_paths = vec![path(&["a"]), path(&["a", "b"]), path(&["x"])];
assert_eq!(
provider.route_destination(&Vec::new(), &child_paths, true, &path(&["a", "b", "c"])),
RouteDecision::Child(1)
);
assert_eq!(
provider.route_destination(&path(&["a"]), &child_paths, true, &path(&["z"])),
RouteDecision::Parent
);
}
#[test]
fn protocol_endpoint_introspection_returns_leaf_summary() {
let mut endpoint = ProtocolEndpoint::new(
path(&["root"]),
Some(Vec::new()),
vec![ChildRoute::registered(path(&["root", "child"]))],
vec![LeafSpec {
name: "service".to_owned(),
procedures: vec!["example.service.v1.invoke".to_owned()],
}],
);
let hook_id = endpoint.allocate_hook_id();
let frame = endpoint
.make_call(path(&["root"]), None, "", Some(hook_id), Vec::new())
.expect("introspection call should encode");
let outcome = endpoint
.receive(&Ingress::Local, frame)
.expect("endpoint should handle introspection");
let EndpointOutcome::Local(LocalEvent::Data {
header,
message: response,
..
}) = &outcome
else {
panic!("expected local data event");
};
assert_eq!(header.packet_type, PacketType::Data);
assert_eq!(header.dst_path, path(&["root"]));
let introspection = deserialize_archived_bytes::<
crate::protocol::introspection::ArchivedEndpointIntrospection,
EndpointIntrospection,
>(&response.data)
.expect("introspection payload should deserialize");
assert!(response.end_hook);
assert_eq!(introspection.sub_endpoints, vec!["child".to_owned()]);
assert_eq!(introspection.leaves.len(), 1);
assert_eq!(introspection.leaves[0].leaf_name, "service");
assert_eq!(
introspection.leaves[0].procedures,
vec!["example.service.v1.invoke".to_owned()]
);
}
#[test]
fn invalid_hook_peer_emits_local_fault_event() {
let mut endpoint = ProtocolEndpoint::new(
Vec::new(),
None,
vec![
ChildRoute::registered(path(&["server"])),
ChildRoute::registered(path(&["intruder"])),
],
Vec::new(),
);
let hook_id = endpoint.allocate_hook_id();
endpoint
.make_call(
path(&["server"]),
None,
"example.service.v1.invoke",
Some(hook_id),
vec![1, 2, 3],
)
.expect("call should establish an active hook");
let valid_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![8],
end_hook: false,
},
)
.expect("valid server data should encode");
endpoint
.receive(&Ingress::Child(path(&["server"])), valid_frame)
.expect("first server data should activate the hook");
let frame = encode_packet(
&PacketHeader {
packet_type: PacketType::Data,
src_path: path(&["intruder"]),
dst_path: Vec::new(),
dst_leaf: None,
hook_id: Some(hook_id),
},
&DataMessage {
procedure_id: "example.service.v1.invoke".to_owned(),
data: vec![9],
end_hook: false,
},
)
.expect("data frame should encode");
let outcome = endpoint
.receive(&Ingress::Child(path(&["intruder"])), frame)
.expect("invalid peer should be handled");
match &outcome {
EndpointOutcome::Local(event) => match event {
LocalEvent::Fault {
header, message, ..
} => {
assert_eq!(header.packet_type, PacketType::Fault);
assert_eq!(header.hook_id, Some(hook_id));
assert_eq!(
message,
&FaultMessage {
fault: ProtocolFault::INVALID_HOOK_PEER,
}
);
}
other => panic!("expected fault event, got {other:?}"),
},
other => panic!("expected local 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.pending(&host_key).is_some());
let activation_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![9],
end_hook: false,
},
)
.expect("activation data should encode");
endpoint
.receive(&Ingress::Child(path(&["server"])), activation_frame)
.expect("first server data should activate the hook");
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::HookKey::new(path(&["client"]), 11),
crate::protocol::tree::PendingHook {
caller_src_path: path(&["client"]),
procedure_id: call.procedure_id.clone(),
local_ended: false,
},
)
.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!(!matches!(outcome, EndpointOutcome::Dropped));
}
#[test]
fn callee_side_end_hook_marks_local_end_before_peer_close() {
let mut endpoint = ProtocolEndpoint::new(path(&["server"]), None, Vec::new(), Vec::new());
endpoint
.add_endpoint_procedure("example.service.v1.invoke")
.expect("procedure registration should succeed");
let frame = encode_packet(
&PacketHeader {
packet_type: PacketType::Call,
src_path: Vec::new(),
dst_path: path(&["server"]),
dst_leaf: None,
hook_id: None,
},
&crate::protocol::CallMessage {
procedure_id: "example.service.v1.invoke".to_owned(),
data: vec![1],
response_hook: Some(crate::protocol::HookTarget {
hook_id: 21,
return_path: Vec::new(),
}),
},
)
.expect("call should encode");
endpoint
.receive(&Ingress::Parent, frame)
.expect("callee should accept call");
let key = crate::protocol::tree::HookKey::new(Vec::new(), 21);
assert!(endpoint.hooks.active(&key).is_some());
endpoint
.send_data(
Vec::new(),
21,
"example.service.v1.invoke",
Vec::new(),
true,
)
.expect("callee local end should succeed");
assert!(endpoint.hooks.active(&key).is_some());
let peer_final = encode_packet(
&PacketHeader {
packet_type: PacketType::Data,
src_path: Vec::new(),
dst_path: path(&["server"]),
dst_leaf: None,
hook_id: Some(21),
},
&DataMessage {
procedure_id: "example.service.v1.invoke".to_owned(),
data: Vec::new(),
end_hook: true,
},
)
.expect("peer final data should encode");
endpoint
.receive(&Ingress::Parent, peer_final)
.expect("callee should accept peer close");
assert!(endpoint.hooks.active(&key).is_none());
}