1use crate::context::ExecutionContext;
29use crate::error::{display_error, format_error, DynamicCliError, ExecutionError, Result};
30use crate::parser::{CliParser, ParsedArgs, ReplParser};
31use crate::registry::CommandRegistry;
32use std::path::Path;
33use std::process;
34
35#[derive(Debug)]
45struct ResolvedSegment {
46 name: String,
48 parsed: ParsedArgs,
50}
51
52pub struct CliInterface {
74 registry: CommandRegistry,
76
77 context: Box<dyn ExecutionContext>,
79}
80
81impl CliInterface {
82 pub fn new(registry: CommandRegistry, context: Box<dyn ExecutionContext>) -> Self {
107 Self { registry, context }
108 }
109
110 pub fn run(mut self, args: Vec<String>) -> Result<()> {
156 if args.is_empty() {
158 return Err(DynamicCliError::Parse(
159 crate::error::ParseError::InvalidSyntax {
160 details: "No command specified".to_string(),
161 hint: Some("Try 'help' to see available commands".to_string()),
162 },
163 ));
164 }
165
166 self.dispatch(&args)
167 }
168
169 fn dispatch(&mut self, args: &[String]) -> Result<()> {
189 let segments = self.segment(args)?;
190
191 if segments.len() == 1 {
192 return self.execute_segment(&segments[0]);
193 }
194
195 self.execute_chain(&segments)
196 }
197
198 fn execute_chain(&mut self, segments: &[ResolvedSegment]) -> Result<()> {
229 let total = segments.len();
230 let mut chain_has_failure = false;
231 let mut triggering_failure: Option<DynamicCliError> = None;
232
233 for (idx, segment) in segments.iter().enumerate() {
234 let position = idx + 1;
235
236 let (requires_success, continue_on_failure) = self
237 .registry
238 .get_definition(&segment.name)
239 .map(|d| (d.requires_success, d.continue_on_failure))
240 .unwrap_or((false, false));
241
242 if chain_has_failure && requires_success {
243 eprintln!(
244 "Skipped: command {}/{} ('{}') — a preceding command failed",
245 position, total, segment.name
246 );
247 continue;
248 }
249
250 if let Err(e) = self.execute_segment(segment) {
251 let wrapped = wrap_chain_error(position, total, &segment.name, e);
252
253 if !chain_has_failure {
254 triggering_failure = Some(wrapped);
255 }
256 chain_has_failure = true;
257
258 if !continue_on_failure {
259 break;
260 }
261 }
262 }
263
264 match triggering_failure {
265 Some(e) => Err(e),
266 None => Ok(()),
267 }
268 }
269
270 fn segment(&self, args: &[String]) -> Result<Vec<ResolvedSegment>> {
302 let mut segments = Vec::new();
303 let mut offset = 0;
304
305 loop {
306 let command_name = &args[offset];
307
308 let resolved_name = self.registry.resolve_name(command_name).ok_or_else(|| {
309 crate::error::ParseError::unknown_command_with_suggestions(
310 command_name,
311 &self
312 .registry
313 .list_commands()
314 .iter()
315 .map(|cmd| cmd.name.clone())
316 .collect::<Vec<_>>(),
317 )
318 })?;
319
320 let definition = self.registry.get_definition(resolved_name).ok_or_else(|| {
321 DynamicCliError::Registry(crate::error::RegistryError::missing_handler(
322 resolved_name,
323 ))
324 })?;
325
326 let parser = CliParser::new(definition);
327 let (parsed_map, consumed) = parser.parse_typed_segment(&args[offset + 1..])?;
328
329 segments.push(ResolvedSegment {
330 name: resolved_name.to_string(),
331 parsed: ParsedArgs::new(parsed_map),
332 });
333
334 let next = offset + 1 + consumed;
335 if next == args.len() {
336 break;
337 }
338
339 if self.registry.resolve_name(&args[next]).is_none() {
340 return Err(crate::error::ParseError::too_many_arguments(
341 &definition.name,
342 definition.arguments.len(),
343 definition.arguments.len() + 1,
344 )
345 .into());
346 }
347
348 offset = next;
349 }
350
351 Ok(segments)
352 }
353
354 fn execute_segment(&mut self, segment: &ResolvedSegment) -> Result<()> {
362 if let Some(handler) = self.registry.get_handler_sync(&segment.name) {
363 handler.execute(&mut *self.context, &segment.parsed)?;
364 } else if let Some(handler) = self.registry.get_handler_async(&segment.name) {
365 futures::executor::block_on(handler.execute(&mut *self.context, &segment.parsed))?;
366 } else {
367 let implementation = self
372 .registry
373 .get_definition(&segment.name)
374 .map(|d| d.implementation.as_str())
375 .unwrap_or("");
376 return Err(DynamicCliError::Execution(
377 crate::error::ExecutionError::handler_not_found(&segment.name, implementation),
378 ));
379 }
380
381 Ok(())
382 }
383
384 pub fn run_and_exit(self, args: Vec<String>) -> ! {
419 match self.run(args) {
420 Ok(()) => process::exit(0),
421 Err(e) => {
422 display_error(&e);
423
424 let exit_code = match e {
426 DynamicCliError::Parse(_) => 2,
427 DynamicCliError::Validation(_) => 2,
428 DynamicCliError::Execution(_) => 1,
429 _ => 3,
430 };
431
432 process::exit(exit_code);
433 }
434 }
435 }
436
437 pub fn run_script(
485 mut self,
486 path: impl AsRef<Path>,
487 policy: ScriptErrorPolicy,
488 ) -> Result<ScriptOutcome> {
489 let path = path.as_ref();
490 let content = std::fs::read_to_string(path).map_err(|e| {
491 DynamicCliError::Execution(ExecutionError::CommandFailed(anyhow::anyhow!(
492 "failed to read script file {}: {}",
493 path.display(),
494 e
495 )))
496 })?;
497
498 let mut outcome = ScriptOutcome {
499 lines_executed: 0,
500 lines_succeeded: 0,
501 failures: Vec::new(),
502 };
503
504 for (idx, raw_line) in content.lines().enumerate() {
505 let line_number = idx + 1;
506 let line = raw_line.trim();
507
508 if line.is_empty() || line.starts_with('#') {
509 continue;
510 }
511
512 outcome.lines_executed += 1;
513
514 let tokens_result = {
521 let tokenizer = ReplParser::new(&self.registry);
522 tokenizer.tokenize(line)
523 };
524
525 let tokens = match tokens_result {
526 Ok(t) => t,
527 Err(e) => {
528 let wrapped = wrap_line_error(line_number, e);
529 if policy == ScriptErrorPolicy::Abort {
530 return Err(wrapped);
531 }
532 outcome.failures.push((line_number, wrapped));
533 continue;
534 }
535 };
536
537 if tokens.is_empty() {
538 continue;
539 }
540
541 match self.dispatch(&tokens) {
542 Ok(()) => outcome.lines_succeeded += 1,
543 Err(e) => {
544 let wrapped = wrap_line_error(line_number, e);
545 if policy == ScriptErrorPolicy::Abort {
546 return Err(wrapped);
547 }
548 outcome.failures.push((line_number, wrapped));
549 }
550 }
551 }
552
553 Ok(outcome)
554 }
555}
556
557fn wrap_line_error(line_number: usize, source: DynamicCliError) -> DynamicCliError {
562 DynamicCliError::Execution(ExecutionError::CommandFailed(anyhow::anyhow!(
563 "line {}: {}",
564 line_number,
565 source
566 )))
567}
568
569fn wrap_chain_error(
577 position: usize,
578 total: usize,
579 name: &str,
580 source: DynamicCliError,
581) -> DynamicCliError {
582 DynamicCliError::Execution(ExecutionError::CommandFailed(anyhow::anyhow!(
583 "Error in command {}/{} ('{}'): {}",
584 position,
585 total,
586 name,
587 format_error(&source)
588 )))
589}
590
591#[derive(Debug, Clone, Copy, PartialEq, Eq)]
593pub enum ScriptErrorPolicy {
594 Abort,
597 Continue,
601}
602
603#[derive(Debug)]
605pub struct ScriptOutcome {
606 pub lines_executed: usize,
608 pub lines_succeeded: usize,
610 pub failures: Vec<(usize, DynamicCliError)>,
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620 use crate::config::schema::{ArgumentDefinition, ArgumentType, CommandDefinition};
621
622 #[derive(Default)]
624 struct TestContext {
625 executed_command: Option<String>,
626 executed_commands: Vec<String>,
631 }
632
633 impl ExecutionContext for TestContext {
634 fn as_any(&self) -> &dyn std::any::Any {
635 self
636 }
637
638 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
639 self
640 }
641 }
642
643 struct TestHandler {
645 name: String,
646 }
647
648 impl crate::executor::CommandHandler for TestHandler {
649 fn execute(&self, context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
650 let ctx = crate::context::downcast_mut::<TestContext>(context)
651 .expect("Failed to downcast context");
652 ctx.executed_command = Some(self.name.clone());
653 ctx.executed_commands.push(self.name.clone());
654 Ok(())
655 }
656 }
657
658 struct FailingHandler {
662 name: String,
663 }
664
665 impl crate::executor::CommandHandler for FailingHandler {
666 fn execute(&self, context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
667 let ctx = crate::context::downcast_mut::<TestContext>(context)
668 .expect("Failed to downcast context");
669 ctx.executed_commands.push(self.name.clone());
670 Err(DynamicCliError::Execution(ExecutionError::CommandFailed(
671 anyhow::anyhow!("{} deliberately failed", self.name),
672 )))
673 }
674 }
675
676 fn create_test_registry() -> CommandRegistry {
677 let mut registry = CommandRegistry::new();
678
679 let cmd_def = CommandDefinition {
681 name: "test".to_string(),
682 aliases: vec!["t".to_string()],
683 description: "Test command".to_string(),
684 required: false,
685 arguments: vec![],
686 options: vec![],
687 implementation: "test_handler".to_string(),
688 continue_on_failure: false,
689 requires_success: false,
690 };
691
692 let handler = Box::new(TestHandler {
693 name: "test".to_string(),
694 });
695
696 registry
697 .register_sync(cmd_def, handler)
698 .expect("Failed to register command");
699
700 registry
701 }
702
703 #[test]
704 fn test_cli_interface_creation() {
705 let registry = create_test_registry();
706 let context = Box::new(TestContext::default());
707
708 let _cli = CliInterface::new(registry, context);
709 }
711
712 #[test]
713 fn test_cli_run_simple_command() {
714 let registry = create_test_registry();
715 let context = Box::new(TestContext::default());
716 let cli = CliInterface::new(registry, context);
717
718 let result = cli.run(vec!["test".to_string()]);
719 assert!(result.is_ok());
720 }
721
722 #[test]
723 fn test_cli_run_with_alias() {
724 let registry = create_test_registry();
725 let context = Box::new(TestContext::default());
726 let cli = CliInterface::new(registry, context);
727
728 let result = cli.run(vec!["t".to_string()]);
729 assert!(result.is_ok());
730 }
731
732 #[test]
733 fn test_cli_empty_args() {
734 let registry = create_test_registry();
735 let context = Box::new(TestContext::default());
736 let cli = CliInterface::new(registry, context);
737
738 let result = cli.run(vec![]);
739 assert!(result.is_err());
740
741 match result.unwrap_err() {
742 DynamicCliError::Parse(crate::error::ParseError::InvalidSyntax { .. }) => {}
743 other => panic!("Expected InvalidSyntax error, got: {:?}", other),
744 }
745 }
746
747 #[test]
748 fn test_cli_unknown_command() {
749 let registry = create_test_registry();
750 let context = Box::new(TestContext::default());
751 let cli = CliInterface::new(registry, context);
752
753 let result = cli.run(vec!["unknown".to_string()]);
754 assert!(result.is_err());
755
756 match result.unwrap_err() {
757 DynamicCliError::Parse(crate::error::ParseError::UnknownCommand { .. }) => {}
758 other => panic!("Expected UnknownCommand error, got: {:?}", other),
759 }
760 }
761
762 #[test]
763 fn test_cli_command_with_args() {
764 let mut registry = CommandRegistry::new();
765
766 let cmd_def = CommandDefinition {
768 name: "greet".to_string(),
769 aliases: vec![],
770 description: "Greet someone".to_string(),
771 required: false,
772 arguments: vec![ArgumentDefinition {
773 name: "name".to_string(),
774 arg_type: ArgumentType::String,
775 required: true,
776 description: "Name to greet".to_string(),
777 validation: vec![],
778 secure: false,
779 }],
780 options: vec![],
781 implementation: "greet_handler".to_string(),
782 continue_on_failure: false,
783 requires_success: false,
784 };
785
786 struct GreetHandler;
787 impl crate::executor::CommandHandler for GreetHandler {
788 fn execute(
789 &self,
790 _context: &mut dyn ExecutionContext,
791 args: &ParsedArgs,
792 ) -> Result<()> {
793 assert_eq!(args.get_scalar("name"), Some("Alice"));
794 Ok(())
795 }
796 }
797
798 registry
799 .register_sync(cmd_def, Box::new(GreetHandler))
800 .unwrap();
801
802 let context = Box::new(TestContext::default());
803 let cli = CliInterface::new(registry, context);
804
805 let result = cli.run(vec!["greet".to_string(), "Alice".to_string()]);
806 assert!(result.is_ok());
807 }
808
809 fn write_script(content: &str) -> tempfile::NamedTempFile {
814 use std::io::Write;
815 let mut file = tempfile::NamedTempFile::new().expect("failed to create temp script file");
816 file.write_all(content.as_bytes())
817 .expect("failed to write temp script file");
818 file
819 }
820
821 #[test]
822 fn test_run_script_all_lines_succeed() {
823 let registry = create_test_registry();
824 let context = Box::new(TestContext::default());
825 let cli = CliInterface::new(registry, context);
826
827 let script = write_script("test\nt\ntest\n");
828 let outcome = cli
829 .run_script(script.path(), ScriptErrorPolicy::Abort)
830 .expect("run_script should succeed when every line succeeds");
831
832 assert_eq!(outcome.lines_executed, 3);
833 assert_eq!(outcome.lines_succeeded, 3);
834 assert!(outcome.failures.is_empty());
835 }
836
837 #[test]
838 fn test_run_script_skips_blank_lines_and_comments() {
839 let registry = create_test_registry();
840 let context = Box::new(TestContext::default());
841 let cli = CliInterface::new(registry, context);
842
843 let script = write_script("# a comment\n\ntest\n \n# another\nt\n");
844 let outcome = cli
845 .run_script(script.path(), ScriptErrorPolicy::Abort)
846 .expect("run_script should succeed");
847
848 assert_eq!(outcome.lines_executed, 2);
850 assert_eq!(outcome.lines_succeeded, 2);
851 }
852
853 #[test]
854 fn test_run_script_continue_policy_records_failures_and_keeps_going() {
855 let registry = create_test_registry();
856 let context = Box::new(TestContext::default());
857 let cli = CliInterface::new(registry, context);
858
859 let script = write_script("test\nunknown_command\ntest\n");
860 let outcome = cli
861 .run_script(script.path(), ScriptErrorPolicy::Continue)
862 .expect("Continue policy should return Ok even with a failing line");
863
864 assert_eq!(outcome.lines_executed, 3);
865 assert_eq!(outcome.lines_succeeded, 2);
866 assert_eq!(outcome.failures.len(), 1);
867 assert_eq!(outcome.failures[0].0, 2); }
869
870 #[test]
871 fn test_run_script_abort_policy_stops_at_first_failure() {
872 let registry = create_test_registry();
873 let context = Box::new(TestContext::default());
874 let cli = CliInterface::new(registry, context);
875
876 let script = write_script("test\nunknown_command\ntest\n");
878 let result = cli.run_script(script.path(), ScriptErrorPolicy::Abort);
879
880 assert!(result.is_err());
881 match result.unwrap_err() {
882 DynamicCliError::Execution(ExecutionError::CommandFailed(e)) => {
883 assert!(e.to_string().contains("line 2"));
884 }
885 other => panic!("Expected wrapped CommandFailed error, got: {:?}", other),
886 }
887 }
888
889 #[test]
890 fn test_run_script_respects_quoted_tokens() {
891 let mut registry = CommandRegistry::new();
892 let cmd_def = CommandDefinition {
893 name: "greet".to_string(),
894 aliases: vec![],
895 description: "Greet someone".to_string(),
896 required: false,
897 arguments: vec![ArgumentDefinition {
898 name: "name".to_string(),
899 arg_type: ArgumentType::String,
900 required: true,
901 description: "Name to greet".to_string(),
902 validation: vec![],
903 secure: false,
904 }],
905 options: vec![],
906 implementation: "greet_handler".to_string(),
907 continue_on_failure: false,
908 requires_success: false,
909 };
910
911 struct GreetHandler;
912 impl crate::executor::CommandHandler for GreetHandler {
913 fn execute(
914 &self,
915 _context: &mut dyn ExecutionContext,
916 args: &ParsedArgs,
917 ) -> Result<()> {
918 assert_eq!(args.get_scalar("name"), Some("Alice Wonderland"));
919 Ok(())
920 }
921 }
922
923 registry
924 .register_sync(cmd_def, Box::new(GreetHandler))
925 .unwrap();
926
927 let context = Box::new(TestContext::default());
928 let cli = CliInterface::new(registry, context);
929
930 let script = write_script(r#"greet "Alice Wonderland""#);
931 let outcome = cli
932 .run_script(script.path(), ScriptErrorPolicy::Abort)
933 .expect("quoted argument should tokenize as a single value");
934
935 assert_eq!(outcome.lines_succeeded, 1);
936 }
937
938 #[test]
939 fn test_run_script_missing_file() {
940 let registry = create_test_registry();
941 let context = Box::new(TestContext::default());
942 let cli = CliInterface::new(registry, context);
943
944 let result = cli.run_script("/nonexistent/path/to/script.txt", ScriptErrorPolicy::Abort);
945 assert!(result.is_err());
946 }
947
948 fn register_arity_command(registry: &mut CommandRegistry, name: &str, arity: usize) {
957 let arguments = (0..arity)
958 .map(|i| ArgumentDefinition {
959 name: format!("arg{}", i),
960 arg_type: ArgumentType::String,
961 required: true,
962 description: format!("Argument {}", i),
963 validation: vec![],
964 secure: false,
965 })
966 .collect();
967
968 let cmd_def = CommandDefinition {
969 name: name.to_string(),
970 aliases: vec![],
971 description: format!("Test command {}", name),
972 required: false,
973 arguments,
974 options: vec![],
975 implementation: format!("{}_handler", name),
976 continue_on_failure: false,
977 requires_success: false,
978 };
979
980 registry
981 .register_sync(
982 cmd_def,
983 Box::new(TestHandler {
984 name: name.to_string(),
985 }),
986 )
987 .expect("Failed to register command");
988 }
989
990 #[test]
991 fn test_segment_single_command_produces_one_segment() {
992 let mut registry = CommandRegistry::new();
995 register_arity_command(&mut registry, "greet", 1);
996 let context = Box::new(TestContext::default());
997 let cli = CliInterface::new(registry, context);
998
999 let args = vec!["greet".to_string(), "Alice".to_string()];
1000 let segments = cli.segment(&args).unwrap();
1001
1002 assert_eq!(segments.len(), 1);
1003 assert_eq!(segments[0].name, "greet");
1004 assert_eq!(segments[0].parsed.get_scalar("arg0"), Some("Alice"));
1005 }
1006
1007 #[test]
1008 fn test_segment_single_command_overflow_still_raises_too_many_arguments() {
1009 let mut registry = CommandRegistry::new();
1013 register_arity_command(&mut registry, "greet", 1);
1014 let context = Box::new(TestContext::default());
1015 let cli = CliInterface::new(registry, context);
1016
1017 let args = vec![
1018 "greet".to_string(),
1019 "Alice".to_string(),
1020 "extra".to_string(),
1021 ];
1022 let result = cli.segment(&args);
1023
1024 assert!(result.is_err());
1025 match result.unwrap_err() {
1026 DynamicCliError::Parse(crate::error::ParseError::TooManyArguments {
1027 command,
1028 expected,
1029 got,
1030 ..
1031 }) => {
1032 assert_eq!(command, "greet");
1033 assert_eq!(expected, 1);
1034 assert_eq!(got, 2);
1035 }
1036 other => panic!("Expected TooManyArguments error, got: {:?}", other),
1037 }
1038 }
1039
1040 #[test]
1041 fn test_segment_multi_command_chain_produces_three_segments() {
1042 let mut registry = CommandRegistry::new();
1048 register_arity_command(&mut registry, "first", 1);
1049 register_arity_command(&mut registry, "second", 1);
1050 register_arity_command(&mut registry, "third", 0);
1051 let context = Box::new(TestContext::default());
1052 let cli = CliInterface::new(registry, context);
1053
1054 let args = vec![
1055 "first".to_string(),
1056 "1".to_string(),
1057 "second".to_string(),
1058 "2".to_string(),
1059 "third".to_string(),
1060 ];
1061 let segments = cli.segment(&args).unwrap();
1062
1063 assert_eq!(segments.len(), 3);
1064 assert_eq!(segments[0].name, "first");
1065 assert_eq!(segments[0].parsed.get_scalar("arg0"), Some("1"));
1066 assert_eq!(segments[1].name, "second");
1067 assert_eq!(segments[1].parsed.get_scalar("arg0"), Some("2"));
1068 assert_eq!(segments[2].name, "third");
1069 }
1070
1071 #[test]
1072 fn test_segment_unknown_command_produces_unknown_command_error() {
1073 let registry = create_test_registry();
1081 let context = Box::new(TestContext::default());
1082 let cli = CliInterface::new(registry, context);
1083
1084 let args = vec!["nope".to_string()];
1085 let result = cli.segment(&args);
1086
1087 assert!(result.is_err());
1088 match result.unwrap_err() {
1089 DynamicCliError::Parse(crate::error::ParseError::UnknownCommand { .. }) => {}
1090 other => panic!("Expected UnknownCommand error, got: {:?}", other),
1091 }
1092 }
1093
1094 #[test]
1095 fn test_segment_repeated_command_name_resolves_each_occurrence_independently() {
1096 let mut registry = CommandRegistry::new();
1100 register_arity_command(&mut registry, "source", 1);
1101 register_arity_command(&mut registry, "run", 0);
1102 let context = Box::new(TestContext::default());
1103 let cli = CliInterface::new(registry, context);
1104
1105 let args = vec![
1106 "source".to_string(),
1107 "modelfile".to_string(),
1108 "source".to_string(),
1109 "solverfile".to_string(),
1110 "run".to_string(),
1111 ];
1112 let segments = cli.segment(&args).unwrap();
1113
1114 assert_eq!(segments.len(), 3);
1115 assert_eq!(segments[0].name, "source");
1116 assert_eq!(segments[0].parsed.get_scalar("arg0"), Some("modelfile"));
1117 assert_eq!(segments[1].name, "source");
1118 assert_eq!(segments[1].parsed.get_scalar("arg0"), Some("solverfile"));
1119 assert_eq!(segments[2].name, "run");
1120 }
1121
1122 #[test]
1123 fn test_dispatch_executes_chain_in_order() {
1124 let mut registry = CommandRegistry::new();
1127 register_arity_command(&mut registry, "first", 1);
1128 register_arity_command(&mut registry, "second", 1);
1129 register_arity_command(&mut registry, "third", 0);
1130 let context = Box::new(TestContext::default());
1131 let mut cli = CliInterface::new(registry, context);
1132
1133 let args = vec![
1134 "first".to_string(),
1135 "1".to_string(),
1136 "second".to_string(),
1137 "2".to_string(),
1138 "third".to_string(),
1139 ];
1140 cli.dispatch(&args).expect("chain should execute fully");
1141
1142 let ctx = crate::context::downcast_ref::<TestContext>(&*cli.context)
1143 .expect("Failed to downcast context");
1144 assert_eq!(
1145 ctx.executed_commands,
1146 vec![
1147 "first".to_string(),
1148 "second".to_string(),
1149 "third".to_string()
1150 ]
1151 );
1152 }
1153
1154 #[test]
1155 fn test_segment_known_limitation_extra_token_matching_command_name_is_silently_absorbed() {
1156 let mut registry = CommandRegistry::new();
1163 register_arity_command(&mut registry, "greet", 1);
1164 register_arity_command(&mut registry, "run", 0);
1165 let context = Box::new(TestContext::default());
1166 let cli = CliInterface::new(registry, context);
1167
1168 let args = vec!["greet".to_string(), "Alice".to_string(), "run".to_string()];
1173 let segments = cli
1174 .segment(&args)
1175 .expect("known limitation: no error is raised here, by design");
1176
1177 assert_eq!(segments.len(), 2);
1178 assert_eq!(segments[0].name, "greet");
1179 assert_eq!(segments[0].parsed.get_scalar("arg0"), Some("Alice"));
1180 assert_eq!(segments[1].name, "run");
1181 }
1182
1183 fn register_chain_command(
1191 registry: &mut CommandRegistry,
1192 name: &str,
1193 continue_on_failure: bool,
1194 requires_success: bool,
1195 fails: bool,
1196 ) {
1197 let cmd_def = CommandDefinition {
1198 name: name.to_string(),
1199 aliases: vec![],
1200 description: format!("Test command {}", name),
1201 required: false,
1202 arguments: vec![],
1203 options: vec![],
1204 implementation: format!("{}_handler", name),
1205 continue_on_failure,
1206 requires_success,
1207 };
1208
1209 let handler: Box<dyn crate::executor::CommandHandler> = if fails {
1210 Box::new(FailingHandler {
1211 name: name.to_string(),
1212 })
1213 } else {
1214 Box::new(TestHandler {
1215 name: name.to_string(),
1216 })
1217 };
1218
1219 registry
1220 .register_sync(cmd_def, handler)
1221 .expect("Failed to register command");
1222 }
1223
1224 #[test]
1225 fn test_execute_chain_continue_on_failure_false_stops_chain() {
1226 let mut registry = CommandRegistry::new();
1227 register_chain_command(&mut registry, "a", false, false, true); register_chain_command(&mut registry, "b", false, false, false);
1229 let context = Box::new(TestContext::default());
1230 let mut cli = CliInterface::new(registry, context);
1231
1232 let args = vec!["a".to_string(), "b".to_string()];
1233 let result = cli.dispatch(&args);
1234
1235 assert!(result.is_err());
1236 assert!(result
1237 .unwrap_err()
1238 .to_string()
1239 .contains("Error in command 1/2 ('a')"));
1240
1241 let ctx = crate::context::downcast_ref::<TestContext>(&*cli.context).unwrap();
1242 assert_eq!(
1243 ctx.executed_commands,
1244 vec!["a".to_string()],
1245 "'b' must never run once 'a' stops the chain"
1246 );
1247 }
1248
1249 #[test]
1250 fn test_execute_chain_continue_on_failure_true_proceeds_and_still_errors() {
1251 let mut registry = CommandRegistry::new();
1252 register_chain_command(&mut registry, "a", true, false, true); register_chain_command(&mut registry, "b", false, false, false);
1254 let context = Box::new(TestContext::default());
1255 let mut cli = CliInterface::new(registry, context);
1256
1257 let args = vec!["a".to_string(), "b".to_string()];
1258 let result = cli.dispatch(&args);
1259
1260 assert!(result.is_err());
1264 assert!(result
1265 .unwrap_err()
1266 .to_string()
1267 .contains("Error in command 1/2 ('a')"));
1268
1269 let ctx = crate::context::downcast_ref::<TestContext>(&*cli.context).unwrap();
1270 assert_eq!(
1271 ctx.executed_commands,
1272 vec!["a".to_string(), "b".to_string()],
1273 "'b' must still run: 'a''s failure was absorbed"
1274 );
1275 }
1276
1277 #[test]
1278 fn test_execute_chain_requires_success_skips_after_earlier_failure() {
1279 let mut registry = CommandRegistry::new();
1280 register_chain_command(&mut registry, "a", true, false, true); register_chain_command(&mut registry, "b", false, true, false); let context = Box::new(TestContext::default());
1283 let mut cli = CliInterface::new(registry, context);
1284
1285 let args = vec!["a".to_string(), "b".to_string()];
1286 let result = cli.dispatch(&args);
1287
1288 assert!(result.is_err());
1289 let ctx = crate::context::downcast_ref::<TestContext>(&*cli.context).unwrap();
1290 assert_eq!(
1291 ctx.executed_commands,
1292 vec!["a".to_string()],
1293 "'b' must be skipped, not executed, once 'a' has failed"
1294 );
1295 }
1296
1297 #[test]
1298 fn test_execute_chain_requires_success_runs_normally_without_a_preceding_failure() {
1299 let mut registry = CommandRegistry::new();
1302 register_chain_command(&mut registry, "a", false, false, false); register_chain_command(&mut registry, "b", false, true, false); let context = Box::new(TestContext::default());
1305 let mut cli = CliInterface::new(registry, context);
1306
1307 let args = vec!["a".to_string(), "b".to_string()];
1308 cli.dispatch(&args)
1309 .expect("no failure anywhere in the chain");
1310
1311 let ctx = crate::context::downcast_ref::<TestContext>(&*cli.context).unwrap();
1312 assert_eq!(
1313 ctx.executed_commands,
1314 vec!["a".to_string(), "b".to_string()]
1315 );
1316 }
1317
1318 #[test]
1319 fn test_execute_chain_reports_repeated_command_name_by_position_not_name_early() {
1320 let mut registry = CommandRegistry::new();
1321 register_chain_command(&mut registry, "ok", false, false, false);
1322 register_chain_command(&mut registry, "source", true, false, true); let context = Box::new(TestContext::default());
1324 let mut cli = CliInterface::new(registry, context);
1325
1326 let args = vec![
1328 "ok".to_string(),
1329 "source".to_string(),
1330 "ok".to_string(),
1331 "ok".to_string(),
1332 ];
1333 let result = cli.dispatch(&args);
1334
1335 assert!(result.is_err());
1336 let message = result.unwrap_err().to_string();
1337 assert!(message.contains("Error in command 2/4 ('source')"));
1338 assert!(!message.contains("4/4"));
1339 }
1340
1341 #[test]
1342 fn test_execute_chain_reports_repeated_command_name_by_position_not_name_late() {
1343 let mut registry = CommandRegistry::new();
1344 register_chain_command(&mut registry, "ok", false, false, false);
1345 register_chain_command(&mut registry, "source", true, false, true); let context = Box::new(TestContext::default());
1347 let mut cli = CliInterface::new(registry, context);
1348
1349 let args = vec![
1354 "ok".to_string(),
1355 "ok".to_string(),
1356 "ok".to_string(),
1357 "source".to_string(),
1358 ];
1359 let result = cli.dispatch(&args);
1360
1361 assert!(result.is_err());
1362 let message = result.unwrap_err().to_string();
1363 assert!(message.contains("Error in command 4/4 ('source')"));
1364 assert!(!message.contains("2/4"));
1365 }
1366
1367 #[test]
1368 fn test_run_script_chain_failure_reports_chain_position_and_line_number() {
1369 let mut registry = CommandRegistry::new();
1375 register_chain_command(&mut registry, "a", false, false, true); register_chain_command(&mut registry, "b", false, false, false);
1377 let context = Box::new(TestContext::default());
1378 let cli = CliInterface::new(registry, context);
1379
1380 let script = write_script("a b\n");
1381 let outcome = cli
1382 .run_script(script.path(), ScriptErrorPolicy::Continue)
1383 .expect("Continue policy should return Ok even with a failing line");
1384
1385 assert_eq!(outcome.failures.len(), 1);
1386 let (line_number, error) = &outcome.failures[0];
1387 assert_eq!(*line_number, 1);
1388 let message = error.to_string();
1389 assert!(message.contains("line 1"));
1390 assert!(message.contains("Error in command 1/2 ('a')"));
1391 }
1392}