demo/scenario.rs
1//! Owns the dashboard data scenario served by the demo example.
2
3// Import dashboard error values used by validation.
4use rust_supervisor::dashboard::error::DashboardError;
5// Import dashboard model contracts served over IPC.
6use rust_supervisor::dashboard::model::{
7 // Continue the demo expression.
8 ControlCommandKind,
9 // Import command request shape.
10 ControlCommandRequest,
11 // Import command result shape.
12 ControlCommandResult,
13 // Import topology criticality values.
14 DashboardCriticality,
15 // Continue the demo expression.
16 DashboardState,
17 // Import event record shape.
18 EventRecord,
19 // Import log record shape.
20 LogRecord,
21 // Import registration state values.
22 RegistrationState,
23 // Import runtime row shape.
24 RuntimeState,
25 // Import topology edge shape.
26 SupervisorEdge,
27 // Continue the demo expression.
28 SupervisorEdgeKind,
29 // Import topology node shape.
30 SupervisorNode,
31 // Import topology node kind values.
32 SupervisorNodeKind,
33 // Import topology graph shape.
34 SupervisorTopology,
35 // Continue the demo expression.
36 TargetConnectionState,
37 // Import target identity shape.
38 TargetProcessIdentity,
39 // Continue the demo expression.
40};
41// Import JSON construction for command deltas and event payloads.
42use serde_json::json;
43// Import ordered maps for stable serialized payloads.
44use std::collections::BTreeMap;
45// Import mutex storage for mutable demo child states.
46use std::sync::Mutex;
47// Import atomic state generation counters.
48use std::sync::atomic::{AtomicU64, Ordering};
49
50// Define the root path shown in the demo topology.
51const ROOT_PATH: &str = "/root";
52// Define the demo configuration version.
53const CONFIG_VERSION: &str = "demo-ui-scenario-v1";
54
55/// Mutable dashboard scenario served by the demo IPC target.
56pub(crate) struct DemoScenario {
57 /// Stable target process identifier.
58 target_id: String,
59 /// Human-readable target name.
60 display_name: String,
61 /// State generation counter.
62 state_generation: AtomicU64,
63 /// Mutable child rows.
64 children: Mutex<Vec<DemoChild>>,
65 /// Command event sequence counter.
66 activity_sequence: AtomicU64,
67 // Continue the demo expression.
68}
69
70// Derive clone and debug helpers for static child declarations.
71#[derive(Clone, Debug)]
72/// One child row in the dashboard demo scenario.
73struct DemoChild {
74 /// Stable child identifier.
75 id: String,
76 /// Human-readable child name.
77 name: String,
78 /// Lifecycle state label.
79 lifecycle: String,
80 /// Health state label.
81 health: String,
82 /// Readiness state label.
83 readiness: String,
84 /// Restart count shown by the UI.
85 restart_count: u64,
86 /// Whether the child remains visible.
87 present: bool,
88 // Continue the demo expression.
89}
90
91// Derive clone and debug helpers for command transition payloads.
92#[derive(Clone, Debug)]
93/// Lifecycle transition caused by one demo command.
94struct CommandTransition {
95 /// Lifecycle state before command application.
96 previous_lifecycle_state: String,
97 /// Lifecycle state after command application.
98 lifecycle_state: String,
99 // Continue the demo expression.
100}
101
102// Continue the demo expression.
103impl DemoScenario {
104 /// Creates the default UI scenario.
105 ///
106 /// # Arguments
107 ///
108 /// - `target_id`: Target process identifier.
109 /// - `display_name`: Human-readable target name.
110 ///
111 /// # Returns
112 ///
113 /// Returns a mutable scenario with the standard demo children.
114 pub(crate) fn new(target_id: String, display_name: String) -> Self {
115 // Store the target identity and initial rows.
116 Self {
117 // Keep the target identifier stable.
118 target_id,
119 // Keep the display name stable.
120 display_name,
121 // Start state generations at one.
122 state_generation: AtomicU64::new(1),
123 // Seed child rows for the UI topology.
124 children: Mutex::new(seed_children()),
125 // Start command activity sequences away from seed records.
126 activity_sequence: AtomicU64::new(1),
127 // End scenario initialization.
128 }
129 // End constructor.
130 }
131
132 /// Returns the target process identifier.
133 ///
134 /// # Arguments
135 ///
136 /// This function has no arguments.
137 ///
138 /// # Returns
139 ///
140 /// Returns the target identifier as a string slice.
141 pub(crate) fn target_id(&self) -> &str {
142 // Return the stored target identifier.
143 &self.target_id
144 // End target identifier access.
145 }
146
147 /// Builds the current dashboard state.
148 ///
149 /// # Arguments
150 ///
151 /// This function has no arguments.
152 ///
153 /// # Returns
154 ///
155 /// Returns a dashboard state payload for UI rendering.
156 pub(crate) fn state(&self) -> DashboardState {
157 // Lock child rows for a consistent state payload.
158 let children = self.children.lock().expect("demo scenario mutex");
159 // Collect visible child rows.
160 let visible = visible_children(&children);
161 // Build the full dashboard state.
162 DashboardState {
163 // Include target identity and connection state.
164 target: self.target_identity(),
165 // Include the topology graph.
166 topology: topology(&visible),
167 // Include runtime rows.
168 runtime_state: runtime_rows(&visible),
169 // Include recent event rows.
170 recent_events: event_records(&self.target_id, &visible),
171 // Include recent log rows.
172 recent_logs: log_records(&self.target_id, &visible),
173 // Show that events can be dropped by bounded buffers.
174 dropped_event_count: 2,
175 // Show that logs can be dropped by bounded buffers.
176 dropped_log_count: 1,
177 // Include a stable demo config version.
178 config_version: CONFIG_VERSION.to_owned(),
179 // Include the generation timestamp.
180 generated_at_unix_nanos: unix_nanos_now(),
181 // Increment state generation on every state build.
182 state_generation: self.state_generation.fetch_add(1, Ordering::Relaxed),
183 // End dashboard state construction.
184 }
185 // End state construction.
186 }
187
188 /// Applies one control command and returns its result.
189 ///
190 /// # Arguments
191 ///
192 /// - `command`: Decoded command request.
193 ///
194 /// # Returns
195 ///
196 /// Returns a structured command result or validation error.
197 pub(crate) fn command_result(
198 // Continue the demo expression.
199 &self,
200 // Continue the demo expression.
201 command: ControlCommandRequest,
202 // Continue the demo expression.
203 ) -> Result<ControlCommandResult, DashboardError> {
204 // Validate command input before state mutation.
205 self.validate_command(&command)?;
206 // Lock child rows while applying the command.
207 let mut children = self.children.lock().expect("demo scenario mutex");
208 // Apply the requested command.
209 let transition = apply_command(&mut children, &command)?;
210 // Build a UI-consumable state delta after mutation.
211 let delta = command_state_delta(
212 // Continue the demo expression.
213 &self.target_id,
214 // Continue the demo expression.
215 &children,
216 // Continue the demo expression.
217 &command,
218 // Continue the demo expression.
219 &transition,
220 // Continue the demo expression.
221 self.activity_sequence.fetch_add(1, Ordering::Relaxed),
222 // Continue the demo expression.
223 self.state_generation.fetch_add(1, Ordering::Relaxed),
224 // Continue the demo expression.
225 );
226 // Return a successful command result.
227 Ok(ControlCommandResult {
228 // Preserve the original command identifier.
229 command_id: command.command_id,
230 // Preserve the target identifier.
231 target_id: command.target_id,
232 // Mark the command as accepted.
233 accepted: true,
234 // Mark the command as completed.
235 status: "completed".to_owned(),
236 // No structured error is present.
237 error: None,
238 // Include the state delta summary.
239 state_delta: Some(delta),
240 // Record command completion time.
241 completed_at_unix_nanos: Some(unix_nanos_now()),
242 // End command result construction.
243 })
244 // End command result.
245 }
246
247 /// Builds the target identity portion of dashboard state.
248 ///
249 /// # Arguments
250 ///
251 /// This function has no arguments.
252 ///
253 /// # Returns
254 ///
255 /// Returns target identity metadata for the state payload.
256 fn target_identity(&self) -> TargetProcessIdentity {
257 // Build the connected target identity.
258 TargetProcessIdentity {
259 // Include target identifier.
260 target_id: self.target_id.clone(),
261 // Include display name.
262 display_name: self.display_name.clone(),
263 // Mark registration as active.
264 registration_state: RegistrationState::Active,
265 // Mark the target IPC as connected.
266 connection_state: TargetConnectionState::Connected,
267 // End target identity construction.
268 }
269 // End target identity construction.
270 }
271
272 /// Validates one command request.
273 ///
274 /// # Arguments
275 ///
276 /// - `command`: Command request supplied by relay.
277 ///
278 /// # Returns
279 ///
280 /// Returns success when the command can be applied.
281 fn validate_command(&self, command: &ControlCommandRequest) -> Result<(), DashboardError> {
282 // Reject commands for another target.
283 if command.target_id != self.target_id {
284 // Return target mismatch validation.
285 return Err(validation(
286 // Continue the demo expression.
287 &self.target_id,
288 // Continue the demo expression.
289 "command target_id must match demo target",
290 // Continue the demo expression.
291 ));
292 // End target mismatch branch.
293 }
294 // Reject missing command identifiers.
295 if command.command_id.trim().is_empty() {
296 // Return command identifier validation.
297 return Err(validation(&self.target_id, "command_id must not be empty"));
298 // End command identifier branch.
299 }
300 // Reject missing reasons.
301 if command.reason.trim().is_empty() {
302 // Return reason validation.
303 return Err(validation(
304 // Continue the demo expression.
305 &self.target_id,
306 // Continue the demo expression.
307 "command reason must not be empty",
308 // Continue the demo expression.
309 ));
310 // End reason branch.
311 }
312 // Reject missing requester identity.
313 if command.requested_by.trim().is_empty() {
314 // Return requester validation.
315 return Err(validation(
316 // Continue the demo expression.
317 &self.target_id,
318 // Continue the demo expression.
319 "requested_by must not be empty",
320 // Continue the demo expression.
321 ));
322 // End requester branch.
323 }
324 // Validate dangerous command confirmation.
325 if is_dangerous(command.command) && !command.confirmed {
326 // Return confirmation validation.
327 return Err(validation(
328 // Continue the demo expression.
329 &self.target_id,
330 // Continue the demo expression.
331 "dangerous command requires confirmation",
332 // Continue the demo expression.
333 ));
334 // End confirmation branch.
335 }
336 // Validate add-child manifests.
337 if command.command == ControlCommandKind::AddChild && missing_child_manifest(command) {
338 // Return child manifest validation.
339 return Err(validation(
340 // Continue the demo expression.
341 &self.target_id,
342 // Continue the demo expression.
343 "add_child requires child_manifest",
344 // Continue the demo expression.
345 ));
346 // End child manifest branch.
347 }
348 // Finish command validation successfully.
349 Ok(())
350 // End command validation.
351 }
352 // Continue the demo expression.
353}
354
355/// Builds the seed child rows for the demo.
356///
357/// # Arguments
358///
359/// This function has no arguments.
360///
361/// # Returns
362///
363/// Returns the static child rows used by the UI.
364fn seed_children() -> Vec<DemoChild> {
365 // Return every standard demo child.
366 vec![
367 // Include a failed child.
368 child(
369 // Continue the demo expression.
370 "duplicate_guard",
371 // Continue the demo expression.
372 "duplicate guard",
373 // Continue the demo expression.
374 "failed",
375 // Continue the demo expression.
376 "unhealthy",
377 // Continue the demo expression.
378 "not_ready",
379 // Continue the demo expression.
380 2,
381 // Continue the demo expression.
382 ),
383 // Include a restarting child.
384 child(
385 // Continue the demo expression.
386 "retry_scheduler",
387 // Continue the demo expression.
388 "retry scheduler",
389 // Continue the demo expression.
390 "restarting",
391 // Continue the demo expression.
392 "stale",
393 // Continue the demo expression.
394 "not_ready",
395 // Continue the demo expression.
396 3,
397 // Continue the demo expression.
398 ),
399 // Include a paused child.
400 child(
401 // Continue the demo expression.
402 "invoice_writer",
403 // Continue the demo expression.
404 "invoice writer",
405 // Continue the demo expression.
406 "paused",
407 // Continue the demo expression.
408 "healthy",
409 // Continue the demo expression.
410 "ready",
411 // Continue the demo expression.
412 0,
413 // Continue the demo expression.
414 ),
415 // Include a quarantined child.
416 child(
417 // Continue the demo expression.
418 "index_stream",
419 // Continue the demo expression.
420 "index stream",
421 // Continue the demo expression.
422 "quarantined",
423 // Continue the demo expression.
424 "unhealthy",
425 // Continue the demo expression.
426 "not_ready",
427 // Continue the demo expression.
428 5,
429 // Continue the demo expression.
430 ),
431 // Include a healthy running child.
432 child(
433 // Continue the demo expression.
434 "healthy_worker",
435 // Continue the demo expression.
436 "healthy worker",
437 // Continue the demo expression.
438 "running",
439 // Continue the demo expression.
440 "healthy",
441 // Continue the demo expression.
442 "ready",
443 // Continue the demo expression.
444 0,
445 // Continue the demo expression.
446 ),
447 // End child list.
448 ]
449 // End seed child construction.
450}
451
452/// Creates one demo child row.
453///
454/// # Arguments
455///
456/// - `id`: Stable child identifier.
457/// - `name`: Human-readable child name.
458/// - `lifecycle`: Lifecycle state label.
459/// - `health`: Health state label.
460/// - `readiness`: Readiness state label.
461/// - `restart_count`: Restart count.
462///
463/// # Returns
464///
465/// Returns a child row.
466fn child(
467 // Continue the demo expression.
468 id: &str,
469 // Continue the demo expression.
470 name: &str,
471 // Continue the demo expression.
472 lifecycle: &str,
473 // Continue the demo expression.
474 health: &str,
475 // Continue the demo expression.
476 readiness: &str,
477 // Continue the demo expression.
478 restart_count: u64,
479 // Continue the demo expression.
480) -> DemoChild {
481 // Build one child row.
482 DemoChild {
483 // Store the child identifier.
484 id: id.to_owned(),
485 // Store the child display name.
486 name: name.to_owned(),
487 // Store the lifecycle label.
488 lifecycle: lifecycle.to_owned(),
489 // Store the health label.
490 health: health.to_owned(),
491 // Store the readiness label.
492 readiness: readiness.to_owned(),
493 // Store the restart count.
494 restart_count,
495 // Mark the row visible.
496 present: true,
497 // End child row construction.
498 }
499 // End child construction.
500}
501
502/// Filters visible child rows.
503///
504/// # Arguments
505///
506/// - `children`: All child rows.
507///
508/// # Returns
509///
510/// Returns visible child rows.
511fn visible_children(children: &[DemoChild]) -> Vec<DemoChild> {
512 // Keep only rows that remain present.
513 children
514 // Iterate through all rows.
515 .iter()
516 // Keep visible rows.
517 .filter(|child| child.present)
518 // Clone visible rows for state construction.
519 .cloned()
520 // Collect visible rows.
521 .collect()
522 // End visible child filtering.
523}
524
525/// Builds topology for visible demo children.
526///
527/// # Arguments
528///
529/// - `children`: Visible child rows.
530///
531/// # Returns
532///
533/// Returns a topology graph.
534fn topology(children: &[DemoChild]) -> SupervisorTopology {
535 // Build the root node.
536 let root = root_node();
537 // Start the node list with the root.
538 let mut nodes = vec![root.clone()];
539 // Start the edge list.
540 let mut edges = Vec::new();
541 // Start the declaration order with the root.
542 let mut declaration_order = vec![ROOT_PATH.to_owned()];
543 // Add one node and edge per child.
544 for (index, child) in children.iter().enumerate() {
545 // Build the child path.
546 let path = child_path(&child.id);
547 // Record declaration order.
548 declaration_order.push(path.clone());
549 // Add the child node.
550 nodes.push(child_node(child, &path));
551 // Add the parent-child edge.
552 edges.push(parent_edge(index, &path));
553 // End child topology row.
554 }
555 // Return the topology.
556 SupervisorTopology {
557 // Include the root node.
558 root,
559 // Include all visible nodes.
560 nodes,
561 // Include all visible edges.
562 edges,
563 // Include declaration order.
564 declaration_order,
565 // End topology construction.
566 }
567 // End topology construction.
568}
569
570/// Builds the root topology node.
571///
572/// # Arguments
573///
574/// This function has no arguments.
575///
576/// # Returns
577///
578/// Returns the root node.
579fn root_node() -> SupervisorNode {
580 // Build the root node.
581 SupervisorNode {
582 // Use the root path as node identifier.
583 node_id: ROOT_PATH.to_owned(),
584 // Root has no child identifier.
585 child_id: None,
586 // Use the root path.
587 path: ROOT_PATH.to_owned(),
588 // Use a readable root name.
589 name: "root supervisor".to_owned(),
590 // Mark the node as root.
591 kind: SupervisorNodeKind::RootSupervisor,
592 // Root has no tags.
593 tags: Vec::new(),
594 // Root is critical.
595 criticality: DashboardCriticality::Critical,
596 // Summarize the root state.
597 state_summary: "root".to_owned(),
598 // Root has no diagnostics.
599 diagnostics: BTreeMap::new(),
600 // End root node construction.
601 }
602 // End root node construction.
603}
604
605/// Builds one child topology node.
606///
607/// # Arguments
608///
609/// - `child`: Child row.
610/// - `path`: Child path.
611///
612/// # Returns
613///
614/// Returns a child node.
615fn child_node(child: &DemoChild, path: &str) -> SupervisorNode {
616 // Build node diagnostics.
617 let diagnostics = diagnostics_for(child);
618 // Build the child node.
619 SupervisorNode {
620 // Use the path as the node identifier.
621 node_id: path.to_owned(),
622 // Include the child identifier.
623 child_id: Some(child.id.clone()),
624 // Include the child path.
625 path: path.to_owned(),
626 // Include the child name.
627 name: child.name.clone(),
628 // Mark the node as a child task.
629 kind: SupervisorNodeKind::ChildTask,
630 // Tag demo rows for filtering.
631 tags: vec!["demo".to_owned(), "ui".to_owned()],
632 // Use standard criticality.
633 criticality: DashboardCriticality::Standard,
634 // Use the lifecycle as state summary.
635 state_summary: child.lifecycle.clone(),
636 // Include diagnostics.
637 diagnostics,
638 // End child node construction.
639 }
640 // End child node construction.
641}
642
643/// Builds diagnostics for one child.
644///
645/// # Arguments
646///
647/// - `child`: Child row.
648///
649/// # Returns
650///
651/// Returns diagnostic fields.
652fn diagnostics_for(child: &DemoChild) -> BTreeMap<String, String> {
653 // Start with an empty map.
654 let mut diagnostics = BTreeMap::new();
655 // Add diagnostics for failed rows.
656 if child.lifecycle == "failed" {
657 // Add failure summary.
658 diagnostics.insert(
659 // Store the diagnostic key.
660 "message".to_owned(),
661 // Store the diagnostic message.
662 "duplicate event window exceeded".to_owned(),
663 // End diagnostic insertion.
664 );
665 // End failed diagnostic branch.
666 }
667 // Return diagnostics.
668 diagnostics
669 // End diagnostics construction.
670}
671
672/// Builds one parent-child edge.
673///
674/// # Arguments
675///
676/// - `index`: Child declaration index.
677/// - `path`: Child path.
678///
679/// # Returns
680///
681/// Returns one topology edge.
682fn parent_edge(index: usize, path: &str) -> SupervisorEdge {
683 // Build the edge.
684 SupervisorEdge {
685 // Build a stable edge identifier.
686 edge_id: format!("parent:{ROOT_PATH}->{path}"),
687 // Root is the source.
688 source_path: ROOT_PATH.to_owned(),
689 // Child path is the target.
690 target_path: path.to_owned(),
691 // Mark the edge as parent-child.
692 kind: SupervisorEdgeKind::ParentChild,
693 // Preserve declaration order.
694 order: index,
695 // End edge construction.
696 }
697 // End edge construction.
698}
699
700/// Builds runtime rows for visible children.
701///
702/// # Arguments
703///
704/// - `children`: Visible child rows.
705///
706/// # Returns
707///
708/// Returns runtime state rows.
709fn runtime_rows(children: &[DemoChild]) -> Vec<RuntimeState> {
710 // Convert children to runtime rows.
711 children
712 // Iterate over visible children.
713 .iter()
714 // Build one runtime row per child.
715 .map(runtime_row)
716 // Collect runtime rows.
717 .collect()
718 // End runtime row construction.
719}
720
721/// Builds one runtime row.
722///
723/// # Arguments
724///
725/// - `child`: Child row.
726///
727/// # Returns
728///
729/// Returns one runtime row.
730fn runtime_row(child: &DemoChild) -> RuntimeState {
731 // Build the runtime row.
732 RuntimeState {
733 // Include the child path.
734 child_path: child_path(&child.id),
735 // Include the lifecycle state.
736 lifecycle_state: child.lifecycle.clone(),
737 // Include the health state.
738 health: child.health.clone(),
739 // Include the readiness state.
740 readiness: child.readiness.clone(),
741 // Include the generation number.
742 generation: child.restart_count,
743 // Include the attempt number.
744 attempt: child.restart_count.saturating_add(1),
745 // Include restart count.
746 restart_count: child.restart_count,
747 // Include last failure when present.
748 last_failure: last_failure(child),
749 // Include last policy decision when present.
750 last_policy_decision: last_policy_decision(child),
751 // Include shutdown state.
752 shutdown_state: "running".to_owned(),
753 // End runtime row construction.
754 }
755 // End runtime row construction.
756}
757
758/// Builds recent events for visible children.
759///
760/// # Arguments
761///
762/// - `target_id`: Target process identifier.
763/// - `children`: Visible child rows.
764///
765/// # Returns
766///
767/// Returns recent event records.
768fn event_records(target_id: &str, children: &[DemoChild]) -> Vec<EventRecord> {
769 // Convert children to event records.
770 children
771 // Iterate over visible children.
772 .iter()
773 // Keep child indexes as stable sequences.
774 .enumerate()
775 // Build one event per child.
776 .map(|(index, child)| event_record(target_id, index, child))
777 // Collect event records.
778 .collect()
779 // End event record construction.
780}
781
782/// Builds one event record.
783///
784/// # Arguments
785///
786/// - `target_id`: Target process identifier.
787/// - `index`: Child index.
788/// - `child`: Child row.
789///
790/// # Returns
791///
792/// Returns one event record.
793fn event_record(target_id: &str, index: usize, child: &DemoChild) -> EventRecord {
794 // Compute a deterministic sequence.
795 let sequence = 1001_u64.saturating_add(index as u64);
796 // Build the event record.
797 EventRecord {
798 // Include target identifier.
799 target_id: target_id.to_owned(),
800 // Include target-local sequence.
801 sequence,
802 // Include correlation identifier.
803 correlation_id: format!("demo-{sequence}"),
804 // Include event type.
805 event_type: event_type(child).to_owned(),
806 // Include severity.
807 severity: severity(child).to_owned(),
808 // Include target path.
809 target_path: child_path(&child.id),
810 // Include child identifier.
811 child_id: Some(child.id.clone()),
812 // Include occurrence time.
813 occurred_at_unix_nanos: unix_nanos_now().saturating_sub(sequence as u128),
814 // Include configuration version.
815 config_version: CONFIG_VERSION.to_owned(),
816 // Include structured payload.
817 payload: json!({
818 // Include the child path field.
819 "child_path": child_path(&child.id),
820 // Include the previous lifecycle state field.
821 "previous_lifecycle_state": "unknown",
822 // Include the lifecycle state field.
823 "lifecycle_state": child.lifecycle,
824 // End event payload object.
825 }),
826 // End event record construction.
827 }
828 // End event record construction.
829}
830
831/// Builds recent log rows for visible children.
832///
833/// # Arguments
834///
835/// - `target_id`: Target process identifier.
836/// - `children`: Visible child rows.
837///
838/// # Returns
839///
840/// Returns recent log records.
841fn log_records(target_id: &str, children: &[DemoChild]) -> Vec<LogRecord> {
842 // Convert children to log rows.
843 children
844 // Iterate over visible children.
845 .iter()
846 // Keep child indexes as stable sequences.
847 .enumerate()
848 // Build one log row per child.
849 .map(|(index, child)| log_record(target_id, index, child))
850 // Collect log rows.
851 .collect()
852 // End log record construction.
853}
854
855/// Builds one log record.
856///
857/// # Arguments
858///
859/// - `target_id`: Target process identifier.
860/// - `index`: Child index.
861/// - `child`: Child row.
862///
863/// # Returns
864///
865/// Returns one log record.
866fn log_record(target_id: &str, index: usize, child: &DemoChild) -> LogRecord {
867 // Compute a deterministic sequence.
868 let sequence = 2001_u64.saturating_add(index as u64);
869 // Build structured log fields.
870 let mut fields = BTreeMap::new();
871 // Insert child path.
872 fields.insert("child_path".to_owned(), child_path(&child.id));
873 // Insert previous lifecycle state.
874 fields.insert("previous_lifecycle_state".to_owned(), "unknown".to_owned());
875 // Insert lifecycle state.
876 fields.insert("lifecycle_state".to_owned(), child.lifecycle.clone());
877 // Build the log record.
878 LogRecord {
879 // Include target identifier.
880 target_id: target_id.to_owned(),
881 // Include log sequence.
882 sequence: Some(sequence),
883 // Include correlation identifier.
884 correlation_id: Some(format!("demo-{sequence}")),
885 // Include log severity.
886 severity: severity(child).to_owned(),
887 // Include log message.
888 message: format!(
889 // Continue the demo expression.
890 "{} transitioned from unknown to {}",
891 // Continue the demo expression.
892 child.name, child.lifecycle,
893 // End transition message expression.
894 ),
895 // Include structured fields.
896 fields,
897 // Include occurrence time.
898 occurred_at_unix_nanos: unix_nanos_now().saturating_sub(sequence as u128),
899 // End log record construction.
900 }
901 // End log record construction.
902}
903
904/// Applies a command to the demo rows.
905///
906/// # Arguments
907///
908/// - `children`: Mutable child rows.
909/// - `command`: Command request.
910///
911/// # Returns
912///
913/// Returns a JSON state delta.
914fn apply_command(
915 // Continue the demo expression.
916 children: &mut Vec<DemoChild>,
917 // Continue the demo expression.
918 command: &ControlCommandRequest,
919 // Continue the demo expression.
920) -> Result<CommandTransition, DashboardError> {
921 // Apply tree shutdown without a child path.
922 if command.command == ControlCommandKind::ShutdownTree {
923 // Read first visible lifecycle before shutdown.
924 let previous = children
925 // Continue the demo expression.
926 .iter()
927 // Continue the demo expression.
928 .find(|child| child.present)
929 // Continue the demo expression.
930 .map(|child| child.lifecycle.clone())
931 // Continue the demo expression.
932 .unwrap_or_else(|| "unknown".to_owned());
933 // Mark visible children as stopped.
934 children
935 // Continue the demo expression.
936 .iter_mut()
937 // Continue the demo expression.
938 .for_each(|child| child.lifecycle = "stopped".to_owned());
939 // Return success.
940 return Ok(CommandTransition {
941 // Preserve the previous lifecycle state.
942 previous_lifecycle_state: previous,
943 // Store the lifecycle after shutdown.
944 lifecycle_state: "stopped".to_owned(),
945 // End transition construction.
946 });
947 // End shutdown branch.
948 }
949 // Resolve the target child identifier.
950 let child_id = command_child_id(command)?;
951 // Apply add-child separately.
952 if command.command == ControlCommandKind::AddChild {
953 // Read the lifecycle before adding or restoring.
954 let previous = children
955 // Continue the demo expression.
956 .iter()
957 // Continue the demo expression.
958 .find(|row| row.id == child_id && row.present)
959 // Continue the demo expression.
960 .map(|row| row.lifecycle.clone())
961 // Continue the demo expression.
962 .unwrap_or_else(|| "absent".to_owned());
963 // Add or restore the requested child.
964 add_child(children, &child_id);
965 // Return success.
966 return Ok(CommandTransition {
967 // Preserve the previous lifecycle state.
968 previous_lifecycle_state: previous,
969 // Store the lifecycle after adding or restoring.
970 lifecycle_state: "running".to_owned(),
971 // End transition construction.
972 });
973 // End add-child branch.
974 }
975 // Find the target child row.
976 let child = children
977 // Iterate through mutable rows.
978 .iter_mut()
979 // Match the target child identifier.
980 .find(|row| row.id == child_id && row.present)
981 // Convert absence into validation.
982 .ok_or_else(|| {
983 // Continue the demo expression.
984 validation(
985 // Continue the demo expression.
986 &command.target_id,
987 // Continue the demo expression.
988 "child_path does not match a visible child",
989 // Continue the demo expression.
990 )
991 // Continue the demo expression.
992 })?;
993 // Preserve the lifecycle before child command application.
994 let previous = child.lifecycle.clone();
995 // Apply the child command.
996 match command.command {
997 // Restart moves the child into restarting state.
998 ControlCommandKind::RestartChild => {
999 // Continue the demo expression.
1000 set_child_state(child, "restarting", "stale", "not_ready")
1001 // Continue the demo expression.
1002 }
1003 // Pause moves the child into paused state.
1004 ControlCommandKind::PauseChild => set_child_state(child, "paused", "healthy", "ready"),
1005 // Resume moves the child into running state.
1006 ControlCommandKind::ResumeChild => set_child_state(child, "running", "healthy", "ready"),
1007 // Quarantine moves the child into quarantined state.
1008 ControlCommandKind::QuarantineChild => {
1009 // Continue the demo expression.
1010 set_child_state(child, "quarantined", "unhealthy", "not_ready")
1011 // Continue the demo expression.
1012 }
1013 // Remove hides the child from later state responses.
1014 ControlCommandKind::RemoveChild => child.present = false,
1015 // Other variants are handled above.
1016 ControlCommandKind::AddChild | ControlCommandKind::ShutdownTree => {} // End command match.
1017 // Continue the demo expression.
1018 }
1019 // Return success.
1020 Ok(CommandTransition {
1021 // Preserve the previous lifecycle state.
1022 previous_lifecycle_state: previous,
1023 // Store the lifecycle after command application.
1024 lifecycle_state: lifecycle_after(command.command).to_owned(),
1025 // End transition construction.
1026 })
1027 // End command application.
1028}
1029
1030/// Adds or restores a child row.
1031///
1032/// # Arguments
1033///
1034/// - `children`: Mutable child rows.
1035/// - `child_id`: Child identifier.
1036///
1037/// # Returns
1038///
1039/// This function has no return value.
1040fn add_child(children: &mut Vec<DemoChild>, child_id: &str) {
1041 // Restore an existing row when it already exists.
1042 if let Some(child) = children.iter_mut().find(|row| row.id == child_id) {
1043 // Mark the existing row present.
1044 child.present = true;
1045 // Mark the existing row running.
1046 set_child_state(child, "running", "healthy", "ready");
1047 // Finish existing row handling.
1048 return;
1049 // End existing row branch.
1050 }
1051 // Add a new row.
1052 children.push(child(child_id, child_id, "running", "healthy", "ready", 0));
1053 // End child addition.
1054}
1055
1056/// Sets child state labels.
1057///
1058/// # Arguments
1059///
1060/// - `child`: Mutable child row.
1061/// - `lifecycle`: Lifecycle state label.
1062/// - `health`: Health state label.
1063/// - `readiness`: Readiness state label.
1064///
1065/// # Returns
1066///
1067/// This function has no return value.
1068fn set_child_state(child: &mut DemoChild, lifecycle: &str, health: &str, readiness: &str) {
1069 // Update lifecycle.
1070 child.lifecycle = lifecycle.to_owned();
1071 // Update health.
1072 child.health = health.to_owned();
1073 // Update readiness.
1074 child.readiness = readiness.to_owned();
1075 // Increment restart count for restarting state.
1076 if lifecycle == "restarting" {
1077 // Increase restart count.
1078 child.restart_count = child.restart_count.saturating_add(1);
1079 // End restarting branch.
1080 }
1081 // End state update.
1082}
1083
1084/// Builds a command delta for the UI.
1085///
1086/// # Arguments
1087///
1088/// - `target_id`: Target identifier.
1089/// - `children`: Current child rows after mutation.
1090/// - `command`: Command request.
1091/// - `sequence`: Command activity sequence.
1092/// - `state_generation`: State generation value.
1093///
1094/// # Returns
1095///
1096/// Returns a JSON delta.
1097fn command_state_delta(
1098 // Accept target identifier.
1099 target_id: &str,
1100 // Accept current child rows.
1101 children: &[DemoChild],
1102 // Accept original command.
1103 command: &ControlCommandRequest,
1104 // Accept lifecycle transition.
1105 transition: &CommandTransition,
1106 // Accept activity sequence.
1107 sequence: u64,
1108 // Accept state generation.
1109 state_generation: u64,
1110 // End command delta signature.
1111) -> serde_json::Value {
1112 // Collect visible rows after command application.
1113 let visible = visible_children(children);
1114 // Build the JSON delta consumed by the UI.
1115 json!({
1116 // Include command kind.
1117 "command": command_name(command.command),
1118 // Include child path.
1119 "child_path": command.target.child_path,
1120 // Include previous lifecycle state.
1121 "previous_lifecycle_state": transition.previous_lifecycle_state.as_str(),
1122 // Include lifecycle state.
1123 "lifecycle_state": transition.lifecycle_state.as_str(),
1124 // Include state generation.
1125 "state_generation": state_generation,
1126 // Include current topology after mutation.
1127 "topology": topology(&visible),
1128 // Include current runtime rows after mutation.
1129 "runtime_state": runtime_rows(&visible),
1130 // Include command event rows.
1131 "recent_events": [command_event_record(target_id, sequence, command, transition)],
1132 // Include command log rows.
1133 "recent_logs": [command_log_record(target_id, sequence, command, transition)],
1134 // Include dropped event count.
1135 "dropped_event_count": 2,
1136 // Include dropped log count.
1137 "dropped_log_count": 1,
1138 // End delta object.
1139 })
1140 // End delta construction.
1141}
1142
1143/// Builds an event row for a command.
1144fn command_event_record(
1145 // Accept target identifier.
1146 target_id: &str,
1147 // Accept activity sequence.
1148 sequence: u64,
1149 // Accept original command.
1150 command: &ControlCommandRequest,
1151 // Accept lifecycle transition.
1152 transition: &CommandTransition,
1153 // End command event signature.
1154) -> EventRecord {
1155 // Resolve target path.
1156 let target_path = command
1157 // Access command target.
1158 .target
1159 // Access child path.
1160 .child_path
1161 // Clone path value.
1162 .clone()
1163 // Use root when the command targets the tree.
1164 .unwrap_or_else(|| ROOT_PATH.to_owned());
1165 // Resolve child id.
1166 let child_id = child_id_from_path(&target_path);
1167 // Build event record.
1168 EventRecord {
1169 // Include target identifier.
1170 target_id: target_id.to_owned(),
1171 // Include command event sequence.
1172 sequence: 7000_u64.saturating_add(sequence),
1173 // Include command identifier.
1174 correlation_id: command.command_id.clone(),
1175 // Include command event type.
1176 event_type: command_event_type(command.command).to_owned(),
1177 // Include command severity.
1178 severity: command_severity(command.command).to_owned(),
1179 // Include affected target path.
1180 target_path,
1181 // Include affected child identifier.
1182 child_id,
1183 // Include occurrence time.
1184 occurred_at_unix_nanos: unix_nanos_now(),
1185 // Include config version.
1186 config_version: CONFIG_VERSION.to_owned(),
1187 // Include command payload.
1188 payload: json!({
1189 // Include child path.
1190 "child_path": command.target.child_path,
1191 // Include command name.
1192 "command": command_name(command.command),
1193 // Include previous lifecycle state.
1194 "previous_lifecycle_state": transition.previous_lifecycle_state.as_str(),
1195 // Include lifecycle state.
1196 "lifecycle_state": transition.lifecycle_state.as_str(),
1197 // Include command reason.
1198 "reason": command.reason,
1199 // End command payload.
1200 }),
1201 // End event record construction.
1202 }
1203 // End event record.
1204}
1205
1206/// Builds a log row for a command.
1207fn command_log_record(
1208 // Accept target identifier.
1209 target_id: &str,
1210 // Accept activity sequence.
1211 sequence: u64,
1212 // Accept original command.
1213 command: &ControlCommandRequest,
1214 // Accept lifecycle transition.
1215 transition: &CommandTransition,
1216 // End command log signature.
1217) -> LogRecord {
1218 // Resolve target path.
1219 let target_path = command
1220 // Access command target.
1221 .target
1222 // Access child path.
1223 .child_path
1224 // Clone path value.
1225 .clone()
1226 // Use root when the command targets the tree.
1227 .unwrap_or_else(|| ROOT_PATH.to_owned());
1228 // Start structured fields.
1229 let mut fields = BTreeMap::new();
1230 // Include child path field.
1231 fields.insert("child_path".to_owned(), target_path.clone());
1232 // Include command field.
1233 fields.insert("command".to_owned(), command_name(command.command).to_owned());
1234 // Include previous lifecycle field.
1235 fields.insert("previous_lifecycle_state".to_owned(), transition.previous_lifecycle_state.clone());
1236 // Include lifecycle field.
1237 fields.insert("lifecycle_state".to_owned(), transition.lifecycle_state.clone());
1238 // Build log record.
1239 LogRecord {
1240 // Include target identifier.
1241 target_id: target_id.to_owned(),
1242 // Include log sequence.
1243 sequence: Some(8000_u64.saturating_add(sequence)),
1244 // Include command identifier.
1245 correlation_id: Some(command.command_id.clone()),
1246 // Include log severity.
1247 severity: command_severity(command.command).to_owned(),
1248 // Include log message.
1249 message: format!(
1250 // Continue the demo expression.
1251 "{} {} completed, transitioned from {} to {}",
1252 // Continue the demo expression.
1253 target_path,
1254 // Continue the demo expression.
1255 command_name(command.command),
1256 // Continue the demo expression.
1257 transition.previous_lifecycle_state,
1258 // Continue the demo expression.
1259 transition.lifecycle_state,
1260 // End transition message expression.
1261 ),
1262 // Include structured fields.
1263 fields,
1264 // Include occurrence time.
1265 occurred_at_unix_nanos: unix_nanos_now(),
1266 // End log record construction.
1267 }
1268 // End log record.
1269}
1270
1271/// Returns command event type.
1272fn command_event_type(command: ControlCommandKind) -> &'static str {
1273 // Match command to event type.
1274 match command {
1275 // Return restart event type.
1276 ControlCommandKind::RestartChild => "child_restarted",
1277 // Return pause event type.
1278 ControlCommandKind::PauseChild => "child_paused",
1279 // Return resume event type.
1280 ControlCommandKind::ResumeChild => "child_resumed",
1281 // Return quarantine event type.
1282 ControlCommandKind::QuarantineChild => "child_quarantined",
1283 // Return remove event type.
1284 ControlCommandKind::RemoveChild => "child_removed",
1285 // Return add event type.
1286 ControlCommandKind::AddChild => "child_added",
1287 // Return shutdown event type.
1288 ControlCommandKind::ShutdownTree => "tree_stopped",
1289 // End event type match.
1290 }
1291 // End event type lookup.
1292}
1293
1294/// Returns command severity.
1295fn command_severity(command: ControlCommandKind) -> &'static str {
1296 // Match command to severity.
1297 match command {
1298 // Mark disruptive commands as warning.
1299 ControlCommandKind::QuarantineChild | ControlCommandKind::ShutdownTree => "warning",
1300 // Mark other commands as info.
1301 _ => "info",
1302 // End severity match.
1303 }
1304 // End severity lookup.
1305}
1306
1307/// Extracts child id from path.
1308fn child_id_from_path(path: &str) -> Option<String> {
1309 // Skip the root path.
1310 if path == ROOT_PATH {
1311 // Return no child identifier.
1312 return None;
1313 // End root branch.
1314 }
1315 // Extract final path segment.
1316 path.rsplit('/')
1317 // Keep non-empty segments.
1318 .find(|segment| !segment.is_empty())
1319 // Convert to owned string.
1320 .map(ToOwned::to_owned)
1321 // End child id extraction.
1322}
1323
1324/// Extracts the child identifier from a command.
1325///
1326/// # Arguments
1327///
1328/// - `command`: Command request.
1329///
1330/// # Returns
1331///
1332/// Returns the final path segment.
1333fn command_child_id(command: &ControlCommandRequest) -> Result<String, DashboardError> {
1334 // Read the command child path.
1335 let path = command.target.child_path.as_deref().ok_or_else(|| {
1336 // Return missing child path validation.
1337 validation(
1338 // Continue the demo expression.
1339 &command.target_id,
1340 // Continue the demo expression.
1341 "child_path is required for child command",
1342 // Continue the demo expression.
1343 )
1344 // End missing child path validation.
1345 })?;
1346 // Extract the final non-empty path segment.
1347 let child_id = path
1348 // Continue the demo expression.
1349 .rsplit('/')
1350 // Continue the demo expression.
1351 .find(|segment| !segment.is_empty())
1352 // Continue the demo expression.
1353 .unwrap_or(path);
1354 // Return the child identifier.
1355 Ok(child_id.to_owned())
1356 // End child identifier extraction.
1357}
1358
1359/// Returns whether a command is dangerous.
1360///
1361/// # Arguments
1362///
1363/// - `command`: Command kind.
1364///
1365/// # Returns
1366///
1367/// Returns whether confirmation is required.
1368fn is_dangerous(command: ControlCommandKind) -> bool {
1369 // Match commands that require confirmation.
1370 matches!(
1371 // Use the supplied command.
1372 command,
1373 // Include remove child.
1374 ControlCommandKind::RemoveChild
1375 // Include add child.
1376 | ControlCommandKind::AddChild
1377 // Include shutdown tree.
1378 | ControlCommandKind::ShutdownTree // End dangerous command match.
1379 // Continue the demo expression.
1380 )
1381 // End dangerous command predicate.
1382}
1383
1384/// Returns whether add-child is missing a manifest.
1385///
1386/// # Arguments
1387///
1388/// - `command`: Command request.
1389///
1390/// # Returns
1391///
1392/// Returns true when the manifest is absent or blank.
1393fn missing_child_manifest(command: &ControlCommandRequest) -> bool {
1394 // Read the optional manifest.
1395 let manifest = command.target.child_manifest.as_deref().unwrap_or_default();
1396 // Return blank status.
1397 manifest.trim().is_empty()
1398 // End manifest predicate.
1399}
1400
1401/// Returns lifecycle after a command.
1402///
1403/// # Arguments
1404///
1405/// - `command`: Command kind.
1406///
1407/// # Returns
1408///
1409/// Returns the lifecycle label after command application.
1410fn lifecycle_after(command: ControlCommandKind) -> &'static str {
1411 // Match command lifecycle outcomes.
1412 match command {
1413 // Restarted children are restarting.
1414 ControlCommandKind::RestartChild => "restarting",
1415 // Paused children are paused.
1416 ControlCommandKind::PauseChild => "paused",
1417 // Resumed children are running.
1418 ControlCommandKind::ResumeChild => "running",
1419 // Quarantined children are quarantined.
1420 ControlCommandKind::QuarantineChild => "quarantined",
1421 // Removed children are removed.
1422 ControlCommandKind::RemoveChild => "removed",
1423 // Added children are running.
1424 ControlCommandKind::AddChild => "running",
1425 // Shutdown is represented separately.
1426 ControlCommandKind::ShutdownTree => "stopped",
1427 // End lifecycle match.
1428 }
1429 // End lifecycle lookup.
1430}
1431
1432/// Returns the command wire name.
1433///
1434/// # Arguments
1435///
1436/// - `command`: Command kind.
1437///
1438/// # Returns
1439///
1440/// Returns the command label.
1441fn command_name(command: ControlCommandKind) -> &'static str {
1442 // Match command names.
1443 match command {
1444 // Return restart name.
1445 ControlCommandKind::RestartChild => "restart_child",
1446 // Return pause name.
1447 ControlCommandKind::PauseChild => "pause_child",
1448 // Return resume name.
1449 ControlCommandKind::ResumeChild => "resume_child",
1450 // Return quarantine name.
1451 ControlCommandKind::QuarantineChild => "quarantine_child",
1452 // Return remove name.
1453 ControlCommandKind::RemoveChild => "remove_child",
1454 // Return add name.
1455 ControlCommandKind::AddChild => "add_child",
1456 // Return shutdown name.
1457 ControlCommandKind::ShutdownTree => "shutdown_tree",
1458 // End command name match.
1459 }
1460 // End command name lookup.
1461}
1462
1463/// Builds child path from identifier.
1464///
1465/// # Arguments
1466///
1467/// - `child_id`: Child identifier.
1468///
1469/// # Returns
1470///
1471/// Returns an absolute demo child path.
1472fn child_path(child_id: &str) -> String {
1473 // Build the path string.
1474 format!("{ROOT_PATH}/{child_id}")
1475 // End child path construction.
1476}
1477
1478/// Returns event type for one child state.
1479///
1480/// # Arguments
1481///
1482/// - `child`: Child row.
1483///
1484/// # Returns
1485///
1486/// Returns a dashboard event type.
1487fn event_type(child: &DemoChild) -> &'static str {
1488 // Match lifecycle state.
1489 match child.lifecycle.as_str() {
1490 // Failed rows emit child_failed.
1491 "failed" => "child_failed",
1492 // Restarting rows emit child_restarted.
1493 "restarting" => "child_restarted",
1494 // Paused rows emit child_paused.
1495 "paused" => "child_paused",
1496 // Quarantined rows emit child_quarantined.
1497 "quarantined" => "child_quarantined",
1498 // Other rows emit child_running.
1499 _ => "child_running",
1500 // End event type match.
1501 }
1502 // End event type lookup.
1503}
1504
1505/// Returns severity for one child state.
1506///
1507/// # Arguments
1508///
1509/// - `child`: Child row.
1510///
1511/// # Returns
1512///
1513/// Returns a dashboard severity label.
1514fn severity(child: &DemoChild) -> &'static str {
1515 // Match lifecycle state.
1516 match child.lifecycle.as_str() {
1517 // Failed and quarantined rows are errors.
1518 "failed" | "quarantined" => "error",
1519 // Restarting rows are warnings.
1520 "restarting" => "warning",
1521 // Other rows are informational.
1522 _ => "info",
1523 // End severity match.
1524 }
1525 // End severity lookup.
1526}
1527
1528/// Returns last failure for failed children.
1529///
1530/// # Arguments
1531///
1532/// - `child`: Child row.
1533///
1534/// # Returns
1535///
1536/// Returns an optional failure string.
1537fn last_failure(child: &DemoChild) -> Option<String> {
1538 // Return failure text for failed rows.
1539 if child.lifecycle == "failed" {
1540 // Return the failure string.
1541 Some("duplicate event window exceeded".to_owned())
1542 // End failed branch.
1543 } else {
1544 // Return no failure for other rows.
1545 None
1546 // End non-failed branch.
1547 }
1548 // End failure lookup.
1549}
1550
1551/// Returns last policy decision for active policy rows.
1552///
1553/// # Arguments
1554///
1555/// - `child`: Child row.
1556///
1557/// # Returns
1558///
1559/// Returns an optional policy decision string.
1560fn last_policy_decision(child: &DemoChild) -> Option<String> {
1561 // Return policy summary for notable states.
1562 if child.lifecycle == "failed" || child.lifecycle == "quarantined" {
1563 // Return quarantine policy.
1564 Some("quarantine".to_owned())
1565 // End policy branch.
1566 } else if child.lifecycle == "restarting" {
1567 // Return restart policy.
1568 Some("restart".to_owned())
1569 // End restart branch.
1570 } else {
1571 // Return no policy summary.
1572 None
1573 // End default branch.
1574 }
1575 // End policy lookup.
1576}
1577
1578/// Builds a validation error for the scenario.
1579///
1580/// # Arguments
1581///
1582/// - `target_id`: Target process identifier.
1583/// - `message`: Validation message.
1584///
1585/// # Returns
1586///
1587/// Returns a dashboard validation error.
1588fn validation(target_id: &str, message: &str) -> DashboardError {
1589 // Create a target-scoped validation error.
1590 DashboardError::validation("command_validate", Some(target_id.to_owned()), message)
1591 // End validation construction.
1592}
1593
1594/// Reads current wall-clock time as Unix nanoseconds.
1595///
1596/// # Arguments
1597///
1598/// This function has no arguments.
1599///
1600/// # Returns
1601///
1602/// Returns zero when the clock is before the Unix epoch.
1603fn unix_nanos_now() -> u128 {
1604 // Read duration since Unix epoch.
1605 std::time::SystemTime::now()
1606 // Convert the system time into a duration.
1607 .duration_since(std::time::UNIX_EPOCH)
1608 // Fall back to zero on clock skew.
1609 .unwrap_or(std::time::Duration::ZERO)
1610 // Convert to nanoseconds.
1611 .as_nanos()
1612 // End time conversion.
1613}
1614
1615// Compile tests only when the demo example is tested directly.
1616#[cfg(test)]
1617// Group scenario tests with the scenario module.
1618mod tests {
1619 // Import the scenario under test.
1620 use super::DemoScenario;
1621 // Import command model values for command tests.
1622 use rust_supervisor::dashboard::model::{
1623 // Import command kind values.
1624 ControlCommandKind,
1625 // Import command request shape.
1626 ControlCommandRequest,
1627 // Import command target shape.
1628 ControlCommandTarget,
1629 // Import dashboard state shape.
1630 DashboardState,
1631 // End imports.
1632 };
1633
1634 // Define the target identifier used by tests.
1635 const TEST_TARGET_ID: &str = "payments-worker-a";
1636 // Define the target display name used by tests.
1637 const TEST_DISPLAY_NAME: &str = "payments worker a";
1638
1639 /// Verifies that the state payload covers the visible UI surface.
1640 #[test]
1641 /// Runs the state surface test.
1642 fn state_contains_ui_surface() {
1643 // Build the demo scenario.
1644 let scenario = scenario();
1645 // Build the current state.
1646 let state = scenario.state();
1647 // Assert topology has root plus children.
1648 assert!(state.topology.nodes.len() >= 6);
1649 // Assert runtime rows include all demo children.
1650 assert!(state.runtime_state.len() >= 5);
1651 // Assert recent events are visible.
1652 assert!(!state.recent_events.is_empty());
1653 // Assert recent logs are visible.
1654 assert!(!state.recent_logs.is_empty());
1655 // Assert dropped event count is visible.
1656 assert_eq!(state.dropped_event_count, 2);
1657 // Assert dropped log count is visible.
1658 assert_eq!(state.dropped_log_count, 1);
1659 // Assert all lifecycle states are present.
1660 for lifecycle in ["failed", "restarting", "paused", "quarantined", "running"] {
1661 // Check the lifecycle state.
1662 let present = has_lifecycle(&state, lifecycle);
1663 // Assert lifecycle presence.
1664 assert!(present);
1665 // End lifecycle assertion.
1666 }
1667 // End state surface test.
1668 }
1669
1670 /// Verifies that command results preserve command identifiers.
1671 #[test]
1672 /// Runs the command result test.
1673 fn command_result_preserves_command_id() {
1674 // Build the demo scenario.
1675 let scenario = scenario();
1676 // Build a pause command.
1677 let command = command(
1678 // Use pause command.
1679 ControlCommandKind::PauseChild,
1680 // Use command identifier.
1681 "cmd-1",
1682 // Use target child path.
1683 "/root/healthy_worker",
1684 // Mark confirmation present.
1685 true,
1686 // End pause command construction.
1687 );
1688 // Apply the command.
1689 let result = scenario.command_result(command).expect("command result");
1690 // Assert command identifier is preserved.
1691 assert_eq!(result.command_id, "cmd-1");
1692 // Assert command completed.
1693 assert_eq!(result.status, "completed");
1694 // Build updated state.
1695 let state = scenario.state();
1696 // Assert the child was paused.
1697 let paused = has_child_state(&state, "/root/healthy_worker", "paused");
1698 // Assert paused child state.
1699 assert!(paused);
1700 // End command result test.
1701 }
1702
1703 /// Verifies that pause command deltas include UI-consumable runtime rows.
1704 #[test]
1705 /// Runs the pause command delta test.
1706 fn pause_child_delta_contains_runtime_state() {
1707 // Build the demo scenario.
1708 let scenario = scenario();
1709 // Build a pause command.
1710 let command = command(
1711 // Use pause command.
1712 ControlCommandKind::PauseChild,
1713 // Use command identifier.
1714 "cmd-pause",
1715 // Use target child path.
1716 "/root/healthy_worker",
1717 // Mark confirmation present.
1718 true,
1719 // End pause command construction.
1720 );
1721 // Apply the command.
1722 let result = scenario.command_result(command).expect("command result");
1723 // Read the delta.
1724 let delta = result.state_delta.expect("state delta");
1725 // Check the paused child appears in delta.
1726 let paused = delta_runtime_has_child_state(&delta, "/root/healthy_worker", "paused");
1727 // Assert the paused child appears in delta.
1728 assert!(paused);
1729 // End pause delta test.
1730 }
1731
1732 /// Verifies that remove command deltas update topology and include logs.
1733 #[test]
1734 /// Runs the remove command delta test.
1735 fn remove_child_delta_removes_topology_node_and_logs_command() {
1736 // Build the demo scenario.
1737 let scenario = scenario();
1738 // Build a remove command.
1739 let command = command(
1740 // Use remove command.
1741 ControlCommandKind::RemoveChild,
1742 // Use command identifier.
1743 "cmd-remove",
1744 // Use target child path.
1745 "/root/healthy_worker",
1746 // Mark confirmation present.
1747 true,
1748 // End remove command construction.
1749 );
1750 // Apply the command.
1751 let result = scenario.command_result(command).expect("command result");
1752 // Read the delta.
1753 let delta = result.state_delta.expect("state delta");
1754 // Check removed child is absent.
1755 let removed = !delta_topology_has_node(&delta, "/root/healthy_worker");
1756 // Assert removed child is absent.
1757 assert!(removed);
1758 // Check remove command log exists.
1759 let logged = delta_logs_contain(&delta, "remove_child");
1760 // Assert remove command log exists.
1761 assert!(logged);
1762 // End remove delta test.
1763 }
1764
1765 /// Verifies that command activity describes lifecycle transitions.
1766 #[test]
1767 /// Runs the lifecycle transition test.
1768 fn command_delta_describes_lifecycle_transition() {
1769 // Build the demo scenario.
1770 let scenario = scenario();
1771 // Build a pause command.
1772 let command = command(
1773 // Use pause command.
1774 ControlCommandKind::PauseChild,
1775 // Use command identifier.
1776 "cmd-transition",
1777 // Use target child path.
1778 "/root/healthy_worker",
1779 // Mark confirmation present.
1780 true,
1781 // End pause command construction.
1782 );
1783 // Apply the command.
1784 let result = scenario.command_result(command).expect("command result");
1785 // Read the delta.
1786 let delta = result.state_delta.expect("state delta");
1787 // Assert the previous lifecycle is present.
1788 assert_eq!(
1789 // Read previous lifecycle from event payload.
1790 delta["recent_events"][0]["payload"]["previous_lifecycle_state"].as_str(),
1791 // Compare expected previous lifecycle.
1792 Some("running"),
1793 // End previous lifecycle assertion.
1794 );
1795 // Assert the current lifecycle is present.
1796 assert_eq!(
1797 // Read current lifecycle from event payload.
1798 delta["recent_events"][0]["payload"]["lifecycle_state"].as_str(),
1799 // Compare expected current lifecycle.
1800 Some("paused"),
1801 // End current lifecycle assertion.
1802 );
1803 // Read the log message.
1804 let message = delta["recent_logs"][0]["message"].as_str().unwrap_or_default();
1805 // Assert the log message describes the transition.
1806 assert!(message.contains("running to paused"));
1807 // Assert structured log fields describe the previous lifecycle.
1808 assert_eq!(
1809 // Read previous lifecycle from log fields.
1810 delta["recent_logs"][0]["fields"]["previous_lifecycle_state"].as_str(),
1811 // Compare expected previous lifecycle.
1812 Some("running"),
1813 // End previous log lifecycle assertion.
1814 );
1815 // Assert structured log fields describe the current lifecycle.
1816 assert_eq!(
1817 // Read current lifecycle from log fields.
1818 delta["recent_logs"][0]["fields"]["lifecycle_state"].as_str(),
1819 // Compare expected current lifecycle.
1820 Some("paused"),
1821 // End current log lifecycle assertion.
1822 );
1823 // End transition delta test.
1824 }
1825
1826 /// Verifies that add-child requires a manifest.
1827 #[test]
1828 /// Runs the add-child validation test.
1829 fn add_child_requires_manifest() {
1830 // Build the demo scenario.
1831 let scenario = scenario();
1832 // Build an add-child command without a manifest.
1833 let command = command(
1834 // Use add-child command.
1835 ControlCommandKind::AddChild,
1836 // Use command identifier.
1837 "cmd-2",
1838 // Use target child path.
1839 "/root/new_worker",
1840 // Mark confirmation present.
1841 true,
1842 // End add-child command construction.
1843 );
1844 // Apply the command.
1845 let error = scenario
1846 // Apply command.
1847 .command_result(command)
1848 // Expect validation error.
1849 .expect_err("validation error");
1850 // Assert validation failure is returned.
1851 assert_eq!(error.code, "validation_failed");
1852 // End validation test.
1853 }
1854
1855 /// Builds a demo command request.
1856 ///
1857 /// # Arguments
1858 ///
1859 /// - `kind`: Command kind.
1860 /// - `command_id`: Command identifier.
1861 /// - `child_path`: Child path.
1862 /// - `confirmed`: Confirmation flag.
1863 ///
1864 /// # Returns
1865 ///
1866 /// Returns a command request.
1867 fn command(
1868 // Accept command kind.
1869 kind: ControlCommandKind,
1870 // Accept command identifier.
1871 command_id: &str,
1872 // Accept child path.
1873 child_path: &str,
1874 // Accept confirmation flag.
1875 confirmed: bool,
1876 // End command helper signature.
1877 ) -> ControlCommandRequest {
1878 // Build command request.
1879 ControlCommandRequest {
1880 // Include command identifier.
1881 command_id: command_id.to_owned(),
1882 // Include target identifier.
1883 target_id: TEST_TARGET_ID.to_owned(),
1884 // Include command kind.
1885 command: kind,
1886 // Include child target.
1887 target: ControlCommandTarget {
1888 // Include child path.
1889 child_path: Some(child_path.to_owned()),
1890 // Omit child manifest.
1891 child_manifest: None,
1892 // End command target.
1893 },
1894 // Include reason.
1895 reason: "demo test".to_owned(),
1896 // Include requester identity.
1897 requested_by: "tester".to_owned(),
1898 // Include confirmation.
1899 confirmed,
1900 // Include request time.
1901 requested_at_unix_nanos: 1,
1902 // End command request.
1903 }
1904 // End command construction.
1905 }
1906
1907 /// Builds a default test scenario.
1908 ///
1909 /// # Arguments
1910 ///
1911 /// This function has no arguments.
1912 ///
1913 /// # Returns
1914 ///
1915 /// Returns a demo scenario.
1916 fn scenario() -> DemoScenario {
1917 // Build the test scenario.
1918 DemoScenario::new(TEST_TARGET_ID.to_owned(), TEST_DISPLAY_NAME.to_owned())
1919 // End test scenario construction.
1920 }
1921
1922 /// Checks whether a delta contains one runtime row with a lifecycle.
1923 fn delta_runtime_has_child_state(
1924 // Accept delta value.
1925 delta: &serde_json::Value,
1926 // Accept child path.
1927 child_path: &str,
1928 // Accept lifecycle.
1929 lifecycle: &str,
1930 // End helper signature.
1931 ) -> bool {
1932 // Access runtime state.
1933 let Some(rows) = delta.get("runtime_state").and_then(|value| value.as_array()) else {
1934 // Return absence.
1935 return false;
1936 // End missing runtime state branch.
1937 };
1938 // Scan runtime rows.
1939 rows.iter().any(|row| {
1940 // Compare child path.
1941 row.get("child_path").and_then(|value| value.as_str()) == Some(child_path)
1942 // Compare lifecycle state.
1943 && row.get("lifecycle_state").and_then(|value| value.as_str()) == Some(lifecycle)
1944 // End runtime row predicate.
1945 })
1946 // End runtime delta lookup.
1947 }
1948
1949 /// Checks whether a delta topology contains one node path.
1950 fn delta_topology_has_node(
1951 // Accept delta value.
1952 delta: &serde_json::Value,
1953 // Accept node path.
1954 path: &str,
1955 // End helper signature.
1956 ) -> bool {
1957 // Access topology nodes.
1958 let Some(nodes) = delta
1959 // Access topology.
1960 .get("topology")
1961 // Access node list.
1962 .and_then(|value| value.get("nodes"))
1963 // Convert to array.
1964 .and_then(|value| value.as_array())
1965 // Handle missing topology.
1966 else {
1967 // Return absence.
1968 return false;
1969 // End missing topology branch.
1970 };
1971 // Scan topology nodes.
1972 nodes
1973 // Iterate nodes.
1974 .iter()
1975 // Match path.
1976 .any(|node| node.get("path").and_then(|value| value.as_str()) == Some(path))
1977 // End topology lookup.
1978 }
1979
1980 /// Checks whether a delta log message contains text.
1981 fn delta_logs_contain(
1982 // Accept delta value.
1983 delta: &serde_json::Value,
1984 // Accept expected text.
1985 expected: &str,
1986 // End helper signature.
1987 ) -> bool {
1988 // Access recent logs.
1989 let Some(logs) = delta
1990 // Access recent logs.
1991 .get("recent_logs")
1992 // Convert to array.
1993 .and_then(|value| value.as_array())
1994 // Handle missing logs.
1995 else {
1996 // Return absence.
1997 return false;
1998 // End missing log branch.
1999 };
2000 // Scan log rows.
2001 logs
2002 // Iterate logs.
2003 .iter()
2004 // Match message text.
2005 .any(|log| {
2006 // Read message.
2007 log.get("message")
2008 // Convert to string.
2009 .and_then(|value| value.as_str())
2010 // Match expected text.
2011 .is_some_and(|message| message.contains(expected))
2012 // End log predicate.
2013 })
2014 // End log lookup.
2015 }
2016
2017 /// Checks whether a lifecycle appears in state.
2018 ///
2019 /// # Arguments
2020 ///
2021 /// - `state`: Dashboard state.
2022 /// - `lifecycle`: Lifecycle label.
2023 ///
2024 /// # Returns
2025 ///
2026 /// Returns true when a runtime row has the lifecycle.
2027 fn has_lifecycle(state: &DashboardState, lifecycle: &str) -> bool {
2028 // Scan runtime rows.
2029 state
2030 // Access runtime rows.
2031 .runtime_state
2032 // Iterate rows.
2033 .iter()
2034 // Match lifecycle.
2035 .any(|row| row.lifecycle_state == lifecycle)
2036 // End lifecycle lookup.
2037 }
2038
2039 /// Checks whether a child has a lifecycle.
2040 ///
2041 /// # Arguments
2042 ///
2043 /// - `state`: Dashboard state.
2044 /// - `child_path`: Child path.
2045 /// - `lifecycle`: Lifecycle label.
2046 ///
2047 /// # Returns
2048 ///
2049 /// Returns true when the child row has the lifecycle.
2050 fn has_child_state(state: &DashboardState, child_path: &str, lifecycle: &str) -> bool {
2051 // Scan runtime rows.
2052 state
2053 // Access runtime rows.
2054 .runtime_state
2055 // Iterate rows.
2056 .iter()
2057 // Match child path and lifecycle.
2058 .any(|row| row.child_path == child_path && row.lifecycle_state == lifecycle)
2059 // End child state lookup.
2060 }
2061 // End scenario tests.
2062}