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
35const COMMANDS_HINT: &str = "Run with '--help' for available commands";
38
39#[derive(Debug)]
49struct ResolvedSegment {
50 name: String,
52 parsed: ParsedArgs,
54}
55
56pub struct CliInterface {
78 registry: CommandRegistry,
80
81 context: Box<dyn ExecutionContext>,
83}
84
85impl CliInterface {
86 pub fn new(registry: CommandRegistry, context: Box<dyn ExecutionContext>) -> Self {
111 Self { registry, context }
112 }
113
114 pub fn run(mut self, args: Vec<String>) -> Result<()> {
160 if args.is_empty() {
162 return Err(DynamicCliError::Parse(
163 crate::error::ParseError::InvalidSyntax {
164 details: "No command specified".to_string(),
165 hint: Some("Try 'help' to see available commands".to_string()),
166 },
167 ));
168 }
169
170 self.dispatch(&args)
171 }
172
173 fn dispatch(&mut self, args: &[String]) -> Result<()> {
194 let segments = self.segment(args)?;
195
196 if segments.len() == 1 {
197 return self.execute_segment(&segments[0]);
198 }
199
200 self.execute_chain(&segments)
201 }
202
203 fn execute_chain(&mut self, segments: &[ResolvedSegment]) -> Result<()> {
234 let total = segments.len();
235 let mut chain_has_failure = false;
236 let mut triggering_failure: Option<DynamicCliError> = None;
237
238 for (idx, segment) in segments.iter().enumerate() {
239 let position = idx + 1;
240
241 let (requires_success, continue_on_failure) = self
242 .registry
243 .get_definition(&segment.name)
244 .map(|d| (d.requires_success, d.continue_on_failure))
245 .unwrap_or((false, false));
246
247 if chain_has_failure && requires_success {
248 eprintln!(
249 "Skipped: command {}/{} ('{}') — a preceding command failed",
250 position, total, segment.name
251 );
252 continue;
253 }
254
255 if let Err(e) = self.execute_segment(segment) {
256 let wrapped = wrap_chain_error(position, total, &segment.name, e);
257
258 if !chain_has_failure {
259 triggering_failure = Some(wrapped);
260 }
261 chain_has_failure = true;
262
263 if !continue_on_failure {
264 break;
265 }
266 }
267 }
268
269 match triggering_failure {
270 Some(e) => Err(e),
271 None => Ok(()),
272 }
273 }
274
275 fn segment(&self, args: &[String]) -> Result<Vec<ResolvedSegment>> {
307 let mut segments = Vec::new();
308 let mut offset = 0;
309
310 loop {
311 let command_name = &args[offset];
312
313 let resolved_name = self.registry.resolve_name(command_name).ok_or_else(|| {
314 crate::error::ParseError::unknown_command_with_hint(
315 command_name,
316 &self
317 .registry
318 .list_commands()
319 .iter()
320 .map(|cmd| cmd.name.clone())
321 .collect::<Vec<_>>(),
322 COMMANDS_HINT,
323 )
324 })?;
325
326 let definition = self.registry.get_definition(resolved_name).ok_or_else(|| {
327 DynamicCliError::Registry(crate::error::RegistryError::missing_handler(
328 resolved_name,
329 ))
330 })?;
331
332 let parser = CliParser::new(definition);
333 let (parsed_map, consumed) = parser.parse_typed_segment(&args[offset + 1..])?;
334
335 segments.push(ResolvedSegment {
336 name: resolved_name.to_string(),
337 parsed: ParsedArgs::new(parsed_map),
338 });
339
340 let next = offset + 1 + consumed;
341 if next == args.len() {
342 break;
343 }
344
345 if self.registry.resolve_name(&args[next]).is_none() {
346 return Err(crate::error::ParseError::too_many_arguments(
347 &definition.name,
348 definition.arguments.len(),
349 definition.arguments.len() + 1,
350 )
351 .into());
352 }
353
354 offset = next;
355 }
356
357 Ok(segments)
358 }
359
360 fn execute_segment(&mut self, segment: &ResolvedSegment) -> Result<()> {
368 if let Some(handler) = self.registry.get_handler_sync(&segment.name) {
369 handler.execute(&mut *self.context, &segment.parsed)?;
370 } else if let Some(handler) = self.registry.get_handler_async(&segment.name) {
371 futures::executor::block_on(handler.execute(&mut *self.context, &segment.parsed))?;
372 } else {
373 let implementation = self
378 .registry
379 .get_definition(&segment.name)
380 .map(|d| d.implementation.as_str())
381 .unwrap_or("");
382 return Err(DynamicCliError::Execution(
383 crate::error::ExecutionError::handler_not_found(&segment.name, implementation),
384 ));
385 }
386
387 Ok(())
388 }
389
390 pub fn run_and_exit(self, args: Vec<String>) -> ! {
425 match self.run(args) {
426 Ok(()) => process::exit(0),
427 Err(e) => {
428 display_error(&e);
429
430 let exit_code = match e {
432 DynamicCliError::Parse(_) => 2,
433 DynamicCliError::Validation(_) => 2,
434 DynamicCliError::Execution(_) => 1,
435 _ => 3,
436 };
437
438 process::exit(exit_code);
439 }
440 }
441 }
442
443 pub fn run_script(
491 mut self,
492 path: impl AsRef<Path>,
493 policy: ScriptErrorPolicy,
494 ) -> Result<ScriptOutcome> {
495 let path = path.as_ref();
496 let content = std::fs::read_to_string(path).map_err(|e| {
497 DynamicCliError::Execution(ExecutionError::CommandFailed(anyhow::anyhow!(
498 "failed to read script file {}: {}",
499 path.display(),
500 e
501 )))
502 })?;
503
504 let mut outcome = ScriptOutcome {
505 lines_executed: 0,
506 lines_succeeded: 0,
507 failures: Vec::new(),
508 };
509
510 for (idx, raw_line) in content.lines().enumerate() {
511 let line_number = idx + 1;
512 let line = raw_line.trim();
513
514 if line.is_empty() || line.starts_with('#') {
515 continue;
516 }
517
518 outcome.lines_executed += 1;
519
520 let tokens_result = {
527 let tokenizer = ReplParser::new(&self.registry);
528 tokenizer.tokenize(line)
529 };
530
531 let tokens = match tokens_result {
532 Ok(t) => t,
533 Err(e) => {
534 let wrapped = wrap_line_error(line_number, e);
535 if policy == ScriptErrorPolicy::Abort {
536 return Err(wrapped);
537 }
538 outcome.failures.push((line_number, wrapped));
539 continue;
540 }
541 };
542
543 if tokens.is_empty() {
544 continue;
545 }
546
547 match self.dispatch(&tokens) {
548 Ok(()) => outcome.lines_succeeded += 1,
549 Err(e) => {
550 let wrapped = wrap_line_error(line_number, e);
551 if policy == ScriptErrorPolicy::Abort {
552 return Err(wrapped);
553 }
554 outcome.failures.push((line_number, wrapped));
555 }
556 }
557 }
558
559 Ok(outcome)
560 }
561}
562
563fn wrap_line_error(line_number: usize, source: DynamicCliError) -> DynamicCliError {
568 DynamicCliError::Execution(ExecutionError::CommandFailed(anyhow::anyhow!(
569 "line {}: {}",
570 line_number,
571 source
572 )))
573}
574
575fn wrap_chain_error(
583 position: usize,
584 total: usize,
585 name: &str,
586 source: DynamicCliError,
587) -> DynamicCliError {
588 DynamicCliError::Execution(ExecutionError::CommandFailed(anyhow::anyhow!(
589 "Error in command {}/{} ('{}'): {}",
590 position,
591 total,
592 name,
593 format_error(&source)
594 )))
595}
596
597#[derive(Debug, Clone, Copy, PartialEq, Eq)]
599pub enum ScriptErrorPolicy {
600 Abort,
603 Continue,
607}
608
609#[derive(Debug)]
611pub struct ScriptOutcome {
612 pub lines_executed: usize,
614 pub lines_succeeded: usize,
616 pub failures: Vec<(usize, DynamicCliError)>,
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626 use crate::config::schema::{ArgumentDefinition, ArgumentType, CommandDefinition};
627
628 #[derive(Default)]
630 struct TestContext {
631 executed_command: Option<String>,
632 executed_commands: Vec<String>,
637 }
638
639 impl ExecutionContext for TestContext {
640 fn as_any(&self) -> &dyn std::any::Any {
641 self
642 }
643
644 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
645 self
646 }
647 }
648
649 struct TestHandler {
651 name: String,
652 }
653
654 impl crate::executor::CommandHandler for TestHandler {
655 fn execute(&self, context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
656 let ctx = crate::context::downcast_mut::<TestContext>(context)
657 .expect("Failed to downcast context");
658 ctx.executed_command = Some(self.name.clone());
659 ctx.executed_commands.push(self.name.clone());
660 Ok(())
661 }
662 }
663
664 struct FailingHandler {
668 name: String,
669 }
670
671 impl crate::executor::CommandHandler for FailingHandler {
672 fn execute(&self, context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
673 let ctx = crate::context::downcast_mut::<TestContext>(context)
674 .expect("Failed to downcast context");
675 ctx.executed_commands.push(self.name.clone());
676 Err(DynamicCliError::Execution(ExecutionError::CommandFailed(
677 anyhow::anyhow!("{} deliberately failed", self.name),
678 )))
679 }
680 }
681
682 fn create_test_registry() -> CommandRegistry {
683 let mut registry = CommandRegistry::new();
684
685 let cmd_def = CommandDefinition {
687 name: "test".to_string(),
688 aliases: vec!["t".to_string()],
689 description: "Test command".to_string(),
690 required: false,
691 arguments: vec![],
692 options: vec![],
693 implementation: "test_handler".to_string(),
694 continue_on_failure: false,
695 requires_success: false,
696 };
697
698 let handler = Box::new(TestHandler {
699 name: "test".to_string(),
700 });
701
702 registry
703 .register_sync(cmd_def, handler)
704 .expect("Failed to register command");
705
706 registry
707 }
708
709 #[test]
710 fn test_cli_interface_creation() {
711 let registry = create_test_registry();
712 let context = Box::new(TestContext::default());
713
714 let _cli = CliInterface::new(registry, context);
715 }
717
718 #[test]
719 fn test_cli_run_simple_command() {
720 let registry = create_test_registry();
721 let context = Box::new(TestContext::default());
722 let cli = CliInterface::new(registry, context);
723
724 let result = cli.run(vec!["test".to_string()]);
725 assert!(result.is_ok());
726 }
727
728 #[test]
729 fn test_cli_run_with_alias() {
730 let registry = create_test_registry();
731 let context = Box::new(TestContext::default());
732 let cli = CliInterface::new(registry, context);
733
734 let result = cli.run(vec!["t".to_string()]);
735 assert!(result.is_ok());
736 }
737
738 #[test]
739 fn test_cli_empty_args() {
740 let registry = create_test_registry();
741 let context = Box::new(TestContext::default());
742 let cli = CliInterface::new(registry, context);
743
744 let result = cli.run(vec![]);
745 assert!(result.is_err());
746
747 match result.unwrap_err() {
748 DynamicCliError::Parse(crate::error::ParseError::InvalidSyntax { .. }) => {}
749 other => panic!("Expected InvalidSyntax error, got: {:?}", other),
750 }
751 }
752
753 #[test]
754 fn test_cli_unknown_command() {
755 let registry = create_test_registry();
756 let context = Box::new(TestContext::default());
757 let cli = CliInterface::new(registry, context);
758
759 let result = cli.run(vec!["unknown".to_string()]);
760 assert!(result.is_err());
761
762 match result.unwrap_err() {
763 DynamicCliError::Parse(crate::error::ParseError::UnknownCommand { hint, .. }) => {
764 assert_eq!(
765 hint.as_deref(),
766 Some("Run with '--help' for available commands")
767 );
768 }
769 other => panic!("Expected UnknownCommand error, got: {:?}", other),
770 }
771 }
772
773 #[test]
774 fn test_cli_command_with_args() {
775 let mut registry = CommandRegistry::new();
776
777 let cmd_def = CommandDefinition {
779 name: "greet".to_string(),
780 aliases: vec![],
781 description: "Greet someone".to_string(),
782 required: false,
783 arguments: vec![ArgumentDefinition {
784 name: "name".to_string(),
785 arg_type: ArgumentType::String,
786 required: true,
787 description: "Name to greet".to_string(),
788 validation: vec![],
789 secure: false,
790 }],
791 options: vec![],
792 implementation: "greet_handler".to_string(),
793 continue_on_failure: false,
794 requires_success: false,
795 };
796
797 struct GreetHandler;
798 impl crate::executor::CommandHandler for GreetHandler {
799 fn execute(
800 &self,
801 _context: &mut dyn ExecutionContext,
802 args: &ParsedArgs,
803 ) -> Result<()> {
804 assert_eq!(args.get_scalar("name"), Some("Alice"));
805 Ok(())
806 }
807 }
808
809 registry
810 .register_sync(cmd_def, Box::new(GreetHandler))
811 .unwrap();
812
813 let context = Box::new(TestContext::default());
814 let cli = CliInterface::new(registry, context);
815
816 let result = cli.run(vec!["greet".to_string(), "Alice".to_string()]);
817 assert!(result.is_ok());
818 }
819
820 fn write_script(content: &str) -> tempfile::NamedTempFile {
825 use std::io::Write;
826 let mut file = tempfile::NamedTempFile::new().expect("failed to create temp script file");
827 file.write_all(content.as_bytes())
828 .expect("failed to write temp script file");
829 file
830 }
831
832 #[test]
833 fn test_run_script_all_lines_succeed() {
834 let registry = create_test_registry();
835 let context = Box::new(TestContext::default());
836 let cli = CliInterface::new(registry, context);
837
838 let script = write_script("test\nt\ntest\n");
839 let outcome = cli
840 .run_script(script.path(), ScriptErrorPolicy::Abort)
841 .expect("run_script should succeed when every line succeeds");
842
843 assert_eq!(outcome.lines_executed, 3);
844 assert_eq!(outcome.lines_succeeded, 3);
845 assert!(outcome.failures.is_empty());
846 }
847
848 #[test]
849 fn test_run_script_skips_blank_lines_and_comments() {
850 let registry = create_test_registry();
851 let context = Box::new(TestContext::default());
852 let cli = CliInterface::new(registry, context);
853
854 let script = write_script("# a comment\n\ntest\n \n# another\nt\n");
855 let outcome = cli
856 .run_script(script.path(), ScriptErrorPolicy::Abort)
857 .expect("run_script should succeed");
858
859 assert_eq!(outcome.lines_executed, 2);
861 assert_eq!(outcome.lines_succeeded, 2);
862 }
863
864 #[test]
865 fn test_run_script_continue_policy_records_failures_and_keeps_going() {
866 let registry = create_test_registry();
867 let context = Box::new(TestContext::default());
868 let cli = CliInterface::new(registry, context);
869
870 let script = write_script("test\nunknown_command\ntest\n");
871 let outcome = cli
872 .run_script(script.path(), ScriptErrorPolicy::Continue)
873 .expect("Continue policy should return Ok even with a failing line");
874
875 assert_eq!(outcome.lines_executed, 3);
876 assert_eq!(outcome.lines_succeeded, 2);
877 assert_eq!(outcome.failures.len(), 1);
878 assert_eq!(outcome.failures[0].0, 2); }
880
881 #[test]
882 fn test_run_script_abort_policy_stops_at_first_failure() {
883 let registry = create_test_registry();
884 let context = Box::new(TestContext::default());
885 let cli = CliInterface::new(registry, context);
886
887 let script = write_script("test\nunknown_command\ntest\n");
889 let result = cli.run_script(script.path(), ScriptErrorPolicy::Abort);
890
891 assert!(result.is_err());
892 match result.unwrap_err() {
893 DynamicCliError::Execution(ExecutionError::CommandFailed(e)) => {
894 assert!(e.to_string().contains("line 2"));
895 }
896 other => panic!("Expected wrapped CommandFailed error, got: {:?}", other),
897 }
898 }
899
900 #[test]
901 fn test_run_script_respects_quoted_tokens() {
902 let mut registry = CommandRegistry::new();
903 let cmd_def = CommandDefinition {
904 name: "greet".to_string(),
905 aliases: vec![],
906 description: "Greet someone".to_string(),
907 required: false,
908 arguments: vec![ArgumentDefinition {
909 name: "name".to_string(),
910 arg_type: ArgumentType::String,
911 required: true,
912 description: "Name to greet".to_string(),
913 validation: vec![],
914 secure: false,
915 }],
916 options: vec![],
917 implementation: "greet_handler".to_string(),
918 continue_on_failure: false,
919 requires_success: false,
920 };
921
922 struct GreetHandler;
923 impl crate::executor::CommandHandler for GreetHandler {
924 fn execute(
925 &self,
926 _context: &mut dyn ExecutionContext,
927 args: &ParsedArgs,
928 ) -> Result<()> {
929 assert_eq!(args.get_scalar("name"), Some("Alice Wonderland"));
930 Ok(())
931 }
932 }
933
934 registry
935 .register_sync(cmd_def, Box::new(GreetHandler))
936 .unwrap();
937
938 let context = Box::new(TestContext::default());
939 let cli = CliInterface::new(registry, context);
940
941 let script = write_script(r#"greet "Alice Wonderland""#);
942 let outcome = cli
943 .run_script(script.path(), ScriptErrorPolicy::Abort)
944 .expect("quoted argument should tokenize as a single value");
945
946 assert_eq!(outcome.lines_succeeded, 1);
947 }
948
949 #[test]
950 fn test_run_script_missing_file() {
951 let registry = create_test_registry();
952 let context = Box::new(TestContext::default());
953 let cli = CliInterface::new(registry, context);
954
955 let result = cli.run_script("/nonexistent/path/to/script.txt", ScriptErrorPolicy::Abort);
956 assert!(result.is_err());
957 }
958
959 fn register_arity_command(registry: &mut CommandRegistry, name: &str, arity: usize) {
968 let arguments = (0..arity)
969 .map(|i| ArgumentDefinition {
970 name: format!("arg{}", i),
971 arg_type: ArgumentType::String,
972 required: true,
973 description: format!("Argument {}", i),
974 validation: vec![],
975 secure: false,
976 })
977 .collect();
978
979 let cmd_def = CommandDefinition {
980 name: name.to_string(),
981 aliases: vec![],
982 description: format!("Test command {}", name),
983 required: false,
984 arguments,
985 options: vec![],
986 implementation: format!("{}_handler", name),
987 continue_on_failure: false,
988 requires_success: false,
989 };
990
991 registry
992 .register_sync(
993 cmd_def,
994 Box::new(TestHandler {
995 name: name.to_string(),
996 }),
997 )
998 .expect("Failed to register command");
999 }
1000
1001 #[test]
1002 fn test_segment_single_command_produces_one_segment() {
1003 let mut registry = CommandRegistry::new();
1006 register_arity_command(&mut registry, "greet", 1);
1007 let context = Box::new(TestContext::default());
1008 let cli = CliInterface::new(registry, context);
1009
1010 let args = vec!["greet".to_string(), "Alice".to_string()];
1011 let segments = cli.segment(&args).unwrap();
1012
1013 assert_eq!(segments.len(), 1);
1014 assert_eq!(segments[0].name, "greet");
1015 assert_eq!(segments[0].parsed.get_scalar("arg0"), Some("Alice"));
1016 }
1017
1018 #[test]
1019 fn test_segment_single_command_overflow_still_raises_too_many_arguments() {
1020 let mut registry = CommandRegistry::new();
1024 register_arity_command(&mut registry, "greet", 1);
1025 let context = Box::new(TestContext::default());
1026 let cli = CliInterface::new(registry, context);
1027
1028 let args = vec![
1029 "greet".to_string(),
1030 "Alice".to_string(),
1031 "extra".to_string(),
1032 ];
1033 let result = cli.segment(&args);
1034
1035 assert!(result.is_err());
1036 match result.unwrap_err() {
1037 DynamicCliError::Parse(crate::error::ParseError::TooManyArguments {
1038 command,
1039 expected,
1040 got,
1041 ..
1042 }) => {
1043 assert_eq!(command, "greet");
1044 assert_eq!(expected, 1);
1045 assert_eq!(got, 2);
1046 }
1047 other => panic!("Expected TooManyArguments error, got: {:?}", other),
1048 }
1049 }
1050
1051 #[test]
1052 fn test_segment_multi_command_chain_produces_three_segments() {
1053 let mut registry = CommandRegistry::new();
1059 register_arity_command(&mut registry, "first", 1);
1060 register_arity_command(&mut registry, "second", 1);
1061 register_arity_command(&mut registry, "third", 0);
1062 let context = Box::new(TestContext::default());
1063 let cli = CliInterface::new(registry, context);
1064
1065 let args = vec![
1066 "first".to_string(),
1067 "1".to_string(),
1068 "second".to_string(),
1069 "2".to_string(),
1070 "third".to_string(),
1071 ];
1072 let segments = cli.segment(&args).unwrap();
1073
1074 assert_eq!(segments.len(), 3);
1075 assert_eq!(segments[0].name, "first");
1076 assert_eq!(segments[0].parsed.get_scalar("arg0"), Some("1"));
1077 assert_eq!(segments[1].name, "second");
1078 assert_eq!(segments[1].parsed.get_scalar("arg0"), Some("2"));
1079 assert_eq!(segments[2].name, "third");
1080 }
1081
1082 #[test]
1083 fn test_segment_unknown_command_produces_unknown_command_error() {
1084 let registry = create_test_registry();
1092 let context = Box::new(TestContext::default());
1093 let cli = CliInterface::new(registry, context);
1094
1095 let args = vec!["nope".to_string()];
1096 let result = cli.segment(&args);
1097
1098 assert!(result.is_err());
1099 match result.unwrap_err() {
1100 DynamicCliError::Parse(crate::error::ParseError::UnknownCommand { .. }) => {}
1101 other => panic!("Expected UnknownCommand error, got: {:?}", other),
1102 }
1103 }
1104
1105 #[test]
1106 fn test_segment_repeated_command_name_resolves_each_occurrence_independently() {
1107 let mut registry = CommandRegistry::new();
1111 register_arity_command(&mut registry, "source", 1);
1112 register_arity_command(&mut registry, "run", 0);
1113 let context = Box::new(TestContext::default());
1114 let cli = CliInterface::new(registry, context);
1115
1116 let args = vec![
1117 "source".to_string(),
1118 "modelfile".to_string(),
1119 "source".to_string(),
1120 "solverfile".to_string(),
1121 "run".to_string(),
1122 ];
1123 let segments = cli.segment(&args).unwrap();
1124
1125 assert_eq!(segments.len(), 3);
1126 assert_eq!(segments[0].name, "source");
1127 assert_eq!(segments[0].parsed.get_scalar("arg0"), Some("modelfile"));
1128 assert_eq!(segments[1].name, "source");
1129 assert_eq!(segments[1].parsed.get_scalar("arg0"), Some("solverfile"));
1130 assert_eq!(segments[2].name, "run");
1131 }
1132
1133 #[test]
1134 fn test_dispatch_executes_chain_in_order() {
1135 let mut registry = CommandRegistry::new();
1138 register_arity_command(&mut registry, "first", 1);
1139 register_arity_command(&mut registry, "second", 1);
1140 register_arity_command(&mut registry, "third", 0);
1141 let context = Box::new(TestContext::default());
1142 let mut cli = CliInterface::new(registry, context);
1143
1144 let args = vec![
1145 "first".to_string(),
1146 "1".to_string(),
1147 "second".to_string(),
1148 "2".to_string(),
1149 "third".to_string(),
1150 ];
1151 cli.dispatch(&args).expect("chain should execute fully");
1152
1153 let ctx = crate::context::downcast_ref::<TestContext>(&*cli.context)
1154 .expect("Failed to downcast context");
1155 assert_eq!(
1156 ctx.executed_commands,
1157 vec![
1158 "first".to_string(),
1159 "second".to_string(),
1160 "third".to_string()
1161 ]
1162 );
1163 }
1164
1165 #[test]
1166 fn test_segment_known_limitation_extra_token_matching_command_name_is_silently_absorbed() {
1167 let mut registry = CommandRegistry::new();
1174 register_arity_command(&mut registry, "greet", 1);
1175 register_arity_command(&mut registry, "run", 0);
1176 let context = Box::new(TestContext::default());
1177 let cli = CliInterface::new(registry, context);
1178
1179 let args = vec!["greet".to_string(), "Alice".to_string(), "run".to_string()];
1184 let segments = cli
1185 .segment(&args)
1186 .expect("known limitation: no error is raised here, by design");
1187
1188 assert_eq!(segments.len(), 2);
1189 assert_eq!(segments[0].name, "greet");
1190 assert_eq!(segments[0].parsed.get_scalar("arg0"), Some("Alice"));
1191 assert_eq!(segments[1].name, "run");
1192 }
1193
1194 fn register_chain_command(
1202 registry: &mut CommandRegistry,
1203 name: &str,
1204 continue_on_failure: bool,
1205 requires_success: bool,
1206 fails: bool,
1207 ) {
1208 let cmd_def = CommandDefinition {
1209 name: name.to_string(),
1210 aliases: vec![],
1211 description: format!("Test command {}", name),
1212 required: false,
1213 arguments: vec![],
1214 options: vec![],
1215 implementation: format!("{}_handler", name),
1216 continue_on_failure,
1217 requires_success,
1218 };
1219
1220 let handler: Box<dyn crate::executor::CommandHandler> = if fails {
1221 Box::new(FailingHandler {
1222 name: name.to_string(),
1223 })
1224 } else {
1225 Box::new(TestHandler {
1226 name: name.to_string(),
1227 })
1228 };
1229
1230 registry
1231 .register_sync(cmd_def, handler)
1232 .expect("Failed to register command");
1233 }
1234
1235 #[test]
1236 fn test_execute_chain_continue_on_failure_false_stops_chain() {
1237 let mut registry = CommandRegistry::new();
1238 register_chain_command(&mut registry, "a", false, false, true); register_chain_command(&mut registry, "b", false, false, false);
1240 let context = Box::new(TestContext::default());
1241 let mut cli = CliInterface::new(registry, context);
1242
1243 let args = vec!["a".to_string(), "b".to_string()];
1244 let result = cli.dispatch(&args);
1245
1246 assert!(result.is_err());
1247 assert!(result
1248 .unwrap_err()
1249 .to_string()
1250 .contains("Error in command 1/2 ('a')"));
1251
1252 let ctx = crate::context::downcast_ref::<TestContext>(&*cli.context).unwrap();
1253 assert_eq!(
1254 ctx.executed_commands,
1255 vec!["a".to_string()],
1256 "'b' must never run once 'a' stops the chain"
1257 );
1258 }
1259
1260 #[test]
1261 fn test_execute_chain_continue_on_failure_true_proceeds_and_still_errors() {
1262 let mut registry = CommandRegistry::new();
1263 register_chain_command(&mut registry, "a", true, false, true); register_chain_command(&mut registry, "b", false, false, false);
1265 let context = Box::new(TestContext::default());
1266 let mut cli = CliInterface::new(registry, context);
1267
1268 let args = vec!["a".to_string(), "b".to_string()];
1269 let result = cli.dispatch(&args);
1270
1271 assert!(result.is_err());
1275 assert!(result
1276 .unwrap_err()
1277 .to_string()
1278 .contains("Error in command 1/2 ('a')"));
1279
1280 let ctx = crate::context::downcast_ref::<TestContext>(&*cli.context).unwrap();
1281 assert_eq!(
1282 ctx.executed_commands,
1283 vec!["a".to_string(), "b".to_string()],
1284 "'b' must still run: 'a''s failure was absorbed"
1285 );
1286 }
1287
1288 #[test]
1289 fn test_execute_chain_requires_success_skips_after_earlier_failure() {
1290 let mut registry = CommandRegistry::new();
1291 register_chain_command(&mut registry, "a", true, false, true); register_chain_command(&mut registry, "b", false, true, false); let context = Box::new(TestContext::default());
1294 let mut cli = CliInterface::new(registry, context);
1295
1296 let args = vec!["a".to_string(), "b".to_string()];
1297 let result = cli.dispatch(&args);
1298
1299 assert!(result.is_err());
1300 let ctx = crate::context::downcast_ref::<TestContext>(&*cli.context).unwrap();
1301 assert_eq!(
1302 ctx.executed_commands,
1303 vec!["a".to_string()],
1304 "'b' must be skipped, not executed, once 'a' has failed"
1305 );
1306 }
1307
1308 #[test]
1309 fn test_execute_chain_requires_success_runs_normally_without_a_preceding_failure() {
1310 let mut registry = CommandRegistry::new();
1313 register_chain_command(&mut registry, "a", false, false, false); register_chain_command(&mut registry, "b", false, true, false); let context = Box::new(TestContext::default());
1316 let mut cli = CliInterface::new(registry, context);
1317
1318 let args = vec!["a".to_string(), "b".to_string()];
1319 cli.dispatch(&args)
1320 .expect("no failure anywhere in the chain");
1321
1322 let ctx = crate::context::downcast_ref::<TestContext>(&*cli.context).unwrap();
1323 assert_eq!(
1324 ctx.executed_commands,
1325 vec!["a".to_string(), "b".to_string()]
1326 );
1327 }
1328
1329 #[test]
1330 fn test_execute_chain_reports_repeated_command_name_by_position_not_name_early() {
1331 let mut registry = CommandRegistry::new();
1332 register_chain_command(&mut registry, "ok", false, false, false);
1333 register_chain_command(&mut registry, "source", true, false, true); let context = Box::new(TestContext::default());
1335 let mut cli = CliInterface::new(registry, context);
1336
1337 let args = vec![
1339 "ok".to_string(),
1340 "source".to_string(),
1341 "ok".to_string(),
1342 "ok".to_string(),
1343 ];
1344 let result = cli.dispatch(&args);
1345
1346 assert!(result.is_err());
1347 let message = result.unwrap_err().to_string();
1348 assert!(message.contains("Error in command 2/4 ('source')"));
1349 assert!(!message.contains("4/4"));
1350 }
1351
1352 #[test]
1353 fn test_execute_chain_reports_repeated_command_name_by_position_not_name_late() {
1354 let mut registry = CommandRegistry::new();
1355 register_chain_command(&mut registry, "ok", false, false, false);
1356 register_chain_command(&mut registry, "source", true, false, true); let context = Box::new(TestContext::default());
1358 let mut cli = CliInterface::new(registry, context);
1359
1360 let args = vec![
1365 "ok".to_string(),
1366 "ok".to_string(),
1367 "ok".to_string(),
1368 "source".to_string(),
1369 ];
1370 let result = cli.dispatch(&args);
1371
1372 assert!(result.is_err());
1373 let message = result.unwrap_err().to_string();
1374 assert!(message.contains("Error in command 4/4 ('source')"));
1375 assert!(!message.contains("2/4"));
1376 }
1377
1378 #[test]
1379 fn test_run_script_chain_failure_reports_chain_position_and_line_number() {
1380 let mut registry = CommandRegistry::new();
1386 register_chain_command(&mut registry, "a", false, false, true); register_chain_command(&mut registry, "b", false, false, false);
1388 let context = Box::new(TestContext::default());
1389 let cli = CliInterface::new(registry, context);
1390
1391 let script = write_script("a b\n");
1392 let outcome = cli
1393 .run_script(script.path(), ScriptErrorPolicy::Continue)
1394 .expect("Continue policy should return Ok even with a failing line");
1395
1396 assert_eq!(outcome.failures.len(), 1);
1397 let (line_number, error) = &outcome.failures[0];
1398 assert_eq!(*line_number, 1);
1399 let message = error.to_string();
1400 assert!(message.contains("line 1"));
1401 assert!(message.contains("Error in command 1/2 ('a')"));
1402 }
1403}