From 5a5323514bf989032937b0549036e0b6503d47e0 Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:44:13 -0600 Subject: [PATCH] document remaining treetest modules --- treetest/src/sim/actions/calls.rs | 34 +++++++++++++++++-- treetest/src/sim/runtime/dispatch.rs | 19 +++++++++++ .../src/sim/runtime/events/application.rs | 14 ++++++++ treetest/src/sim/runtime/events/local.rs | 7 ++++ 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/treetest/src/sim/actions/calls.rs b/treetest/src/sim/actions/calls.rs index f8e9ce2..06da104 100644 --- a/treetest/src/sim/actions/calls.rs +++ b/treetest/src/sim/actions/calls.rs @@ -1,4 +1,9 @@ //! Root-issued calls and injected traffic. +//! +//! These helpers form the high-level API that both the TUI and the tests use. +//! Each helper prepares the right destination metadata, teaches the root what it +//! can know locally, and then hands one concrete call or data packet to the +//! runtime layer. use crate::model::{NodeId, format_hook_ref, format_leaf_ref, format_path}; use unshell::protocol::{DataMessage, PacketHeader, PacketType}; @@ -21,6 +26,8 @@ impl Simulation { &mut self, node_id: NodeId, ) -> Result { + // Snapshot the destination path now so the later packet build cannot be + // confused by any further selection or scenario changes. let path = self.tree.node(node_id).path.clone(); self.dispatch_root_call(path.clone(), None, "", Vec::new())?; Ok(ActionResult { @@ -36,12 +43,18 @@ impl Simulation { leaf_name: &str, ) -> Result { let node_path = self.tree.node(node_id).path.clone(); + + // Fail fast if the selected leaf name is not valid in ground truth. self.require_leaf(node_id, leaf_name)?; + let node = self.tree.node(node_id).clone(); if let Some(leaf_spec) = node.leaves.iter().find(|leaf| leaf.name == leaf_name) { + // The root already knows a leaf exists because the user targeted it + // directly, even before the remote introspection result returns. self.root_knowledge .remember_leaf_from_spec(&node, leaf_spec); } + self.dispatch_root_call(node_path, Some(leaf_name.to_owned()), "", Vec::new())?; Ok(ActionResult { label: format!( @@ -62,6 +75,8 @@ impl Simulation { let node_path = self.tree.node(node_id).path.clone(); let node_display = self.tree.node(node_id).display_path(); let node = self.tree.node(node_id).clone(); + + // Clone the procedure list out before mutating learned state below. let procedures = self.require_leaf(node_id, leaf_name)?.procedures.clone(); if let Some(leaf_spec) = node .leaves @@ -71,6 +86,7 @@ impl Simulation { self.root_knowledge .remember_leaf_from_spec(&node, leaf_spec); } + let procedure_id = procedures .first() @@ -79,6 +95,7 @@ impl Simulation { node_path: node_display.clone(), procedure_id: "".to_owned(), })?; + self.dispatch_root_call( node_path, Some(leaf_name.to_owned()), @@ -103,7 +120,11 @@ impl Simulation { ) -> Result { let node_path = self.tree.node(node_id).path.clone(); let node_display = self.tree.node(node_id).display_path(); + + // Keep the public helper strict so ordinary UI actions cannot target a + // non-existent endpoint procedure by mistake. self.require_endpoint_procedure(node_id, procedure_id)?; + let node = self.tree.node(node_id).clone(); if let Some(procedure) = node .endpoint_procedures @@ -113,6 +134,7 @@ impl Simulation { self.root_knowledge .remember_endpoint_procedure(&node, procedure); } + self.dispatch_root_call(node_path, None, procedure_id, data)?; Ok(ActionResult { label: format!("Call {procedure_id} on {}", node_display), @@ -120,8 +142,10 @@ impl Simulation { }) } - /// Sends a raw call without demo-side validation so tests can exercise - /// remote `UnknownLeaf` and `UnknownProcedure` fault behavior. + /// Sends a raw call without demo-side validation. + /// + /// Rationale: tests need one escape hatch that can deliberately trigger + /// remote `UnknownLeaf` and `UnknownProcedure` faults. pub fn call_unchecked( &mut self, node_id: NodeId, @@ -159,11 +183,13 @@ impl Simulation { text: &str, end_hook: bool, ) -> Result { + // Fetch the peer path and procedure contract from the active hook model. let snapshot = self .hooks .get(&hook_id) .cloned() .ok_or(SimError::UnknownHook(hook_id))?; + let frame = self.nodes[self.root_id.0] .endpoint .make_data( @@ -174,6 +200,7 @@ impl Simulation { end_hook, ) .map_err(|error| SimError::Protocol(error.to_string()))?; + self.record_trace( self.root_id, format!( @@ -199,6 +226,9 @@ impl Simulation { ) -> Result { let from_path = self.tree.node(from_node_id).path.clone(); let to_path = self.tree.node(to_node_id).path.clone(); + + // Build the packet by hand so the sender path can intentionally violate + // the active hook's expected peer relationship. let header = PacketHeader { packet_type: PacketType::Data, src_path: from_path.clone(), diff --git a/treetest/src/sim/runtime/dispatch.rs b/treetest/src/sim/runtime/dispatch.rs index 556b8ee..89ce720 100644 --- a/treetest/src/sim/runtime/dispatch.rs +++ b/treetest/src/sim/runtime/dispatch.rs @@ -1,4 +1,8 @@ //! Packet dispatch and routing glue. +//! +//! This layer sits directly above the protocol endpoint runtime. It converts one +//! local action into framed traffic, hands that traffic to the endpoint, and then +//! forwards the resulting frames across the simulated tree. use unshell::protocol::FrameBytes; use unshell::protocol::tree::{Endpoint, Ingress, RouteDecision}; @@ -8,6 +12,7 @@ use crate::model::{NodeId, format_leaf_ref, format_path}; use super::super::types::{Envelope, HookSnapshot, SimError, Simulation, TraceEvent}; impl Simulation { + /// Builds a root-originated `Call` and feeds it into the runtime. pub(crate) fn dispatch_root_call( &mut self, dst_path: Vec, @@ -15,6 +20,8 @@ impl Simulation { procedure_id: &str, data: Vec, ) -> Result<(), SimError> { + // Hook allocation happens on the root host because the root is the hook + // owner for every user-driven action in the demo. let hook_id = self.nodes[self.root_id.0].endpoint.allocate_hook_id(); let frame = self.nodes[self.root_id.0] .endpoint @@ -26,6 +33,9 @@ impl Simulation { data, ) .map_err(|error| SimError::Protocol(error.to_string()))?; + + // Track enough metadata locally to render hooks and learn from returned + // data later, even before the first response arrives. self.hooks.insert( hook_id, HookSnapshot { @@ -38,6 +48,7 @@ impl Simulation { last_message: format!("created for {}", format_path(&dst_path)), }, ); + self.record_trace( self.root_id, format!( @@ -57,6 +68,7 @@ impl Simulation { self.process_local_frame(self.root_id, frame) } + /// Delivers a frame into one endpoint as locally-originated traffic. pub(crate) fn process_local_frame( &mut self, node_id: NodeId, @@ -69,6 +81,7 @@ impl Simulation { self.process_outcome(node_id, outcome) } + /// Applies one endpoint outcome to the simulated transport. pub(crate) fn process_outcome( &mut self, node_id: NodeId, @@ -107,6 +120,9 @@ impl Simulation { let parent_id = self.nodes[node_id.0] .parent .ok_or_else(|| SimError::Protocol("missing parent route".to_owned()))?; + + // Parent ingress needs the child path because the protocol + // runtime validates source-path claims against ingress side. let child_path = self.node(node_id).path.clone(); self.record_trace( node_id, @@ -141,6 +157,7 @@ impl Simulation { Ok(()) } + /// Appends one entry to the rolling trace buffer. pub(crate) fn record_trace(&mut self, node_id: NodeId, summary: String) { let node_path = self.node(node_id).display_path(); self.trace.push_back(TraceEvent { @@ -149,6 +166,8 @@ impl Simulation { summary, }); self.next_tick += 1; + + // Cap trace growth so the TUI remains responsive during long sessions. while self.trace.len() > 200 { self.trace.pop_front(); } diff --git a/treetest/src/sim/runtime/events/application.rs b/treetest/src/sim/runtime/events/application.rs index ef95e5b..b20373f 100644 --- a/treetest/src/sim/runtime/events/application.rs +++ b/treetest/src/sim/runtime/events/application.rs @@ -1,4 +1,9 @@ //! Application-procedure handling layered over protocol calls. +//! +//! The core `unshell` runtime validates routing, hooks, and introspection, but +//! it intentionally does not know what a demo procedure should do. This module +//! is the thin application layer that turns validated local calls into concrete +//! demo behavior. use unshell::protocol::{CallMessage, PacketHeader}; @@ -7,6 +12,8 @@ use crate::model::{EndpointProcedureKind, EndpointProcedureSpec, NodeId}; use super::super::super::types::{SimError, Simulation}; impl Simulation { + /// Handles an application-visible `Call` that the protocol runtime already + /// accepted and delivered locally. pub(super) fn handle_application_call( &mut self, node_id: NodeId, @@ -17,6 +24,8 @@ impl Simulation { return Ok(()); }; + // Clone the procedure spec once so later reply generation can borrow the + // rest of the simulator state freely. let procedure = self .lookup_endpoint_procedure(node_id, &message.procedure_id)? .clone(); @@ -60,6 +69,8 @@ impl Simulation { } } EndpointProcedureKind::Chat => { + // Persist chat state outside the protocol runtime because the + // protocol itself does not define chat semantics. self.chat_sessions.insert( hook.hook_id, super::super::super::types::ChatSession { @@ -86,6 +97,7 @@ impl Simulation { Ok(()) } + /// Resolves one endpoint procedure from the ground-truth node metadata. pub(super) fn lookup_endpoint_procedure( &self, node_id: NodeId, @@ -101,6 +113,7 @@ impl Simulation { }) } + /// Ensures one named leaf exists on the target node. pub(crate) fn require_leaf( &self, node_id: NodeId, @@ -116,6 +129,7 @@ impl Simulation { }) } + /// Ensures one endpoint procedure exists on the target node. pub(crate) fn require_endpoint_procedure( &self, node_id: NodeId, diff --git a/treetest/src/sim/runtime/events/local.rs b/treetest/src/sim/runtime/events/local.rs index 4009db3..804f3c6 100644 --- a/treetest/src/sim/runtime/events/local.rs +++ b/treetest/src/sim/runtime/events/local.rs @@ -1,4 +1,7 @@ //! Protocol local-event handling. +//! +//! These handlers translate validated protocol events into trace entries, +//! learned root knowledge, and higher-level demo state such as the chat helper. use unshell::protocol::tree::LocalEvent; @@ -7,6 +10,7 @@ use crate::model::{NodeId, format_hook_ref, format_leaf_ref}; use super::super::super::types::{RecordedEvent, SimError, Simulation}; impl Simulation { + /// Handles one local event emitted by the protocol runtime. pub(crate) fn handle_local_event( &mut self, node_id: NodeId, @@ -26,8 +30,10 @@ impl Simulation { ) ), ); + if let Some(hook_id) = header.hook_id { if let Some(snapshot) = self.hooks.get_mut(&hook_id) { + // Keep the most recent human-readable payload in the UI. snapshot.last_message = if text.is_empty() { format!("binary payload ({} bytes)", message.data.len()) } else { @@ -102,6 +108,7 @@ impl Simulation { snapshot.closed = true; snapshot.last_message = format!("fault 0x{:02X}", message.fault.0); } + // Any protocol fault ends the application-level chat too. self.chat_sessions.remove(&hook_id); } self.recorded_events.push(RecordedEvent::Fault {