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 let buf = crate::commands::dashboard::test_support::rendered_buffer(&terminal);
619 assert!(buf.contains("Agents"), "{buf}");
620 }
621
622 #[test]
623 fn dashboard_agent_struct_fields_from_mod() {
624 let agent = DashboardAgent {
625 id: "run-test".to_string(),
626 blueprint_name: "tester".to_string(),
627 stage: "init".to_string(),
628 stage_index: 0,
629 num_stages: 1,
630 status: AgentDisplayStatus::Idle,
631 tokens_in: 0,
632 tokens_out: 0,
633 cached_tokens: 0,
634 iteration: 0,
635 waiting_prompt: None,
636 pending_request: None,
637 last_answered_request_id: None,
638 context_snapshot: None,
639 stages: vec![],
640 workdir: "/tmp".to_string(),
641 task: "test task".to_string(),
642 title: None,
643 model: None,
644 parent_id: None,
645 depth: 0,
646 started_at: 0,
647 last_progress_at: None,
648 active_until: None,
649 waiting_secs: 0,
650 graph_info: None,
651 accepts_messages: false,
652 taint_summary: vec![],
653 };
654 assert_eq!(agent.id, "run-test");
655 assert_eq!(agent.blueprint_name, "tester");
656 assert_eq!(agent.stage, "init");
657 }
658
659 use crate::tui::{TestBackendHarness, TestEventSource, TestSetup, key, test_terminal};
671 use crossterm::event::KeyCode;
672
673 #[tokio::test]
674 async fn run_dashboard_loop_quits_on_q_from_main_list() {
675 let mut dashboard = make_test_dashboard();
676 let control = no_daemon_control();
677 let mut terminal = test_terminal();
678 let mouse = |kind, column, row| {
683 Event::Mouse(crossterm::event::MouseEvent {
684 kind,
685 column,
686 row,
687 modifiers: crossterm::event::KeyModifiers::NONE,
688 })
689 };
690 use crossterm::event::{MouseButton, MouseEventKind};
691 let mut events = TestEventSource::new(vec![
692 Event::Resize(80, 24),
693 mouse(MouseEventKind::ScrollUp, 0, 0),
694 mouse(MouseEventKind::ScrollDown, 0, 0),
695 mouse(MouseEventKind::Moved, 0, 0),
698 mouse(MouseEventKind::Down(MouseButton::Left), 3, 15),
699 mouse(MouseEventKind::Drag(MouseButton::Left), 20, 16),
700 mouse(MouseEventKind::Up(MouseButton::Left), 20, 16),
701 key(KeyCode::Char('q')),
702 ]);
703
704 let result = run_dashboard_loop(
705 &mut dashboard,
706 &control,
707 &mut terminal,
708 &mut events,
709 Duration::from_millis(1),
710 )
711 .await;
712
713 assert!(result.is_ok());
714 assert!(dashboard.should_quit);
715 }
716
717 #[tokio::test]
718 async fn run_dashboard_loop_no_event_tick_then_quits() {
719 let mut dashboard = make_test_dashboard();
723 let control = no_daemon_control();
724 let mut terminal = test_terminal();
725 let mut events = TestEventSource::new_with_nones(vec![None, Some(key(KeyCode::Char('q')))]);
728
729 let result = run_dashboard_loop(
730 &mut dashboard,
731 &control,
732 &mut terminal,
733 &mut events,
734 Duration::from_millis(1),
735 )
736 .await;
737
738 assert!(result.is_ok());
739 assert!(dashboard.should_quit);
740 }
741
742 #[tokio::test]
743 async fn run_dashboard_loop_ignores_non_press_and_other_events() {
744 let mut dashboard = make_test_dashboard();
747 let control = no_daemon_control();
748 let mut terminal = test_terminal();
749 let release = Event::Key(crossterm::event::KeyEvent::new_with_kind(
750 KeyCode::Char('x'),
751 crossterm::event::KeyModifiers::empty(),
752 crossterm::event::KeyEventKind::Release,
753 ));
754 let mut events =
755 TestEventSource::new(vec![release, Event::FocusGained, key(KeyCode::Char('q'))]);
756
757 let result = run_dashboard_loop(
758 &mut dashboard,
759 &control,
760 &mut terminal,
761 &mut events,
762 Duration::from_millis(1),
763 )
764 .await;
765
766 assert!(result.is_ok());
767 assert!(dashboard.should_quit);
768 }
769
770 #[tokio::test]
771 async fn run_dashboard_loop_propagates_event_source_error() {
772 let mut dashboard = make_test_dashboard();
773 let control = no_daemon_control();
774 let mut terminal = test_terminal();
775 let mut events = TestEventSource::failing();
776
777 let result = run_dashboard_loop(
778 &mut dashboard,
779 &control,
780 &mut terminal,
781 &mut events,
782 Duration::from_millis(1),
783 )
784 .await;
785
786 assert!(result.is_err());
787 }
788
789 #[tokio::test]
808 async fn run_dashboard_loop_propagates_draw_error() {
809 let mut dashboard = make_test_dashboard();
814 let control = no_daemon_control();
815 let mut terminal = Terminal::new(TestBackendHarness::failing(120, 40)).unwrap();
816 let mut events = TestEventSource::new(vec![]); let result = run_dashboard_loop(
819 &mut dashboard,
820 &control,
821 &mut terminal,
822 &mut events,
823 Duration::from_millis(1),
824 )
825 .await;
826
827 assert!(result.is_err());
828 }
829
830 #[tokio::test]
854 async fn execute_core_happy_path_quits_on_esc() {
855 crate::runstate::with_isolated_runs_dir_async(
856 "execute_core_happy_path_quits_on_esc",
857 |_d| async move {
858 let control = no_daemon_control();
859 let mut dashboard = init_dashboard(control.clone(), |_| false);
860 let mut setup = TestSetup::new();
861 let mut events = TestEventSource::new(vec![key(KeyCode::Char('q'))]);
862 let result = execute_core(&mut dashboard, &control, &mut setup, &mut events).await;
863 assert!(result.is_ok());
864 assert!(dashboard.should_quit);
865 },
866 )
867 .await;
868 }
869
870 #[tokio::test]
871 async fn execute_with_loads_config_inits_and_runs_the_loop() {
872 crate::config::with_isolated_config_path_async(
877 "execute_with_dashboard",
878 |_fake_dir| async move {
879 crate::runstate::with_isolated_runs_dir_async(
880 "execute_with_dashboard",
881 |_d| async move {
882 let mut setup = TestSetup::new();
883 let mut events = TestEventSource::new(vec![key(KeyCode::Char('q'))]);
884 let result =
885 execute_with(no_daemon_control(), &mut setup, &mut events, |_| false)
886 .await;
887 assert!(result.is_ok());
888 },
889 )
890 .await;
891 },
892 )
893 .await;
894 }
895
896 #[tokio::test]
897 async fn execute_core_enable_error_propagates() {
898 crate::runstate::with_isolated_runs_dir_async(
899 "execute_core_enable_error_propagates",
900 |_d| async move {
901 let control = no_daemon_control();
902 let mut dashboard = init_dashboard(control.clone(), |_| false);
903 let mut setup = TestSetup {
905 enable_should_fail: true,
906 create_should_fail: false,
907 draw_should_fail: false,
908 };
909 let mut events = TestEventSource::new(vec![]);
910 let result = execute_core(&mut dashboard, &control, &mut setup, &mut events).await;
911 assert!(result.is_err());
912 },
913 )
914 .await;
915 }
916
917 #[tokio::test]
918 async fn execute_core_create_terminal_error_propagates() {
919 crate::runstate::with_isolated_runs_dir_async(
920 "execute_core_create_terminal_error_propagates",
921 |_d| async move {
922 let control = no_daemon_control();
923 let mut dashboard = init_dashboard(control.clone(), |_| false);
924 let mut setup = TestSetup {
927 enable_should_fail: false,
928 create_should_fail: true,
929 draw_should_fail: false,
930 };
931 let mut events = TestEventSource::new(vec![]);
932 let result = execute_core(&mut dashboard, &control, &mut setup, &mut events).await;
933 assert!(result.is_err());
934 },
935 )
936 .await;
937 }
938
939 #[tokio::test]
940 async fn execute_core_loop_error_propagates() {
941 crate::runstate::with_isolated_runs_dir_async(
942 "execute_core_loop_error_propagates",
943 |_d| async move {
944 let control = no_daemon_control();
945 let mut dashboard = init_dashboard(control.clone(), |_| false);
946 let mut setup = TestSetup::new();
947 let mut events = TestEventSource::failing();
948 let result = execute_core(&mut dashboard, &control, &mut setup, &mut events).await;
949 assert!(result.is_err());
950 },
951 )
952 .await;
953 }
954
955 }