mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
finish treetest documentation sweep
This commit is contained in:
@@ -18,6 +18,10 @@ use crate::{
|
||||
use super::super::App;
|
||||
|
||||
impl App {
|
||||
/// Renders the inspector pane for the current selection.
|
||||
///
|
||||
/// Rationale: the inspector is the only pane whose data source changes with
|
||||
/// inspector mode, so it owns the `ground truth` vs `realistic` branch.
|
||||
pub(super) fn render_inspector(&self, frame: &mut Frame<'_>, area: ratatui::layout::Rect) {
|
||||
let selection = self.selected();
|
||||
let body = match self.simulation.inspector_mode {
|
||||
@@ -33,6 +37,7 @@ impl App {
|
||||
);
|
||||
}
|
||||
|
||||
/// Renders the inspector using full scenario truth.
|
||||
fn render_ground_truth_inspector(&self, selection: &Selection) -> Text<'static> {
|
||||
match selection {
|
||||
Selection::Node(node_id) => {
|
||||
@@ -84,6 +89,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the inspector using only what the root host has learned.
|
||||
fn render_realistic_inspector(&self, selection: &Selection) -> Text<'static> {
|
||||
match selection {
|
||||
Selection::Node(node_id) => {
|
||||
@@ -133,6 +139,9 @@ impl App {
|
||||
}
|
||||
Text::from(lines)
|
||||
} else {
|
||||
// Showing an explicit unknown state is better than silently
|
||||
// falling back to ground truth, because the whole point of
|
||||
// realistic mode is to expose what the root does not know.
|
||||
Text::from(vec![
|
||||
Line::from(node.display_path()).bold(),
|
||||
Line::from(
|
||||
|
||||
@@ -73,6 +73,23 @@ pub struct DemoNode {
|
||||
|
||||
impl DemoNode {
|
||||
/// Returns a display path that keeps the root easy to recognize in the UI.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use treetest::model::{DemoNode, NodeId};
|
||||
///
|
||||
/// let node = DemoNode {
|
||||
/// id: NodeId(0),
|
||||
/// parent: None,
|
||||
/// children: Vec::new(),
|
||||
/// path: vec!["alpha".to_owned()],
|
||||
/// title: "Alpha".to_owned(),
|
||||
/// description: String::new(),
|
||||
/// leaves: Vec::new(),
|
||||
/// endpoint_procedures: Vec::new(),
|
||||
/// };
|
||||
/// assert_eq!(node.display_path(), "/alpha");
|
||||
/// ```
|
||||
pub fn display_path(&self) -> String {
|
||||
format_path(&self.path)
|
||||
}
|
||||
@@ -89,6 +106,7 @@ pub struct DemoTree {
|
||||
impl DemoTree {
|
||||
/// Builds a flattened tree from a recursive specification.
|
||||
pub fn from_root(spec: &NodeSpec) -> Self {
|
||||
// Flatten once so later UI and simulation code can use stable ids.
|
||||
let mut nodes = Vec::new();
|
||||
let mut path_index = BTreeMap::new();
|
||||
let root = Self::push_node(spec, None, &[], &mut nodes, &mut path_index);
|
||||
@@ -106,6 +124,8 @@ impl DemoTree {
|
||||
nodes: &mut Vec<DemoNode>,
|
||||
path_index: &mut BTreeMap<Vec<String>, NodeId>,
|
||||
) -> NodeId {
|
||||
// Node ids are assigned in insertion order so parent/child relationships
|
||||
// can be expressed without any separate allocation table.
|
||||
let id = NodeId(nodes.len());
|
||||
let path = if spec.segment.is_empty() {
|
||||
base_path.to_vec()
|
||||
@@ -130,6 +150,7 @@ impl DemoTree {
|
||||
let child_ids = spec
|
||||
.children
|
||||
.iter()
|
||||
// Recurse after inserting the parent so children can record `parent: Some(id)`.
|
||||
.map(|child| Self::push_node(child, Some(id), &path, nodes, path_index))
|
||||
.collect::<Vec<_>>();
|
||||
nodes[id.0].children = child_ids;
|
||||
@@ -137,6 +158,9 @@ impl DemoTree {
|
||||
}
|
||||
|
||||
/// Returns the node with the given id.
|
||||
///
|
||||
/// Rationale: indexing by `NodeId` keeps later code short and avoids passing
|
||||
/// mutable references deep through the UI and simulator layers.
|
||||
pub fn node(&self, id: NodeId) -> &DemoNode {
|
||||
&self.nodes[id.0]
|
||||
}
|
||||
@@ -175,6 +199,11 @@ pub struct ScenarioDefinition {
|
||||
}
|
||||
|
||||
/// Formats a path the same way throughout the UI and tests.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// assert_eq!(treetest::model::format_path(&[]), "/");
|
||||
/// ```
|
||||
pub fn format_path(path: &[String]) -> String {
|
||||
if path.is_empty() {
|
||||
"/".to_owned()
|
||||
@@ -184,11 +213,21 @@ pub fn format_path(path: &[String]) -> String {
|
||||
}
|
||||
|
||||
/// Formats a leaf reference using the protocol document's descriptive syntax.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// assert_eq!(treetest::model::format_leaf_ref(&["a".into()], "echo"), "/a { leaf: echo }");
|
||||
/// ```
|
||||
pub fn format_leaf_ref(path: &[String], leaf_name: &str) -> String {
|
||||
format!("{} {{ leaf: {} }}", format_path(path), leaf_name)
|
||||
}
|
||||
|
||||
/// Formats a hook reference using the protocol document's descriptive syntax.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// assert_eq!(treetest::model::format_hook_ref(&[], 7), "/ { hook: 7 }");
|
||||
/// ```
|
||||
pub fn format_hook_ref(path: &[String], hook_id: u64) -> String {
|
||||
format!("{} {{ hook: {} }}", format_path(path), hook_id)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
//! Simulator stepping helpers.
|
||||
//!
|
||||
//! These helpers are intentionally tiny. They advance the mailboxes one frame at
|
||||
//! a time or until idle, which keeps the step-by-step demo behavior deterministic
|
||||
//! and easy to explain in the UI.
|
||||
|
||||
use crossbeam_channel::TryRecvError;
|
||||
use unshell::protocol::decode_frame;
|
||||
@@ -28,6 +32,8 @@ impl Simulation {
|
||||
return Ok(true);
|
||||
}
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
// A disconnected mailbox means the simulated topology is no
|
||||
// longer internally consistent, so surface it as a hard error.
|
||||
return Err(SimError::Protocol("mailbox disconnected".to_owned()));
|
||||
}
|
||||
Err(TryRecvError::Empty) => {}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
//! Read-only simulator queries used by tests and UI widgets.
|
||||
//!
|
||||
//! Keeping these accessors separate makes it clear which simulator APIs mutate
|
||||
//! protocol state and which ones merely summarize it for assertions or display.
|
||||
|
||||
use crate::model::Selection;
|
||||
|
||||
@@ -9,6 +12,8 @@ use super::super::types::{RecordedEvent, Simulation};
|
||||
impl Simulation {
|
||||
/// Returns the latest fault observed at the root, if any.
|
||||
pub fn latest_root_fault(&self) -> Option<&FaultMessage> {
|
||||
// Walk newest-to-oldest because the footer and tests only care about the
|
||||
// most recent root-visible result.
|
||||
self.recorded_events
|
||||
.iter()
|
||||
.rev()
|
||||
@@ -22,6 +27,7 @@ impl Simulation {
|
||||
|
||||
/// Returns the latest root data message as utf-8 for tests and status text.
|
||||
pub fn latest_root_data_text(&self) -> Option<String> {
|
||||
// Lossy decoding keeps the query usable even for non-text payloads.
|
||||
self.recorded_events
|
||||
.iter()
|
||||
.rev()
|
||||
|
||||
@@ -51,12 +51,15 @@ pub struct RootKnowledge {
|
||||
}
|
||||
|
||||
impl RootKnowledge {
|
||||
/// Builds the initial root knowledge from static scenario truth.
|
||||
pub(super) fn new(tree: &crate::model::DemoTree) -> Self {
|
||||
let mut knowledge = Self {
|
||||
nodes: BTreeMap::new(),
|
||||
};
|
||||
for node in &tree.nodes {
|
||||
if node.path.is_empty() || node.path.len() == 1 {
|
||||
// Realistic mode intentionally starts with root plus direct children,
|
||||
// not the full transitive tree.
|
||||
let direct_child = node.path.len() == 1;
|
||||
let mut learned = LearnedNode {
|
||||
path: node.path.clone(),
|
||||
@@ -69,6 +72,8 @@ impl RootKnowledge {
|
||||
};
|
||||
|
||||
if node.path.is_empty() {
|
||||
// The root always knows its own procedures and leaves because
|
||||
// those are locally configured, not discovered remotely.
|
||||
learned.endpoint_procedures = node
|
||||
.endpoint_procedures
|
||||
.iter()
|
||||
@@ -101,6 +106,7 @@ impl RootKnowledge {
|
||||
knowledge
|
||||
}
|
||||
|
||||
/// Returns an existing learned node or creates a new placeholder record.
|
||||
pub(super) fn ensure_node(&mut self, demo_node: &crate::model::DemoNode) -> &mut LearnedNode {
|
||||
let direct_child = demo_node.path.len() == 1;
|
||||
self.nodes
|
||||
@@ -121,6 +127,8 @@ impl RootKnowledge {
|
||||
demo_node: &crate::model::DemoNode,
|
||||
procedure: &EndpointProcedureSpec,
|
||||
) {
|
||||
// Procedures are keyed by full `procedure_id`, so repeated observation
|
||||
// simply enriches one existing record instead of duplicating it.
|
||||
let learned_node = self.ensure_node(demo_node);
|
||||
push_procedure(
|
||||
&mut learned_node.endpoint_procedures,
|
||||
@@ -134,6 +142,8 @@ impl RootKnowledge {
|
||||
demo_node: &crate::model::DemoNode,
|
||||
leaf_spec: &crate::model::LeafSpec,
|
||||
) {
|
||||
// Direct user targeting is enough for the root to remember a leaf exists,
|
||||
// even before remote introspection returns richer confirmation.
|
||||
let learned_node = self.ensure_node(demo_node);
|
||||
let leaf = ensure_leaf(
|
||||
&mut learned_node.leaves,
|
||||
@@ -154,6 +164,8 @@ impl RootKnowledge {
|
||||
demo_node: &crate::model::DemoNode,
|
||||
introspection: &EndpointIntrospection,
|
||||
) {
|
||||
// Endpoint introspection is the moment a node becomes explicitly known to
|
||||
// have been queried rather than merely inferred by path.
|
||||
let learned_node = self.ensure_node(demo_node);
|
||||
learned_node.endpoint_introspected = true;
|
||||
for summary in &introspection.leaves {
|
||||
@@ -195,18 +207,23 @@ impl RootKnowledge {
|
||||
}
|
||||
|
||||
pub(super) fn clear_deeper_than_one_hop(&mut self) {
|
||||
// This powers the realistic-mode reset, which forgets transitive state
|
||||
// and keeps only root-local plus direct-child knowledge.
|
||||
self.nodes.retain(|path, _| path.len() <= 1);
|
||||
}
|
||||
|
||||
/// Returns one learned node by absolute path.
|
||||
pub fn node(&self, path: &[String]) -> Option<&LearnedNode> {
|
||||
self.nodes.get(path)
|
||||
}
|
||||
|
||||
/// Returns every path currently known to the root host.
|
||||
pub fn known_paths(&self) -> Vec<Vec<String>> {
|
||||
self.nodes.keys().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns one learned leaf entry, creating it if necessary.
|
||||
fn ensure_leaf<'a>(
|
||||
leaves: &'a mut Vec<LearnedLeaf>,
|
||||
leaf_name: String,
|
||||
@@ -227,6 +244,7 @@ fn ensure_leaf<'a>(
|
||||
leaves.last_mut().expect("just pushed")
|
||||
}
|
||||
|
||||
/// Inserts or enriches one learned procedure entry.
|
||||
fn push_procedure(
|
||||
procedures: &mut Vec<LearnedProcedure>,
|
||||
procedure_id: String,
|
||||
@@ -236,6 +254,8 @@ fn push_procedure(
|
||||
.iter_mut()
|
||||
.find(|procedure| procedure.procedure_id == procedure_id)
|
||||
{
|
||||
// Preserve the first available description, then upgrade missing details
|
||||
// later if richer information is learned from introspection or config.
|
||||
if existing.description.is_none() {
|
||||
existing.description = description;
|
||||
}
|
||||
|
||||
@@ -18,43 +18,58 @@ use super::knowledge::{InspectorMode, RootKnowledge};
|
||||
/// User-facing outcome of a root-originated action.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ActionResult {
|
||||
/// Human-readable summary shown in the footer after one action completes.
|
||||
pub label: String,
|
||||
/// Hook id allocated for the action, if the action opened or used one.
|
||||
pub hook_id: Option<u64>,
|
||||
}
|
||||
|
||||
/// Snapshot of a hook interaction observed by the demo.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HookSnapshot {
|
||||
/// Hook identifier scoped to the root host.
|
||||
pub hook_id: u64,
|
||||
/// Host path for the hook, usually the root in this demo.
|
||||
pub host_path: Vec<String>,
|
||||
/// Peer endpoint currently associated with the hook.
|
||||
pub peer_path: Vec<String>,
|
||||
/// Procedure contract that established the hook.
|
||||
pub procedure_id: String,
|
||||
/// Optional target leaf when the originating call addressed one leaf.
|
||||
pub target_leaf: Option<String>,
|
||||
/// Whether the hook has finished normally or faulted.
|
||||
pub closed: bool,
|
||||
/// Most recent human-readable payload summary for the UI.
|
||||
pub last_message: String,
|
||||
}
|
||||
|
||||
/// Trace entry shown in the UI and asserted in tests.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TraceEvent {
|
||||
/// Monotonic event number assigned by the simulator.
|
||||
pub tick: u64,
|
||||
/// Display path of the node that emitted the trace line.
|
||||
pub node_path: String,
|
||||
/// Human-readable event summary.
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
/// Summary of one local protocol event.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RecordedEvent {
|
||||
/// Local hook data event.
|
||||
Data {
|
||||
node_path: String,
|
||||
header: PacketHeader,
|
||||
message: DataMessage,
|
||||
},
|
||||
/// Local protocol fault event.
|
||||
Fault {
|
||||
node_path: String,
|
||||
header: PacketHeader,
|
||||
message: FaultMessage,
|
||||
},
|
||||
/// Local call-delivery event.
|
||||
Call {
|
||||
node_path: String,
|
||||
header: PacketHeader,
|
||||
@@ -86,15 +101,22 @@ pub enum SimError {
|
||||
/// Fully built simulation for one scenario.
|
||||
#[derive(Debug)]
|
||||
pub struct Simulation {
|
||||
/// Active scenario definition the simulation was built from.
|
||||
pub scenario: ScenarioDefinition,
|
||||
/// Flattened tree model used by both simulator and UI.
|
||||
pub tree: DemoTree,
|
||||
pub(super) nodes: Vec<SimNode>,
|
||||
pub(super) root_id: NodeId,
|
||||
pub(super) next_tick: u64,
|
||||
/// Rolling trace buffer shown in the UI.
|
||||
pub trace: VecDeque<TraceEvent>,
|
||||
/// Exact local events emitted by the protocol runtime.
|
||||
pub recorded_events: Vec<RecordedEvent>,
|
||||
/// Live and historical hook snapshots for display.
|
||||
pub hooks: BTreeMap<u64, HookSnapshot>,
|
||||
/// Which knowledge view the inspector currently renders.
|
||||
pub inspector_mode: InspectorMode,
|
||||
/// Root-host knowledge accumulated from direct config and observed traffic.
|
||||
pub root_knowledge: RootKnowledge,
|
||||
pub(super) chat_sessions: BTreeMap<u64, ChatSession>,
|
||||
}
|
||||
@@ -102,25 +124,36 @@ pub struct Simulation {
|
||||
/// Per-node runtime wiring used by the simulator.
|
||||
#[derive(Debug)]
|
||||
pub(super) struct SimNode {
|
||||
/// Optional parent node in the explicit tree.
|
||||
pub(super) parent: Option<NodeId>,
|
||||
/// Child node ids in display order.
|
||||
pub(super) children: Vec<NodeId>,
|
||||
/// Backing protocol runtime for this endpoint.
|
||||
pub(super) endpoint: ProtocolEndpoint,
|
||||
/// Mailbox sender used by other nodes when forwarding frames here.
|
||||
pub(super) tx: Sender<Envelope>,
|
||||
/// Mailbox receiver consumed by `Simulation::step`.
|
||||
pub(super) rx: Receiver<Envelope>,
|
||||
}
|
||||
|
||||
/// Internal packet delivery envelope.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct Envelope {
|
||||
/// Ingress side seen by the receiving protocol runtime.
|
||||
pub(super) ingress: Ingress,
|
||||
/// Fully framed packet bytes.
|
||||
pub(super) frame: FrameBytes,
|
||||
}
|
||||
|
||||
/// Application-level chat state layered on top of hook traffic.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct ChatSession {
|
||||
/// Node hosting the application-level chat behavior.
|
||||
pub(super) node_id: NodeId,
|
||||
/// Hook id that the chat session is bound to.
|
||||
pub(super) hook_id: u64,
|
||||
/// Path of the hook host to which replies must be routed.
|
||||
pub(super) host_path: Vec<String>,
|
||||
/// Procedure contract associated with the chat stream.
|
||||
pub(super) procedure_id: String,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user