1mod construct;
4mod context_tree;
5mod graph;
6mod graph_layout;
7mod helpers;
8mod history;
9mod input;
10mod mcp;
11mod new_run;
12mod render;
13mod selection;
14mod state;
15#[cfg(test)]
16mod test_support;
17mod types;
18
19use crate::tui::theme;
24
25pub use helpers::yank_to_clipboard_via;
26pub use types::{AgentDisplayStatus, DashboardAgent, DashboardArgs};
27
28pub use crate::tui::{CrosstermEventSource, EventSource, TerminalSetup};
33
34use crossterm::event::{Event, KeyEventKind};
35use leviath_runtime::control_socket::{ControlClient, ControlRequest, ControlResponse};
36use ratatui::Terminal;
37use std::time::Duration;
38use tokio::sync::mpsc;
39
40use state::Dashboard;
41use types::DaemonCommand;
42
43async fn daemon_background_loop(
47 control: ControlClient,
48 mut cmd_rx: mpsc::UnboundedReceiver<DaemonCommand>,
49 outcomes: mpsc::UnboundedSender<types::DaemonOutcome>,
50) {
51 while let Some(cmd) = cmd_rx.recv().await {
52 let (run_id, request, what) = match cmd {
53 DaemonCommand::Cancel { run_id } => {
54 (run_id.clone(), ControlRequest::Cancel { run_id }, "cancel")
55 }
56 DaemonCommand::Pause { run_id } => {
57 (run_id.clone(), ControlRequest::Pause { run_id }, "pause")
58 }
59 DaemonCommand::Resume { run_id } => {
60 (run_id.clone(), ControlRequest::Resume { run_id }, "resume")
61 }
62 DaemonCommand::Answer { response } => (
63 response.request_id.clone(),
64 ControlRequest::AnswerInteraction { response },
65 "answer",
66 ),
67 DaemonCommand::Message { agent_id, content } => (
68 agent_id.clone(),
69 ControlRequest::Message {
70 agent_id,
71 content,
72 target_region: None,
73 },
74 "message",
75 ),
76 };
77 let outcome = match control.request(&request).await {
80 Ok(ControlResponse::Ok { ok: true }) => types::DaemonOutcome {
81 run_id,
82 message: String::new(),
83 ok: true,
84 },
85 Ok(ControlResponse::Ok { ok: false }) => types::DaemonOutcome {
86 run_id,
87 message: format!("the daemon has no such run to {what}"),
88 ok: false,
89 },
90 Ok(other) => types::DaemonOutcome {
91 run_id,
92 message: format!("unexpected daemon response to {what}: {other:?}"),
93 ok: false,
94 },
95 Err(e) => types::DaemonOutcome {
96 run_id,
97 message: format!("{what} failed: {e}"),
98 ok: false,
99 },
100 };
101 if outcomes.send(outcome).is_err() {
103 return;
104 }
105 }
106}
107
108async fn execute_core<S: TerminalSetup, E: EventSource>(
118 dashboard: &mut Dashboard,
119 control: &ControlClient,
120 setup: &mut S,
121 events: &mut E,
122) -> anyhow::Result<()> {
123 setup.enable()?;
124 let mut terminal = setup.create_terminal()?;
125 let tick_rate = Duration::from_millis(100);
126 run_dashboard_loop(dashboard, control, &mut terminal, events, tick_rate).await?;
127 setup.disable();
128 setup.print_done();
129 Ok(())
130}
131
132async fn run_dashboard_loop<B: ratatui::backend::Backend>(
154 dashboard: &mut Dashboard,
155 control: &ControlClient,
156 terminal: &mut Terminal<B>,
157 events: &mut impl EventSource,
158 tick_rate: Duration,
159) -> anyhow::Result<()> {
160 loop {
161 dashboard.tick_count += 1;
162 dashboard.tick_toasts();
163
164 dashboard.sync_interactions(control).await;
167
168 dashboard.sync_daemon_runs(control).await;
171
172 dashboard.sync_from_run_state();
175
176 dashboard.drain_mcp_outcomes();
178
179 dashboard.drain_spawn_outcomes();
181 dashboard.open_pending_run();
183
184 dashboard.drain_daemon_outcomes();
186
187 terminal
189 .draw(|frame| dashboard.draw(frame))
190 .map_err(|e| anyhow::anyhow!("terminal draw failed: {e}"))?;
191
192 if let Some(event) = events.poll_event(tick_rate)? {
194 match event {
195 Event::Key(key) if key.kind == KeyEventKind::Press => {
196 dashboard.handle_key(key);
197 }
198 Event::Mouse(m) => dashboard.handle_mouse(m),
201 Event::Resize(_, _) => {
202 }
204 _ => {}
205 }
206 }
207
208 if dashboard.should_quit {
209 return Ok(());
210 }
211 }
212}
213
214fn init_dashboard(control: ControlClient, yank_fn: fn(&str) -> bool) -> Dashboard {
219 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
220 let mcp_ctx = types::McpContext {
223 config_path: crate::config::Config::config_path(),
224 store_path: leviath_mcp::AuthStore::default_path().unwrap_or_default(),
225 opener: std::sync::Arc::new(leviath_sys::open_url),
226 clock: mcp_system_now,
227 };
228 let mut dashboard = Dashboard::new_with_log_path(
229 cmd_tx,
230 crate::runstate::dashboard_log_path(),
231 yank_fn,
232 mcp_ctx,
233 new_run::production_new_run_context(),
234 );
235
236 let daemon_outcome_tx = dashboard
240 .take_daemon_outcome_tx()
241 .expect("a fresh dashboard has its daemon outcome sender");
242 tokio::spawn(daemon_background_loop(
243 control.clone(),
244 cmd_rx,
245 daemon_outcome_tx,
246 ));
247
248 let (mcp_cmd_rx, mcp_outcome_tx) = dashboard
251 .take_mcp_bg_ends()
252 .expect("a fresh dashboard has its MCP background channel ends");
253 tokio::spawn(mcp::mcp_background_loop(
254 dashboard.mcp_context(),
255 mcp_cmd_rx,
256 mcp_outcome_tx,
257 ));
258
259 let (spawn_cmd_rx, spawn_outcome_tx) = dashboard
262 .take_spawn_bg_ends()
263 .expect("a fresh dashboard has its spawn background channel ends");
264 tokio::spawn(new_run::spawn_background_loop(
265 control,
266 spawn_cmd_rx,
267 spawn_outcome_tx,
268 ));
269
270 dashboard.add_log("Dashboard started. Press `n` to start an agent run.".to_string());
271
272 dashboard
273}
274
275fn mcp_system_now() -> u64 {
277 std::time::SystemTime::now()
278 .duration_since(std::time::UNIX_EPOCH)
279 .map(|d| d.as_secs())
280 .unwrap_or(0)
281}
282
283pub async fn execute_with<S: TerminalSetup, E: EventSource>(
299 control: ControlClient,
300 setup: &mut S,
301 events: &mut E,
302 yank_fn: fn(&str) -> bool,
303) -> anyhow::Result<()> {
304 let mut dashboard = init_dashboard(control.clone(), yank_fn);
305 execute_core(&mut dashboard, &control, setup, events).await
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 use crate::commands::dashboard::test_support::make_test_dashboard;
313
314 #[test]
315 fn dashboard_args_can_be_constructed() {
316 let _args = DashboardArgs {};
317 }
318
319 #[test]
320 fn mcp_system_now_advances_past_the_epoch() {
321 assert!(mcp_system_now() > 1_600_000_000);
322 }
323
324 #[test]
325 fn agent_display_status_variants_display() {
326 let statuses = vec![
327 AgentDisplayStatus::Active,
328 AgentDisplayStatus::Waiting,
329 AgentDisplayStatus::Complete,
330 AgentDisplayStatus::CompleteInteractive,
331 AgentDisplayStatus::Error("test error".to_string()),
332 AgentDisplayStatus::Idle,
333 AgentDisplayStatus::Cancelled,
334 ];
335 for status in statuses {
336 let display = format!("{}", status);
337 assert!(!display.is_empty());
338 }
339 }
340
341 fn no_daemon_control() -> ControlClient {
344 let dir = std::env::temp_dir().join("leviath-dash-no-daemon");
345 ControlClient::new(leviath_runtime::control_socket::control_id(&dir))
346 }
347
348 fn recording_daemon(dir: &std::path::Path) -> (ControlClient, tokio::task::JoinHandle<String>) {
351 use leviath_runtime::control_socket::{bind_control_listener, control_id};
352 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
353 let id = control_id(dir);
354 let mut listener = bind_control_listener(&id).unwrap();
355 let handle = tokio::spawn(async move {
356 let stream = listener
357 .accept()
358 .await
359 .expect("accept succeeds")
360 .expect("our own connection is admitted");
361 let (read_half, mut write_half) = tokio::io::split(stream);
362 let mut lines = BufReader::new(read_half).lines();
363 let req = lines.next_line().await.unwrap().unwrap_or_default();
364 write_half
365 .write_all(b"{\"result\":\"ok\",\"ok\":true}\n")
366 .await
367 .unwrap();
368 req
369 });
370 (ControlClient::new(id), handle)
371 }
372
373 fn replying_daemon(
376 dir: &std::path::Path,
377 reply: Option<&'static str>,
378 ) -> (ControlClient, tokio::task::JoinHandle<()>) {
379 use leviath_runtime::control_socket::{bind_control_listener, control_id};
380 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
381 let id = control_id(dir);
382 let mut listener = bind_control_listener(&id).unwrap();
383 let handle = tokio::spawn(async move {
384 let stream = listener
385 .accept()
386 .await
387 .expect("accept succeeds")
388 .expect("our own connection is admitted");
389 let (read_half, mut write_half) = tokio::io::split(stream);
390 let mut lines = BufReader::new(read_half).lines();
391 let _ = lines.next_line().await;
392 if let Some(reply) = reply {
393 let _ = write_half.write_all(format!("{reply}\n").as_bytes()).await;
394 }
395 });
396 (ControlClient::new(id), handle)
397 }
398
399 async fn cancel_outcome(reply: Option<&'static str>) -> types::DaemonOutcome {
401 let dir = tempfile::tempdir().unwrap();
402 let (control, server) = replying_daemon(dir.path(), reply);
403 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<DaemonCommand>();
404 let (out_tx, mut out_rx) = mpsc::unbounded_channel();
405 tokio::spawn(daemon_background_loop(control, cmd_rx, out_tx));
406 cmd_tx
407 .send(DaemonCommand::Cancel {
408 run_id: "run-1".to_string(),
409 })
410 .unwrap();
411 let outcome = tokio::time::timeout(std::time::Duration::from_secs(5), out_rx.recv())
412 .await
413 .expect("an outcome was reported")
414 .expect("the loop is alive");
415 let _ = server.await;
416 outcome
417 }
418
419 #[tokio::test]
422 async fn daemon_background_loop_reports_each_outcome() {
423 let ok = cancel_outcome(Some(r#"{"result":"ok","ok":true}"#)).await;
424 assert!(ok.ok, "an applied cancel is reported as success");
425 assert_eq!(ok.run_id, "run-1");
426
427 let missing = cancel_outcome(Some(r#"{"result":"ok","ok":false}"#)).await;
428 assert!(!missing.ok);
429 assert!(missing.message.contains("no such run to cancel"));
430
431 let odd = cancel_outcome(Some(r#"{"result":"spawned","run_id":"x"}"#)).await;
432 assert!(!odd.ok);
433 assert!(odd.message.contains("unexpected daemon response"));
434
435 let broken = cancel_outcome(None).await;
437 assert!(!broken.ok);
438 assert!(
439 broken.message.contains("cancel failed"),
440 "got: {}",
441 broken.message
442 );
443 }
444
445 async fn command_outcome(
447 cmd: DaemonCommand,
448 reply: Option<&'static str>,
449 ) -> types::DaemonOutcome {
450 let dir = tempfile::tempdir().unwrap();
451 let (control, server) = replying_daemon(dir.path(), reply);
452 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<DaemonCommand>();
453 let (out_tx, mut out_rx) = mpsc::unbounded_channel();
454 tokio::spawn(daemon_background_loop(control, cmd_rx, out_tx));
455 cmd_tx.send(cmd).unwrap();
456 let outcome = tokio::time::timeout(std::time::Duration::from_secs(5), out_rx.recv())
457 .await
458 .expect("an outcome was reported")
459 .expect("the loop is alive");
460 let _ = server.await;
461 outcome
462 }
463
464 #[tokio::test]
467 async fn daemon_background_loop_forwards_pause_and_resume() {
468 let ok = command_outcome(
469 DaemonCommand::Pause {
470 run_id: "run-1".to_string(),
471 },
472 Some(r#"{"result":"ok","ok":true}"#),
473 )
474 .await;
475 assert!(ok.ok);
476 assert_eq!(ok.run_id, "run-1");
477
478 let refused = command_outcome(
479 DaemonCommand::Pause {
480 run_id: "run-1".to_string(),
481 },
482 Some(r#"{"result":"ok","ok":false}"#),
483 )
484 .await;
485 assert!(!refused.ok);
486 assert!(refused.message.contains("no such run to pause"));
487
488 let ok = command_outcome(
489 DaemonCommand::Resume {
490 run_id: "run-1".to_string(),
491 },
492 Some(r#"{"result":"ok","ok":true}"#),
493 )
494 .await;
495 assert!(ok.ok);
496
497 let refused = command_outcome(
498 DaemonCommand::Resume {
499 run_id: "run-1".to_string(),
500 },
501 Some(r#"{"result":"ok","ok":false}"#),
502 )
503 .await;
504 assert!(!refused.ok);
505 assert!(refused.message.contains("no such run to resume"));
506 }
507
508 #[tokio::test]
511 async fn daemon_background_loop_exits_when_the_dashboard_is_gone() {
512 let dir = tempfile::tempdir().unwrap();
513 let (control, _server) = replying_daemon(dir.path(), Some(r#"{"result":"ok","ok":true}"#));
514 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<DaemonCommand>();
515 let (out_tx, out_rx) = mpsc::unbounded_channel();
516 drop(out_rx); let handle = tokio::spawn(daemon_background_loop(control, cmd_rx, out_tx));
518 cmd_tx
519 .send(DaemonCommand::Cancel {
520 run_id: "run-1".to_string(),
521 })
522 .unwrap();
523 tokio::time::timeout(std::time::Duration::from_secs(5), handle)
524 .await
525 .expect("the loop returned")
526 .unwrap();
527 }
528
529 #[tokio::test]
532 async fn init_dashboard_seeds_startup_log_and_forwards_commands() {
533 crate::runstate::with_isolated_runs_dir_async(
534 "init_dashboard_seeds_startup_log",
535 |_d| async move {
536 let dashboard = init_dashboard(no_daemon_control(), |_| false);
537 assert!(
538 dashboard
539 .log
540 .iter()
541 .any(|entry| entry.message.contains("Dashboard started"))
542 );
543 dashboard
546 .cmd_tx
547 .send(DaemonCommand::Cancel {
548 run_id: "nope".to_string(),
549 })
550 .unwrap();
551 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
552 },
553 )
554 .await;
555 }
556
557 #[tokio::test]
560 async fn daemon_background_loop_forwards_cancel() {
561 let dir = tempfile::tempdir().unwrap();
562 let (control, server) = recording_daemon(dir.path());
563 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<DaemonCommand>();
564 let (out_tx, _out_rx) = mpsc::unbounded_channel();
565 tokio::spawn(daemon_background_loop(control, cmd_rx, out_tx));
566 cmd_tx
567 .send(DaemonCommand::Cancel {
568 run_id: "run-1".to_string(),
569 })
570 .unwrap();
571 let req = server.await.unwrap();
572 assert!(req.contains("cancel"));
573 assert!(req.contains("run-1"));
574 }
575
576 #[tokio::test]
577 async fn daemon_background_loop_forwards_answer() {
578 let dir = tempfile::tempdir().unwrap();
579 let (control, server) = recording_daemon(dir.path());
580 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<DaemonCommand>();
581 let (out_tx, _out_rx) = mpsc::unbounded_channel();
582 tokio::spawn(daemon_background_loop(control, cmd_rx, out_tx));
583 cmd_tx
584 .send(DaemonCommand::Answer {
585 response: leviath_core::interaction::InteractionResponse::text("q1", "yes"),
586 })
587 .unwrap();
588 let req = server.await.unwrap();
589 assert!(req.contains("answer_interaction"));
590 assert!(req.contains("q1"));
591 }
592
593 #[tokio::test]
594 async fn daemon_background_loop_forwards_message() {
595 let dir = tempfile::tempdir().unwrap();
596 let (control, server) = recording_daemon(dir.path());
597 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<DaemonCommand>();
598 let (out_tx, _out_rx) = mpsc::unbounded_channel();
599 tokio::spawn(daemon_background_loop(control, cmd_rx, out_tx));
600 cmd_tx
601 .send(DaemonCommand::Message {
602 agent_id: "a1".to_string(),
603 content: "hi there".to_string(),
604 })
605 .unwrap();
606 let req = server.await.unwrap();
607 assert!(req.contains("message"));
608 assert!(req.contains("hi there"));
609 }
610
611 #[tokio::test]
612 async fn daemon_background_loop_exits_when_channel_dropped() {
613 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<DaemonCommand>();
614 let (out_tx, _out_rx) = mpsc::unbounded_channel();
615 let handle = tokio::spawn(daemon_background_loop(no_daemon_control(), cmd_rx, out_tx));
616 drop(cmd_tx);
617 let result = tokio::time::timeout(std::time::Duration::from_millis(500), handle).await;
618 assert!(result.is_ok());
619 }
620
621 #[test]
624 fn dashboard_new_and_initial_state() {
625 let dash = make_test_dashboard();
626 assert!(!dash.should_quit);
627 assert!(!dash.detail_view);
628 assert!(!dash.show_help);
629 }
630
631 #[test]
632 fn dashboard_draw_renders_without_panic() {
633 use ratatui::Terminal;
634 use ratatui::backend::TestBackend;
635 let backend = TestBackend::new(120, 40);
636 let mut terminal = Terminal::new(backend).unwrap();
637 let mut dash = make_test_dashboard();
638 terminal.draw(|f| dash.draw(f)).unwrap();
639 let buf = crate::commands::dashboard::test_support::rendered_buffer(&terminal);
642 assert!(buf.contains("Agent Runs"), "{buf}");
643 }
644
645 #[test]
646 fn dashboard_agent_struct_fields_from_mod() {
647 let agent = DashboardAgent {
648 id: "run-test".to_string(),
649 blueprint_name: "tester".to_string(),
650 stage: "init".to_string(),
651 stage_index: 0,
652 num_stages: 1,
653 status: AgentDisplayStatus::Idle,
654 tokens_in: 0,
655 tokens_out: 0,
656 cached_tokens: 0,
657 iteration: 0,
658 waiting_prompt: None,
659 pending_request: None,
660 last_answered_request_id: None,
661 context_snapshot: None,
662 stages: vec![],
663 workdir: "/tmp".to_string(),
664 task: "test task".to_string(),
665 title: None,
666 model: None,
667 parent_id: None,
668 depth: 0,
669 started_at: 0,
670 last_progress_at: None,
671 active_until: None,
672 waiting_secs: 0,
673 graph_info: None,
674 accepts_messages: false,
675 taint_summary: vec![],
676 };
677 assert_eq!(agent.id, "run-test");
678 assert_eq!(agent.blueprint_name, "tester");
679 assert_eq!(agent.stage, "init");
680 }
681
682 use crate::tui::{TestBackendHarness, TestEventSource, TestSetup, key, test_terminal};
694 use crossterm::event::KeyCode;
695
696 #[tokio::test]
697 async fn run_dashboard_loop_quits_on_q_from_main_list() {
698 let mut dashboard = make_test_dashboard();
699 let control = no_daemon_control();
700 let mut terminal = test_terminal();
701 let mouse = |kind, column, row| {
706 Event::Mouse(crossterm::event::MouseEvent {
707 kind,
708 column,
709 row,
710 modifiers: crossterm::event::KeyModifiers::NONE,
711 })
712 };
713 use crossterm::event::{MouseButton, MouseEventKind};
714 let mut events = TestEventSource::new(vec![
715 Event::Resize(80, 24),
716 mouse(MouseEventKind::ScrollUp, 0, 0),
717 mouse(MouseEventKind::ScrollDown, 0, 0),
718 mouse(MouseEventKind::Moved, 0, 0),
721 mouse(MouseEventKind::Down(MouseButton::Left), 3, 15),
722 mouse(MouseEventKind::Drag(MouseButton::Left), 20, 16),
723 mouse(MouseEventKind::Up(MouseButton::Left), 20, 16),
724 key(KeyCode::Char('q')),
725 ]);
726
727 let result = run_dashboard_loop(
728 &mut dashboard,
729 &control,
730 &mut terminal,
731 &mut events,
732 Duration::from_millis(1),
733 )
734 .await;
735
736 assert!(result.is_ok());
737 assert!(dashboard.should_quit);
738 }
739
740 #[tokio::test]
741 async fn run_dashboard_loop_no_event_tick_then_quits() {
742 let mut dashboard = make_test_dashboard();
746 let control = no_daemon_control();
747 let mut terminal = test_terminal();
748 let mut events = TestEventSource::new_with_nones(vec![None, Some(key(KeyCode::Char('q')))]);
751
752 let result = run_dashboard_loop(
753 &mut dashboard,
754 &control,
755 &mut terminal,
756 &mut events,
757 Duration::from_millis(1),
758 )
759 .await;
760
761 assert!(result.is_ok());
762 assert!(dashboard.should_quit);
763 }
764
765 #[tokio::test]
766 async fn run_dashboard_loop_ignores_non_press_and_other_events() {
767 let mut dashboard = make_test_dashboard();
770 let control = no_daemon_control();
771 let mut terminal = test_terminal();
772 let release = Event::Key(crossterm::event::KeyEvent::new_with_kind(
773 KeyCode::Char('x'),
774 crossterm::event::KeyModifiers::empty(),
775 crossterm::event::KeyEventKind::Release,
776 ));
777 let mut events =
778 TestEventSource::new(vec![release, Event::FocusGained, key(KeyCode::Char('q'))]);
779
780 let result = run_dashboard_loop(
781 &mut dashboard,
782 &control,
783 &mut terminal,
784 &mut events,
785 Duration::from_millis(1),
786 )
787 .await;
788
789 assert!(result.is_ok());
790 assert!(dashboard.should_quit);
791 }
792
793 #[tokio::test]
794 async fn run_dashboard_loop_propagates_event_source_error() {
795 let mut dashboard = make_test_dashboard();
796 let control = no_daemon_control();
797 let mut terminal = test_terminal();
798 let mut events = TestEventSource::failing();
799
800 let result = run_dashboard_loop(
801 &mut dashboard,
802 &control,
803 &mut terminal,
804 &mut events,
805 Duration::from_millis(1),
806 )
807 .await;
808
809 assert!(result.is_err());
810 }
811
812 #[tokio::test]
831 async fn run_dashboard_loop_propagates_draw_error() {
832 let mut dashboard = make_test_dashboard();
837 let control = no_daemon_control();
838 let mut terminal = Terminal::new(TestBackendHarness::failing(120, 40)).unwrap();
839 let mut events = TestEventSource::new(vec![]); let result = run_dashboard_loop(
842 &mut dashboard,
843 &control,
844 &mut terminal,
845 &mut events,
846 Duration::from_millis(1),
847 )
848 .await;
849
850 assert!(result.is_err());
851 }
852
853 #[tokio::test]
877 async fn execute_core_happy_path_quits_on_esc() {
878 crate::runstate::with_isolated_runs_dir_async(
879 "execute_core_happy_path_quits_on_esc",
880 |_d| async move {
881 let control = no_daemon_control();
882 let mut dashboard = init_dashboard(control.clone(), |_| false);
883 let mut setup = TestSetup::new();
884 let mut events = TestEventSource::new(vec![key(KeyCode::Char('q'))]);
885 let result = execute_core(&mut dashboard, &control, &mut setup, &mut events).await;
886 assert!(result.is_ok());
887 assert!(dashboard.should_quit);
888 },
889 )
890 .await;
891 }
892
893 #[tokio::test]
894 async fn execute_with_loads_config_inits_and_runs_the_loop() {
895 crate::config::with_isolated_config_path_async(
900 "execute_with_dashboard",
901 |_fake_dir| async move {
902 crate::runstate::with_isolated_runs_dir_async(
903 "execute_with_dashboard",
904 |_d| async move {
905 let mut setup = TestSetup::new();
906 let mut events = TestEventSource::new(vec![key(KeyCode::Char('q'))]);
907 let result =
908 execute_with(no_daemon_control(), &mut setup, &mut events, |_| false)
909 .await;
910 assert!(result.is_ok());
911 },
912 )
913 .await;
914 },
915 )
916 .await;
917 }
918
919 #[tokio::test]
920 async fn execute_core_enable_error_propagates() {
921 crate::runstate::with_isolated_runs_dir_async(
922 "execute_core_enable_error_propagates",
923 |_d| async move {
924 let control = no_daemon_control();
925 let mut dashboard = init_dashboard(control.clone(), |_| false);
926 let mut setup = TestSetup {
928 enable_should_fail: true,
929 create_should_fail: false,
930 draw_should_fail: false,
931 };
932 let mut events = TestEventSource::new(vec![]);
933 let result = execute_core(&mut dashboard, &control, &mut setup, &mut events).await;
934 assert!(result.is_err());
935 },
936 )
937 .await;
938 }
939
940 #[tokio::test]
941 async fn execute_core_create_terminal_error_propagates() {
942 crate::runstate::with_isolated_runs_dir_async(
943 "execute_core_create_terminal_error_propagates",
944 |_d| async move {
945 let control = no_daemon_control();
946 let mut dashboard = init_dashboard(control.clone(), |_| false);
947 let mut setup = TestSetup {
950 enable_should_fail: false,
951 create_should_fail: true,
952 draw_should_fail: false,
953 };
954 let mut events = TestEventSource::new(vec![]);
955 let result = execute_core(&mut dashboard, &control, &mut setup, &mut events).await;
956 assert!(result.is_err());
957 },
958 )
959 .await;
960 }
961
962 #[tokio::test]
963 async fn execute_core_loop_error_propagates() {
964 crate::runstate::with_isolated_runs_dir_async(
965 "execute_core_loop_error_propagates",
966 |_d| async move {
967 let control = no_daemon_control();
968 let mut dashboard = init_dashboard(control.clone(), |_| false);
969 let mut setup = TestSetup::new();
970 let mut events = TestEventSource::failing();
971 let result = execute_core(&mut dashboard, &control, &mut setup, &mut events).await;
972 assert!(result.is_err());
973 },
974 )
975 .await;
976 }
977
978 }