1mod builder;
67
68pub use builder::OrcsAppBuilder;
69
70use crate::AppError;
71use crate::SharedPrinterSlot;
72use orcs_event::Signal;
73use orcs_runtime::auth::{DefaultGrantStore, GrantPolicy};
74use orcs_runtime::io::{ConsoleRenderer, InputParser};
75use orcs_runtime::session::SessionStore;
76use orcs_runtime::{
77 ConfigResolver, IOInput, IOInputHandle, IOOutput, IOOutputHandle, InputCommand, InputContext,
78 LocalFileStore, OrcsConfig, OrcsEngine, SessionAsset,
79};
80use orcs_types::{ChannelId, Principal, PrincipalId};
81use rustyline::highlight::{CmdKind, Highlighter};
82use rustyline::history::DefaultHistory;
83use rustyline::{
84 Completer, Editor, ExternalPrinter as RustylineExternalPrinter, Helper, Hinter, Validator,
85};
86use std::collections::HashMap;
87use std::path::PathBuf;
88use std::sync::Arc;
89
90#[derive(Debug, Clone)]
94struct PendingApproval {
95 id: String,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101enum LoopControl {
102 Continue,
104 Exit,
106 Pause,
108}
109
110#[derive(Debug)]
112enum ReadlineEvent {
113 Line(String),
115 Eof,
117}
118
119#[derive(Completer, Helper, Hinter, Validator)]
132struct OrcsHelper;
133
134impl Highlighter for OrcsHelper {
135 fn highlight_char(&self, _line: &str, _pos: usize, kind: CmdKind) -> bool {
136 kind == CmdKind::Other
140 }
141}
142
143pub struct OrcsApp {
147 pub(super) config: OrcsConfig,
149 pub(super) resolver: Box<dyn ConfigResolver>,
151 pub(super) engine: OrcsEngine,
152 pub(super) principal: Principal,
154 pub(super) renderer: ConsoleRenderer,
156 pub(super) io_input: IOInputHandle,
161 pub(super) io_output: IOOutputHandle,
163 pending_approval: Option<PendingApproval>,
165 pub(super) store: LocalFileStore,
167 pub(super) session: SessionAsset,
169 pub(super) is_resumed: bool,
171 pub(super) restored_count: usize,
173 pub(super) shared_grants: Arc<DefaultGrantStore>,
179 pub(super) component_routes: HashMap<String, ChannelId>,
183 printer_slot: SharedPrinterSlot,
188}
189
190impl OrcsApp {
191 #[must_use]
196 pub fn builder(resolver: impl ConfigResolver + 'static) -> OrcsAppBuilder {
197 OrcsAppBuilder::new(resolver)
198 }
199
200 #[must_use]
202 pub fn engine(&self) -> &OrcsEngine {
203 &self.engine
204 }
205
206 pub fn engine_mut(&mut self) -> &mut OrcsEngine {
208 &mut self.engine
209 }
210
211 #[must_use]
213 pub fn config(&self) -> &OrcsConfig {
214 &self.config
215 }
216
217 pub fn reload_config(&mut self) -> Result<(), AppError> {
226 self.config = self
227 .resolver
228 .resolve()
229 .map_err(|e| AppError::Config(e.to_string()))?;
230 Ok(())
231 }
232
233 #[must_use]
235 pub fn renderer(&self) -> &ConsoleRenderer {
236 &self.renderer
237 }
238
239 #[must_use]
241 pub fn pending_approval_id(&self) -> Option<&str> {
242 self.pending_approval.as_ref().map(|p| p.id.as_str())
243 }
244
245 #[must_use]
247 pub fn session_id(&self) -> &str {
248 &self.session.id
249 }
250
251 #[must_use]
253 pub fn is_resumed(&self) -> bool {
254 self.is_resumed
255 }
256
257 #[must_use]
259 pub fn restored_count(&self) -> usize {
260 self.restored_count
261 }
262
263 pub async fn save_session(&mut self) -> Result<(), AppError> {
272 for (fqn, snapshot) in self.engine.collected_snapshots() {
274 self.session
275 .component_snapshots
276 .insert(fqn.clone(), snapshot.clone());
277 }
278 match self.shared_grants.list_grants() {
280 Ok(grants) => {
281 tracing::debug!("Saving {} grant(s) to session", grants.len());
282 self.session.save_grants(grants);
283 }
284 Err(e) => {
285 tracing::error!("Failed to list grants for session save: {e}");
286 }
287 }
288 self.session.touch();
289 self.store.save(&self.session).await?;
290 Ok(())
291 }
292
293 pub async fn load_session(&mut self, session_id: &str) -> Result<(), AppError> {
303 self.session = self.store.load(session_id).await?;
304 Ok(())
305 }
306
307 fn history_path(&self) -> PathBuf {
309 self.config.paths.history_file_or_default()
310 }
311
312 fn spawn_readline_thread(
320 history_path: PathBuf,
321 ) -> (
322 tokio::sync::mpsc::UnboundedReceiver<ReadlineEvent>,
323 Option<Box<dyn RustylineExternalPrinter + Send>>,
324 ) {
325 let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel();
326 let (printer_tx, printer_rx) =
327 std::sync::mpsc::sync_channel::<Option<Box<dyn RustylineExternalPrinter + Send>>>(1);
328
329 if let Err(e) = std::thread::Builder::new()
330 .name("orcs-readline".into())
331 .spawn(move || {
332 let config = rustyline::Config::builder().auto_add_history(true).build();
333
334 let mut rl: Editor<OrcsHelper, DefaultHistory> = match Editor::with_config(config) {
335 Ok(editor) => editor,
336 Err(e) => {
337 tracing::error!("Failed to create readline editor: {e}");
338 let _ = printer_tx.send(None);
339 return;
340 }
341 };
342 rl.set_helper(Some(OrcsHelper));
343
344 if let Err(e) = rl.load_history(&history_path) {
346 tracing::debug!("History load: {e} (expected on first run)");
347 }
348
349 let printer = rl
351 .create_external_printer()
352 .ok()
353 .map(|p| Box::new(p) as Box<dyn RustylineExternalPrinter + Send>);
354 let _ = printer_tx.send(printer);
355
356 loop {
358 match rl.readline("orcs> ") {
359 Ok(line) => {
360 let _ = rl.save_history(&history_path);
362
363 if event_tx.send(ReadlineEvent::Line(line)).is_err() {
364 break; }
366 }
367 Err(rustyline::error::ReadlineError::Interrupted) => {
368 continue;
370 }
371 Err(rustyline::error::ReadlineError::Eof) => {
372 let _ = event_tx.send(ReadlineEvent::Eof);
373 break;
374 }
375 Err(e) => {
376 tracing::error!("Readline error: {e}");
377 let _ = event_tx.send(ReadlineEvent::Eof);
378 break;
379 }
380 }
381 }
382
383 let _ = rl.save_history(&history_path);
385 })
386 {
387 tracing::error!("failed to spawn readline thread: {e}");
388 }
389
390 let printer = printer_rx.recv().ok().flatten();
392
393 (event_rx, printer)
394 }
395
396 fn render_safe(&self, output: &IOOutput) {
401 if let Some(formatted) = Self::format_io_output(output, self.renderer.is_verbose()) {
402 match self.printer_slot.try_print(formatted) {
403 crate::PrintResult::Sent | crate::PrintResult::Dropped => return,
404 crate::PrintResult::NoPrinter => {}
405 }
406 }
407 self.renderer.render_output(output);
409 }
410
411 fn format_io_output(output: &IOOutput, verbose: bool) -> Option<String> {
415 match output {
416 IOOutput::Print { text, style } => match style {
417 orcs_runtime::OutputStyle::Normal | orcs_runtime::OutputStyle::Info => {
418 Some(format!("{text}\n"))
419 }
420 orcs_runtime::OutputStyle::Warn => Some(format!("[WARN] {text}\n")),
421 orcs_runtime::OutputStyle::Error => Some(format!("[ERROR] {text}\n")),
422 orcs_runtime::OutputStyle::Success => Some(format!("\x1B[32m{text}\x1B[0m\n")),
423 orcs_runtime::OutputStyle::Debug => {
424 if verbose {
425 Some(format!("[DEBUG] {text}\n"))
426 } else {
427 None
428 }
429 }
430 },
431 IOOutput::Prompt { message } => Some(format!("{message} ")),
432 IOOutput::ShowApprovalRequest {
433 id,
434 operation,
435 description,
436 } => Some(format!(
437 "\n [{operation}] {id} - {description}\n Enter 'y' to approve, 'n' to reject:\n"
438 )),
439 IOOutput::ShowApproved { approval_id } => {
440 Some(format!(" \u{2713} Approved: {approval_id}\n"))
441 }
442 IOOutput::ShowRejected {
443 approval_id,
444 reason,
445 } => {
446 if let Some(reason) = reason {
447 Some(format!(" \u{2717} Rejected: {approval_id} ({reason})\n"))
448 } else {
449 Some(format!(" \u{2717} Rejected: {approval_id}\n"))
450 }
451 }
452 IOOutput::ShowProcessing {
453 component,
454 operation,
455 } => Some(format!(" [{component}] Processing ({operation})...\n")),
456 IOOutput::Clear => Some("\x1B[2J\x1B[1;1H".to_string()),
457 }
458 }
459
460 pub async fn run_command(&mut self, command: &str) -> Result<i32, AppError> {
479 self.engine.start();
480
481 let trimmed = command.trim();
482 if trimmed.is_empty() {
483 self.engine.stop();
484 self.engine.shutdown().await;
485 return Ok(0);
486 }
487
488 if trimmed.starts_with('@') {
491 let cmd = InputParser.parse(trimmed);
492 match cmd {
493 InputCommand::ComponentMessage {
494 ref target,
495 ref message,
496 } => {
497 self.handle_component_message(target, message);
498 }
499 _ => {
500 eprintln!("Invalid component target: {trimmed}");
502 self.engine.stop();
503 self.engine.shutdown().await;
504 return Ok(1);
505 }
506 }
507 } else {
508 let io_input = IOInput::line(command);
510 if let Err(e) = self.io_input.try_send(io_input) {
511 eprintln!("Failed to send command: {e:?}");
512 self.engine.stop();
513 self.engine.shutdown().await;
514 return Ok(1);
515 }
516 }
517
518 const IDLE: tokio::time::Duration = tokio::time::Duration::from_secs(3);
522 const MAX_WAIT: tokio::time::Duration = tokio::time::Duration::from_secs(120);
523
524 let start = tokio::time::Instant::now();
525 let mut received_output = false;
526 let mut exit_code: i32 = 0;
527
528 loop {
529 if start.elapsed() > MAX_WAIT {
530 tracing::warn!(
531 "Command mode: max timeout reached ({}s)",
532 MAX_WAIT.as_secs()
533 );
534 break;
535 }
536
537 let timeout = if received_output { IDLE } else { MAX_WAIT };
538
539 tokio::select! {
540 Some(io_out) = self.io_output.recv() => {
541 match &io_out {
542 IOOutput::Print { text, style } => {
543 received_output = true;
544 if style.is_warning_or_error() {
545 exit_code = 1;
546 eprintln!("{text}");
547 } else {
548 println!("{text}");
549 }
550 }
551 IOOutput::Prompt { .. } => {
552 break;
554 }
555 IOOutput::ShowApprovalRequest { .. } => {
556 eprintln!("Command requires interactive approval. Use interactive mode.");
558 exit_code = 1;
559 break;
560 }
561 _ => {}
562 }
563 }
564 () = tokio::time::sleep(timeout) => {
565 tracing::debug!(
566 "Command mode: timeout (received_output={received_output})",
567 );
568 break;
569 }
570 }
571 }
572
573 self.engine.stop();
574 self.engine.shutdown().await;
575 Ok(exit_code)
576 }
577
578 pub async fn run_interactive(&mut self) -> Result<(), AppError> {
590 let io_id = self.engine.io_channel();
591 tracing::info!("Starting interactive mode (IO channel: {})", io_id);
592
593 self.engine.start();
594
595 self.renderer.render_output(&IOOutput::info(
597 "Interactive mode started. Type 'q' to quit, 'help' for commands.",
598 ));
599 if self.is_resumed {
600 self.renderer.render_output(&IOOutput::info(format!(
601 "Resumed session: {} ({} component(s) restored)",
602 self.session.id, self.restored_count
603 )));
604 } else {
605 self.renderer
606 .render_output(&IOOutput::info(format!("Session ID: {}", self.session.id)));
607 }
608
609 let (mut readline_rx, printer) = Self::spawn_readline_thread(self.history_path());
612 if let Some(p) = printer {
613 self.printer_slot.set(p);
614 }
615
616 let mut should_save = false;
617
618 tokio::task::yield_now().await;
621
622 loop {
623 if !self.engine.is_running() {
624 break;
625 }
626
627 tokio::select! {
631 biased;
632
633 Some(io_out) = self.io_output.recv() => {
634 self.handle_io_output(io_out);
635 }
636 event = readline_rx.recv() => {
637 match event {
638 Some(ReadlineEvent::Line(line)) => {
639 match self.handle_input(&line) {
640 LoopControl::Continue => {}
641 LoopControl::Exit => break,
642 LoopControl::Pause => {
643 should_save = true;
644 break;
645 }
646 }
647 }
648 Some(ReadlineEvent::Eof) | None => {
649 tracing::debug!("readline: EOF or channel closed");
650 break;
651 }
652 }
653 }
654 }
655 }
656
657 self.printer_slot.clear();
660
661 self.engine.stop();
662 self.engine.shutdown().await;
663
664 if should_save {
665 self.renderer
666 .render_output(&IOOutput::info("Saving session..."));
667 self.save_session().await?;
668 self.renderer.render_output(&IOOutput::info(format!(
669 "Session saved: {}",
670 self.session.id
671 )));
672 self.renderer.render_output(&IOOutput::info(format!(
673 "Resume with: orcs --resume {}",
674 self.session.id
675 )));
676 }
677
678 self.renderer
679 .render_output(&IOOutput::info("Shutting down..."));
680 Ok(())
681 }
682
683 fn handle_input(&mut self, line: &str) -> LoopControl {
694 let cmd = InputParser.parse(line);
695
696 match &cmd {
697 InputCommand::Quit => {
698 tracing::info!("Quit requested");
699 return LoopControl::Exit;
700 }
701 InputCommand::Veto => {
702 tracing::warn!("Veto signal sent");
703 self.send_io_input(line);
705 return LoopControl::Exit;
706 }
707 InputCommand::Approve { approval_id } => {
708 self.handle_approve_via_io(line, approval_id.as_deref());
709 }
710 InputCommand::Reject {
711 approval_id,
712 reason: _,
713 } => {
714 self.handle_reject_via_io(line, approval_id.as_deref());
715 }
716 InputCommand::Pause => {
717 tracing::info!("Pause requested");
718 return LoopControl::Pause;
719 }
720 InputCommand::Resume => {
721 self.render_safe(&IOOutput::info(
723 "Resume is handled at startup. Use: orcs --resume",
724 ));
725 }
726 InputCommand::Steer { message } => {
727 let io_id = self.engine.io_channel();
728 self.engine.signal(Signal::steer(
729 io_id,
730 message.as_str(),
731 Principal::User(PrincipalId::new()),
732 ));
733 self.render_safe(&IOOutput::info(format!("Steer: {message}")));
734 }
735 InputCommand::Empty => {
736 }
738 InputCommand::ComponentMessage { target, message } => {
739 self.handle_component_message(target, message);
740 }
741 InputCommand::Unknown { input } => {
742 if input == "help" {
743 self.show_help();
744 } else if !input.is_empty() {
745 self.handle_echo_input(input.as_str());
747 }
748 }
749 }
750
751 LoopControl::Continue
752 }
753
754 fn send_io_input(&self, line: &str) {
758 let context = self.build_input_context();
759 let io_input = IOInput::line_with_context(line, context);
760
761 if let Err(e) = self.io_input.try_send(io_input) {
762 tracing::warn!("Failed to send IOInput: {:?}", e);
763 }
764 }
765
766 fn build_input_context(&self) -> InputContext {
768 match &self.pending_approval {
769 Some(pending) => InputContext::with_approval_id(&pending.id),
770 None => InputContext::empty(),
771 }
772 }
773
774 fn handle_echo_input(&mut self, input: &str) {
780 let io_input = IOInput::line(input);
782
783 if let Err(e) = self.io_input.try_send(io_input) {
784 self.render_safe(&IOOutput::error(format!("Failed to send message: {e:?}")));
785 }
786 }
787
788 fn handle_component_message(&mut self, target: &str, message: &str) {
793 let channel_id = match self.component_routes.get(target) {
794 Some(id) => *id,
795 None => {
796 let available: Vec<&str> =
797 self.component_routes.keys().map(|k| k.as_str()).collect();
798 self.render_safe(&IOOutput::error(format!(
799 "Unknown component: @{}. Available: {}",
800 target,
801 available.join(", ")
802 )));
803 return;
804 }
805 };
806
807 let event = orcs_runtime::Event {
808 category: orcs_event::EventCategory::UserInput,
809 operation: "input".to_string(),
810 source: orcs_types::ComponentId::builtin("app"),
811 payload: serde_json::json!({ "message": message }),
812 };
813
814 match self.engine.inject_event(channel_id, event) {
815 Ok(()) => {
816 tracing::debug!("Routed @{} to channel {}", target, channel_id);
817 }
818 Err(e) => {
819 self.render_safe(&IOOutput::error(format!(
820 "Failed to route to @{}: {}",
821 target, e
822 )));
823 }
824 }
825 }
826
827 fn handle_approve_via_io(&mut self, _line: &str, explicit_id: Option<&str>) {
832 let approval_id = explicit_id
833 .map(String::from)
834 .or_else(|| self.pending_approval.as_ref().map(|p| p.id.clone()));
835
836 if let Some(id) = approval_id {
837 let signal = Signal::approve(&id, self.principal.clone());
839 self.engine.signal(signal);
840 tracing::info!(approval_id = %id, "Approved");
841 if self.pending_approval.as_ref().map(|p| p.id.as_str()) == Some(id.as_str()) {
845 self.pending_approval = None;
846 }
847 } else {
848 self.render_safe(&IOOutput::warn("No pending approval. Use: y <id>"));
849 }
850 }
851
852 fn handle_reject_via_io(&mut self, _line: &str, explicit_id: Option<&str>) {
856 let approval_id = explicit_id
857 .map(String::from)
858 .or_else(|| self.pending_approval.as_ref().map(|p| p.id.clone()));
859
860 if let Some(id) = approval_id {
861 let signal = Signal::reject(&id, None, self.principal.clone());
863 self.engine.signal(signal);
864 tracing::info!(approval_id = %id, "Rejected");
865 if self.pending_approval.as_ref().map(|p| p.id.as_str()) == Some(id.as_str()) {
869 self.pending_approval = None;
870 }
871 } else {
872 self.render_safe(&IOOutput::warn("No pending approval. Use: n <id> [reason]"));
873 }
874 }
875
876 fn show_help(&self) {
878 self.render_safe(&IOOutput::info(
879 "Commands:\n\
880 \x20 y [id] - Approve pending request\n\
881 \x20 n [id] [reason] - Reject pending request\n\
882 \x20 p / pause - Pause execution\n\
883 \x20 r / resume - Resume execution\n\
884 \x20 s <message> - Steer with instruction\n\
885 \x20 q / quit - Quit application\n\
886 \x20 veto / stop - Emergency stop",
887 ));
888 }
889
890 fn handle_io_output(&mut self, io_out: IOOutput) {
895 if let IOOutput::ShowApprovalRequest { ref id, .. } = io_out {
897 self.pending_approval = Some(PendingApproval { id: id.clone() });
898 }
899
900 self.render_safe(&io_out);
901 }
902}
903
904#[cfg(test)]
905mod tests {
906 use super::*;
907 use orcs_runtime::sandbox::ProjectSandbox;
908 use orcs_runtime::NoOpResolver;
909 use orcs_runtime::WorkDir;
910 use std::path::PathBuf;
911
912 fn test_policy() -> Arc<dyn orcs_runtime::sandbox::SandboxPolicy> {
913 Arc::new(ProjectSandbox::new(".").expect("test sandbox"))
914 }
915
916 #[test]
919 fn orcs_helper_forces_refresh_on_char_insert() {
920 let helper = OrcsHelper;
921 assert!(
922 helper.highlight_char("", 0, CmdKind::Other),
923 "CmdKind::Other (edit_insert) should force full line refresh"
924 );
925 }
926
927 #[test]
928 fn orcs_helper_skips_refresh_on_cursor_move() {
929 let helper = OrcsHelper;
930 assert!(
931 !helper.highlight_char("abc", 1, CmdKind::MoveCursor),
932 "CmdKind::MoveCursor should not trigger refresh"
933 );
934 }
935
936 #[test]
937 fn orcs_helper_skips_refresh_on_forced_refresh() {
938 let helper = OrcsHelper;
939 assert!(
940 !helper.highlight_char("abc", 0, CmdKind::ForcedRefresh),
941 "CmdKind::ForcedRefresh should not trigger extra refresh from helper"
942 );
943 }
944
945 #[tokio::test]
946 async fn builder_default() {
947 let app = OrcsApp::builder(NoOpResolver)
948 .with_sandbox(test_policy())
949 .build()
950 .await
951 .expect("builder should succeed with NoOpResolver");
952 assert!(app.pending_approval_id().is_none());
953 }
954
955 #[tokio::test]
956 async fn builder_with_custom_resolver() {
957 struct VerboseResolver;
958 impl ConfigResolver for VerboseResolver {
959 fn resolve(&self) -> Result<OrcsConfig, orcs_runtime::ConfigError> {
960 let mut config = OrcsConfig::default();
961 config.ui.verbose = true;
962 Ok(config)
963 }
964 }
965
966 let app = OrcsApp::builder(VerboseResolver)
967 .with_sandbox(test_policy())
968 .build()
969 .await
970 .expect("builder should succeed with VerboseResolver");
971 assert!(app.config().ui.verbose);
972 }
973
974 #[tokio::test]
975 async fn app_engine_access() {
976 let app = OrcsApp::builder(NoOpResolver)
977 .with_sandbox(test_policy())
978 .build()
979 .await
980 .expect("builder should succeed");
981 assert!(!app.engine().is_running());
982 }
983
984 #[tokio::test]
985 async fn app_engine_parallel_access() {
986 let app = OrcsApp::builder(NoOpResolver)
987 .with_sandbox(test_policy())
988 .build()
989 .await
990 .expect("builder should succeed");
991 let io = app.engine().io_channel();
992 let world = app.engine().world_read();
993 let w = world.read().await;
994 assert!(w.get(&io).is_some());
995 }
996
997 #[tokio::test]
998 async fn app_session_id() {
999 let app = OrcsApp::builder(NoOpResolver)
1000 .with_sandbox(test_policy())
1001 .build()
1002 .await
1003 .expect("builder should succeed");
1004 assert!(!app.session_id().is_empty());
1005 }
1006
1007 #[tokio::test]
1008 async fn app_new_session_not_resumed() {
1009 let app = OrcsApp::builder(NoOpResolver)
1010 .with_sandbox(test_policy())
1011 .build()
1012 .await
1013 .expect("builder should succeed");
1014 assert!(!app.is_resumed());
1015 assert_eq!(app.restored_count(), 0);
1016 }
1017
1018 #[tokio::test]
1019 async fn app_save_and_load_session() {
1020 let wd = WorkDir::temporary().expect("should create temp WorkDir for session test");
1021 let temp_dir = wd.path().to_path_buf();
1022
1023 struct SessionResolver {
1024 session_dir: PathBuf,
1025 }
1026 impl ConfigResolver for SessionResolver {
1027 fn resolve(&self) -> Result<OrcsConfig, orcs_runtime::ConfigError> {
1028 let mut config = OrcsConfig::default();
1029 config.paths.session_dir = Some(self.session_dir.clone());
1030 Ok(config)
1031 }
1032 }
1033
1034 let session_id = {
1035 let mut app = OrcsApp::builder(SessionResolver {
1036 session_dir: temp_dir.clone(),
1037 })
1038 .with_sandbox(test_policy())
1039 .build()
1040 .await
1041 .expect("builder should succeed with SessionResolver");
1042
1043 let id = app.session_id().to_string();
1044 app.save_session()
1045 .await
1046 .expect("save session should succeed");
1047 id
1048 };
1049
1050 let app = OrcsApp::builder(SessionResolver {
1051 session_dir: temp_dir.clone(),
1052 })
1053 .with_sandbox(test_policy())
1054 .resume(&session_id)
1055 .build()
1056 .await
1057 .expect("resume should succeed for saved session");
1058
1059 assert!(app.is_resumed());
1060 assert_eq!(app.session_id(), session_id);
1061 }
1062
1063 #[tokio::test]
1064 async fn app_resume_nonexistent_session_fails() {
1065 let wd = WorkDir::temporary().expect("should create temp WorkDir for resume test");
1066 let temp_dir = wd.path().to_path_buf();
1067
1068 struct SessionResolver {
1069 session_dir: PathBuf,
1070 }
1071 impl ConfigResolver for SessionResolver {
1072 fn resolve(&self) -> Result<OrcsConfig, orcs_runtime::ConfigError> {
1073 let mut config = OrcsConfig::default();
1074 config.paths.session_dir = Some(self.session_dir.clone());
1075 Ok(config)
1076 }
1077 }
1078
1079 let result = OrcsApp::builder(SessionResolver {
1080 session_dir: temp_dir.clone(),
1081 })
1082 .with_sandbox(test_policy())
1083 .resume("nonexistent-session-id")
1084 .build()
1085 .await;
1086
1087 assert!(result.is_err());
1088 }
1089
1090 #[tokio::test]
1091 async fn app_reload_config() {
1092 use std::sync::atomic::{AtomicBool, Ordering};
1093
1094 struct ToggleResolver {
1095 debug: Arc<AtomicBool>,
1096 }
1097 impl ConfigResolver for ToggleResolver {
1098 fn resolve(&self) -> Result<OrcsConfig, orcs_runtime::ConfigError> {
1099 Ok(OrcsConfig {
1100 debug: self.debug.load(Ordering::Relaxed),
1101 ..OrcsConfig::default()
1102 })
1103 }
1104 }
1105
1106 let flag = Arc::new(AtomicBool::new(false));
1107 let mut app = OrcsApp::builder(ToggleResolver {
1108 debug: Arc::clone(&flag),
1109 })
1110 .with_sandbox(test_policy())
1111 .build()
1112 .await
1113 .expect("builder should succeed with ToggleResolver");
1114
1115 assert!(!app.config().debug);
1116
1117 flag.store(true, Ordering::Relaxed);
1118 app.reload_config().expect("reload config should succeed");
1119
1120 assert!(app.config().debug);
1121 }
1122}