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