mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
document remaining treetest modules
This commit is contained in:
@@ -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(),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user