1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
//! Demo mode — a **hands-free guided tour** that ROBOT-DRIVES the live viz
//! through every tab via the EXISTING control channel ([`super::control`]),
//! with pauses between beats so the motion (tab-switch repaint, graph
//! pan/zoom, 3D call-graph + funnel spin, palette re-skin/glow) actually plays.
//!
//! It is the *drive-half* of the robot-UI-tester loop wired to a script: each
//! beat is one [`VizCommand`](super::control::VizCommand) the demo writes to the
//! control channel (`$NORNIR_VIZ_CMD`) on a timer; the running viz polls + applies
//! it next frame ([`super::app::UrdrThreadsApp::poll_control_channel`]) exactly as
//! if a human (or the `viz.click` MCP tool) had clicked. The result shows up in
//! the next `$NORNIR_VIZ_STATE` dump — so the tour is robot-VERIFIED, not just
//! visual (see `tests/viz_demo.rs`).
//!
//! **FC-7 determinism:** time is *injected*. The driver never reads a wall clock
//! itself — [`DemoDriver::due`] takes the elapsed-ms as an argument. The live
//! update loop passes a real `Instant::elapsed()`; the headless test passes
//! synthetic values, so the scripted schedule is fully deterministic.
use super::control::{VizCommand, VizField};
/// One scripted beat of the tour: once `at_ms` of demo time has elapsed, the
/// demo emits `cmd` (a real control-channel command) and logs `label`.
#[derive(Clone, Debug)]
pub struct DemoStep {
/// Demo-relative time (ms since the tour started) at which this beat fires.
pub at_ms: u64,
/// Human-readable caption for the beat (shown in the action trail).
pub label: String,
/// The control-channel command this beat issues — a real `viz.click`.
pub cmd: VizCommand,
}
/// The scripted-tour driver. Holds the beats + a cursor into them; emits each
/// due beat exactly once. Pure (no I/O, no clock) so it is trivially testable —
/// the app wraps it with a real `Instant`, a test feeds it synthetic elapsed-ms.
#[derive(Clone, Debug)]
pub struct DemoDriver {
steps: Vec<DemoStep>,
next: usize,
}
impl DemoDriver {
/// Wrap an explicit script.
pub fn new(steps: Vec<DemoStep>) -> Self {
Self { steps, next: 0 }
}
/// The default showcase tour: walk every tab the demo brief names —
/// Arch/Metro, the dep + call graphs, Knowledge, Funnel (with a live
/// `run_demo` click so its 3D DAG fills + spins), the Test matrix, the
/// Warehouse Deck, Release, Leaderboard and Security — re-skinning the whole
/// viz twice so the palette glow plays, and returning home. `beat_ms` is the
/// pause between beats (so motion settles before the next click).
pub fn tour(beat_ms: u64) -> Self {
let mut b = TourBuilder::new(beat_ms);
b.tab("Nornir", "home: server + workspace lifecycle");
b.tab("Architecture", "🏛 Architecture wiring board");
b.tab("Metro", "🚇 Metro coverage transit lines");
b.tab("DepGraph", "🔗 Dep graph (pan/zoom)");
b.tab("CallGraph", "🕸 Call graph 3D (auto-spin)");
b.tab("Knowledge", "📚 Knowledge index");
b.palette("cyberpunk-neon", "🎨 re-skin → cyberpunk-neon (glow)");
b.tab("Funnel", "🗂 Funnel DAG");
b.set_field("funnel.demo_size", "8", "Funnel: size the demo DAG");
b.click("funnel.run_demo", "Funnel: ▶ run the 3D demo (fills + spins)");
b.tab("Test", "🧪 Test matrix");
b.tab("WarehouseDeck", "🏢 Warehouse Deck (wall of panes)");
b.tab("Release", "🚀 Release pipeline");
b.tab("Leaderboard", "🏆 Leaderboard");
b.palette("nordic-aurora", "🎨 re-skin → nordic-aurora");
b.tab("Security", "🛡 Security");
b.tab("Nornir", "↩ back home");
Self::new(b.steps)
}
/// Total beats in the script.
pub fn len(&self) -> usize {
self.steps.len()
}
/// `true` when the script has no beats.
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
/// `true` once every beat in this pass has been emitted. A live demo window
/// loops by [`rewind`](Self::rewind)ing on a rebased clock; the deterministic
/// test uses this to stop after one pass.
pub fn is_done(&self) -> bool {
self.next >= self.steps.len()
}
/// How long (ms) the full one-pass script runs — the `at_ms` of the last beat.
pub fn duration_ms(&self) -> u64 {
self.steps.last().map(|s| s.at_ms).unwrap_or(0)
}
/// The distinct tab names the tour visits, in first-seen order — for the
/// headless coverage assertion ("drives ≥N distinct views").
pub fn tabs(&self) -> Vec<String> {
let mut seen: Vec<String> = Vec::new();
for s in &self.steps {
if let Some(t) = &s.cmd.tab {
if !seen.iter().any(|x| x == t) {
seen.push(t.clone());
}
}
}
seen
}
/// **FC-7, time injected.** Return the next beat that is due at `elapsed_ms`
/// (advancing the cursor) or `None` if nothing is due yet / the pass is done.
/// At most one beat is returned per call so a caller that writes to the
/// consume-once channel never clobbers an unconsumed command. Looping is the
/// caller's job (rebase its clock + [`rewind`](Self::rewind) on `is_done`).
pub fn due(&mut self, elapsed_ms: u64) -> Option<DemoStep> {
let step = self.steps.get(self.next)?;
if elapsed_ms >= step.at_ms {
let out = step.clone();
self.next += 1;
Some(out)
} else {
None
}
}
/// Reset the cursor to the top (for a looped restart on a fresh clock).
pub fn rewind(&mut self) {
self.next = 0;
}
}
/// Small helper that assigns each beat a cumulative `at_ms` so the tour script
/// reads as a simple sequence.
struct TourBuilder {
beat_ms: u64,
t: u64,
steps: Vec<DemoStep>,
}
impl TourBuilder {
fn new(beat_ms: u64) -> Self {
// First beat fires one interval in, so the window is up before the tour
// starts clicking.
Self { beat_ms: beat_ms.max(1), t: beat_ms.max(1), steps: Vec::new() }
}
fn push(&mut self, label: &str, cmd: VizCommand) {
self.steps.push(DemoStep { at_ms: self.t, label: label.to_string(), cmd });
self.t += self.beat_ms;
}
fn tab(&mut self, name: &str, label: &str) {
self.push(label, VizCommand { tab: Some(name.to_string()), ..Default::default() });
}
fn palette(&mut self, name: &str, label: &str) {
self.push(label, VizCommand { palette: Some(name.to_string()), ..Default::default() });
}
fn set_field(&mut self, name: &str, value: &str, label: &str) {
self.push(
label,
VizCommand {
set_field: Some(VizField { name: name.to_string(), value: value.to_string() }),
..Default::default()
},
);
}
fn click(&mut self, key: &str, label: &str) {
self.push(label, VizCommand { click_id: Some(key.to_string()), ..Default::default() });
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tour_walks_every_named_view() {
let d = DemoDriver::tour(2000);
let tabs = d.tabs();
// The demo brief's named tabs are all present.
for want in ["Architecture", "Metro", "Knowledge", "Funnel", "Test", "WarehouseDeck", "Release", "Security"] {
assert!(tabs.iter().any(|t| t == want), "tour visits {want}: {tabs:?}");
}
assert!(tabs.len() >= 7, "tour walks ≥7 distinct views, got {}", tabs.len());
}
#[test]
fn due_is_time_injected_and_fires_once_per_beat() {
let mut d = DemoDriver::tour(1000);
let n = d.len();
// Before the first beat is due, nothing fires.
assert!(d.due(0).is_none());
// Stepping the injected clock to the end yields exactly `n` beats, in order.
let mut fired = 0usize;
for ms in (0..=d.duration_ms() + 10).step_by(50) {
while let Some(_step) = d.due(ms) {
fired += 1;
}
}
assert_eq!(fired, n, "every beat fires exactly once across the run");
assert!(d.is_done(), "a once() tour reports done after the last beat");
}
}