1mod context_tree;
4mod graph;
5mod graph_layout;
6mod helpers;
7mod history;
8mod input;
9mod mcp;
10mod render;
11mod selection;
12mod state;
13#[cfg(test)]
14mod test_support;
15mod types;
16
17use crate::tui::theme;
22
23pub use helpers::yank_to_clipboard_via;
24pub use types::{AgentDisplayStatus, DashboardAgent, DashboardArgs};
25
26pub use crate::tui::{CrosstermEventSource, EventSource, TerminalSetup};
31
32use crossterm::event::{Event, KeyEventKind};
33use leviath_runtime::control_socket::{ControlClient, ControlRequest, ControlResponse};
34use ratatui::Terminal;
35use std::time::Duration;
36use tokio::sync::mpsc;
37
38use state::Dashboard;
39use types::DaemonCommand;
40
41async fn daemon_background_loop(
45 control: ControlClient,
46 mut cmd_rx: mpsc::UnboundedReceiver<DaemonCommand>,
47 outcomes: mpsc::UnboundedSender<types::DaemonOutcome>,
48) {
49 while let Some(cmd) = cmd_rx.recv().await {
50 let (run_id, request, what) = match cmd {
51 DaemonCommand::Cancel { run_id } => {
52 (run_id.clone(), ControlRequest::Cancel { run_id }, "cancel")
53 }
54 DaemonCommand::Pause { run_id } => {
55 (run_id.clone(), ControlRequest::Pause { run_id }, "pause")
56 }
57 DaemonCommand::Resume { run_id } => {
58 (run_id.clone(), ControlRequest::Resume { run_id }, "resume")
59 }
60 DaemonCommand::Answer { response } => (
61 response.request_id.clone(),
62 ControlRequest::AnswerInteraction { response },
63 "answer",
64 ),
65 DaemonCommand::Message { agent_id, content } => (
66 agent_id.clone(),
67 ControlRequest::Message {
68 agent_id,
69 content,
70 target_region: None,
71 },
72 "message",
73 ),
74 };
75 let outcome = match control.request(&request).await {
78 Ok(ControlResponse::Ok { ok: true }) => types::DaemonOutcome {
79 run_id,
80 message: String::new(),
81 ok: true,
82 },
83 Ok(ControlResponse::Ok { ok: false }) => types::DaemonOutcome {
84 run_id,
85 message: format!("the daemon has no such run to {what}"),
86 ok: false,
87 },
88 Ok(other) => types::DaemonOutcome {
89 run_id,
90 message: format!("unexpected daemon response to {what}: {other:?}"),
91 ok: false,
92 },
93 Err(e) => types::DaemonOutcome {
94 run_id,
95 message: format!("{what} failed: {e}"),
96 ok: false,
97 },
98 };
99 if outcomes.send(outcome).is_err() {
101 return;
102 }
103 }
104}
105
106async fn execute_core<S: TerminalSetup, E: EventSource>(
116 dashboard: &mut Dashboard,
117 control: &ControlClient,
118 setup: &mut S,
119 events: &mut E,
120) -> anyhow::Result<()> {
121 setup.enable()?;
122 let mut terminal = setup.create_terminal()?;
123 let tick_rate = Duration::from_millis(100);
124 run_dashboard_loop(dashboard, control, &mut terminal, events, tick_rate).await?;
125 setup.disable();
126 setup.print_done();
127 Ok(())
128}
129
130async fn run_dashboard_loop<B: ratatui::backend::Backend>(
152 dashboard: &mut Dashboard,
153 control: &ControlClient,
154 terminal: &mut Terminal<B>,
155 events: &mut impl EventSource,
156 tick_rate: Duration,
157) -> anyhow::Result<()> {
158 loop {
159 dashboard.tick_count += 1;
160 dashboard.tick_toasts();
161
162 dashboard.sync_interactions(control).await;
165
166 dashboard.sync_daemon_runs(control).await;
169
170 dashboard.sync_from_run_state();
173
174 dashboard.drain_mcp_outcomes();
176
177 dashboard.drain_daemon_outcomes();
179
180 terminal
182 .draw(|frame| dashboard.draw(frame))
183 .map_err(|e| anyhow::anyhow!("terminal draw failed: {e}"))?;
184
185 if let Some(event) = events.poll_event(tick_rate)? {
187 match event {
188 Event::Key(key) if key.kind == KeyEventKind::Press => {
189 dashboard.handle_key(key);
190 }
191 Event::Mouse(m) => dashboard.handle_mouse(m),
194 Event::Resize(_, _) => {
195 }
197 _ => {}
198 }
199 }
200
201 if dashboard.should_quit {
202 return Ok(());
203 }
204 }
205}
206
207fn init_dashboard(control: ControlClient, yank_fn: fn(&str) -> bool) -> Dashboard {
212 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
213 let mcp_ctx = types::McpContext {
216 config_path: crate::config::Config::config_path(),
217 store_path: leviath_mcp::AuthStore::default_path().unwrap_or_default(),
218 opener: std::sync::Arc::new(leviath_sys::open_url),
219 clock: mcp_system_now,
220 };
221 let mut dashboard = Dashboard::new_with_log_path(
222 cmd_tx,
223 crate::runstate::dashboard_log_path(),
224 yank_fn,
225 mcp_ctx,
226 );
227
228 let daemon_outcome_tx = dashboard
232 .take_daemon_outcome_tx()
233 .expect("a fresh dashboard has its daemon outcome sender");
234 tokio::spawn(daemon_background_loop(control, cmd_rx, daemon_outcome_tx));
235
236 let (mcp_cmd_rx, mcp_outcome_tx) = dashboard
239 .take_mcp_bg_ends()
240 .expect("a fresh dashboard has its MCP background channel ends");
241 tokio::spawn(mcp::mcp_background_loop(
242 dashboard.mcp_context(),
243 mcp_cmd_rx,
244 mcp_outcome_tx,
245 ));
246
247 dashboard.add_log("Dashboard started. Use `lev run <agent>` to start an agent.".to_string());
248
249 dashboard
250}
251
252fn mcp_system_now() -> u64 {
254 std::time::SystemTime::now()
255 .duration_since(std::time::UNIX_EPOCH)
256 .map(|d| d.as_secs())
257 .unwrap_or(0)
258}
259
260pub async fn execute_with<S: TerminalSetup, E: EventSource>(
276 control: ControlClient,
277 setup: &mut S,
278 events: &mut E,
279 yank_fn: fn(&str) -> bool,
280) -> anyhow::Result<()> {
281 let mut dashboard = init_dashboard(control.clone(), yank_fn);
282 execute_core(&mut dashboard, &control, setup, events).await
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 use crate::commands::dashboard::test_support::make_test_dashboard;
290
291 #[test]
292 fn dashboard_args_can_be_constructed() {
293 let _args = DashboardArgs {};
294 }
295
296 #[test]
297 fn mcp_system_now_advances_past_the_epoch() {
298 assert!(mcp_system_now() > 1_600_000_000);
299 }
300
301 #[test]
302 fn agent_display_status_variants_display() {
303 let statuses = vec![
304 AgentDisplayStatus::Active,
305 AgentDisplayStatus::Waiting,
306 AgentDisplayStatus::Complete,
307 AgentDisplayStatus::CompleteInteractive,
308 AgentDisplayStatus::Error("test error".to_string()),
309 AgentDisplayStatus::Idle,
310 AgentDisplayStatus::Cancelled,
311 ];
312 for status in statuses {
313 let display = format!("{}", status);
314 assert!(!display.is_empty());
315 }
316 }
317
318 fn no_daemon_control() -> ControlClient {
321 let dir = std::env::temp_dir().join("leviath-dash-no-daemon");
322 ControlClient::new(leviath_runtime::control_socket::control_id(&dir))
323 }
324
325 fn recording_daemon(dir: &std::path::Path) -> (ControlClient, tokio::task::JoinHandle<String>) {
328 use leviath_runtime::control_socket::{bind_control_listener, control_id};
329 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
330 let id = control_id(dir);
331 let mut listener = bind_control_listener(&id).unwrap();
332 let handle = tokio::spawn(async move {
333 let stream = listener
334 .accept()
335 .await
336 .expect("accept succeeds")
337 .expect("our own connection is admitted");
338 let (read_half, mut write_half) = tokio::io::split(stream);
339 let mut lines = BufReader::new(read_half).lines();
340 let req = lines.next_line().await.unwrap().unwrap_or_default();
341 write_half
342 .write_all(b"{\"result\":\"ok\",\"ok\":true}\n")
343 .await
344 .unwrap();
345 req
346 });
347 (ControlClient::new(id), handle)
348 }
349
350 fn replying_daemon(
353 dir: &std::path::Path,
354 reply: Option<&'static str>,
355 ) -> (ControlClient, tokio::task::JoinHandle<()>) {
356 use leviath_runtime::control_socket::{bind_control_listener, control_id};
357 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
358 let id = control_id(dir);
359 let mut listener = bind_control_listener(&id).unwrap();
360 let handle = tokio::spawn(async move {
361 let stream = listener
362 .accept()
363 .await
364 .expect("accept succeeds")
365 .expect("our own connection is admitted");
366 let (read_half, mut write_half) = tokio::io::split(stream);
367 let mut lines = BufReader::new(read_half).lines();
368 let _ = lines.next_line().await;
369 if let Some(reply) = reply {
370 let _ = write_half.write_all(format!("{reply}\n").as_bytes()).await;
371 }
372 });
373 (ControlClient::new(id), handle)
374 }
375
376 async fn cancel_outcome(reply: Option<&'static str>) -> types::DaemonOutcome {
378 let dir = tempfile::tempdir().unwrap();
379 let (control, server) = replying_daemon(dir.path(), reply);
380 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<DaemonCommand>();
381 let (out_tx, mut out_rx) = mpsc::unbounded_channel();
382 tokio::spawn(daemon_background_loop(control, cmd_rx, out_tx));
383 cmd_tx
384 .send(DaemonCommand::Cancel {
385 run_id: "run-1".to_string(),
386 })
387 .unwrap();
388 let outcome = tokio::time::timeout(std::time::Duration::from_secs(5), out_rx.recv())
389 .await
390 .expect("an outcome was reported")
391 .expect("the loop is alive");
392 let _ = server.await;
393 outcome
394 }
395
396 #[tokio::test]
399 async fn daemon_background_loop_reports_each_outcome() {
400 let ok = cancel_outcome(Some(r#"{"result":"ok","ok":true}"#)).await;
401 assert!(ok.ok, "an applied cancel is reported as success");
402 assert_eq!(ok.run_id, "run-1");
403
404 let missing = cancel_outcome(Some(r#"{"result":"ok","ok":false}"#)).await;
405 assert!(!missing.ok);
406 assert!(missing.message.contains("no such run to cancel"));
407
408 let odd = cancel_outcome(Some(r#"{"result":"spawned","run_id":"x"}"#)).await;
409 assert!(!odd.ok);
410 assert!(odd.message.contains("unexpected daemon response"));
411
412 let broken = cancel_outcome(None).await;
414 assert!(!broken.ok);
415 assert!(
416 broken.message.contains("cancel failed"),
417 "got: {}",
418 broken.message
419 );
420 }
421
422 async fn command_outcome(
424 cmd: DaemonCommand,
425 reply: Option<&'static str>,
426 ) -> types::DaemonOutcome {
427 let dir = tempfile::tempdir().unwrap();
428 let (control, server) = replying_daemon(dir.path(), reply);
429 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<DaemonCommand>();
430 let (out_tx, mut out_rx) = mpsc::unbounded_channel();
431 tokio::spawn(daemon_background_loop(control, cmd_rx, out_tx));
432 cmd_tx.send(cmd).unwrap();
433 let outcome = tokio::time::timeout(std::time::Duration::from_secs(5), out_rx.recv())
434 .await
435 .expect("an outcome was reported")
436 .expect("the loop is alive");
437 let _ = server.await;
438 outcome
439 }
440
441 #[tokio::test]
444 async fn daemon_background_loop_forwards_pause_and_resume() {
445 let ok = command_outcome(
446 DaemonCommand::Pause {
447 run_id: "run-1".to_string(),
448 },
449 Some(r#"{"result":"ok","ok":true}"#),
450 )
451 .await;
452 assert!(ok.ok);
453 assert_eq!(ok.run_id, "run-1");
454
455 let refused = command_outcome(
456 DaemonCommand::Pause {
457 run_id: "run-1".to_string(),
458 },
459 Some(r#"{"result":"ok","ok":false}"#),
460 )
461 .await;
462 assert!(!refused.ok);
463 assert!(refused.message.contains("no such run to pause"));
464
465 let ok = command_outcome(
466 DaemonCommand::Resume {
467 run_id: "run-1".to_string(),
468 },
469 Some(r#"{"result":"ok","ok":true}"#),
470 )
471 .await;
472 assert!(ok.ok);
473
474 let refused = command_outcome(
475 DaemonCommand::Resume {
476 run_id: "run-1".to_string(),
477 },
478 Some(r#"{"result":"ok","ok":false}"#),
479 )
480 .await;
481 assert!(!refused.ok);
482 assert!(refused.message.contains("no such run to resume"));
483 }
484
485 #[tokio::test]
488 async fn daemon_background_loop_exits_when_the_dashboard_is_gone() {
489 let dir = tempfile::tempdir().unwrap();
490 let (control, _server) = replying_daemon(dir.path(), Some(r#"{"result":"ok","ok":true}"#));
491 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<DaemonCommand>();
492 let (out_tx, out_rx) = mpsc::unbounded_channel();
493 drop(out_rx); let handle = tokio::spawn(daemon_background_loop(control, cmd_rx, out_tx));
495 cmd_tx
496 .send(DaemonCommand::Cancel {
497 run_id: "run-1".to_string(),
498 })
499 .unwrap();
500 tokio::time::timeout(std::time::Duration::from_secs(5), handle)
501 .await
502 .expect("the loop returned")
503 .unwrap();
504 }
505
506 #[tokio::test]
509 async fn init_dashboard_seeds_startup_log_and_forwards_commands() {
510 crate::runstate::with_isolated_runs_dir_async(
511 "init_dashboard_seeds_startup_log",
512 |_d| async move {
513 let dashboard = init_dashboard(no_daemon_control(), |_| false);
514 assert!(
515 dashboard
516 .log
517 .iter()
518 .any(|entry| entry.message.contains("Dashboard started"))
519 );
520 dashboard
523 .cmd_tx
524 .send(DaemonCommand::Cancel {
525 run_id: "nope".to_string(),
526 })
527 .unwrap();
528 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
529 },
530 )
531 .await;
532 }
533
534 #[tokio::test]
537 async fn daemon_background_loop_forwards_cancel() {
538 let dir = tempfile::tempdir().unwrap();
539 let (control, server) = recording_daemon(dir.path());
540 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<DaemonCommand>();
541 let (out_tx, _out_rx) = mpsc::unbounded_channel();
542 tokio::spawn(daemon_background_loop(control, cmd_rx, out_tx));
543 cmd_tx
544 .send(DaemonCommand::Cancel {
545 run_id: "run-1".to_string(),
546 })
547 .unwrap();
548 let req = server.await.unwrap();
549 assert!(req.contains("cancel"));
550 assert!(req.contains("run-1"));
551 }
552
553 #[tokio::test]
554 async fn daemon_background_loop_forwards_answer() {
555 let dir = tempfile::tempdir().unwrap();
556 let (control, server) = recording_daemon(dir.path());
557 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<DaemonCommand>();
558 let (out_tx, _out_rx) = mpsc::unbounded_channel();
559 tokio::spawn(daemon_background_loop(control, cmd_rx, out_tx));
560 cmd_tx
561 .send(DaemonCommand::Answer {
562 response: leviath_core::interaction::InteractionResponse::text("q1", "yes"),
563 })
564 .unwrap();
565 let req = server.await.unwrap();
566 assert!(req.contains("answer_interaction"));
567 assert!(req.contains("q1"));
568 }
569
570 #[tokio::test]
571 async fn daemon_background_loop_forwards_message() {
572 let dir = tempfile::tempdir().unwrap();
573 let (control, server) = recording_daemon(dir.path());
574 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<DaemonCommand>();
575 let (out_tx, _out_rx) = mpsc::unbounded_channel();
576 tokio::spawn(daemon_background_loop(control, cmd_rx, out_tx));
577 cmd_tx
578 .send(DaemonCommand::Message {
579 agent_id: "a1".to_string(),
580 content: "hi there".to_string(),
581 })
582 .unwrap();
583 let req = server.await.unwrap();
584 assert!(req.contains("message"));
585 assert!(req.contains("hi there"));
586 }
587
588 #[tokio::test]
589 async fn daemon_background_loop_exits_when_channel_dropped() {
590 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<DaemonCommand>();
591 let (out_tx, _out_rx) = mpsc::unbounded_channel();
592 let handle = tokio::spawn(daemon_background_loop(no_daemon_control(), cmd_rx, out_tx));
593 drop(cmd_tx);
594 let result = tokio::time::timeout(std::time::Duration::from_millis(500), handle).await;
595 assert!(result.is_ok());
596 }
597
598 #[test]
601 fn dashboard_new_and_initial_state() {
602 let dash = make_test_dashboard();
603 assert!(!dash.should_quit);
604 assert!(!dash.detail_view);
605 assert!(!dash.show_help);
606 }
607
608 #[test]
609 fn dashboard_draw_renders_without_panic() {
610 use ratatui::Terminal;
611 use ratatui::backend::TestBackend;
612 let backend = TestBackend::new(120, 40);
613 let mut terminal = Terminal::new(backend).unwrap();
614 let mut dash = make_test_dashboard();
615 terminal.draw(|f| dash.draw(f)).unwrap();
616 }
617
618 #[test]
619 fn dashboard_agent_struct_fields_from_mod() {
620 let agent = DashboardAgent {
621 id: "run-test".to_string(),
622 blueprint_name: "tester".to_string(),
623 stage: "init".to_string(),
624 stage_index: 0,
625 num_stages: 1,
626 status: AgentDisplayStatus::Idle,
627 tokens_in: 0,
628 tokens_out: 0,
629 cached_tokens: 0,
630 iteration: 0,
631 waiting_prompt: None,
632 pending_request: None,
633 last_answered_request_id: None,
634 context_snapshot: None,
635 stages: vec![],
636 workdir: "/tmp".to_string(),
637 task: "test task".to_string(),
638 title: None,
639 model: None,
640 parent_id: None,
641 depth: 0,
642 started_at: 0,
643 last_progress_at: None,
644 active_until: None,
645 waiting_secs: 0,
646 graph_info: None,
647 accepts_messages: false,
648 taint_summary: vec![],
649 };
650 assert_eq!(agent.id, "run-test");
651 assert_eq!(agent.blueprint_name, "tester");
652 assert_eq!(agent.stage, "init");
653 }
654
655 use crate::tui::{TestBackendHarness, TestEventSource, TestSetup, key, test_terminal};
667 use crossterm::event::KeyCode;
668
669 #[tokio::test]
670 async fn run_dashboard_loop_quits_on_q_from_main_list() {
671 let mut dashboard = make_test_dashboard();
672 let control = no_daemon_control();
673 let mut terminal = test_terminal();
674 let mouse = |kind, column, row| {
679 Event::Mouse(crossterm::event::MouseEvent {
680 kind,
681 column,
682 row,
683 modifiers: crossterm::event::KeyModifiers::NONE,
684 })
685 };
686 use crossterm::event::{MouseButton, MouseEventKind};
687 let mut events = TestEventSource::new(vec![
688 Event::Resize(80, 24),
689 mouse(MouseEventKind::ScrollUp, 0, 0),
690 mouse(MouseEventKind::ScrollDown, 0, 0),
691 mouse(MouseEventKind::Moved, 0, 0),
694 mouse(MouseEventKind::Down(MouseButton::Left), 3, 15),
695 mouse(MouseEventKind::Drag(MouseButton::Left), 20, 16),
696 mouse(MouseEventKind::Up(MouseButton::Left), 20, 16),
697 key(KeyCode::Char('q')),
698 ]);
699
700 let result = run_dashboard_loop(
701 &mut dashboard,
702 &control,
703 &mut terminal,
704 &mut events,
705 Duration::from_millis(1),
706 )
707 .await;
708
709 assert!(result.is_ok());
710 assert!(dashboard.should_quit);
711 }
712
713 #[tokio::test]
714 async fn run_dashboard_loop_no_event_tick_then_quits() {
715 let mut dashboard = make_test_dashboard();
719 let control = no_daemon_control();
720 let mut terminal = test_terminal();
721 let mut events = TestEventSource::new_with_nones(vec![None, Some(key(KeyCode::Char('q')))]);
724
725 let result = run_dashboard_loop(
726 &mut dashboard,
727 &control,
728 &mut terminal,
729 &mut events,
730 Duration::from_millis(1),
731 )
732 .await;
733
734 assert!(result.is_ok());
735 assert!(dashboard.should_quit);
736 }
737
738 #[tokio::test]
739 async fn run_dashboard_loop_ignores_non_press_and_other_events() {
740 let mut dashboard = make_test_dashboard();
743 let control = no_daemon_control();
744 let mut terminal = test_terminal();
745 let release = Event::Key(crossterm::event::KeyEvent::new_with_kind(
746 KeyCode::Char('x'),
747 crossterm::event::KeyModifiers::empty(),
748 crossterm::event::KeyEventKind::Release,
749 ));
750 let mut events =
751 TestEventSource::new(vec![release, Event::FocusGained, key(KeyCode::Char('q'))]);
752
753 let result = run_dashboard_loop(
754 &mut dashboard,
755 &control,
756 &mut terminal,
757 &mut events,
758 Duration::from_millis(1),
759 )
760 .await;
761
762 assert!(result.is_ok());
763 assert!(dashboard.should_quit);
764 }
765
766 #[tokio::test]
767 async fn run_dashboard_loop_propagates_event_source_error() {
768 let mut dashboard = make_test_dashboard();
769 let control = no_daemon_control();
770 let mut terminal = test_terminal();
771 let mut events = TestEventSource::failing();
772
773 let result = run_dashboard_loop(
774 &mut dashboard,
775 &control,
776 &mut terminal,
777 &mut events,
778 Duration::from_millis(1),
779 )
780 .await;
781
782 assert!(result.is_err());
783 }
784
785 #[tokio::test]
804 async fn run_dashboard_loop_propagates_draw_error() {
805 let mut dashboard = make_test_dashboard();
810 let control = no_daemon_control();
811 let mut terminal = Terminal::new(TestBackendHarness::failing(120, 40)).unwrap();
812 let mut events = TestEventSource::new(vec![]); let result = run_dashboard_loop(
815 &mut dashboard,
816 &control,
817 &mut terminal,
818 &mut events,
819 Duration::from_millis(1),
820 )
821 .await;
822
823 assert!(result.is_err());
824 }
825
826 #[tokio::test]
850 async fn execute_core_happy_path_quits_on_esc() {
851 crate::runstate::with_isolated_runs_dir_async(
852 "execute_core_happy_path_quits_on_esc",
853 |_d| async move {
854 let control = no_daemon_control();
855 let mut dashboard = init_dashboard(control.clone(), |_| false);
856 let mut setup = TestSetup::new();
857 let mut events = TestEventSource::new(vec![key(KeyCode::Char('q'))]);
858 let result = execute_core(&mut dashboard, &control, &mut setup, &mut events).await;
859 assert!(result.is_ok());
860 assert!(dashboard.should_quit);
861 },
862 )
863 .await;
864 }
865
866 #[tokio::test]
867 async fn execute_with_loads_config_inits_and_runs_the_loop() {
868 crate::config::with_isolated_config_path_async(
873 "execute_with_dashboard",
874 |_fake_dir| async move {
875 crate::runstate::with_isolated_runs_dir_async(
876 "execute_with_dashboard",
877 |_d| async move {
878 let mut setup = TestSetup::new();
879 let mut events = TestEventSource::new(vec![key(KeyCode::Char('q'))]);
880 let result =
881 execute_with(no_daemon_control(), &mut setup, &mut events, |_| false)
882 .await;
883 assert!(result.is_ok());
884 },
885 )
886 .await;
887 },
888 )
889 .await;
890 }
891
892 #[tokio::test]
893 async fn execute_core_enable_error_propagates() {
894 crate::runstate::with_isolated_runs_dir_async(
895 "execute_core_enable_error_propagates",
896 |_d| async move {
897 let control = no_daemon_control();
898 let mut dashboard = init_dashboard(control.clone(), |_| false);
899 let mut setup = TestSetup {
901 enable_should_fail: true,
902 create_should_fail: false,
903 };
904 let mut events = TestEventSource::new(vec![]);
905 let result = execute_core(&mut dashboard, &control, &mut setup, &mut events).await;
906 assert!(result.is_err());
907 },
908 )
909 .await;
910 }
911
912 #[tokio::test]
913 async fn execute_core_create_terminal_error_propagates() {
914 crate::runstate::with_isolated_runs_dir_async(
915 "execute_core_create_terminal_error_propagates",
916 |_d| async move {
917 let control = no_daemon_control();
918 let mut dashboard = init_dashboard(control.clone(), |_| false);
919 let mut setup = TestSetup {
922 enable_should_fail: false,
923 create_should_fail: true,
924 };
925 let mut events = TestEventSource::new(vec![]);
926 let result = execute_core(&mut dashboard, &control, &mut setup, &mut events).await;
927 assert!(result.is_err());
928 },
929 )
930 .await;
931 }
932
933 #[tokio::test]
934 async fn execute_core_loop_error_propagates() {
935 crate::runstate::with_isolated_runs_dir_async(
936 "execute_core_loop_error_propagates",
937 |_d| async move {
938 let control = no_daemon_control();
939 let mut dashboard = init_dashboard(control.clone(), |_| false);
940 let mut setup = TestSetup::new();
941 let mut events = TestEventSource::failing();
942 let result = execute_core(&mut dashboard, &control, &mut setup, &mut events).await;
943 assert!(result.is_err());
944 },
945 )
946 .await;
947 }
948
949 }