diff --git a/src/protocol/introspection.rs b/src/protocol/introspection.rs index 1f046e1..be3f423 100644 --- a/src/protocol/introspection.rs +++ b/src/protocol/introspection.rs @@ -9,6 +9,8 @@ pub const INTROSPECTION_PROCEDURE_ID: &str = ""; /// Endpoint-wide introspection payload. #[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct EndpointIntrospection { + /// Direct child path segments currently registered under this endpoint. + pub sub_endpoints: Vec, /// Hosted leaves and their supported procedures. pub leaves: Vec, } diff --git a/src/protocol/tests/protocol.rs b/src/protocol/tests/protocol.rs index 50698df..fe26281 100644 --- a/src/protocol/tests/protocol.rs +++ b/src/protocol/tests/protocol.rs @@ -16,11 +16,11 @@ fn packet_framing_roundtrip_preserves_header_and_payload() { packet_type: PacketType::Call, src_path: path(&["root", "caller"]), dst_path: path(&["root", "callee"]), - dst_leaf: Some("echo".to_owned()), + dst_leaf: Some("service".to_owned()), hook_id: None, }; let call = CallMessage { - procedure_id: "unshell.echo.v1.alpha.invoke".to_owned(), + procedure_id: "example.service.v1.invoke".to_owned(), data: vec![1, 2, 3, 4], response_hook: Some(HookTarget { hook_id: 7, @@ -45,7 +45,7 @@ fn header_and_call_validation_reject_invalid_combinations() { packet_type: PacketType::Data, src_path: path(&["peer"]), dst_path: path(&["host"]), - dst_leaf: Some("echo".to_owned()), + dst_leaf: Some("service".to_owned()), hook_id: None, }; assert_eq!( @@ -59,11 +59,11 @@ fn header_and_call_validation_reject_invalid_combinations() { packet_type: PacketType::Call, src_path: path(&["caller"]), dst_path: path(&["callee"]), - dst_leaf: Some("echo".to_owned()), + dst_leaf: Some("service".to_owned()), hook_id: None, }; let invalid_call = CallMessage { - procedure_id: "unshell.echo.v1.alpha.invoke".to_owned(), + procedure_id: "example.service.v1.invoke".to_owned(), data: Vec::new(), response_hook: Some(HookTarget { hook_id: 5, @@ -79,18 +79,10 @@ fn header_and_call_validation_reject_invalid_combinations() { } #[test] -fn procedure_validation_accepts_introspection_and_rejects_bad_shapes() { +fn procedure_validation_accepts_introspection_and_non_empty_opaque_ids() { assert_eq!(validate_procedure_id(""), Ok(())); - assert_eq!( - validate_procedure_id("unshell.echo.v01.alpha.invoke"), - Ok(()) - ); - assert_eq!( - validate_procedure_id("contains spaces"), - Err(ValidationError::ProcedureId( - "procedure identifier should use alphanumeric characters, dots, and underscores" - )) - ); + assert_eq!(validate_procedure_id("example.service.v01.invoke"), Ok(())); + assert_eq!(validate_procedure_id("contains spaces"), Ok(())); } #[test] diff --git a/src/protocol/tests/tree.rs b/src/protocol/tests/tree.rs index abffde9..4293c4d 100644 --- a/src/protocol/tests/tree.rs +++ b/src/protocol/tests/tree.rs @@ -1,7 +1,7 @@ use alloc::{borrow::ToOwned, string::String, vec, vec::Vec}; use crate::protocol::tree::{ - DefaultRouteProvider, Endpoint, Ingress, LeafBehavior, LeafNode, LeafSpec, LocalEvent, + ChildRoute, DefaultRouteProvider, Endpoint, Ingress, LeafNode, LeafSpec, LocalEvent, ProtocolEndpoint, RouteDecision, RouteProvider, TreeNode, }; use crate::protocol::{ @@ -19,8 +19,8 @@ fn tree_node_paths_flatten_explicitly() { children: vec![TreeNode::Endpoint { segment: "branch".to_owned(), leaves: vec![LeafNode { - name: "echo".to_owned(), - procedures: vec!["unshell.echo.v1.alpha.invoke".to_owned()], + name: "service".to_owned(), + procedures: vec!["example.service.v1.invoke".to_owned()], }], children: vec![TreeNode::Endpoint { segment: "leaf".to_owned(), @@ -60,11 +60,10 @@ fn protocol_endpoint_introspection_returns_leaf_summary() { let mut endpoint = ProtocolEndpoint::new( path(&["root"]), Some(Vec::new()), - Vec::new(), + vec![ChildRoute::registered(path(&["root", "child"]))], vec![LeafSpec { - name: "echo".to_owned(), - procedures: vec!["unshell.echo.v1.alpha.invoke".to_owned()], - behavior: LeafBehavior::Echo, + name: "service".to_owned(), + procedures: vec!["example.service.v1.invoke".to_owned()], }], ); @@ -96,11 +95,12 @@ fn protocol_endpoint_introspection_returns_leaf_summary() { .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, "echo"); + assert_eq!(introspection.leaves[0].leaf_name, "service"); assert_eq!( introspection.leaves[0].procedures, - vec!["unshell.echo.v1.alpha.invoke".to_owned()] + vec!["example.service.v1.invoke".to_owned()] ); } @@ -113,7 +113,7 @@ fn invalid_hook_peer_emits_local_fault_event() { .make_call( path(&["server"]), None, - "unshell.echo.v1.alpha.invoke", + "example.service.v1.invoke", Some(hook_id), vec![1, 2, 3], ) @@ -128,7 +128,7 @@ fn invalid_hook_peer_emits_local_fault_event() { hook_id: Some(hook_id), }, &DataMessage { - procedure_id: "unshell.echo.v1.alpha.invoke".to_owned(), + procedure_id: "example.service.v1.invoke".to_owned(), data: vec![9], end_hook: false, }, diff --git a/src/protocol/tree/endpoint/core.rs b/src/protocol/tree/endpoint/core.rs index 7bdc086..c02085c 100644 --- a/src/protocol/tree/endpoint/core.rs +++ b/src/protocol/tree/endpoint/core.rs @@ -47,13 +47,6 @@ impl ChildRoute { } } -/// Test leaf behavior implemented by the endpoint runtime. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum LeafBehavior { - /// Mirrors the incoming payload back over the declared response hook. - Echo, -} - /// Static leaf metadata used for procedure dispatch and introspection. #[derive(Debug, Clone, PartialEq, Eq)] pub struct LeafSpec { @@ -61,8 +54,6 @@ pub struct LeafSpec { pub name: String, /// Procedures supported by the leaf. pub procedures: Vec, - /// Built-in behavior used by the lightweight test runtime. - pub behavior: LeafBehavior, } /// Where a frame entered the local endpoint. diff --git a/src/protocol/tree/endpoint/introspection.rs b/src/protocol/tree/endpoint/introspection.rs index aa149e5..2aa1198 100644 --- a/src/protocol/tree/endpoint/introspection.rs +++ b/src/protocol/tree/endpoint/introspection.rs @@ -42,6 +42,12 @@ impl ProtocolEndpoint { .to_vec() } else { to_bytes::(&EndpointIntrospection { + sub_endpoints: self + .children + .iter() + .filter(|child| child.state == super::core::ConnectionState::Registered) + .filter_map(|child| child.path.get(self.path.len()).cloned()) + .collect(), leaves: self .leaves .values() diff --git a/src/protocol/tree/endpoint/mod.rs b/src/protocol/tree/endpoint/mod.rs index d132437..7cbbe35 100644 --- a/src/protocol/tree/endpoint/mod.rs +++ b/src/protocol/tree/endpoint/mod.rs @@ -17,6 +17,6 @@ mod introspection; mod receive; pub use core::{ - ChildRoute, ConnectionState, Endpoint, EndpointError, EndpointOutcome, Ingress, LeafBehavior, - LeafSpec, LocalEvent, ProtocolEndpoint, + ChildRoute, ConnectionState, Endpoint, EndpointError, EndpointOutcome, Ingress, LeafSpec, + LocalEvent, ProtocolEndpoint, }; diff --git a/src/protocol/tree/endpoint/receive.rs b/src/protocol/tree/endpoint/receive.rs index 41fa27e..84c2279 100644 --- a/src/protocol/tree/endpoint/receive.rs +++ b/src/protocol/tree/endpoint/receive.rs @@ -6,8 +6,8 @@ use alloc::vec; use crate::protocol::{ - CallMessage, DataMessage, PacketType, ProtocolFault, decode_frame, - introspection::INTROSPECTION_PROCEDURE_ID, validate_call, validate_header, + CallMessage, PacketType, ProtocolFault, decode_frame, introspection::INTROSPECTION_PROCEDURE_ID, + validate_call, validate_header, }; use super::super::{HookKey, PendingHook, RouteDecision}; @@ -77,51 +77,10 @@ impl ProtocolEndpoint { self.hooks.activate_pending(key, header.src_path.clone()); } - match header - .dst_leaf - .as_ref() - .and_then(|name| self.leaves.get(name)) - { - Some(leaf) if leaf.behavior == super::core::LeafBehavior::Echo && key.is_some() => { - let hook = message.response_hook.expect("synchronized"); - let response = DataMessage { - procedure_id: message.procedure_id.clone(), - data: message.data, - end_hook: true, - }; - let response_header = crate::protocol::PacketHeader { - packet_type: PacketType::Data, - src_path: self.path.clone(), - dst_path: hook.return_path.clone(), - dst_leaf: None, - hook_id: Some(hook.hook_id), - }; - let route = self.decide_route(&hook.return_path); - self.hooks - .remove_active(&HookKey::new(hook.return_path.clone(), hook.hook_id)); - - match route { - RouteDecision::Local => Ok(EndpointOutcome { - events: vec![LocalEvent::Data { - header: response_header, - message: response, - }], - ..EndpointOutcome::default() - }), - _ => { - let frame = crate::protocol::encode_packet(&response_header, &response)?; - Ok(EndpointOutcome { - forwards: vec![(route, frame)], - ..EndpointOutcome::default() - }) - } - } - } - _ => Ok(EndpointOutcome { - events: vec![LocalEvent::Call { header, message }], - ..EndpointOutcome::default() - }), - } + Ok(EndpointOutcome { + events: vec![LocalEvent::Call { header, message }], + ..EndpointOutcome::default() + }) } } diff --git a/src/protocol/tree/mod.rs b/src/protocol/tree/mod.rs index d930d09..73f83db 100644 --- a/src/protocol/tree/mod.rs +++ b/src/protocol/tree/mod.rs @@ -5,8 +5,8 @@ mod hook; mod routing; pub use endpoint::{ - ChildRoute, ConnectionState, Endpoint, EndpointError, EndpointOutcome, Ingress, LeafBehavior, - LeafSpec, LocalEvent, ProtocolEndpoint, + ChildRoute, ConnectionState, Endpoint, EndpointError, EndpointOutcome, Ingress, LeafSpec, + LocalEvent, ProtocolEndpoint, }; pub use hook::{ActiveHook, HookConflict, HookKey, HookTable, PendingHook}; pub use routing::{ diff --git a/src/protocol/validation.rs b/src/protocol/validation.rs index 08a52c4..a73827d 100644 --- a/src/protocol/validation.rs +++ b/src/protocol/validation.rs @@ -60,7 +60,7 @@ pub fn validate_header(header: &PacketHeader) -> Result<(), ValidationError> { Ok(()) } -/// Validates the canonical dotted `procedure_id` shape. +/// Validates the protocol-level `procedure_id` invariant. pub fn validate_procedure_id(procedure_id: &str) -> Result<(), ValidationError> { if procedure_id == INTROSPECTION_PROCEDURE_ID { return Ok(()); @@ -72,15 +72,6 @@ pub fn validate_procedure_id(procedure_id: &str) -> Result<(), ValidationError> )); } - if !procedure_id - .chars() - .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '.') - { - return Err(ValidationError::ProcedureId( - "procedure identifier should use alphanumeric characters, dots, and underscores", - )); - } - Ok(()) } diff --git a/treetest/src/model.rs b/treetest/src/model.rs index aa05921..78f59e4 100644 --- a/treetest/src/model.rs +++ b/treetest/src/model.rs @@ -14,7 +14,7 @@ pub struct NodeId(pub usize); /// Supported demo leaf kinds. #[derive(Debug, Clone, PartialEq, Eq)] pub enum LeafKind { - /// Uses the built-in echo leaf behavior from `unshell`. + /// Demo leaf that returns the incoming payload through the declared hook. Echo, } diff --git a/treetest/src/scenarios/simple.rs b/treetest/src/scenarios/simple.rs index 1f60f69..707dde1 100644 --- a/treetest/src/scenarios/simple.rs +++ b/treetest/src/scenarios/simple.rs @@ -74,7 +74,7 @@ fn echo_leaf() -> ScenarioDefinition { name: "Echo Leaf".to_owned(), description: "Call a concrete leaf and watch the hook finish normally.".to_owned(), highlights: vec![ - "The leaf uses the built-in `Echo` behavior from the core runtime.".to_owned(), + "The demo application echoes the payload after the protocol runtime delivers the call locally.".to_owned(), "The final response sets `end_hook = true`.".to_owned(), ], root: NodeSpec { diff --git a/treetest/src/sim/build.rs b/treetest/src/sim/build.rs index 35c9d1d..3888e64 100644 --- a/treetest/src/sim/build.rs +++ b/treetest/src/sim/build.rs @@ -6,9 +6,9 @@ use std::collections::{BTreeMap, VecDeque}; use crossbeam_channel::unbounded; -use unshell::protocol::tree::{ChildRoute, ConnectionState, LeafBehavior, ProtocolEndpoint}; +use unshell::protocol::tree::{ChildRoute, ConnectionState, ProtocolEndpoint}; -use crate::model::{DemoTree, LeafKind, NodeId, ScenarioDefinition, Selection}; +use crate::model::{DemoTree, NodeId, ScenarioDefinition, Selection}; use super::knowledge::{InspectorMode, RootKnowledge}; use super::types::{ChatSession, SimError, SimNode, Simulation}; @@ -53,9 +53,6 @@ impl Simulation { .map(|leaf| unshell::protocol::tree::LeafSpec { name: leaf.name.clone(), procedures: leaf.procedures.clone(), - behavior: match leaf.kind { - LeafKind::Echo => LeafBehavior::Echo, - }, }) .collect::>(); diff --git a/treetest/src/sim/runtime/events/application.rs b/treetest/src/sim/runtime/events/application.rs index 41377ef..29713fc 100644 --- a/treetest/src/sim/runtime/events/application.rs +++ b/treetest/src/sim/runtime/events/application.rs @@ -7,7 +7,7 @@ use unshell::protocol::{CallMessage, PacketHeader}; -use crate::model::{EndpointProcedureKind, EndpointProcedureSpec, NodeId}; +use crate::model::{EndpointProcedureKind, EndpointProcedureSpec, LeafKind, NodeId}; use super::super::super::types::{SimError, Simulation}; @@ -17,13 +17,32 @@ impl Simulation { pub(super) fn handle_application_call( &mut self, node_id: NodeId, - _header: &PacketHeader, + header: &PacketHeader, message: &CallMessage, ) -> Result<(), SimError> { let Some(hook) = &message.response_hook else { return Ok(()); }; + if let Some(leaf_name) = &header.dst_leaf { + let leaf = self.require_leaf(node_id, leaf_name)?.clone(); + match leaf.kind { + LeafKind::Echo => { + let frame = self.make_endpoint_data_frame( + node_id, + hook.return_path.clone(), + hook.hook_id, + message.procedure_id.clone(), + message.data.clone(), + true, + )?; + self.record_trace(node_id, format!("leaf {leaf_name} echoed {} bytes", message.data.len())); + self.process_local_frame(node_id, frame)?; + } + } + return Ok(()); + } + // Clone the procedure spec once so later reply generation can borrow the // rest of the simulator state freely. let procedure = self