document remaining treetest modules

This commit is contained in:
Michael Mikovsky
2026-04-24 17:44:13 -06:00
parent ef62befe9a
commit 5a5323514b
4 changed files with 72 additions and 2 deletions
+32 -2
View File
@@ -1,4 +1,9 @@
//! Root-issued calls and injected traffic. //! 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 crate::model::{NodeId, format_hook_ref, format_leaf_ref, format_path};
use unshell::protocol::{DataMessage, PacketHeader, PacketType}; use unshell::protocol::{DataMessage, PacketHeader, PacketType};
@@ -21,6 +26,8 @@ impl Simulation {
&mut self, &mut self,
node_id: NodeId, node_id: NodeId,
) -> Result<ActionResult, SimError> { ) -> Result<ActionResult, SimError> {
// 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(); let path = self.tree.node(node_id).path.clone();
self.dispatch_root_call(path.clone(), None, "", Vec::new())?; self.dispatch_root_call(path.clone(), None, "", Vec::new())?;
Ok(ActionResult { Ok(ActionResult {
@@ -36,12 +43,18 @@ impl Simulation {
leaf_name: &str, leaf_name: &str,
) -> Result<ActionResult, SimError> { ) -> Result<ActionResult, SimError> {
let node_path = self.tree.node(node_id).path.clone(); 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)?; self.require_leaf(node_id, leaf_name)?;
let node = self.tree.node(node_id).clone(); let node = self.tree.node(node_id).clone();
if let Some(leaf_spec) = node.leaves.iter().find(|leaf| leaf.name == leaf_name) { 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 self.root_knowledge
.remember_leaf_from_spec(&node, leaf_spec); .remember_leaf_from_spec(&node, leaf_spec);
} }
self.dispatch_root_call(node_path, Some(leaf_name.to_owned()), "", Vec::new())?; self.dispatch_root_call(node_path, Some(leaf_name.to_owned()), "", Vec::new())?;
Ok(ActionResult { Ok(ActionResult {
label: format!( label: format!(
@@ -62,6 +75,8 @@ impl Simulation {
let node_path = self.tree.node(node_id).path.clone(); let node_path = self.tree.node(node_id).path.clone();
let node_display = self.tree.node(node_id).display_path(); let node_display = self.tree.node(node_id).display_path();
let node = self.tree.node(node_id).clone(); 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(); let procedures = self.require_leaf(node_id, leaf_name)?.procedures.clone();
if let Some(leaf_spec) = node if let Some(leaf_spec) = node
.leaves .leaves
@@ -71,6 +86,7 @@ impl Simulation {
self.root_knowledge self.root_knowledge
.remember_leaf_from_spec(&node, leaf_spec); .remember_leaf_from_spec(&node, leaf_spec);
} }
let procedure_id = let procedure_id =
procedures procedures
.first() .first()
@@ -79,6 +95,7 @@ impl Simulation {
node_path: node_display.clone(), node_path: node_display.clone(),
procedure_id: "<missing>".to_owned(), procedure_id: "<missing>".to_owned(),
})?; })?;
self.dispatch_root_call( self.dispatch_root_call(
node_path, node_path,
Some(leaf_name.to_owned()), Some(leaf_name.to_owned()),
@@ -103,7 +120,11 @@ impl Simulation {
) -> Result<ActionResult, SimError> { ) -> Result<ActionResult, SimError> {
let node_path = self.tree.node(node_id).path.clone(); let node_path = self.tree.node(node_id).path.clone();
let node_display = self.tree.node(node_id).display_path(); 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)?; self.require_endpoint_procedure(node_id, procedure_id)?;
let node = self.tree.node(node_id).clone(); let node = self.tree.node(node_id).clone();
if let Some(procedure) = node if let Some(procedure) = node
.endpoint_procedures .endpoint_procedures
@@ -113,6 +134,7 @@ impl Simulation {
self.root_knowledge self.root_knowledge
.remember_endpoint_procedure(&node, procedure); .remember_endpoint_procedure(&node, procedure);
} }
self.dispatch_root_call(node_path, None, procedure_id, data)?; self.dispatch_root_call(node_path, None, procedure_id, data)?;
Ok(ActionResult { Ok(ActionResult {
label: format!("Call {procedure_id} on {}", node_display), 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 /// Sends a raw call without demo-side validation.
/// remote `UnknownLeaf` and `UnknownProcedure` fault behavior. ///
/// Rationale: tests need one escape hatch that can deliberately trigger
/// remote `UnknownLeaf` and `UnknownProcedure` faults.
pub fn call_unchecked( pub fn call_unchecked(
&mut self, &mut self,
node_id: NodeId, node_id: NodeId,
@@ -159,11 +183,13 @@ impl Simulation {
text: &str, text: &str,
end_hook: bool, end_hook: bool,
) -> Result<ActionResult, SimError> { ) -> Result<ActionResult, SimError> {
// Fetch the peer path and procedure contract from the active hook model.
let snapshot = self let snapshot = self
.hooks .hooks
.get(&hook_id) .get(&hook_id)
.cloned() .cloned()
.ok_or(SimError::UnknownHook(hook_id))?; .ok_or(SimError::UnknownHook(hook_id))?;
let frame = self.nodes[self.root_id.0] let frame = self.nodes[self.root_id.0]
.endpoint .endpoint
.make_data( .make_data(
@@ -174,6 +200,7 @@ impl Simulation {
end_hook, end_hook,
) )
.map_err(|error| SimError::Protocol(error.to_string()))?; .map_err(|error| SimError::Protocol(error.to_string()))?;
self.record_trace( self.record_trace(
self.root_id, self.root_id,
format!( format!(
@@ -199,6 +226,9 @@ impl Simulation {
) -> Result<ActionResult, SimError> { ) -> Result<ActionResult, SimError> {
let from_path = self.tree.node(from_node_id).path.clone(); let from_path = self.tree.node(from_node_id).path.clone();
let to_path = self.tree.node(to_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 { let header = PacketHeader {
packet_type: PacketType::Data, packet_type: PacketType::Data,
src_path: from_path.clone(), src_path: from_path.clone(),
+19
View File
@@ -1,4 +1,8 @@
//! Packet dispatch and routing glue. //! 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::FrameBytes;
use unshell::protocol::tree::{Endpoint, Ingress, RouteDecision}; 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}; use super::super::types::{Envelope, HookSnapshot, SimError, Simulation, TraceEvent};
impl Simulation { impl Simulation {
/// Builds a root-originated `Call` and feeds it into the runtime.
pub(crate) fn dispatch_root_call( pub(crate) fn dispatch_root_call(
&mut self, &mut self,
dst_path: Vec<String>, dst_path: Vec<String>,
@@ -15,6 +20,8 @@ impl Simulation {
procedure_id: &str, procedure_id: &str,
data: Vec<u8>, data: Vec<u8>,
) -> Result<(), SimError> { ) -> 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 hook_id = self.nodes[self.root_id.0].endpoint.allocate_hook_id();
let frame = self.nodes[self.root_id.0] let frame = self.nodes[self.root_id.0]
.endpoint .endpoint
@@ -26,6 +33,9 @@ impl Simulation {
data, data,
) )
.map_err(|error| SimError::Protocol(error.to_string()))?; .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( self.hooks.insert(
hook_id, hook_id,
HookSnapshot { HookSnapshot {
@@ -38,6 +48,7 @@ impl Simulation {
last_message: format!("created for {}", format_path(&dst_path)), last_message: format!("created for {}", format_path(&dst_path)),
}, },
); );
self.record_trace( self.record_trace(
self.root_id, self.root_id,
format!( format!(
@@ -57,6 +68,7 @@ impl Simulation {
self.process_local_frame(self.root_id, frame) self.process_local_frame(self.root_id, frame)
} }
/// Delivers a frame into one endpoint as locally-originated traffic.
pub(crate) fn process_local_frame( pub(crate) fn process_local_frame(
&mut self, &mut self,
node_id: NodeId, node_id: NodeId,
@@ -69,6 +81,7 @@ impl Simulation {
self.process_outcome(node_id, outcome) self.process_outcome(node_id, outcome)
} }
/// Applies one endpoint outcome to the simulated transport.
pub(crate) fn process_outcome( pub(crate) fn process_outcome(
&mut self, &mut self,
node_id: NodeId, node_id: NodeId,
@@ -107,6 +120,9 @@ impl Simulation {
let parent_id = self.nodes[node_id.0] let parent_id = self.nodes[node_id.0]
.parent .parent
.ok_or_else(|| SimError::Protocol("missing parent route".to_owned()))?; .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(); let child_path = self.node(node_id).path.clone();
self.record_trace( self.record_trace(
node_id, node_id,
@@ -141,6 +157,7 @@ impl Simulation {
Ok(()) Ok(())
} }
/// Appends one entry to the rolling trace buffer.
pub(crate) fn record_trace(&mut self, node_id: NodeId, summary: String) { pub(crate) fn record_trace(&mut self, node_id: NodeId, summary: String) {
let node_path = self.node(node_id).display_path(); let node_path = self.node(node_id).display_path();
self.trace.push_back(TraceEvent { self.trace.push_back(TraceEvent {
@@ -149,6 +166,8 @@ impl Simulation {
summary, summary,
}); });
self.next_tick += 1; self.next_tick += 1;
// Cap trace growth so the TUI remains responsive during long sessions.
while self.trace.len() > 200 { while self.trace.len() > 200 {
self.trace.pop_front(); self.trace.pop_front();
} }
@@ -1,4 +1,9 @@
//! Application-procedure handling layered over protocol calls. //! 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}; use unshell::protocol::{CallMessage, PacketHeader};
@@ -7,6 +12,8 @@ use crate::model::{EndpointProcedureKind, EndpointProcedureSpec, NodeId};
use super::super::super::types::{SimError, Simulation}; use super::super::super::types::{SimError, Simulation};
impl Simulation { impl Simulation {
/// Handles an application-visible `Call` that the protocol runtime already
/// accepted and delivered locally.
pub(super) fn handle_application_call( pub(super) fn handle_application_call(
&mut self, &mut self,
node_id: NodeId, node_id: NodeId,
@@ -17,6 +24,8 @@ impl Simulation {
return Ok(()); return Ok(());
}; };
// Clone the procedure spec once so later reply generation can borrow the
// rest of the simulator state freely.
let procedure = self let procedure = self
.lookup_endpoint_procedure(node_id, &message.procedure_id)? .lookup_endpoint_procedure(node_id, &message.procedure_id)?
.clone(); .clone();
@@ -60,6 +69,8 @@ impl Simulation {
} }
} }
EndpointProcedureKind::Chat => { EndpointProcedureKind::Chat => {
// Persist chat state outside the protocol runtime because the
// protocol itself does not define chat semantics.
self.chat_sessions.insert( self.chat_sessions.insert(
hook.hook_id, hook.hook_id,
super::super::super::types::ChatSession { super::super::super::types::ChatSession {
@@ -86,6 +97,7 @@ impl Simulation {
Ok(()) Ok(())
} }
/// Resolves one endpoint procedure from the ground-truth node metadata.
pub(super) fn lookup_endpoint_procedure( pub(super) fn lookup_endpoint_procedure(
&self, &self,
node_id: NodeId, node_id: NodeId,
@@ -101,6 +113,7 @@ impl Simulation {
}) })
} }
/// Ensures one named leaf exists on the target node.
pub(crate) fn require_leaf( pub(crate) fn require_leaf(
&self, &self,
node_id: NodeId, node_id: NodeId,
@@ -116,6 +129,7 @@ impl Simulation {
}) })
} }
/// Ensures one endpoint procedure exists on the target node.
pub(crate) fn require_endpoint_procedure( pub(crate) fn require_endpoint_procedure(
&self, &self,
node_id: NodeId, node_id: NodeId,
+7
View File
@@ -1,4 +1,7 @@
//! Protocol local-event handling. //! 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; 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}; use super::super::super::types::{RecordedEvent, SimError, Simulation};
impl Simulation { impl Simulation {
/// Handles one local event emitted by the protocol runtime.
pub(crate) fn handle_local_event( pub(crate) fn handle_local_event(
&mut self, &mut self,
node_id: NodeId, node_id: NodeId,
@@ -26,8 +30,10 @@ impl Simulation {
) )
), ),
); );
if let Some(hook_id) = header.hook_id { if let Some(hook_id) = header.hook_id {
if let Some(snapshot) = self.hooks.get_mut(&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() { snapshot.last_message = if text.is_empty() {
format!("binary payload ({} bytes)", message.data.len()) format!("binary payload ({} bytes)", message.data.len())
} else { } else {
@@ -102,6 +108,7 @@ impl Simulation {
snapshot.closed = true; snapshot.closed = true;
snapshot.last_message = format!("fault 0x{:02X}", message.fault.0); 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.chat_sessions.remove(&hook_id);
} }
self.recorded_events.push(RecordedEvent::Fault { self.recorded_events.push(RecordedEvent::Fault {