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 wait_reason: None,
660 pending_request: None,
661 last_answered_request_id: None,
662 context_snapshot: None,
663 stages: vec![],
664 workdir: "/tmp".to_string(),
665 task: "test task".to_string(),
666 title: None,
667 model: None,
668 parent_id: None,
669 depth: 0,
670 started_at: 0,
671 last_progress_at: None,
672 active_until: None,
673 waiting_secs: 0,
674 graph_info: None,
675 accepts_messages: false,
676 taint_summary: vec![],
677 };
678 assert_eq!(agent.id, "run-test");
679 assert_eq!(agent.blueprint_name, "tester");
680 assert_eq!(agent.stage, "init");
681 }
682
683 use crate::tui::{TestBackendHarness, TestEventSource, TestSetup, key, test_terminal};
695 use crossterm::event::KeyCode;
696
697 #[tokio::test]
698 async fn run_dashboard_loop_quits_on_q_from_main_list() {
699 let mut dashboard = make_test_dashboard();
700 let control = no_daemon_control();
701 let mut terminal = test_terminal();
702 let mouse = |kind, column, row| {
707 Event::Mouse(crossterm::event::MouseEvent {
708 kind,
709 column,
710 row,
711 modifiers: crossterm::event::KeyModifiers::NONE,
712 })
713 };
714 use crossterm::event::{MouseButton, MouseEventKind};
715 let mut events = TestEventSource::new(vec![
716 Event::Resize(80, 24),
717 mouse(MouseEventKind::ScrollUp, 0, 0),
718 mouse(MouseEventKind::ScrollDown, 0, 0),
719 mouse(MouseEventKind::Moved, 0, 0),
722 mouse(MouseEventKind::Down(MouseButton::Left), 3, 15),
723 mouse(MouseEventKind::Drag(MouseButton::Left), 20, 16),
724 mouse(MouseEventKind::Up(MouseButton::Left), 20, 16),
725 key(KeyCode::Char('q')),
726 ]);
727
728 let result = run_dashboard_loop(
729 &mut dashboard,
730 &control,
731 &mut terminal,
732 &mut events,
733 Duration::from_millis(1),
734 )
735 .await;
736
737 assert!(result.is_ok());
738 assert!(dashboard.should_quit);
739 }
740
741 #[tokio::test]
742 async fn run_dashboard_loop_no_event_tick_then_quits() {
743 let mut dashboard = make_test_dashboard();
747 let control = no_daemon_control();
748 let mut terminal = test_terminal();
749 let mut events = TestEventSource::new_with_nones(vec![None, Some(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_ignores_non_press_and_other_events() {
768 let mut dashboard = make_test_dashboard();
771 let control = no_daemon_control();
772 let mut terminal = test_terminal();
773 let release = Event::Key(crossterm::event::KeyEvent::new_with_kind(
774 KeyCode::Char('x'),
775 crossterm::event::KeyModifiers::empty(),
776 crossterm::event::KeyEventKind::Release,
777 ));
778 let mut events =
779 TestEventSource::new(vec![release, Event::FocusGained, key(KeyCode::Char('q'))]);
780
781 let result = run_dashboard_loop(
782 &mut dashboard,
783 &control,
784 &mut terminal,
785 &mut events,
786 Duration::from_millis(1),
787 )
788 .await;
789
790 assert!(result.is_ok());
791 assert!(dashboard.should_quit);
792 }
793
794 #[tokio::test]
795 async fn run_dashboard_loop_propagates_event_source_error() {
796 let mut dashboard = make_test_dashboard();
797 let control = no_daemon_control();
798 let mut terminal = test_terminal();
799 let mut events = TestEventSource::failing();
800
801 let result = run_dashboard_loop(
802 &mut dashboard,
803 &control,
804 &mut terminal,
805 &mut events,
806 Duration::from_millis(1),
807 )
808 .await;
809
810 assert!(result.is_err());
811 }
812
813 #[tokio::test]
832 async fn run_dashboard_loop_propagates_draw_error() {
833 let mut dashboard = make_test_dashboard();
838 let control = no_daemon_control();
839 let mut terminal = Terminal::new(TestBackendHarness::failing(120, 40)).unwrap();
840 let mut events = TestEventSource::new(vec![]); let result = run_dashboard_loop(
843 &mut dashboard,
844 &control,
845 &mut terminal,
846 &mut events,
847 Duration::from_millis(1),
848 )
849 .await;
850
851 assert!(result.is_err());
852 }
853
854 #[tokio::test]
878 async fn execute_core_happy_path_quits_on_esc() {
879 crate::runstate::with_isolated_runs_dir_async(
880 "execute_core_happy_path_quits_on_esc",
881 |_d| async move {
882 let control = no_daemon_control();
883 let mut dashboard = init_dashboard(control.clone(), |_| false);
884 let mut setup = TestSetup::new();
885 let mut events = TestEventSource::new(vec![key(KeyCode::Char('q'))]);
886 let result = execute_core(&mut dashboard, &control, &mut setup, &mut events).await;
887 assert!(result.is_ok());
888 assert!(dashboard.should_quit);
889 },
890 )
891 .await;
892 }
893
894 #[tokio::test]
895 async fn execute_with_loads_config_inits_and_runs_the_loop() {
896 crate::config::with_isolated_config_path_async(
901 "execute_with_dashboard",
902 |_fake_dir| async move {
903 crate::runstate::with_isolated_runs_dir_async(
904 "execute_with_dashboard",
905 |_d| async move {
906 let mut setup = TestSetup::new();
907 let mut events = TestEventSource::new(vec![key(KeyCode::Char('q'))]);
908 let result =
909 execute_with(no_daemon_control(), &mut setup, &mut events, |_| false)
910 .await;
911 assert!(result.is_ok());
912 },
913 )
914 .await;
915 },
916 )
917 .await;
918 }
919
920 #[tokio::test]
921 async fn execute_core_enable_error_propagates() {
922 crate::runstate::with_isolated_runs_dir_async(
923 "execute_core_enable_error_propagates",
924 |_d| async move {
925 let control = no_daemon_control();
926 let mut dashboard = init_dashboard(control.clone(), |_| false);
927 let mut setup = TestSetup {
929 enable_should_fail: true,
930 create_should_fail: false,
931 draw_should_fail: false,
932 };
933 let mut events = TestEventSource::new(vec![]);
934 let result = execute_core(&mut dashboard, &control, &mut setup, &mut events).await;
935 assert!(result.is_err());
936 },
937 )
938 .await;
939 }
940
941 #[tokio::test]
942 async fn execute_core_create_terminal_error_propagates() {
943 crate::runstate::with_isolated_runs_dir_async(
944 "execute_core_create_terminal_error_propagates",
945 |_d| async move {
946 let control = no_daemon_control();
947 let mut dashboard = init_dashboard(control.clone(), |_| false);
948 let mut setup = TestSetup {
951 enable_should_fail: false,
952 create_should_fail: true,
953 draw_should_fail: false,
954 };
955 let mut events = TestEventSource::new(vec![]);
956 let result = execute_core(&mut dashboard, &control, &mut setup, &mut events).await;
957 assert!(result.is_err());
958 },
959 )
960 .await;
961 }
962
963 #[tokio::test]
964 async fn execute_core_loop_error_propagates() {
965 crate::runstate::with_isolated_runs_dir_async(
966 "execute_core_loop_error_propagates",
967 |_d| async move {
968 let control = no_daemon_control();
969 let mut dashboard = init_dashboard(control.clone(), |_| false);
970 let mut setup = TestSetup::new();
971 let mut events = TestEventSource::failing();
972 let result = execute_core(&mut dashboard, &control, &mut setup, &mut events).await;
973 assert!(result.is_err());
974 },
975 )
976 .await;
977 }
978
979 }