From 0f54b53a79cdb3595daee5a45cf8cc754f2cecac Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Sat, 9 May 2026 12:47:51 -0600 Subject: [PATCH] Fix runtime child route forwarding --- API.md | 2 - unshell-runtime/src/node/runtime.rs | 71 ++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/API.md b/API.md index 9fab9d4..93dde60 100644 --- a/API.md +++ b/API.md @@ -285,8 +285,6 @@ connection closes or unregisters - Local outbound calls through the runtime are not implemented. - Connection registration does not yet atomically update endpoint routes. - Disconnect does not yet clean hooks, sessions, route state, and queued effects. -- `RouteDecision::Child(index)` still depends on index compatibility with the - existing `ProtocolEndpoint` route table. - Child ingress still allocates because the existing `Ingress::Child` owns a `Vec`. diff --git a/unshell-runtime/src/node/runtime.rs b/unshell-runtime/src/node/runtime.rs index 636fe94..36b929b 100644 --- a/unshell-runtime/src/node/runtime.rs +++ b/unshell-runtime/src/node/runtime.rs @@ -236,7 +236,10 @@ where .endpoint .endpoint() .child_routes() - .get(index) + .iter() + // RouteDecision indexes are compiled from registered children only. + .filter(|child| child.registered) + .nth(index) .and_then(|child| { self.connections .registered_by_path(ConnectionDirection::Child, &child.path) @@ -393,6 +396,72 @@ mod tests { assert_eq!(runtime.transport().sent[0].0, child); } + #[test] + fn child_route_decision_uses_registered_child_order() { + let parent = ConnectionId::new(1); + let unregistered_child = ConnectionId::new(2); + let registered_child = ConnectionId::new(3); + let mut connections = Connections::new(); + connections.push(Connection::registered( + parent, + ConnectionDirection::Parent, + vec![], + ConnectionGeneration::INITIAL, + )); + connections.push(Connection::registered( + unregistered_child, + ConnectionDirection::Child, + vec![String::from("agent"), String::from("spare")], + ConnectionGeneration::INITIAL, + )); + connections.push(Connection::registered( + registered_child, + ConnectionDirection::Child, + vec![String::from("agent"), String::from("grand")], + ConnectionGeneration::INITIAL, + )); + + let endpoint = ProtocolEndpoint::new( + vec![String::from("agent")], + Some(vec![]), + vec![ + ChildRoute { + path: vec![String::from("agent"), String::from("spare")], + registered: false, + }, + ChildRoute::registered(vec![String::from("agent"), String::from("grand")]), + ], + vec![], + ); + + let frame = encode_packet( + &PacketHeader { + packet_type: PacketType::Call, + src_path: vec![], + dst_path: vec![String::from("agent"), String::from("grand")], + dst_leaf: None, + hook_id: None, + }, + &CallMessage { + procedure_id: String::from("org.example.v1.echo.invoke"), + data: vec![], + response_hook: None, + }, + ) + .expect("frame encodes"); + + let transport = RecordingTransport { + inbound: Some((parent, frame)), + sent: Vec::new(), + }; + let mut runtime = NodeRuntime::new(EndpointState::new(endpoint), connections, transport); + + let outcome = runtime.tick(TickBudget::default()).expect("tick succeeds"); + + assert_eq!(outcome.outbound_frames, 1); + assert_eq!(runtime.transport().sent[0].0, registered_child); + } + #[test] fn receive_keeps_local_events_queued_for_leaf_dispatch() { let parent = ConnectionId::new(1);