1use std::path::PathBuf;
34use std::sync::Arc;
35
36use rustyline::completion::{Completer, Pair};
37use rustyline::error::ReadlineError;
38use rustyline::highlight::Highlighter;
39use rustyline::hint::Hinter;
40use rustyline::validate::Validator;
41use rustyline::{CompletionType, Config, Context, Editor, Helper};
42
43use crate::config::directive::{
44 effective_directives, DirectiveDefinition, ReplDirective, DIRECTIVE_PREFIX,
45};
46use crate::config::schema::CommandsConfig;
47use crate::context::ExecutionContext;
48use crate::error::{display_error, DynamicCliError, ExecutionError, ParseError, Result};
49use crate::help::{command_help_footer, DefaultHelpFormatter, HelpFormatter};
50use crate::parser::{ParsedArgs, ReplParser};
51use crate::registry::CommandRegistry;
52
53struct DcliCompleter {
79 registry: Arc<CommandRegistry>,
81
82 config: Option<Arc<CommandsConfig>>,
85
86 directives: Arc<[DirectiveDefinition]>,
89}
90
91impl DcliCompleter {
92 fn new(
93 registry: Arc<CommandRegistry>,
94 config: Option<Arc<CommandsConfig>>,
95 directives: Arc<[DirectiveDefinition]>,
96 ) -> Self {
97 Self {
98 registry,
99 config,
100 directives,
101 }
102 }
103
104 fn directives_matching(&self, typed: &str) -> Vec<String> {
107 self.directives
108 .iter()
109 .flat_map(|def| std::iter::once(&def.name).chain(&def.aliases))
110 .map(|name| format!("{}{}", DIRECTIVE_PREFIX, name))
111 .filter(|candidate| candidate.starts_with(typed))
112 .collect()
113 }
114
115 fn flags_for(&self, command_name: &str) -> Vec<String> {
120 let config = match &self.config {
121 Some(c) => c,
122 None => return vec![],
123 };
124
125 let cmd_def = match config.commands.iter().find(|c| c.name == command_name) {
126 Some(d) => d,
127 None => return vec![],
128 };
129
130 let mut flags = Vec::new();
131 for opt in &cmd_def.options {
132 if let Some(long) = &opt.long {
133 flags.push(format!("--{}", long));
134 }
135 if let Some(short) = &opt.short {
136 flags.push(format!("-{}", short));
137 }
138 }
139 flags
140 }
141}
142
143impl Completer for DcliCompleter {
144 type Candidate = Pair;
145
146 fn complete(
147 &self,
148 line: &str,
149 pos: usize,
150 _ctx: &Context<'_>,
151 ) -> rustyline::Result<(usize, Vec<Pair>)> {
152 let line = &line[..pos];
154 let tokens: Vec<&str> = line.split_whitespace().collect();
155
156 let completing_first_token =
159 tokens.is_empty() || (tokens.len() == 1 && !line.ends_with(' '));
160
161 if completing_first_token {
162 let prefix = tokens.first().copied().unwrap_or("");
163 let start = pos - prefix.len();
164
165 let names: Vec<String> = if prefix.starts_with(DIRECTIVE_PREFIX) {
168 self.directives_matching(prefix)
169 } else {
170 self.registry
171 .list_commands()
172 .into_iter()
173 .flat_map(|def| {
174 let mut names = vec![def.name.clone()];
175 names.extend(def.aliases.clone());
176 names
177 })
178 .filter(|name| name.starts_with(prefix))
179 .collect()
180 };
181
182 let mut candidates: Vec<Pair> = names
183 .into_iter()
184 .map(|name| Pair {
185 display: name.clone(),
186 replacement: name,
187 })
188 .collect();
189
190 candidates.sort_by(|a, b| a.display.cmp(&b.display));
191 return Ok((start, candidates));
192 }
193
194 let command_token = tokens[0];
197 let canonical = match self.registry.resolve_name(command_token) {
198 Some(name) => name.to_string(),
199 None => return Ok((pos, vec![])),
200 };
201
202 let current_word = if line.ends_with(' ') {
204 ""
205 } else {
206 tokens.last().copied().unwrap_or("")
207 };
208
209 let is_flag_context = current_word.is_empty() || current_word.starts_with('-');
212
213 if !is_flag_context {
214 return Ok((pos, vec![]));
215 }
216
217 let start = pos - current_word.len();
218 let mut candidates: Vec<Pair> = self
219 .flags_for(&canonical)
220 .into_iter()
221 .filter(|flag| flag.starts_with(current_word))
222 .map(|flag| Pair {
223 display: flag.clone(),
224 replacement: flag,
225 })
226 .collect();
227
228 candidates.sort_by(|a, b| a.display.cmp(&b.display));
229 Ok((start, candidates))
230 }
231}
232
233struct DcliHelper {
241 completer: DcliCompleter,
242}
243
244impl DcliHelper {
245 fn new(
246 registry: Arc<CommandRegistry>,
247 config: Option<Arc<CommandsConfig>>,
248 directives: Arc<[DirectiveDefinition]>,
249 ) -> Self {
250 Self {
251 completer: DcliCompleter::new(registry, config, directives),
252 }
253 }
254}
255
256impl Helper for DcliHelper {}
257
258impl Completer for DcliHelper {
259 type Candidate = Pair;
260
261 fn complete(
262 &self,
263 line: &str,
264 pos: usize,
265 ctx: &Context<'_>,
266 ) -> rustyline::Result<(usize, Vec<Pair>)> {
267 self.completer.complete(line, pos, ctx)
268 }
269}
270
271impl Hinter for DcliHelper {
273 type Hint = String;
274}
275
276impl Highlighter for DcliHelper {}
277
278impl Validator for DcliHelper {}
279
280pub struct ReplInterface {
339 registry: Arc<CommandRegistry>,
342
343 context: Box<dyn ExecutionContext>,
345
346 prompt: String,
349
350 prompt_suffix: String,
359
360 editor: Editor<DcliHelper, rustyline::history::DefaultHistory>,
362
363 history_path: Option<PathBuf>,
365
366 config: Option<Arc<CommandsConfig>>,
369
370 help_formatter: Option<Box<dyn HelpFormatter>>,
373
374 prompt_multiline: Option<String>,
383
384 directives: Arc<[DirectiveDefinition]>,
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
393enum SessionControl {
394 Continue,
396 Leave { save_history: bool },
398}
399
400impl ReplInterface {
401 pub fn new(
448 registry: CommandRegistry,
449 context: Box<dyn ExecutionContext>,
450 prompt: String,
451 config: Option<CommandsConfig>,
452 help_formatter: Option<Box<dyn HelpFormatter>>,
453 ) -> Result<Self> {
454 let registry = Arc::new(registry);
456
457 let prompt_suffix = config
462 .as_ref()
463 .map(|c| c.metadata.prompt_suffix.clone())
464 .unwrap_or_else(crate::config::schema::default_prompt_suffix);
465
466 let directives: Arc<[DirectiveDefinition]> = effective_directives(
468 config
469 .as_ref()
470 .map(|c| c.directives.as_slice())
471 .unwrap_or_default(),
472 )
473 .into();
474
475 let config: Option<Arc<CommandsConfig>> = config.map(Arc::new);
477
478 let rl_config = Config::builder()
480 .completion_type(CompletionType::List)
481 .build();
482
483 let helper = DcliHelper::new(
484 Arc::clone(®istry),
485 config.clone(),
486 Arc::clone(&directives),
487 );
488
489 let mut editor = Editor::with_config(rl_config).map_err(|e| {
490 ExecutionError::CommandFailed(anyhow::anyhow!("Failed to initialize REPL: {}", e))
491 })?;
492 editor.set_helper(Some(helper));
493
494 let history_path = Self::get_history_path(&prompt);
496
497 let mut repl = Self {
498 registry,
499 context,
500 prompt: format!("{}{}", prompt, prompt_suffix),
501 prompt_suffix,
502 editor,
503 history_path,
504 config,
505 help_formatter,
506 prompt_multiline: None,
507 directives,
508 };
509
510 repl.load_history();
511
512 Ok(repl)
513 }
514
515 pub fn with_prompt_multiline(mut self, prompt: String) -> Self {
550 self.prompt_multiline = Some(prompt);
551 self
552 }
553
554 fn effective_prompt_multiline(&self) -> String {
571 match &self.prompt_multiline {
572 Some(base) => format!("{}{}", base, self.prompt_suffix),
573 None => format!("...{}", self.prompt_suffix.trim_start()),
574 }
575 }
576
577 fn render_help(&self, command: Option<&str>) -> Option<String> {
587 let config = self.config.as_deref()?;
588 let default_formatter = DefaultHelpFormatter::new();
591 let formatter: &dyn HelpFormatter =
592 self.help_formatter.as_deref().unwrap_or(&default_formatter);
593
594 Some(match command {
595 Some(cmd) => formatter.format_command(config, cmd),
596 None => {
597 let mut out = formatter.format_app(config);
598 out.push_str(&formatter.format_directives(&self.directives));
599 out.push_str(&command_help_footer(
600 "Type",
601 &format!(
602 "{}{} <command>",
603 DIRECTIVE_PREFIX,
604 self.help_directive_name()
605 ),
606 ));
607 out
608 }
609 })
610 }
611
612 fn try_handle_help(&self, line: &str) -> Option<String> {
630 let trimmed = line.trim();
631
632 if trimmed == "--help" || trimmed == "-h" {
633 return self.render_help(None);
634 }
635
636 if let Some(rest) = trimmed
637 .strip_prefix("--help ")
638 .or_else(|| trimmed.strip_prefix("-h "))
639 {
640 let cmd = rest.trim();
641 if !cmd.is_empty() {
642 return self.render_help(Some(cmd));
643 }
644 }
645
646 let parts: Vec<&str> = trimmed.split_whitespace().collect();
647 if parts.len() >= 2 {
648 let last = *parts.last().unwrap();
649 if last == "--help" || last == "-h" {
650 return self.render_help(Some(parts[0]));
651 }
652 }
653
654 None
655 }
656
657 fn handle_line(&mut self, line: &str) -> Result<SessionControl> {
664 let line = line.trim();
665
666 if line.starts_with(DIRECTIVE_PREFIX) {
667 return self.dispatch_directive(line);
668 }
669
670 self.execute_line(line)?;
671 Ok(SessionControl::Continue)
672 }
673
674 fn resolve_directive(&self, name: &str) -> Option<ReplDirective> {
677 self.directives
678 .iter()
679 .find(|def| def.name == name || def.aliases.iter().any(|alias| alias == name))
680 .map(|def| def.implementation)
681 }
682
683 fn directive_definition(&self, directive: ReplDirective) -> &DirectiveDefinition {
685 self.directives
686 .iter()
687 .find(|def| def.implementation == directive)
688 .expect("directive table covers every ReplDirective")
690 }
691
692 fn dispatch_directive(&mut self, line: &str) -> Result<SessionControl> {
697 let body = &line[DIRECTIVE_PREFIX.len_utf8()..];
698 let (name, rest) = match body.split_once(char::is_whitespace) {
699 Some((name, rest)) => (name, rest.trim()),
700 None => (body, ""),
701 };
702
703 let directive = self
704 .resolve_directive(name)
705 .ok_or_else(|| self.unknown_directive_error(name))?;
706
707 match directive {
709 ReplDirective::Help => {
710 if rest.split_whitespace().count() > 1 {
711 return Err(self.usage_error(name, directive, "takes at most one command"));
712 }
713 let command = (!rest.is_empty()).then_some(rest);
714 let output = self.render_help(command).ok_or_else(|| {
715 DynamicCliError::Execution(ExecutionError::CommandFailed(anyhow::anyhow!(
716 "help is not available"
717 )))
718 })?;
719 print!("{}", output);
720 Ok(SessionControl::Continue)
721 }
722 ReplDirective::Load => {
723 if rest.is_empty() {
724 return Err(self.usage_error(name, directive, "requires a file path"));
725 }
726 self.load_script(rest)?;
727 Ok(SessionControl::Continue)
728 }
729 ReplDirective::Quit | ReplDirective::Exit => {
730 if !rest.is_empty() {
731 return Err(self.usage_error(name, directive, "takes no argument"));
732 }
733 Ok(SessionControl::Leave {
734 save_history: directive == ReplDirective::Quit,
735 })
736 }
737 }
738 }
739
740 fn usage_error(&self, typed: &str, directive: ReplDirective, problem: &str) -> DynamicCliError {
742 DynamicCliError::Parse(ParseError::InvalidSyntax {
743 details: format!("`{}{}` {}", DIRECTIVE_PREFIX, typed, problem),
744 hint: Some(format!(
745 "Usage: {}",
746 self.directive_definition(directive).usage_line()
747 )),
748 })
749 }
750
751 fn unknown_directive_error(&self, typed: &str) -> DynamicCliError {
754 let available: Vec<String> = self
755 .directives
756 .iter()
757 .map(|def| format!("{}{}", DIRECTIVE_PREFIX, def.name))
758 .collect();
759 DynamicCliError::Parse(ParseError::InvalidSyntax {
760 details: format!("Unknown directive '{}{}'", DIRECTIVE_PREFIX, typed),
761 hint: Some(format!("Available directives: {}", available.join(", "))),
762 })
763 }
764
765 fn complete_unknown_command(&self, error: DynamicCliError) -> DynamicCliError {
777 match error {
778 DynamicCliError::Parse(ParseError::UnknownCommand {
779 command,
780 mut suggestions,
781 ..
782 }) => {
783 if self.resolve_directive(&command).is_some() {
784 let directive = format!("{}{}", DIRECTIVE_PREFIX, command);
785 suggestions.retain(|s| *s != directive);
786 suggestions.insert(0, directive);
787 }
788 DynamicCliError::Parse(ParseError::UnknownCommand {
789 command,
790 suggestions,
791 hint: Some(format!(
792 "Type '{}{}' for available commands",
793 DIRECTIVE_PREFIX,
794 self.help_directive_name()
795 )),
796 })
797 }
798 other => other,
799 }
800 }
801
802 fn help_directive_name(&self) -> &str {
804 &self.directive_definition(ReplDirective::Help).name
805 }
806
807 fn load_script(&mut self, path: &str) -> Result<()> {
827 let content = std::fs::read_to_string(path).map_err(|e| {
828 DynamicCliError::Execution(ExecutionError::CommandFailed(anyhow::anyhow!(
829 "failed to read script file {}: {}",
830 path,
831 e
832 )))
833 })?;
834
835 let mut attempted = 0usize;
836 let mut succeeded = 0usize;
837
838 for (idx, raw_line) in content.lines().enumerate() {
839 let line_number = idx + 1;
840 let script_line = raw_line.trim();
841
842 if script_line.is_empty() || script_line.starts_with('#') {
843 continue;
844 }
845
846 attempted += 1;
847
848 match self.handle_line(script_line) {
849 Ok(SessionControl::Continue) => succeeded += 1,
850 Ok(SessionControl::Leave { .. }) => {
851 let typed = script_line.split_whitespace().next().unwrap_or_default();
853 eprintln!(
854 " :load {} — line {}: '{}' cannot be used in a script; line skipped",
855 path, line_number, typed
856 );
857 }
858 Err(e) => {
859 eprintln!(" :load {} — line {}:", path, line_number);
860 display_error(&e);
861 }
862 }
863 }
864
865 println!(":load {path}: {succeeded}/{attempted} line(s) succeeded");
866 Ok(())
867 }
868
869 fn has_secure_arg(
875 &self,
876 command_name: &str,
877 parsed_args: &std::collections::HashMap<String, String>,
878 ) -> bool {
879 let config = match &self.config {
880 Some(c) => c,
881 None => return false,
882 };
883
884 let cmd_def = match config.commands.iter().find(|c| c.name == command_name) {
885 Some(d) => d,
886 None => return false,
887 };
888
889 cmd_def
890 .arguments
891 .iter()
892 .any(|arg| arg.secure && parsed_args.contains_key(&arg.name))
893 }
894
895 fn get_history_path(app_name: &str) -> Option<PathBuf> {
903 dirs::data_local_dir().map(|data_dir| data_dir.join(app_name).join("history"))
904 }
905
906 fn load_history(&mut self) {
908 if let Some(ref path) = self.history_path {
909 if let Some(parent) = path.parent() {
910 let _ = std::fs::create_dir_all(parent);
911 }
912 let _ = self.editor.load_history(path);
913 }
914 }
915
916 fn save_history(&mut self) {
920 if let Some(ref path) = self.history_path {
921 if let Err(e) = self.editor.save_history(path) {
922 eprintln!("Warning: Failed to save command history: {}", e);
923 }
924 }
925 }
926
927 pub fn run(mut self) -> Result<()> {
989 let mut continuation_buffer: Vec<String> = Vec::new();
990
991 loop {
992 let prompt = if continuation_buffer.is_empty() {
993 self.prompt.clone()
994 } else {
995 self.effective_prompt_multiline()
996 };
997
998 let readline = self.editor.readline(&prompt);
999
1000 match readline {
1001 Ok(raw_line) => {
1002 let line = match Self::accumulate_line(&mut continuation_buffer, &raw_line) {
1003 Some(line) => line,
1004 None => continue,
1007 };
1008
1009 if line.is_empty() {
1010 continue;
1011 }
1012
1013 match self.handle_line(&line) {
1017 Ok(SessionControl::Continue) => {}
1018 Ok(SessionControl::Leave { save_history }) => {
1019 self.end_session(save_history);
1020 break;
1021 }
1022 Err(e) => {
1023 display_error(&e);
1024 }
1025 }
1026 }
1027
1028 Err(ReadlineError::Interrupted) => {
1029 println!("^C");
1030 continuation_buffer.clear();
1033 continue;
1034 }
1035
1036 Err(ReadlineError::Eof) => {
1038 println!();
1039 self.end_session(true);
1040 break;
1041 }
1042
1043 Err(err) => {
1046 eprintln!("Error reading input: {}", err);
1047 break;
1048 }
1049 }
1050 }
1051
1052 Ok(())
1053 }
1054
1055 fn end_session(&mut self, save_history: bool) {
1058 if save_history {
1059 self.save_history();
1060 }
1061 println!("Goodbye!");
1062 }
1063
1064 fn accumulate_line(buffer: &mut Vec<String>, raw_line: &str) -> Option<String> {
1083 let trimmed = raw_line.trim();
1084
1085 if let Some(fragment) = trimmed.strip_suffix('\\') {
1086 buffer.push(fragment.trim_end().to_string());
1087 return None;
1088 }
1089
1090 if buffer.is_empty() {
1091 return Some(trimmed.to_string());
1092 }
1093
1094 buffer.push(trimmed.to_string());
1095 Some(std::mem::take(buffer).join(" "))
1096 }
1097
1098 fn execute_line(&mut self, line: &str) -> Result<()> {
1108 if let Some(output) = self.try_handle_help(line) {
1109 print!("{}", output);
1110 return Ok(());
1111 }
1112
1113 let parser = ReplParser::new(&self.registry);
1114 let parsed = parser
1115 .parse_line(line)
1116 .map_err(|e| self.complete_unknown_command(e))?;
1117
1118 if !self.has_secure_arg(&parsed.command_name, &parsed.arguments) {
1121 let _ = self.editor.add_history_entry(line);
1122 }
1123
1124 let parsed_args = ParsedArgs::from_scalars(parsed.arguments);
1137 if let Some(handler) = self.registry.get_handler_sync(&parsed.command_name) {
1138 handler.execute(&mut *self.context, &parsed_args)?;
1139 } else if let Some(handler) = self.registry.get_handler_async(&parsed.command_name) {
1140 futures::executor::block_on(handler.execute(&mut *self.context, &parsed_args))?;
1141 } else {
1142 return Err(DynamicCliError::Execution(
1143 ExecutionError::handler_not_found(&parsed.command_name, "unknown"),
1144 ));
1145 }
1146
1147 Ok(())
1148 }
1149}
1150
1151#[cfg(test)]
1156mod tests {
1157 use super::*;
1158 use crate::config::schema::{
1159 ArgumentDefinition, ArgumentType, CommandDefinition, OptionDefinition,
1160 };
1161 use rustyline::history::History;
1162 use std::collections::HashMap;
1163
1164 #[derive(Default)]
1165 struct TestContext {
1166 executed_commands: Vec<String>,
1167 }
1168
1169 impl ExecutionContext for TestContext {
1170 fn as_any(&self) -> &dyn std::any::Any {
1171 self
1172 }
1173 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
1174 self
1175 }
1176 }
1177
1178 struct TestHandler {
1179 name: String,
1180 }
1181
1182 impl crate::executor::CommandHandler for TestHandler {
1183 fn execute(&self, context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
1184 let ctx = crate::context::downcast_mut::<TestContext>(context)
1185 .expect("Failed to downcast context");
1186 ctx.executed_commands.push(self.name.clone());
1187 Ok(())
1188 }
1189 }
1190
1191 fn create_test_registry() -> CommandRegistry {
1192 let mut registry = CommandRegistry::new();
1193 let cmd_def = CommandDefinition {
1194 name: "test".to_string(),
1195 aliases: vec!["t".to_string()],
1196 description: "Test command".to_string(),
1197 required: false,
1198 arguments: vec![],
1199 options: vec![],
1200 implementation: "test_handler".to_string(),
1201 continue_on_failure: false,
1202 requires_success: false,
1203 };
1204 registry
1205 .register_sync(
1206 cmd_def,
1207 Box::new(TestHandler {
1208 name: "test".to_string(),
1209 }),
1210 )
1211 .unwrap();
1212 registry
1213 }
1214
1215 fn make_help_config() -> CommandsConfig {
1216 use crate::config::schema::{CommandsConfig, Metadata};
1217 CommandsConfig {
1218 metadata: Metadata {
1219 version: "1.0.0".to_string(),
1220 prompt: "testapp".to_string(),
1221 prompt_suffix: " > ".to_string(),
1222 },
1223 commands: vec![CommandDefinition {
1224 name: "hello".to_string(),
1225 aliases: vec!["hi".to_string()],
1226 description: "Say hello".to_string(),
1227 required: false,
1228 arguments: vec![],
1229 options: vec![OptionDefinition {
1230 name: "loud".to_string(),
1231 short: Some("l".to_string()),
1232 long: Some("loud".to_string()),
1233 option_type: ArgumentType::Bool,
1234 required: false,
1235 default: Some("false".to_string()),
1236 description: "Loud greeting".to_string(),
1237 choices: vec![],
1238 repeatable: false,
1239 option_parameters: HashMap::new(),
1240 }],
1241 implementation: "hello_handler".to_string(),
1242 continue_on_failure: false,
1243 requires_success: false,
1244 }],
1245 global_options: vec![],
1246 directives: vec![],
1247 }
1248 }
1249
1250 #[test]
1253 fn test_repl_interface_creation() {
1254 let registry = create_test_registry();
1255 let context = Box::new(TestContext::default());
1256 let repl = ReplInterface::new(registry, context, "test".to_string(), None, None);
1257 assert!(repl.is_ok());
1258 }
1259
1260 #[test]
1261 fn test_repl_interface_creation_with_config() {
1262 let registry = create_test_registry();
1263 let context = Box::new(TestContext::default());
1264 let config = make_help_config();
1265 let repl = ReplInterface::new(registry, context, "test".to_string(), Some(config), None);
1266 assert!(repl.is_ok());
1267 }
1268
1269 #[test]
1272 fn test_repl_execute_line() {
1273 let registry = create_test_registry();
1274 let context = Box::new(TestContext::default());
1275 let mut repl =
1276 ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
1277 let result = repl.execute_line("test");
1278 assert!(result.is_ok());
1279 let ctx = crate::context::downcast_ref::<TestContext>(&*repl.context).unwrap();
1280 assert_eq!(ctx.executed_commands, vec!["test".to_string()]);
1281 }
1282
1283 #[test]
1284 fn test_repl_execute_with_alias() {
1285 let registry = create_test_registry();
1286 let context = Box::new(TestContext::default());
1287 let mut repl =
1288 ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
1289 assert!(repl.execute_line("t").is_ok());
1290 }
1291
1292 #[test]
1293 fn test_repl_execute_unknown_command() {
1294 let registry = create_test_registry();
1295 let context = Box::new(TestContext::default());
1296 let mut repl =
1297 ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
1298 let result = repl.execute_line("unknown");
1299 assert!(result.is_err());
1300 match result.unwrap_err() {
1301 DynamicCliError::Parse(_) => {}
1302 other => panic!("Expected Parse error, got: {:?}", other),
1303 }
1304 }
1305
1306 #[test]
1307 fn test_repl_empty_line() {
1308 let registry = create_test_registry();
1309 let context = Box::new(TestContext::default());
1310 let mut repl =
1311 ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
1312 assert!(repl.execute_line("").is_err());
1313 }
1314
1315 #[test]
1316 fn test_repl_command_with_args() {
1317 let mut registry = CommandRegistry::new();
1318 let cmd_def = CommandDefinition {
1319 name: "greet".to_string(),
1320 aliases: vec![],
1321 description: "Greet someone".to_string(),
1322 required: false,
1323 arguments: vec![ArgumentDefinition {
1324 name: "name".to_string(),
1325 arg_type: ArgumentType::String,
1326 required: true,
1327 description: "Name".to_string(),
1328 validation: vec![],
1329 secure: false,
1330 }],
1331 options: vec![],
1332 implementation: "greet_handler".to_string(),
1333 continue_on_failure: false,
1334 requires_success: false,
1335 };
1336
1337 struct GreetHandler;
1338 impl crate::executor::CommandHandler for GreetHandler {
1339 fn execute(&self, _ctx: &mut dyn ExecutionContext, args: &ParsedArgs) -> Result<()> {
1340 assert_eq!(args.get_scalar("name"), Some("Alice"));
1341 Ok(())
1342 }
1343 }
1344
1345 registry
1346 .register_sync(cmd_def, Box::new(GreetHandler))
1347 .unwrap();
1348 let context = Box::new(TestContext::default());
1349 let mut repl =
1350 ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
1351 assert!(repl.execute_line("greet Alice").is_ok());
1352 }
1353
1354 #[test]
1357 fn test_repl_history_path() {
1358 let path = ReplInterface::get_history_path("myapp");
1359 if let Some(p) = path {
1360 let path_str = p.to_str().unwrap();
1361 assert!(path_str.contains("myapp"), "path should contain app name");
1362 assert!(
1363 path_str.ends_with("history"),
1364 "path should end with 'history', got: {}",
1365 path_str
1366 );
1367 }
1368 }
1369
1370 #[test]
1373 fn test_try_handle_help_without_config_returns_none() {
1374 let registry = create_test_registry();
1375 let context = Box::new(TestContext::default());
1376 let repl = ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
1377 assert!(repl.try_handle_help("--help").is_none());
1378 assert!(repl.try_handle_help("-h").is_none());
1379 }
1380
1381 #[test]
1384 fn test_try_handle_help_without_formatter_uses_default() {
1385 colored::control::set_override(false);
1386 let registry = create_test_registry();
1387 let context = Box::new(TestContext::default());
1388 let config = make_help_config();
1389 let repl =
1390 ReplInterface::new(registry, context, "test".to_string(), Some(config), None).unwrap();
1391
1392 let formatter = DefaultHelpFormatter::new();
1393 let expected_app = format!(
1394 "{}{}\nType ':help <command>' for more information on a command.\n",
1395 formatter.format_app(repl.config.as_deref().unwrap()),
1396 formatter.format_directives(&effective_directives(&[]))
1397 );
1398 let expected_cmd =
1399 DefaultHelpFormatter::new().format_command(repl.config.as_deref().unwrap(), "hello");
1400
1401 assert_eq!(repl.try_handle_help("--help"), Some(expected_app.clone()));
1402 assert_eq!(repl.try_handle_help("-h"), Some(expected_app));
1403 for line in ["--help hello", "-h hello", "hello --help", "hello -h"] {
1404 assert_eq!(
1405 repl.try_handle_help(line),
1406 Some(expected_cmd.clone()),
1407 "line: {line}"
1408 );
1409 }
1410 }
1411
1412 #[test]
1415 fn test_execute_line_help_without_formatter() {
1416 colored::control::set_override(false);
1417 let registry = create_test_registry();
1418 let context = Box::new(TestContext::default());
1419 let config = make_help_config();
1420 let mut repl =
1421 ReplInterface::new(registry, context, "test".to_string(), Some(config), None).unwrap();
1422 assert!(repl.execute_line("--help").is_ok());
1423 assert!(repl.execute_line("hello --help").is_ok());
1424 }
1425
1426 #[test]
1427 fn test_try_handle_help_global() {
1428 colored::control::set_override(false);
1429 let registry = create_test_registry();
1430 let context = Box::new(TestContext::default());
1431 let config = make_help_config();
1432 let repl = ReplInterface::new(
1433 registry,
1434 context,
1435 "test".to_string(),
1436 Some(config),
1437 Some(Box::new(DefaultHelpFormatter::new())),
1438 )
1439 .unwrap();
1440 let out = repl.try_handle_help("--help");
1441 assert!(out.is_some());
1442 let out = out.unwrap();
1443 assert!(out.contains("testapp"));
1444 assert!(out.contains("hello"));
1445 }
1446
1447 #[test]
1448 fn test_try_handle_help_short_flag() {
1449 colored::control::set_override(false);
1450 let registry = create_test_registry();
1451 let context = Box::new(TestContext::default());
1452 let config = make_help_config();
1453 let repl = ReplInterface::new(
1454 registry,
1455 context,
1456 "test".to_string(),
1457 Some(config),
1458 Some(Box::new(DefaultHelpFormatter::new())),
1459 )
1460 .unwrap();
1461 let out = repl.try_handle_help("-h");
1462 assert!(out.is_some());
1463 assert!(out.unwrap().contains("testapp"));
1464 }
1465
1466 #[test]
1467 fn test_try_handle_help_with_command_prefix() {
1468 colored::control::set_override(false);
1469 let registry = create_test_registry();
1470 let context = Box::new(TestContext::default());
1471 let config = make_help_config();
1472 let repl = ReplInterface::new(
1473 registry,
1474 context,
1475 "test".to_string(),
1476 Some(config),
1477 Some(Box::new(DefaultHelpFormatter::new())),
1478 )
1479 .unwrap();
1480 let out = repl.try_handle_help("--help hello");
1481 assert!(out.is_some());
1482 assert!(out.unwrap().contains("hello"));
1483 let out2 = repl.try_handle_help("-h hello");
1484 assert!(out2.is_some());
1485 }
1486
1487 #[test]
1488 fn test_try_handle_help_command_suffix() {
1489 colored::control::set_override(false);
1490 let registry = create_test_registry();
1491 let context = Box::new(TestContext::default());
1492 let config = make_help_config();
1493 let repl = ReplInterface::new(
1494 registry,
1495 context,
1496 "test".to_string(),
1497 Some(config),
1498 Some(Box::new(DefaultHelpFormatter::new())),
1499 )
1500 .unwrap();
1501 let out = repl.try_handle_help("hello --help");
1502 assert!(out.is_some());
1503 assert!(out.unwrap().contains("hello"));
1504 let out2 = repl.try_handle_help("hello -h");
1505 assert!(out2.is_some());
1506 }
1507
1508 #[test]
1509 fn test_try_handle_help_alias() {
1510 colored::control::set_override(false);
1511 let registry = create_test_registry();
1512 let context = Box::new(TestContext::default());
1513 let config = make_help_config();
1514 let repl = ReplInterface::new(
1515 registry,
1516 context,
1517 "test".to_string(),
1518 Some(config),
1519 Some(Box::new(DefaultHelpFormatter::new())),
1520 )
1521 .unwrap();
1522 let out = repl.try_handle_help("--help hi");
1523 assert!(out.is_some());
1524 assert!(out.unwrap().contains("hello"));
1525 }
1526
1527 #[test]
1528 fn test_execute_line_help_intercepted() {
1529 colored::control::set_override(false);
1530 let registry = create_test_registry();
1531 let context = Box::new(TestContext::default());
1532 let config = make_help_config();
1533 let mut repl = ReplInterface::new(
1534 registry,
1535 context,
1536 "test".to_string(),
1537 Some(config),
1538 Some(Box::new(DefaultHelpFormatter::new())),
1539 )
1540 .unwrap();
1541 assert!(repl.execute_line("--help").is_ok());
1542 }
1543
1544 #[test]
1545 fn test_execute_line_normal_command_still_works_with_formatter() {
1546 let registry = create_test_registry();
1547 let context = Box::new(TestContext::default());
1548 let config = make_help_config();
1549 let mut repl = ReplInterface::new(
1550 registry,
1551 context,
1552 "test".to_string(),
1553 Some(config),
1554 Some(Box::new(DefaultHelpFormatter::new())),
1555 )
1556 .unwrap();
1557 assert!(repl.execute_line("test").is_ok());
1558 }
1559
1560 fn default_directives() -> Arc<[DirectiveDefinition]> {
1563 effective_directives(&[]).into()
1564 }
1565
1566 #[test]
1567 fn test_completer_commands_empty_input() {
1568 let registry = Arc::new(create_test_registry());
1569 let completer = DcliCompleter::new(Arc::clone(®istry), None, default_directives());
1570 let history = rustyline::history::DefaultHistory::new();
1571 let ctx = rustyline::Context::new(&history);
1572 let (_, candidates) = completer.complete("", 0, &ctx).unwrap();
1573 let names: Vec<&str> = candidates.iter().map(|p| p.display.as_str()).collect();
1574 assert!(names.contains(&"test"));
1575 assert!(names.contains(&"t"));
1576 }
1577
1578 #[test]
1579 fn test_completer_commands_prefix_filter() {
1580 let registry = Arc::new(create_test_registry());
1581 let completer = DcliCompleter::new(Arc::clone(®istry), None, default_directives());
1582 let history = rustyline::history::DefaultHistory::new();
1583 let ctx = rustyline::Context::new(&history);
1584 let (_, candidates) = completer.complete("te", 2, &ctx).unwrap();
1585 let names: Vec<&str> = candidates.iter().map(|p| p.display.as_str()).collect();
1586 assert!(names.contains(&"test"));
1587 assert!(!names.contains(&"t"));
1588 }
1589
1590 #[test]
1591 fn test_completer_flags_after_command() {
1592 let config = Arc::new(make_help_config());
1593 let mut registry = CommandRegistry::new();
1595 let cmd_def = make_help_config().commands.into_iter().next().unwrap();
1596 struct DummyHandler;
1597 impl crate::executor::CommandHandler for DummyHandler {
1598 fn execute(&self, _: &mut dyn ExecutionContext, _: &ParsedArgs) -> Result<()> {
1599 Ok(())
1600 }
1601 }
1602 registry
1603 .register_sync(cmd_def, Box::new(DummyHandler))
1604 .unwrap();
1605 let registry = Arc::new(registry);
1606
1607 let completer = DcliCompleter::new(
1608 Arc::clone(®istry),
1609 Some(Arc::clone(&config)),
1610 default_directives(),
1611 );
1612 let history = rustyline::history::DefaultHistory::new();
1613 let ctx = rustyline::Context::new(&history);
1614
1615 let (_, candidates) = completer.complete("hello ", 6, &ctx).unwrap();
1617 let names: Vec<&str> = candidates.iter().map(|p| p.display.as_str()).collect();
1618 assert!(
1619 names.contains(&"--loud"),
1620 "expected --loud, got {:?}",
1621 names
1622 );
1623 assert!(names.contains(&"-l"), "expected -l, got {:?}", names);
1624 }
1625
1626 #[test]
1627 fn test_completer_flags_prefix_filter() {
1628 let config = Arc::new(make_help_config());
1629 let mut registry = CommandRegistry::new();
1630 let cmd_def = make_help_config().commands.into_iter().next().unwrap();
1631 struct DummyHandler;
1632 impl crate::executor::CommandHandler for DummyHandler {
1633 fn execute(&self, _: &mut dyn ExecutionContext, _: &ParsedArgs) -> Result<()> {
1634 Ok(())
1635 }
1636 }
1637 registry
1638 .register_sync(cmd_def, Box::new(DummyHandler))
1639 .unwrap();
1640 let registry = Arc::new(registry);
1641
1642 let completer = DcliCompleter::new(
1643 Arc::clone(®istry),
1644 Some(Arc::clone(&config)),
1645 default_directives(),
1646 );
1647 let history = rustyline::history::DefaultHistory::new();
1648 let ctx = rustyline::Context::new(&history);
1649
1650 let (_, candidates) = completer.complete("hello --l", 9, &ctx).unwrap();
1652 let names: Vec<&str> = candidates.iter().map(|p| p.display.as_str()).collect();
1653 assert!(names.contains(&"--loud"));
1654 assert!(!names.contains(&"-l"));
1655 }
1656
1657 #[test]
1658 fn test_completer_no_flags_for_unknown_command() {
1659 let config = Arc::new(make_help_config());
1660 let registry = Arc::new(create_test_registry());
1661 let completer = DcliCompleter::new(
1662 Arc::clone(®istry),
1663 Some(Arc::clone(&config)),
1664 default_directives(),
1665 );
1666 let history = rustyline::history::DefaultHistory::new();
1667 let ctx = rustyline::Context::new(&history);
1668 let (_, candidates) = completer.complete("unknown ", 8, &ctx).unwrap();
1670 assert!(candidates.is_empty());
1671 }
1672
1673 fn make_secure_registry_and_config() -> (CommandRegistry, CommandsConfig) {
1677 use crate::config::schema::{CommandsConfig, Metadata};
1678
1679 let cmd_def = CommandDefinition {
1680 name: "login".to_string(),
1681 aliases: vec![],
1682 description: "Login command".to_string(),
1683 required: false,
1684 arguments: vec![
1685 ArgumentDefinition {
1686 name: "username".to_string(),
1687 arg_type: ArgumentType::String,
1688 required: true,
1689 description: "Username".to_string(),
1690 validation: vec![],
1691 secure: false,
1692 },
1693 ArgumentDefinition {
1694 name: "password".to_string(),
1695 arg_type: ArgumentType::String,
1696 required: true,
1697 description: "Password".to_string(),
1698 validation: vec![],
1699 secure: true,
1700 },
1701 ],
1702 options: vec![],
1703 implementation: "login_handler".to_string(),
1704 continue_on_failure: false,
1705 requires_success: false,
1706 };
1707
1708 struct LoginHandler;
1709 impl crate::executor::CommandHandler for LoginHandler {
1710 fn execute(&self, _ctx: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
1711 Ok(())
1712 }
1713 }
1714
1715 let mut registry = CommandRegistry::new();
1716 registry
1717 .register_sync(cmd_def.clone(), Box::new(LoginHandler))
1718 .unwrap();
1719
1720 let config = CommandsConfig {
1721 metadata: Metadata {
1722 version: "1.0.0".to_string(),
1723 prompt: "testapp".to_string(),
1724 prompt_suffix: " > ".to_string(),
1725 },
1726 commands: vec![cmd_def],
1727 global_options: vec![],
1728 directives: vec![],
1729 };
1730
1731 (registry, config)
1732 }
1733
1734 #[test]
1735 fn test_has_secure_arg_returns_false_without_config() {
1736 let registry = create_test_registry();
1737 let context = Box::new(TestContext::default());
1738 let repl = ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
1739
1740 let mut args = HashMap::new();
1741 args.insert("password".to_string(), "secret".to_string());
1742
1743 assert!(!repl.has_secure_arg("login", &args));
1744 }
1745
1746 #[test]
1747 fn test_has_secure_arg_returns_false_when_no_secure_field() {
1748 let registry = create_test_registry();
1749 let context = Box::new(TestContext::default());
1750 let config = make_help_config();
1751 let repl =
1752 ReplInterface::new(registry, context, "test".to_string(), Some(config), None).unwrap();
1753
1754 let mut args = HashMap::new();
1755 args.insert("loud".to_string(), "true".to_string());
1756
1757 assert!(!repl.has_secure_arg("hello", &args));
1758 }
1759
1760 #[test]
1761 fn test_has_secure_arg_returns_true_when_secure_argument_present() {
1762 let (registry, config) = make_secure_registry_and_config();
1763 let context = Box::new(TestContext::default());
1764 let repl =
1765 ReplInterface::new(registry, context, "test".to_string(), Some(config), None).unwrap();
1766
1767 let mut args = HashMap::new();
1768 args.insert("username".to_string(), "alice".to_string());
1769 args.insert("password".to_string(), "secret".to_string());
1770
1771 assert!(repl.has_secure_arg("login", &args));
1772 }
1773
1774 #[test]
1775 fn test_has_secure_arg_returns_false_when_only_non_secure_present() {
1776 let (registry, config) = make_secure_registry_and_config();
1777 let context = Box::new(TestContext::default());
1778 let repl =
1779 ReplInterface::new(registry, context, "test".to_string(), Some(config), None).unwrap();
1780
1781 let mut args = HashMap::new();
1783 args.insert("username".to_string(), "alice".to_string());
1784
1785 assert!(!repl.has_secure_arg("login", &args));
1786 }
1787
1788 #[test]
1789 fn test_has_secure_arg_returns_false_for_unknown_command() {
1790 let (registry, config) = make_secure_registry_and_config();
1791 let context = Box::new(TestContext::default());
1792 let repl =
1793 ReplInterface::new(registry, context, "test".to_string(), Some(config), None).unwrap();
1794
1795 let mut args = HashMap::new();
1796 args.insert("password".to_string(), "secret".to_string());
1797
1798 assert!(!repl.has_secure_arg("nonexistent", &args));
1799 }
1800
1801 #[test]
1804 fn test_execute_line_with_secure_arg_does_not_add_to_history() {
1805 let (registry, config) = make_secure_registry_and_config();
1806 let context = Box::new(TestContext::default());
1807 let mut repl =
1808 ReplInterface::new(registry, context, "test".to_string(), Some(config), None).unwrap();
1809
1810 let result = repl.execute_line("login alice secret");
1811 assert!(result.is_ok());
1812
1813 let history = repl.editor.history();
1815 let in_history = (0..history.len()).any(|i| {
1816 history
1817 .get(i, rustyline::history::SearchDirection::Forward)
1818 .ok()
1819 .flatten()
1820 .map(|e| e.entry.as_ref() == "login alice secret")
1821 .unwrap_or(false)
1822 });
1823 assert!(
1824 !in_history,
1825 "secure command line must not be written to history"
1826 );
1827 }
1828
1829 #[test]
1830 fn test_execute_line_without_secure_arg_adds_to_history() {
1831 let registry = create_test_registry();
1832 let context = Box::new(TestContext::default());
1833 let mut repl =
1834 ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
1835
1836 let result = repl.execute_line("test");
1837 assert!(result.is_ok());
1838
1839 let history = repl.editor.history();
1841 let in_history = (0..history.len()).any(|i| {
1842 history
1843 .get(i, rustyline::history::SearchDirection::Forward)
1844 .ok()
1845 .flatten()
1846 .map(|e| e.entry.as_ref() == "test")
1847 .unwrap_or(false)
1848 });
1849 assert!(
1850 in_history,
1851 "non-secure command line must be written to history"
1852 );
1853 }
1854
1855 fn write_script(content: &str) -> tempfile::NamedTempFile {
1858 use std::io::Write;
1859 let mut file = tempfile::NamedTempFile::new().expect("failed to create temp script file");
1860 file.write_all(content.as_bytes())
1861 .expect("failed to write temp script file");
1862 file
1863 }
1864
1865 #[test]
1866 fn test_load_executes_each_line_via_handle_line() {
1867 let registry = create_test_registry();
1868 let context = Box::new(TestContext::default());
1869 let mut repl =
1870 ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
1871
1872 let script = write_script("test\nt\n");
1873 let line = format!(":load {}", script.path().display());
1874
1875 assert!(repl.handle_line(&line).is_ok());
1876
1877 let ctx = crate::context::downcast_ref::<TestContext>(&*repl.context).unwrap();
1878 assert_eq!(ctx.executed_commands, vec!["test", "test"]);
1879 }
1880
1881 #[test]
1882 fn test_load_skips_blank_lines_and_comments() {
1883 let registry = create_test_registry();
1884 let context = Box::new(TestContext::default());
1885 let mut repl =
1886 ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
1887
1888 let script = write_script("# a comment\n\ntest\n \n# another\n");
1889 let line = format!(":load {}", script.path().display());
1890
1891 assert!(repl.handle_line(&line).is_ok());
1892
1893 let ctx = crate::context::downcast_ref::<TestContext>(&*repl.context).unwrap();
1894 assert_eq!(ctx.executed_commands, vec!["test"]);
1895 }
1896
1897 #[test]
1898 fn test_load_continues_past_a_failing_line() {
1899 let registry = create_test_registry();
1900 let context = Box::new(TestContext::default());
1901 let mut repl =
1902 ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
1903
1904 let script = write_script("test\nunknown_command\ntest\n");
1905 let line = format!(":load {}", script.path().display());
1906
1907 let result = repl.handle_line(&line);
1911 assert!(result.is_ok());
1912
1913 let ctx = crate::context::downcast_ref::<TestContext>(&*repl.context).unwrap();
1914 assert_eq!(ctx.executed_commands, vec!["test", "test"]);
1915 }
1916
1917 #[test]
1918 fn test_load_missing_path_argument_is_an_error() {
1919 let registry = create_test_registry();
1920 let context = Box::new(TestContext::default());
1921 let mut repl =
1922 ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
1923
1924 let result = repl.handle_line(":load");
1925 assert!(result.is_err());
1926 }
1927
1928 #[test]
1929 fn test_load_missing_file_is_an_error() {
1930 let registry = create_test_registry();
1931 let context = Box::new(TestContext::default());
1932 let mut repl =
1933 ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
1934
1935 let result = repl.handle_line(":load /nonexistent/path/to/script.txt");
1936 assert!(result.is_err());
1937 }
1938
1939 #[test]
1940 fn test_load_line_itself_is_not_added_to_history() {
1941 let registry = create_test_registry();
1942 let context = Box::new(TestContext::default());
1943 let mut repl =
1944 ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
1945
1946 let script = write_script("test\n");
1947 let line = format!(":load {}", script.path().display());
1948 assert!(repl.handle_line(&line).is_ok());
1949
1950 let history = repl.editor.history();
1951 let load_in_history = (0..history.len()).any(|i| {
1952 history
1953 .get(i, rustyline::history::SearchDirection::Forward)
1954 .ok()
1955 .flatten()
1956 .map(|e| e.entry.starts_with(":load"))
1957 .unwrap_or(false)
1958 });
1959 assert!(
1960 !load_in_history,
1961 ":load line itself must not be written to history"
1962 );
1963 }
1964
1965 #[test]
1970 fn test_repl_line_is_never_chained_across_multiple_commands() {
1971 let mut registry = CommandRegistry::new();
1979 for name in ["first", "second"] {
1980 let cmd_def = CommandDefinition {
1981 name: name.to_string(),
1982 aliases: vec![],
1983 description: format!("Test command {}", name),
1984 required: false,
1985 arguments: vec![ArgumentDefinition {
1986 name: "value".to_string(),
1987 arg_type: ArgumentType::String,
1988 required: true,
1989 description: "Value".to_string(),
1990 validation: vec![],
1991 secure: false,
1992 }],
1993 options: vec![],
1994 implementation: format!("{}_handler", name),
1995 continue_on_failure: false,
1996 requires_success: false,
1997 };
1998 registry
1999 .register_sync(
2000 cmd_def,
2001 Box::new(TestHandler {
2002 name: name.to_string(),
2003 }),
2004 )
2005 .unwrap();
2006 }
2007
2008 let context = Box::new(TestContext::default());
2009 let mut repl =
2010 ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
2011
2012 let result = repl.execute_line("first 1 second");
2015
2016 assert!(result.is_err());
2017 match result.unwrap_err() {
2018 DynamicCliError::Parse(ParseError::TooManyArguments { command, .. }) => {
2019 assert_eq!(command, "first");
2020 }
2021 other => panic!("Expected TooManyArguments error, got: {:?}", other),
2022 }
2023
2024 let ctx = crate::context::downcast_ref::<TestContext>(&*repl.context).unwrap();
2027 assert!(ctx.executed_commands.is_empty());
2028 }
2029
2030 #[test]
2035 fn test_prompt_uses_default_suffix_without_config() {
2036 let registry = create_test_registry();
2037 let context = Box::new(TestContext::default());
2038 let repl = ReplInterface::new(registry, context, "myapp".to_string(), None, None).unwrap();
2039
2040 assert_eq!(repl.prompt, "myapp > ");
2043 }
2044
2045 #[test]
2046 fn test_prompt_honours_configured_suffix() {
2047 let registry = create_test_registry();
2050 let context = Box::new(TestContext::default());
2051 let mut config = make_help_config();
2052 config.metadata.prompt_suffix = " $ ".to_string();
2053
2054 let repl =
2055 ReplInterface::new(registry, context, "rpn".to_string(), Some(config), None).unwrap();
2056
2057 assert_eq!(repl.prompt, "rpn $ ");
2058 }
2059
2060 #[test]
2061 fn test_effective_prompt_multiline_default_derivation() {
2062 let registry = create_test_registry();
2065 let context = Box::new(TestContext::default());
2066 let repl = ReplInterface::new(registry, context, "rpn".to_string(), None, None).unwrap();
2067
2068 assert_eq!(repl.prompt, "rpn > ");
2069 assert_eq!(repl.effective_prompt_multiline(), "...> ");
2070 }
2071
2072 #[test]
2073 fn test_effective_prompt_multiline_uses_configured_suffix() {
2074 let registry = create_test_registry();
2079 let context = Box::new(TestContext::default());
2080 let mut config = make_help_config();
2081 config.metadata.prompt_suffix = " $ ".to_string();
2082
2083 let repl =
2084 ReplInterface::new(registry, context, "rpn".to_string(), Some(config), None).unwrap();
2085
2086 assert_eq!(repl.effective_prompt_multiline(), "...$ ");
2087 }
2088
2089 #[test]
2090 fn test_with_prompt_multiline_overrides_base_only() {
2091 let registry = create_test_registry();
2094 let context = Box::new(TestContext::default());
2095 let mut config = make_help_config();
2096 config.metadata.prompt_suffix = " $ ".to_string();
2097
2098 let repl = ReplInterface::new(registry, context, "rpn".to_string(), Some(config), None)
2099 .unwrap()
2100 .with_prompt_multiline("_ _ _".to_string());
2101
2102 assert_eq!(repl.effective_prompt_multiline(), "_ _ _ $ ");
2103 }
2104
2105 #[test]
2110 fn test_accumulate_line_single_line_no_continuation() {
2111 let mut buffer: Vec<String> = Vec::new();
2112 let result = ReplInterface::accumulate_line(&mut buffer, "test");
2113
2114 assert_eq!(result, Some("test".to_string()));
2115 assert!(buffer.is_empty());
2116 }
2117
2118 #[test]
2119 fn test_accumulate_line_trailing_backslash_buffers_and_returns_none() {
2120 let mut buffer: Vec<String> = Vec::new();
2121 let result = ReplInterface::accumulate_line(&mut buffer, "push \\");
2122
2123 assert_eq!(result, None);
2124 assert_eq!(buffer, vec!["push".to_string()]);
2125 }
2126
2127 #[test]
2128 fn test_accumulate_line_multi_fragment_reconstruction() {
2129 let mut buffer: Vec<String> = Vec::new();
2132
2133 assert_eq!(
2134 ReplInterface::accumulate_line(&mut buffer, "config \\"),
2135 None
2136 );
2137 assert_eq!(
2138 ReplInterface::accumulate_line(&mut buffer, "--format model \\"),
2139 None
2140 );
2141 let result = ReplInterface::accumulate_line(&mut buffer, "source=model.yml");
2142
2143 assert_eq!(
2144 result,
2145 Some("config --format model source=model.yml".to_string())
2146 );
2147 assert!(buffer.is_empty());
2149 }
2150
2151 #[test]
2152 fn test_accumulate_line_strips_marker_and_surrounding_whitespace() {
2153 let mut buffer: Vec<String> = Vec::new();
2157 ReplInterface::accumulate_line(&mut buffer, "test \\");
2158 let result = ReplInterface::accumulate_line(&mut buffer, " arg");
2159
2160 assert_eq!(result, Some("test arg".to_string()));
2161 }
2162
2163 #[test]
2164 fn test_run_multiline_reconstruction_reaches_execute_line_once() {
2165 let registry = create_test_registry();
2170 let context = Box::new(TestContext::default());
2171 let mut repl =
2172 ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
2173
2174 let mut buffer: Vec<String> = Vec::new();
2175 assert_eq!(ReplInterface::accumulate_line(&mut buffer, "te \\"), None);
2176 let line = ReplInterface::accumulate_line(&mut buffer, "st").unwrap();
2177 assert_eq!(line, "te st");
2178
2179 let result = repl.execute_line(&line);
2182 assert!(result.is_err());
2183 }
2184
2185 #[test]
2186 fn test_load_line_ending_in_backslash_is_not_continued() {
2187 let registry = create_test_registry();
2192 let context = Box::new(TestContext::default());
2193 let mut repl =
2194 ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
2195
2196 let script = write_script("test \\\ntest\n");
2197 let line = format!(":load {}", script.path().display());
2198
2199 assert!(repl.handle_line(&line).is_ok());
2203
2204 let ctx = crate::context::downcast_ref::<TestContext>(&*repl.context).unwrap();
2205 assert_eq!(ctx.executed_commands, vec!["test"]);
2207 }
2208
2209 use crate::config::directive::DirectiveDefinition;
2212
2213 fn repl_with(
2214 config: Option<CommandsConfig>,
2215 formatter: Option<Box<dyn HelpFormatter>>,
2216 ) -> ReplInterface {
2217 let registry = create_test_registry();
2218 let context = Box::new(TestContext::default());
2219 ReplInterface::new(registry, context, "test".to_string(), config, formatter).unwrap()
2220 }
2221
2222 fn config_with_directives(directives: Vec<DirectiveDefinition>) -> CommandsConfig {
2224 let mut config = make_help_config();
2225 config.directives = directives;
2226 config
2227 }
2228
2229 fn override_directive(
2230 implementation: ReplDirective,
2231 name: &str,
2232 aliases: &[&str],
2233 ) -> DirectiveDefinition {
2234 DirectiveDefinition {
2235 implementation,
2236 name: name.to_string(),
2237 aliases: aliases.iter().map(|a| a.to_string()).collect(),
2238 description: "Custom".to_string(),
2239 }
2240 }
2241
2242 fn history_len(repl: &ReplInterface) -> usize {
2243 repl.editor.history().len()
2244 }
2245
2246 fn executed(repl: &ReplInterface) -> Vec<String> {
2247 crate::context::downcast_ref::<TestContext>(&*repl.context)
2248 .unwrap()
2249 .executed_commands
2250 .clone()
2251 }
2252
2253 fn invalid_syntax(result: Result<SessionControl>) -> (String, String) {
2255 match result {
2256 Err(DynamicCliError::Parse(ParseError::InvalidSyntax { details, hint })) => {
2257 (details, hint.unwrap_or_default())
2258 }
2259 other => panic!("Expected InvalidSyntax error, got {:?}", other),
2260 }
2261 }
2262
2263 fn unknown_command_suggestions(result: Result<SessionControl>) -> Vec<String> {
2265 match result {
2266 Err(DynamicCliError::Parse(ParseError::UnknownCommand { suggestions, .. })) => {
2267 suggestions
2268 }
2269 other => panic!("Expected UnknownCommand error, got {:?}", other),
2270 }
2271 }
2272
2273 fn unknown_command_hint(result: Result<SessionControl>) -> String {
2275 match result {
2276 Err(DynamicCliError::Parse(ParseError::UnknownCommand { hint, .. })) => {
2277 hint.expect("the REPL sets the hint")
2278 }
2279 other => panic!("Expected UnknownCommand error, got {:?}", other),
2280 }
2281 }
2282
2283 #[test]
2284 fn test_directive_table_defaults_without_config() {
2285 let repl = repl_with(None, None);
2286 assert_eq!(&repl.directives[..], effective_directives(&[]).as_slice());
2287 assert_eq!(repl.resolve_directive("q"), Some(ReplDirective::Quit));
2288 assert_eq!(repl.resolve_directive("?"), Some(ReplDirective::Help));
2289 assert_eq!(repl.resolve_directive("test"), None);
2290 }
2291
2292 #[test]
2293 fn test_directive_help_forms_without_formatter() {
2294 colored::control::set_override(false);
2295 let mut repl = repl_with(Some(make_help_config()), None);
2296 for line in [
2297 ":help",
2298 ":h",
2299 ":?",
2300 ":help hello",
2301 ":h hi",
2302 " :help hello ",
2303 ] {
2304 assert_eq!(
2305 repl.handle_line(line).unwrap(),
2306 SessionControl::Continue,
2307 "{}",
2308 line
2309 );
2310 }
2311 }
2312
2313 #[test]
2314 fn test_directive_help_forms_with_formatter() {
2315 colored::control::set_override(false);
2316 let mut repl = repl_with(
2317 Some(make_help_config()),
2318 Some(Box::new(DefaultHelpFormatter::new())),
2319 );
2320 for line in [":help", ":h", ":?", ":help hello"] {
2321 assert_eq!(
2322 repl.handle_line(line).unwrap(),
2323 SessionControl::Continue,
2324 "{}",
2325 line
2326 );
2327 }
2328 }
2329
2330 #[test]
2331 fn test_directive_help_renders_like_help_flag() {
2332 colored::control::set_override(false);
2333 let repl = repl_with(Some(make_help_config()), None);
2334 assert_eq!(repl.render_help(None), repl.try_handle_help("--help"));
2335 assert_eq!(
2336 repl.render_help(Some("hello")),
2337 repl.try_handle_help("--help hello")
2338 );
2339 }
2340
2341 #[test]
2344 fn test_unknown_command_hint_names_help_directive() {
2345 let mut repl = repl_with(Some(make_help_config()), None);
2346 assert_eq!(
2347 unknown_command_hint(repl.handle_line("frobnicate")),
2348 "Type ':help' for available commands"
2349 );
2350 }
2351
2352 #[test]
2353 fn test_unknown_command_hint_without_config() {
2354 let mut repl = repl_with(None, None);
2356 assert_eq!(
2357 unknown_command_hint(repl.handle_line("frobnicate")),
2358 "Type ':help' for available commands"
2359 );
2360 }
2361
2362 #[test]
2363 fn test_unknown_command_hint_with_directive_suggestion() {
2364 colored::control::set_override(false);
2365 let mut repl = repl_with(Some(make_help_config()), None);
2366 let error = repl.handle_line("quit").unwrap_err();
2367 let formatted = crate::error::format_error(&error);
2368
2369 let suggestion_at = formatted.find(":quit").expect("directive suggested");
2370 let hint_at = formatted
2371 .find("Type ':help' for available commands")
2372 .expect("hint shown");
2373 assert!(suggestion_at < hint_at, "{}", formatted);
2374 }
2375
2376 #[test]
2377 fn test_help_hints_use_translated_help_directive() {
2378 colored::control::set_override(false);
2379 let mut repl = repl_with(
2380 Some(config_with_directives(vec![override_directive(
2381 ReplDirective::Help,
2382 "aide",
2383 &["?"],
2384 )])),
2385 None,
2386 );
2387
2388 assert_eq!(
2389 unknown_command_hint(repl.handle_line("frobnicate")),
2390 "Type ':aide' for available commands"
2391 );
2392
2393 let app_help = repl.render_help(None).unwrap();
2394 assert!(
2395 app_help.ends_with("\nType ':aide <command>' for more information on a command.\n"),
2396 "{}",
2397 app_help
2398 );
2399 assert!(!app_help.contains(":help"), "{}", app_help);
2400 }
2401
2402 #[test]
2403 fn test_app_help_footer_names_help_directive() {
2404 colored::control::set_override(false);
2405 let repl = repl_with(Some(make_help_config()), None);
2406 let config = repl.config.as_deref().unwrap();
2407
2408 let formatter = DefaultHelpFormatter::new();
2409 assert_eq!(
2410 repl.render_help(None).unwrap(),
2411 format!(
2412 "{}{}\nType ':help <command>' for more information on a command.\n",
2413 formatter.format_app(config),
2414 formatter.format_directives(&repl.directives)
2415 )
2416 );
2417 assert_eq!(
2419 repl.render_help(Some("hello")).unwrap(),
2420 DefaultHelpFormatter::new().format_command(config, "hello")
2421 );
2422 }
2423
2424 #[test]
2425 fn test_directive_help_without_config_is_not_available() {
2426 let mut repl = repl_with(None, None);
2427 let err = repl.handle_line(":help").unwrap_err();
2428 assert!(err.to_string().contains("help is not available"), "{}", err);
2429 }
2430
2431 #[test]
2432 fn test_directive_help_with_two_arguments_is_a_usage_error() {
2433 let mut repl = repl_with(Some(make_help_config()), None);
2434 let (details, hint) = invalid_syntax(repl.handle_line(":h hello extra"));
2435 assert!(details.contains("`:h`"), "{}", details);
2436 assert_eq!(hint, "Usage: :help [command]");
2437 }
2438
2439 #[test]
2440 fn test_directive_quit_and_exit_leave() {
2441 let mut repl = repl_with(None, None);
2442 let save = SessionControl::Leave { save_history: true };
2443 let discard = SessionControl::Leave {
2444 save_history: false,
2445 };
2446 assert_eq!(repl.handle_line(":quit").unwrap(), save);
2447 assert_eq!(repl.handle_line(":q").unwrap(), save);
2448 assert_eq!(repl.handle_line(":exit").unwrap(), discard);
2449 }
2450
2451 #[test]
2452 fn test_directive_quit_with_argument_is_a_usage_error() {
2453 let mut repl = repl_with(None, None);
2454 let (details, hint) = invalid_syntax(repl.handle_line(":q now"));
2455 assert!(details.contains("takes no argument"), "{}", details);
2456 assert_eq!(hint, "Usage: :quit");
2457 }
2458
2459 #[test]
2460 fn test_directive_load_without_path_shows_usage() {
2461 let mut repl = repl_with(None, None);
2462 let (details, hint) = invalid_syntax(repl.handle_line(":load"));
2463 assert!(details.contains("requires a file path"), "{}", details);
2464 assert_eq!(hint, "Usage: :load <path>");
2465 }
2466
2467 static HISTORY_FILE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
2474
2475 fn history_file_lock() -> std::sync::MutexGuard<'static, ()> {
2476 HISTORY_FILE_LOCK
2477 .lock()
2478 .unwrap_or_else(|poisoned| poisoned.into_inner())
2479 }
2480
2481 #[test]
2482 fn test_end_session_with_save_writes_history_file() {
2483 let _lock = history_file_lock();
2484 let dir = tempfile::tempdir().unwrap();
2485 let path = dir.path().join("history");
2486 let mut repl = repl_with(None, None);
2487 repl.history_path = Some(path.clone());
2488
2489 assert!(repl.handle_line("test").is_ok());
2490 repl.end_session(true);
2491
2492 let content = std::fs::read_to_string(&path).unwrap();
2493 assert!(content.lines().any(|l| l == "test"), "{}", content);
2494 }
2495
2496 #[test]
2497 fn test_end_session_without_save_leaves_history_file_unchanged() {
2498 let _lock = history_file_lock();
2499 let dir = tempfile::tempdir().unwrap();
2500 let path = dir.path().join("history");
2501 std::fs::write(&path, "previous\n").unwrap();
2502 let mut repl = repl_with(None, None);
2503 repl.history_path = Some(path.clone());
2504
2505 assert!(repl.handle_line("test").is_ok());
2506 repl.end_session(false);
2507
2508 assert_eq!(std::fs::read_to_string(&path).unwrap(), "previous\n");
2509 }
2510
2511 #[test]
2512 fn test_drop_does_not_write_history_file() {
2513 let _lock = history_file_lock();
2514 let dir = tempfile::tempdir().unwrap();
2515 let path = dir.path().join("history");
2516 let mut repl = repl_with(None, None);
2517 repl.history_path = Some(path.clone());
2518
2519 assert!(repl.handle_line("test").is_ok());
2520 drop(repl);
2521
2522 assert!(!path.exists());
2523 }
2524
2525 #[test]
2526 fn test_directive_lines_are_not_added_to_history() {
2527 colored::control::set_override(false);
2528 let mut repl = repl_with(Some(make_help_config()), None);
2529 let before = history_len(&repl);
2531 for line in [":help", ":quit", ":exit", ":unknown"] {
2532 let _ = repl.handle_line(line);
2533 }
2534 assert_eq!(history_len(&repl), before);
2535 }
2536
2537 #[test]
2538 fn test_bare_quit_and_exit_no_longer_leave() {
2539 let mut repl = repl_with(None, None);
2540
2541 let suggestions = unknown_command_suggestions(repl.handle_line("quit"));
2542 assert_eq!(suggestions.first().map(String::as_str), Some(":quit"));
2543
2544 let suggestions = unknown_command_suggestions(repl.handle_line("exit"));
2545 assert_eq!(suggestions.first().map(String::as_str), Some(":exit"));
2546
2547 let suggestions = unknown_command_suggestions(repl.handle_line("q"));
2548 assert_eq!(suggestions.first().map(String::as_str), Some(":q"));
2549 }
2550
2551 #[test]
2552 fn test_unknown_command_unrelated_to_directives_gets_no_directive() {
2553 let mut repl = repl_with(None, None);
2554 let suggestions = unknown_command_suggestions(repl.handle_line("frobnicate"));
2555 assert!(suggestions.iter().all(|s| !s.starts_with(':')));
2556 }
2557
2558 #[test]
2559 fn test_application_command_named_like_a_directive_runs() {
2560 let mut registry = create_test_registry();
2561 let exit = CommandDefinition {
2562 name: "exit".to_string(),
2563 aliases: vec![],
2564 description: "Application exit command".to_string(),
2565 required: false,
2566 arguments: vec![],
2567 options: vec![],
2568 implementation: "exit_handler".to_string(),
2569 continue_on_failure: false,
2570 requires_success: false,
2571 };
2572 registry
2573 .register_sync(
2574 exit,
2575 Box::new(TestHandler {
2576 name: "exit".to_string(),
2577 }),
2578 )
2579 .unwrap();
2580 let context = Box::new(TestContext::default());
2581 let mut repl =
2582 ReplInterface::new(registry, context, "test".to_string(), None, None).unwrap();
2583
2584 assert_eq!(repl.handle_line("exit").unwrap(), SessionControl::Continue);
2585 assert_eq!(executed(&repl), vec!["exit"]);
2586 assert_eq!(
2588 repl.handle_line(":exit").unwrap(),
2589 SessionControl::Leave {
2590 save_history: false
2591 }
2592 );
2593 }
2594
2595 #[test]
2596 fn test_translated_directive_names_replace_defaults() {
2597 let mut repl = repl_with(
2598 Some(config_with_directives(vec![
2599 override_directive(ReplDirective::Quit, "quitter", &[]),
2600 override_directive(ReplDirective::Load, "charger", &["c"]),
2601 ])),
2602 None,
2603 );
2604
2605 assert_eq!(
2606 repl.handle_line(":quitter").unwrap(),
2607 SessionControl::Leave { save_history: true }
2608 );
2609 assert!(repl.handle_line(":quit").is_err());
2611 assert!(repl.handle_line(":q").is_err());
2612 assert_eq!(
2614 repl.handle_line(":exit").unwrap(),
2615 SessionControl::Leave {
2616 save_history: false
2617 }
2618 );
2619
2620 let script = write_script("test\n");
2621 let line = format!(":c {}", script.path().display());
2622 assert!(repl.handle_line(&line).is_ok());
2623 assert_eq!(executed(&repl), vec!["test"]);
2624
2625 let (_, hint) = invalid_syntax(repl.handle_line(":charger"));
2626 assert_eq!(hint, "Usage: :charger <path>");
2627
2628 let suggestions = unknown_command_suggestions(repl.handle_line("quitter"));
2629 assert_eq!(suggestions.first().map(String::as_str), Some(":quitter"));
2630 let suggestions = unknown_command_suggestions(repl.handle_line("quit"));
2632 assert!(!suggestions.iter().any(|s| s == ":quit"));
2633 }
2634
2635 #[test]
2636 fn test_unknown_directive_lists_effective_directives() {
2637 let mut repl = repl_with(
2638 Some(config_with_directives(vec![override_directive(
2639 ReplDirective::Help,
2640 "aide",
2641 &[],
2642 )])),
2643 None,
2644 );
2645
2646 let (details, hint) = invalid_syntax(repl.handle_line(":foo"));
2647 assert_eq!(details, "Unknown directive ':foo'");
2648 assert_eq!(hint, "Available directives: :aide, :load, :quit, :exit");
2649
2650 let (details, _) = invalid_syntax(repl.handle_line(":"));
2651 assert_eq!(details, "Unknown directive ':'");
2652 }
2653
2654 fn complete_with(directives: Arc<[DirectiveDefinition]>, line: &str) -> (usize, Vec<String>) {
2659 let completer = DcliCompleter::new(Arc::new(create_test_registry()), None, directives);
2660 let history = rustyline::history::DefaultHistory::new();
2661 let ctx = rustyline::Context::new(&history);
2662 let (start, candidates) = completer.complete(line, line.len(), &ctx).unwrap();
2663 (
2664 start,
2665 candidates
2666 .into_iter()
2667 .map(|pair| pair.replacement)
2668 .collect(),
2669 )
2670 }
2671
2672 #[test]
2673 fn test_completer_colon_lists_all_directives() {
2674 let (start, names) = complete_with(default_directives(), ":");
2675 assert_eq!(start, 0);
2676 assert_eq!(
2677 names,
2678 vec![":?", ":exit", ":h", ":help", ":load", ":q", ":quit"]
2679 );
2680 }
2681
2682 #[test]
2683 fn test_completer_directive_prefix() {
2684 assert_eq!(complete_with(default_directives(), ":he").1, vec![":help"]);
2685 assert_eq!(
2686 complete_with(default_directives(), ":q").1,
2687 vec![":q", ":quit"]
2688 );
2689 assert!(complete_with(default_directives(), ":zz").1.is_empty());
2690 assert_eq!(
2692 complete_with(default_directives(), " :lo"),
2693 (2, vec![":load".to_string()])
2694 );
2695 }
2696
2697 #[test]
2698 fn test_completer_directives_follow_configuration() {
2699 let table: Arc<[DirectiveDefinition]> =
2700 effective_directives(&[override_directive(ReplDirective::Help, "aide", &["a"])]).into();
2701 assert_eq!(
2702 complete_with(Arc::clone(&table), ":a").1,
2703 vec![":a", ":aide"]
2704 );
2705 assert!(complete_with(table, ":h").1.is_empty());
2706 }
2707
2708 #[test]
2709 fn test_completer_commands_unchanged_by_directives() {
2710 let (_, names) = complete_with(default_directives(), "");
2713 assert!(names.iter().all(|n| !n.starts_with(':')), "{:?}", names);
2714 assert!(names.contains(&"test".to_string()));
2715 assert!(complete_with(default_directives(), ":help ").1.is_empty());
2716 }
2717
2718 #[test]
2719 fn test_completer_shares_repl_directive_table() {
2720 let repl = repl_with(
2721 Some(config_with_directives(vec![override_directive(
2722 ReplDirective::Quit,
2723 "quitter",
2724 &[],
2725 )])),
2726 None,
2727 );
2728 let helper = repl.editor.helper().expect("helper installed");
2729 assert!(Arc::ptr_eq(&helper.completer.directives, &repl.directives));
2730 }
2731
2732 #[test]
2733 fn test_app_help_lists_directives_before_footer() {
2734 colored::control::set_override(false);
2735 let repl = repl_with(Some(make_help_config()), None);
2736 let help = repl.render_help(None).unwrap();
2737
2738 let commands_at = help.find("COMMANDS:").expect("commands");
2739 let directives_at = help.find("DIRECTIVES:").expect("directives");
2740 let footer_at = help.find("Type ':help <command>'").expect("footer");
2741 assert!(
2742 commands_at < directives_at && directives_at < footer_at,
2743 "{}",
2744 help
2745 );
2746 for usage in [":help [command]", ":load <path>", ":quit", ":exit"] {
2747 assert!(help.contains(usage), "{}", help);
2748 }
2749 }
2750
2751 #[test]
2752 fn test_app_help_lists_translated_directives() {
2753 colored::control::set_override(false);
2754 let repl = repl_with(
2755 Some(config_with_directives(vec![
2756 override_directive(ReplDirective::Help, "aide", &["?"]),
2757 override_directive(ReplDirective::Load, "charger", &[]),
2758 ])),
2759 None,
2760 );
2761 let help = repl.render_help(None).unwrap();
2762
2763 assert!(help.contains(":aide [command]"), "{}", help);
2764 assert!(help.contains("[aliases: :?]"), "{}", help);
2765 assert!(help.contains(":charger <path>"), "{}", help);
2766 assert!(!help.contains(":help"), "{}", help);
2767 assert!(!help.contains(":load"), "{}", help);
2768 assert_eq!(Some(help), repl.try_handle_help("--help"));
2770 }
2771
2772 #[test]
2773 fn test_app_help_uses_custom_directive_listing() {
2774 struct ListingFormatter;
2775 impl HelpFormatter for ListingFormatter {
2776 fn format_app(&self, _: &CommandsConfig) -> String {
2777 "APP\n".to_string()
2778 }
2779 fn format_command(&self, _: &CommandsConfig, command: &str) -> String {
2780 command.to_string()
2781 }
2782 fn format_directives(&self, directives: &[DirectiveDefinition]) -> String {
2783 let names: Vec<&str> = directives.iter().map(|d| d.name.as_str()).collect();
2784 format!("DIR {}\n", names.join(","))
2785 }
2786 }
2787
2788 colored::control::set_override(false);
2789 let repl = repl_with(Some(make_help_config()), Some(Box::new(ListingFormatter)));
2790 assert_eq!(
2791 repl.render_help(None).unwrap(),
2792 "APP\nDIR help,load,quit,exit\n\
2793 \nType ':help <command>' for more information on a command.\n"
2794 );
2795 }
2796
2797 #[test]
2798 fn test_command_help_has_no_directive_list() {
2799 colored::control::set_override(false);
2800 let repl = repl_with(Some(make_help_config()), None);
2801 let help = repl.render_help(Some("hello")).unwrap();
2802 assert!(!help.contains("DIRECTIVES:"), "{}", help);
2803 }
2804
2805 #[test]
2806 fn test_load_refuses_leaving_directives_and_runs_other_lines() {
2807 colored::control::set_override(false);
2808 let mut repl = repl_with(Some(make_help_config()), None);
2809
2810 let script = write_script("test\n:quit\n:q\n:exit\n:help\ntest\n");
2811 let line = format!(":load {}", script.path().display());
2812
2813 assert_eq!(repl.handle_line(&line).unwrap(), SessionControl::Continue);
2814 assert_eq!(executed(&repl), vec!["test", "test"]);
2815 }
2816}