Improve Rust code clarity across the workspace

Document public APIs and non-obvious control flow so the protocol, simulator, and macro crates are easier to follow. Tighten a few helper paths and feature gates while preserving behavior and keeping the workspace warning-free.
This commit is contained in:
Michael Mikovsky
2026-04-25 11:11:19 -06:00
parent f49af7fa22
commit ba3f28a78c
26 changed files with 571 additions and 402 deletions
+123 -120
View File
@@ -8,6 +8,19 @@
use super::{App, AppError, NodeId, Selection};
impl App {
/// Drains queued simulator work, refreshes the visible selection list, and
/// reports the action result in the footer.
fn finish_action(
&mut self,
preferred_node: Option<NodeId>,
label: impl Into<String>,
) -> Result<(), AppError> {
let steps = self.simulation.drain()?;
self.refresh_selections(preferred_node);
self.status = format!("{} ({steps} steps)", label.into());
Ok(())
}
/// Performs protocol introspection for the current selection.
///
/// Rationale: node and leaf introspection share one key because the protocol
@@ -18,18 +31,14 @@ impl App {
// Route the blank procedure to endpoint-wide introspection.
let result = self.simulation.call_endpoint_introspection(node_id)?;
// Drain immediately so the inspector reflects the learned state.
let steps = self.simulation.drain()?;
self.refresh_selections(Some(node_id));
self.status = format!("{} ({steps} steps)", result.label);
self.finish_action(Some(node_id), result.label)?;
}
Selection::Leaf { node_id, leaf_name } => {
// Route the blank procedure to one specific leaf.
let result = self
.simulation
.call_leaf_introspection(node_id, &leaf_name)?;
let steps = self.simulation.drain()?;
self.refresh_selections(Some(node_id));
self.status = format!("{} ({steps} steps)", result.label);
self.finish_action(Some(node_id), result.label)?;
}
}
Ok(())
@@ -40,102 +49,99 @@ impl App {
/// Rationale: the payload is fixed so the demo highlights packet flow rather
/// than turning the TUI into a line editor.
pub(super) fn perform_echo(&mut self) -> Result<(), AppError> {
if let Selection::Leaf { node_id, leaf_name } = self.selected().clone() {
let result =
self.simulation
.call_echo_leaf(node_id, &leaf_name, "demo echo from root")?;
let steps = self.simulation.drain()?;
self.refresh_selections(Some(node_id));
self.status = format!("{} ({steps} steps)", result.label);
} else {
let Selection::Leaf { node_id, leaf_name } = self.selected().clone() else {
self.status = "Select a leaf first, then press e.".to_owned();
}
return Ok(());
};
let result = self
.simulation
.call_echo_leaf(node_id, &leaf_name, "demo echo from root")?;
self.finish_action(Some(node_id), result.label)?;
Ok(())
}
/// Calls the first endpoint-level procedure on the selected node.
pub(super) fn perform_ping(&mut self) -> Result<(), AppError> {
if let Selection::Node(node_id) = self.selected().clone() {
if let Some(procedure_id) = self
.simulation
.node(node_id)
.endpoint_procedures
.first()
.map(|procedure| procedure.procedure_id.clone())
{
let result = self.simulation.call_endpoint_procedure(
node_id,
&procedure_id,
b"ping".to_vec(),
)?;
let steps = self.simulation.drain()?;
self.refresh_selections(Some(node_id));
self.status = format!("{} ({steps} steps)", result.label);
} else {
self.status = "Selected node has no endpoint procedures.".to_owned();
}
} else {
let Selection::Node(node_id) = self.selected().clone() else {
self.status = "Select a node first, then press p.".to_owned();
}
return Ok(());
};
let Some(procedure_id) = self
.simulation
.node(node_id)
.endpoint_procedures
.first()
.map(|procedure| procedure.procedure_id.clone())
else {
self.status = "Selected node has no endpoint procedures.".to_owned();
return Ok(());
};
let result =
self.simulation
.call_endpoint_procedure(node_id, &procedure_id, b"ping".to_vec())?;
self.finish_action(Some(node_id), result.label)?;
Ok(())
}
/// Calls the chunked-response procedure on the selected node.
pub(super) fn perform_chunked(&mut self) -> Result<(), AppError> {
if let Selection::Node(node_id) = self.selected().clone() {
if let Some(procedure_id) = self
.simulation
.node(node_id)
.endpoint_procedures
.iter()
.find(|procedure| {
procedure.description.contains("chunk")
|| procedure.procedure_id.contains("chunked")
})
.map(|procedure| procedure.procedure_id.clone())
{
let result = self.simulation.call_endpoint_procedure(
node_id,
&procedure_id,
b"chunk please".to_vec(),
)?;
let steps = self.simulation.drain()?;
self.refresh_selections(Some(node_id));
self.status = format!("{} ({steps} steps)", result.label);
} else {
self.status = "Selected node has no chunked procedure.".to_owned();
}
} else {
let Selection::Node(node_id) = self.selected().clone() else {
self.status = "Select a node first, then press c.".to_owned();
}
return Ok(());
};
let Some(procedure_id) = self
.simulation
.node(node_id)
.endpoint_procedures
.iter()
.find(|procedure| {
procedure.description.contains("chunk")
|| procedure.procedure_id.contains("chunked")
})
.map(|procedure| procedure.procedure_id.clone())
else {
self.status = "Selected node has no chunked procedure.".to_owned();
return Ok(());
};
let result = self.simulation.call_endpoint_procedure(
node_id,
&procedure_id,
b"chunk please".to_vec(),
)?;
self.finish_action(Some(node_id), result.label)?;
Ok(())
}
/// Opens a long-lived chat hook on the selected node.
pub(super) fn perform_chat_call(&mut self) -> Result<(), AppError> {
if let Selection::Node(node_id) = self.selected().clone() {
if let Some(procedure_id) = self
.simulation
.node(node_id)
.endpoint_procedures
.iter()
.find(|procedure| procedure.procedure_id.contains("chat"))
.map(|procedure| procedure.procedure_id.clone())
{
let result = self.simulation.call_endpoint_procedure(
node_id,
&procedure_id,
b"open chat".to_vec(),
)?;
let steps = self.simulation.drain()?;
self.refresh_selections(Some(node_id));
self.status = format!("{} ({steps} steps)", result.label);
} else {
self.status = "Selected node has no chat procedure.".to_owned();
}
} else {
let Selection::Node(node_id) = self.selected().clone() else {
self.status = "Select a node first, then press h.".to_owned();
}
return Ok(());
};
let Some(procedure_id) = self
.simulation
.node(node_id)
.endpoint_procedures
.iter()
.find(|procedure| procedure.procedure_id.contains("chat"))
.map(|procedure| procedure.procedure_id.clone())
else {
self.status = "Selected node has no chat procedure.".to_owned();
return Ok(());
};
let result = self.simulation.call_endpoint_procedure(
node_id,
&procedure_id,
b"open chat".to_vec(),
)?;
self.finish_action(Some(node_id), result.label)?;
Ok(())
}
@@ -144,57 +150,54 @@ impl App {
/// Rationale: using the latest hook keeps the demo simple while still
/// exposing bidirectional hook behavior.
pub(super) fn perform_chat_data(&mut self) -> Result<(), AppError> {
if let Some(hook_id) = self.simulation.hook_ids().last().copied() {
let result =
self.simulation
.send_root_hook_data(hook_id, "hello from the root", false)?;
let steps = self.simulation.drain()?;
self.refresh_selections(None);
self.status = format!("{} ({steps} steps)", result.label);
} else {
let Some(hook_id) = self.simulation.hook_ids().last().copied() else {
self.status = "No known hook yet. Press h to open chat first.".to_owned();
}
return Ok(());
};
let result = self
.simulation
.send_root_hook_data(hook_id, "hello from the root", false)?;
self.finish_action(None, result.label)?;
Ok(())
}
/// Ends the newest known chat hook from the root side.
pub(super) fn perform_chat_bye(&mut self) -> Result<(), AppError> {
if let Some(hook_id) = self.simulation.hook_ids().last().copied() {
let result = self.simulation.send_root_hook_data(hook_id, "bye", true)?;
let steps = self.simulation.drain()?;
self.refresh_selections(None);
self.status = format!("{} ({steps} steps)", result.label);
} else {
let Some(hook_id) = self.simulation.hook_ids().last().copied() else {
self.status = "No known hook yet. Press h to open chat first.".to_owned();
}
return Ok(());
};
let result = self.simulation.send_root_hook_data(hook_id, "bye", true)?;
self.finish_action(None, result.label)?;
Ok(())
}
/// Injects intentionally invalid hook data to exercise fault handling.
pub(super) fn perform_invalid_fault_demo(&mut self) -> Result<(), AppError> {
if let Some(hook_id) = self.simulation.hook_ids().last().copied() {
// The root is always node zero in every built-in scenario.
let root_id = NodeId(0);
if self.simulation.tree.nodes.len() > 1 {
// The first child is enough to spoof a wrong peer path.
let attacker = NodeId(1);
let result = self.simulation.inject_invalid_peer_data(
attacker,
root_id,
hook_id,
"demo.endpoint.v1.chat.session",
"spoofed data",
)?;
let steps = self.simulation.drain()?;
self.refresh_selections(None);
self.status = format!("{} ({steps} steps)", result.label);
} else {
self.status =
"This scenario has no second node for invalid-peer traffic.".to_owned();
}
} else {
let Some(hook_id) = self.simulation.hook_ids().last().copied() else {
self.status = "Open a hook first before injecting invalid traffic.".to_owned();
return Ok(());
};
if self.simulation.tree.nodes.len() <= 1 {
self.status = "This scenario has no second node for invalid-peer traffic.".to_owned();
return Ok(());
}
// The root is always node zero in every built-in scenario.
let root_id = NodeId(0);
// The first child is enough to spoof a wrong peer path.
let attacker = NodeId(1);
let result = self.simulation.inject_invalid_peer_data(
attacker,
root_id,
hook_id,
"demo.endpoint.v1.chat.session",
"spoofed data",
)?;
self.finish_action(None, result.label)?;
Ok(())
}
}
+4 -2
View File
@@ -11,7 +11,7 @@ mod ui;
use ratatui::DefaultTerminal;
use crate::{
model::{NodeId, Selection},
model::{NodeId, ScenarioDefinition, Selection},
scenarios::built_in_scenarios,
sim::Simulation,
};
@@ -19,8 +19,10 @@ use crate::{
/// Errors returned by the TUI application.
#[derive(Debug, thiserror::Error)]
pub enum AppError {
/// Terminal setup, teardown, or input/output failed.
#[error(transparent)]
Io(#[from] std::io::Error),
/// The simulator rejected an operation or could not advance.
#[error(transparent)]
Sim(#[from] crate::sim::SimError),
}
@@ -32,7 +34,7 @@ pub fn run() -> Result<(), AppError> {
#[derive(Debug)]
struct App {
scenarios: Vec<crate::model::ScenarioDefinition>,
scenarios: Vec<ScenarioDefinition>,
scenario_index: usize,
simulation: Simulation,
selection_index: usize,
+15 -7
View File
@@ -68,11 +68,19 @@ impl App {
terminal.draw(|frame| self.render(frame))?;
// Poll with a timeout so redraws stay responsive without busy-spinning.
if event::poll(Duration::from_millis(100))?
&& let Event::Key(key) = event::read()?
&& key.kind == KeyEventKind::Press
&& !self.handle_key(key.code)?
{
if !event::poll(Duration::from_millis(100))? {
continue;
}
let Event::Key(key) = event::read()? else {
continue;
};
if key.kind != KeyEventKind::Press {
continue;
}
if !self.handle_key(key.code)? {
break;
}
}
@@ -176,14 +184,14 @@ impl App {
/// so selection repair needs to happen in one dedicated place.
pub(super) fn refresh_selections(&mut self, preferred_node: Option<NodeId>) {
// Prefer an explicit node if the caller knows what should stay selected.
let current = preferred_node.unwrap_or_else(|| self.selected().node_id());
let selected_node_id = preferred_node.unwrap_or_else(|| self.selected().node_id());
self.selections = ui::build_selections(&self.simulation);
// Fall back to the first row when the previous node disappeared.
self.selection_index = self
.selections
.iter()
.position(|selection| selection.node_id() == current)
.position(|selection| selection.node_id() == selected_node_id)
.unwrap_or(0);
}
}
+4 -4
View File
@@ -20,7 +20,7 @@ impl App {
pub(crate) fn render(&self, frame: &mut Frame<'_>) {
// Split the screen into a small header, a large working area, and a
// persistent status/footer region.
let chunks = Layout::default()
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
@@ -29,9 +29,9 @@ impl App {
])
.split(frame.area());
self.render_header(frame, chunks[0]);
self.render_body(frame, chunks[1]);
self.render_footer(frame, chunks[2]);
self.render_header(frame, rows[0]);
self.render_body(frame, rows[1]);
self.render_footer(frame, rows[2]);
}
/// Renders the scenario header bar.
+2 -2
View File
@@ -60,7 +60,7 @@ impl App {
/// while ground-truth mode intentionally exposes the entire scenario tree.
pub(crate) fn build_selections(simulation: &Simulation) -> Vec<Selection> {
let mut selections = Vec::new();
let node_ids: Vec<_> = match simulation.inspector_mode {
let visible_node_ids: Vec<_> = match simulation.inspector_mode {
InspectorMode::GroundTruth => simulation.tree.nodes.iter().map(|node| node.id).collect(),
InspectorMode::Realistic => simulation
.root_knowledge
@@ -70,7 +70,7 @@ pub(crate) fn build_selections(simulation: &Simulation) -> Vec<Selection> {
.collect(),
};
for node_id in node_ids {
for node_id in visible_node_ids {
let node = simulation.node(node_id);
selections.push(Selection::Node(node.id));
match simulation.inspector_mode {
+66 -46
View File
@@ -13,40 +13,56 @@ use crate::model::EndpointProcedureSpec;
/// Root inspector mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InspectorMode {
/// Render the full scenario definition, including information the root has
/// not yet learned through traffic or introspection.
GroundTruth,
/// Render only the subset of state the root host could plausibly know.
Realistic,
}
/// Learned procedure metadata stored by the root host.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LearnedProcedure {
/// Stable protocol identifier for the learned procedure.
pub procedure_id: String,
/// Optional human-readable description learned from config or introspection.
pub description: Option<String>,
}
/// Learned leaf metadata stored by the root host.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LearnedLeaf {
/// Leaf name relative to the endpoint path.
pub leaf_name: String,
/// Optional human-readable description for the leaf.
pub description: Option<String>,
/// Procedures currently known on the leaf.
pub procedures: Vec<LearnedProcedure>,
}
/// Learned endpoint metadata stored by the root host.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LearnedNode {
/// Absolute node path from the root.
pub path: Vec<String>,
/// Optional display title shown in the inspector.
pub title: Option<String>,
/// Optional endpoint description shown in the inspector.
pub description: Option<String>,
/// Whether the node is a direct child of the root.
pub direct_child: bool,
/// Endpoint-level procedures known on the node itself.
pub endpoint_procedures: Vec<LearnedProcedure>,
/// Leaf metadata currently known for the node.
pub leaves: Vec<LearnedLeaf>,
/// Whether endpoint introspection definitely ran against this node.
pub endpoint_introspected: bool,
}
/// Root-host knowledge accumulated from local configuration and observed traffic.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RootKnowledge {
/// Learned nodes keyed by their absolute path.
pub nodes: BTreeMap<Vec<String>, LearnedNode>,
}
@@ -58,49 +74,9 @@ impl RootKnowledge {
};
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(),
title: Some(node.title.clone()),
description: Some(node.description.clone()),
direct_child,
endpoint_procedures: Vec::new(),
leaves: Vec::new(),
endpoint_introspected: node.path.is_empty(),
};
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()
.map(|procedure| LearnedProcedure {
procedure_id: procedure.procedure_id.clone(),
description: Some(procedure.description.clone()),
})
.collect();
learned.leaves = node
.leaves
.iter()
.map(|leaf| LearnedLeaf {
leaf_name: leaf.name.clone(),
description: Some(leaf.description.clone()),
procedures: leaf
.procedures
.iter()
.map(|procedure_id| LearnedProcedure {
procedure_id: procedure_id.clone(),
description: Some(leaf.description.clone()),
})
.collect(),
})
.collect();
}
knowledge.nodes.insert(node.path.clone(), learned);
knowledge
.nodes
.insert(node.path.clone(), initial_learned_node(node));
}
}
knowledge
@@ -223,12 +199,56 @@ impl RootKnowledge {
}
}
/// Builds the root's initial record for one statically known node.
fn initial_learned_node(node: &crate::model::DemoNode) -> LearnedNode {
let mut learned = LearnedNode {
path: node.path.clone(),
title: Some(node.title.clone()),
description: Some(node.description.clone()),
direct_child: node.path.len() == 1,
endpoint_procedures: Vec::new(),
leaves: Vec::new(),
endpoint_introspected: node.path.is_empty(),
};
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()
.map(|procedure| LearnedProcedure {
procedure_id: procedure.procedure_id.clone(),
description: Some(procedure.description.clone()),
})
.collect();
learned.leaves = node
.leaves
.iter()
.map(|leaf| LearnedLeaf {
leaf_name: leaf.name.clone(),
description: Some(leaf.description.clone()),
procedures: leaf
.procedures
.iter()
.map(|procedure_id| LearnedProcedure {
procedure_id: procedure_id.clone(),
description: Some(leaf.description.clone()),
})
.collect(),
})
.collect();
}
learned
}
/// Returns one learned leaf entry, creating it if necessary.
fn ensure_leaf<'a>(
leaves: &'a mut Vec<LearnedLeaf>,
fn ensure_leaf(
leaves: &mut Vec<LearnedLeaf>,
leaf_name: String,
description: Option<String>,
) -> &'a mut LearnedLeaf {
) -> &mut LearnedLeaf {
if let Some(index) = leaves.iter().position(|leaf| leaf.leaf_name == leaf_name) {
if leaves[index].description.is_none() {
leaves[index].description = description;
+41 -30
View File
@@ -32,16 +32,14 @@ impl Simulation {
match procedure.kind {
EndpointProcedureKind::Ping => {
let reply = format!("pong from {}", self.node(node_id).display_path());
let frame = self.nodes[node_id.0]
.endpoint
.make_data(
hook.return_path.clone(),
hook.hook_id,
procedure.procedure_id.clone(),
reply.clone().into_bytes(),
true,
)
.map_err(|error| SimError::Protocol(error.to_string()))?;
let frame = self.make_endpoint_data_frame(
node_id,
hook.return_path.clone(),
hook.hook_id,
procedure.procedure_id.clone(),
reply.clone().into_bytes(),
true,
)?;
self.record_trace(node_id, format!("endpoint sent ping reply: {reply}"));
self.process_local_frame(node_id, frame)?;
}
@@ -54,16 +52,14 @@ impl Simulation {
.iter()
.enumerate()
{
let frame = self.nodes[node_id.0]
.endpoint
.make_data(
hook.return_path.clone(),
hook.hook_id,
procedure.procedure_id.clone(),
text.as_bytes().to_vec(),
index == 2,
)
.map_err(|error| SimError::Protocol(error.to_string()))?;
let frame = self.make_endpoint_data_frame(
node_id,
hook.return_path.clone(),
hook.hook_id,
procedure.procedure_id.clone(),
text.as_bytes().to_vec(),
index == 2,
)?;
self.record_trace(node_id, format!("endpoint sent chunk {}", index + 1));
self.process_local_frame(node_id, frame)?;
}
@@ -80,16 +76,14 @@ impl Simulation {
procedure_id: procedure.procedure_id.clone(),
},
);
let frame = self.nodes[node_id.0]
.endpoint
.make_data(
hook.return_path.clone(),
hook.hook_id,
procedure.procedure_id.clone(),
b"chat ready".to_vec(),
false,
)
.map_err(|error| SimError::Protocol(error.to_string()))?;
let frame = self.make_endpoint_data_frame(
node_id,
hook.return_path.clone(),
hook.hook_id,
procedure.procedure_id.clone(),
b"chat ready".to_vec(),
false,
)?;
self.record_trace(node_id, "chat handler opened session".to_owned());
self.process_local_frame(node_id, frame)?;
}
@@ -97,6 +91,23 @@ impl Simulation {
Ok(())
}
/// Builds one endpoint-originated data frame after application logic decides
/// what to send back on an already-validated hook.
fn make_endpoint_data_frame(
&mut self,
node_id: NodeId,
return_path: Vec<String>,
hook_id: u64,
procedure_id: String,
data: Vec<u8>,
end_hook: bool,
) -> Result<unshell::protocol::FrameBytes, SimError> {
self.nodes[node_id.0]
.endpoint
.make_data(return_path, hook_id, procedure_id, data, end_hook)
.map_err(|error| SimError::Protocol(error.to_string()))
}
/// Resolves one endpoint procedure from the ground-truth node metadata.
pub(super) fn lookup_endpoint_procedure(
&self,
+53 -42
View File
@@ -20,51 +20,24 @@ impl Simulation {
match event {
LocalEvent::Data { header, message } => {
let text = String::from_utf8_lossy(&message.data).to_string();
self.record_trace(
node_id,
format!(
"local Data on {}: {text}",
format_hook_ref(
self.node(node_id).path.as_slice(),
header.hook_id.unwrap_or(0)
)
),
let hook_ref = format_hook_ref(
self.node(node_id).path.as_slice(),
header.hook_id.unwrap_or(0),
);
self.record_trace(node_id, format!("local Data on {hook_ref}: {text}"));
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 {
text.clone()
};
if message.end_hook {
snapshot.closed = true;
}
}
self.update_hook_snapshot(hook_id, &text, message.data.len(), message.end_hook);
if node_id == self.root_id {
self.learn_from_root_data(hook_id, &message);
}
}
if let Some(session) = self
.chat_sessions
.get(&header.hook_id.unwrap_or(0))
.cloned()
.filter(|session| session.node_id == node_id)
{
if let Some(session) = self.chat_session_for_event(node_id, header.hook_id) {
// Rationale: chat responses are implemented here instead of in the
// core endpoint so the protocol crate stays generic. The simulator
// acts as the application layer sitting above validated hook traffic.
let reply = if text.eq_ignore_ascii_case("bye") {
Some(("chat session closed".to_owned(), true))
} else if !text.is_empty() {
Some((format!("chat ack: {}", text.to_uppercase()), false))
} else {
None
};
let reply = chat_reply_for_text(&text);
if let Some((reply, end_hook)) = reply {
let frame = self.nodes[session.node_id.0]
@@ -92,16 +65,13 @@ impl Simulation {
});
}
LocalEvent::Fault { header, message } => {
let hook_ref = format_hook_ref(
self.node(node_id).path.as_slice(),
header.hook_id.unwrap_or(0),
);
self.record_trace(
node_id,
format!(
"local Fault on {}: 0x{:02X}",
format_hook_ref(
self.node(node_id).path.as_slice(),
header.hook_id.unwrap_or(0)
),
message.fault.0
),
format!("local Fault on {hook_ref}: 0x{:02X}", message.fault.0),
);
if let Some(hook_id) = header.hook_id {
if let Some(snapshot) = self.hooks.get_mut(&hook_id) {
@@ -140,4 +110,45 @@ impl Simulation {
}
Ok(())
}
fn update_hook_snapshot(
&mut self,
hook_id: u64,
text: &str,
payload_len: usize,
end_hook: bool,
) {
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 ({payload_len} bytes)")
} else {
text.to_owned()
};
if end_hook {
snapshot.closed = true;
}
}
}
fn chat_session_for_event(
&self,
node_id: NodeId,
hook_id: Option<u64>,
) -> Option<super::super::super::types::ChatSession> {
self.chat_sessions
.get(&hook_id.unwrap_or(0))
.cloned()
.filter(|session| session.node_id == node_id)
}
}
fn chat_reply_for_text(text: &str) -> Option<(String, bool)> {
if text.eq_ignore_ascii_case("bye") {
return Some(("chat session closed".to_owned(), true));
}
if text.is_empty() {
return None;
}
Some((format!("chat ack: {}", text.to_uppercase()), false))
}
+35 -17
View File
@@ -1,4 +1,8 @@
//! Root-side knowledge learning from returned data.
//!
//! The simulator learns only from data that arrives back at the root on a known
//! hook. This keeps the realistic inspector aligned with what the UI-triggered
//! action actually observed.
use unshell::protocol::{
DataMessage, EndpointIntrospection, LeafIntrospection, deserialize_archived_bytes,
@@ -17,23 +21,7 @@ impl Simulation {
let demo_node = self.node(node_id).clone();
if snapshot.procedure_id.is_empty() {
if snapshot.target_leaf.is_some() {
if let Ok(introspection) = deserialize_archived_bytes::<
unshell::protocol::introspection::ArchivedLeafIntrospection,
LeafIntrospection,
>(&message.data)
{
self.root_knowledge
.remember_leaf_introspection(&demo_node, &introspection);
}
} else if let Ok(introspection) = deserialize_archived_bytes::<
unshell::protocol::introspection::ArchivedEndpointIntrospection,
EndpointIntrospection,
>(&message.data)
{
self.root_knowledge
.remember_endpoint_introspection(&demo_node, &introspection);
}
self.learn_from_root_introspection(&snapshot, &demo_node, message);
return;
}
@@ -54,3 +42,33 @@ impl Simulation {
}
}
}
impl Simulation {
fn learn_from_root_introspection(
&mut self,
snapshot: &super::super::types::HookSnapshot,
demo_node: &crate::model::DemoNode,
message: &DataMessage,
) {
if snapshot.target_leaf.is_some() {
if let Ok(introspection) = deserialize_archived_bytes::<
unshell::protocol::introspection::ArchivedLeafIntrospection,
LeafIntrospection,
>(&message.data)
{
self.root_knowledge
.remember_leaf_introspection(demo_node, &introspection);
}
return;
}
if let Ok(introspection) = deserialize_archived_bytes::<
unshell::protocol::introspection::ArchivedEndpointIntrospection,
EndpointIntrospection,
>(&message.data)
{
self.root_knowledge
.remember_endpoint_introspection(demo_node, &introspection);
}
}
}