dynamic_cli/
builder.rs

1//! Fluent builder API for creating CLI/REPL applications
2//!
3//! This module provides a builder pattern for easily constructing
4//! CLI and REPL applications with minimal boilerplate.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use dynamic_cli::prelude::*;
10//! use std::collections::HashMap;
11//!
12//! // Define context
13//! #[derive(Default)]
14//! struct MyContext;
15//!
16//! impl ExecutionContext for MyContext {
17//!     fn as_any(&self) -> &dyn std::any::Any { self }
18//!     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
19//! }
20//!
21//! // Define handler
22//! struct HelloCommand;
23//!
24//! impl CommandHandler for HelloCommand {
25//!     fn execute(
26//!         &self,
27//!         _context: &mut dyn ExecutionContext,
28//!         args: &HashMap<String, String>,
29//!     ) -> dynamic_cli::Result<()> {
30//!         println!("Hello!");
31//!         Ok(())
32//!     }
33//! }
34//!
35//! # fn main() -> dynamic_cli::Result<()> {
36//! // Build and run
37//! CliBuilder::new()
38//!     .config_file("commands.yaml")
39//!     .context(Box::new(MyContext::default()))
40//!     .register_handler("hello_handler", Box::new(HelloCommand))
41//!     .build()?
42//!     .run()
43//! # }
44//! ```
45
46use crate::config::loader::load_config;
47use crate::config::schema::CommandsConfig;
48use crate::context::ExecutionContext;
49use crate::error::{ConfigError, DynamicCliError, Result};
50use crate::executor::CommandHandler;
51use crate::interface::{CliInterface, ReplInterface};
52use crate::registry::CommandRegistry;
53use std::collections::HashMap;
54use std::path::PathBuf;
55
56/// Fluent builder for creating CLI/REPL applications
57///
58/// Provides a chainable API for configuring and building applications.
59/// Automatically loads configuration, registers handlers, and creates
60/// the appropriate interface (CLI or REPL).
61///
62/// # Builder Pattern
63///
64/// The builder follows the standard Rust builder pattern:
65/// - Methods consume `self` and return `Self`
66/// - Final `build()` method consumes the builder and returns the app
67///
68/// # Example
69///
70/// ```no_run
71/// use dynamic_cli::prelude::*;
72///
73/// # #[derive(Default)]
74/// # struct MyContext;
75/// # impl ExecutionContext for MyContext {
76/// #     fn as_any(&self) -> &dyn std::any::Any { self }
77/// #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
78/// # }
79/// # struct MyHandler;
80/// # impl CommandHandler for MyHandler {
81/// #     fn execute(&self, _: &mut dyn ExecutionContext, _: &std::collections::HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
82/// # }
83/// # fn main() -> dynamic_cli::Result<()> {
84/// let app = CliBuilder::new()
85///     .config_file("commands.yaml")
86///     .context(Box::new(MyContext::default()))
87///     .register_handler("my_handler", Box::new(MyHandler))
88///     .prompt("myapp")
89///     .build()?;
90/// # Ok(())
91/// # }
92/// ```
93pub struct CliBuilder {
94    /// Path to configuration file
95    config_path: Option<PathBuf>,
96
97    /// Loaded configuration
98    config: Option<CommandsConfig>,
99
100    /// Execution context
101    context: Option<Box<dyn ExecutionContext>>,
102
103    /// Registered command handlers (name -> handler)
104    handlers: HashMap<String, Box<dyn CommandHandler>>,
105
106    /// REPL prompt (if None, will use config default or "cli")
107    prompt: Option<String>,
108}
109
110impl CliBuilder {
111    /// Create a new builder
112    ///
113    /// # Example
114    ///
115    /// ```
116    /// use dynamic_cli::CliBuilder;
117    ///
118    /// let builder = CliBuilder::new();
119    /// ```
120    pub fn new() -> Self {
121        Self {
122            config_path: None,
123            config: None,
124            context: None,
125            handlers: HashMap::new(),
126            prompt: None,
127        }
128    }
129
130    /// Specify the configuration file
131    ///
132    /// The file will be loaded during `build()`. Supports YAML and JSON formats.
133    ///
134    /// # Arguments
135    ///
136    /// * `path` - Path to the configuration file (`.yaml`, `.yml`, or `.json`)
137    ///
138    /// # Example
139    ///
140    /// ```
141    /// use dynamic_cli::CliBuilder;
142    ///
143    /// let builder = CliBuilder::new()
144    ///     .config_file("commands.yaml");
145    /// ```
146    pub fn config_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
147        self.config_path = Some(path.into());
148        self
149    }
150
151    /// Provide a pre-loaded configuration
152    ///
153    /// Use this instead of `config_file()` if you want to load and potentially
154    /// modify the configuration before building.
155    ///
156    /// # Arguments
157    ///
158    /// * `config` - Loaded and validated configuration
159    ///
160    /// # Example
161    ///
162    /// ```no_run
163    /// use dynamic_cli::{CliBuilder, config::loader::load_config};
164    ///
165    /// # fn main() -> dynamic_cli::Result<()> {
166    /// let mut config = load_config("commands.yaml")?;
167    /// // Modify config if needed...
168    ///
169    /// let builder = CliBuilder::new()
170    ///     .config(config);
171    /// # Ok(())
172    /// # }
173    /// ```
174    pub fn config(mut self, config: CommandsConfig) -> Self {
175        self.config = Some(config);
176        self
177    }
178
179    /// Set the execution context
180    ///
181    /// The context will be passed to all command handlers and can store
182    /// application state.
183    ///
184    /// # Arguments
185    ///
186    /// * `context` - Boxed execution context implementing `ExecutionContext`
187    ///
188    /// # Example
189    ///
190    /// ```
191    /// use dynamic_cli::prelude::*;
192    ///
193    /// #[derive(Default)]
194    /// struct MyContext {
195    ///     count: u32,
196    /// }
197    ///
198    /// impl ExecutionContext for MyContext {
199    ///     fn as_any(&self) -> &dyn std::any::Any { self }
200    ///     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
201    /// }
202    ///
203    /// let builder = CliBuilder::new()
204    ///     .context(Box::new(MyContext::default()));
205    /// ```
206    pub fn context(mut self, context: Box<dyn ExecutionContext>) -> Self {
207        self.context = Some(context);
208        self
209    }
210
211    /// Register a command handler
212    ///
213    /// Associates a handler with the command's implementation name from the config.
214    /// The name must match the `implementation` field in the command definition.
215    ///
216    /// # Arguments
217    ///
218    /// * `name` - Implementation name from the configuration
219    /// * `handler` - Boxed command handler implementing `CommandHandler`
220    ///
221    /// # Example
222    ///
223    /// ```
224    /// use dynamic_cli::prelude::*;
225    /// use std::collections::HashMap;
226    ///
227    /// struct MyCommand;
228    ///
229    /// impl CommandHandler for MyCommand {
230    ///     fn execute(
231    ///         &self,
232    ///         _ctx: &mut dyn ExecutionContext,
233    ///         _args: &HashMap<String, String>,
234    ///     ) -> dynamic_cli::Result<()> {
235    ///         println!("Executed!");
236    ///         Ok(())
237    ///     }
238    /// }
239    ///
240    /// let builder = CliBuilder::new()
241    ///     .register_handler("my_command", Box::new(MyCommand));
242    /// ```
243    pub fn register_handler(
244        mut self,
245        name: impl Into<String>,
246        handler: Box<dyn CommandHandler>,
247    ) -> Self {
248        self.handlers.insert(name.into(), handler);
249        self
250    }
251
252    /// Set the REPL prompt
253    ///
254    /// Only used in REPL mode. If not specified, uses the prompt from
255    /// the configuration or defaults to "cli".
256    ///
257    /// # Arguments
258    ///
259    /// * `prompt` - Prompt prefix (e.g., "myapp" displays as "myapp > ")
260    ///
261    /// # Example
262    ///
263    /// ```
264    /// use dynamic_cli::CliBuilder;
265    ///
266    /// let builder = CliBuilder::new()
267    ///     .prompt("myapp");
268    /// ```
269    pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
270        self.prompt = Some(prompt.into());
271        self
272    }
273
274    /// Build the application
275    ///
276    /// Performs the following steps:
277    /// 1. Load configuration (if `config_file()` was used)
278    /// 2. Validate that a context was provided
279    /// 3. Create the command registry
280    /// 4. Register all command handlers
281    /// 5. Verify that all required commands have handlers
282    /// 6. Create the `CliApp`
283    ///
284    /// # Returns
285    ///
286    /// A configured `CliApp` ready to run
287    ///
288    /// # Errors
289    ///
290    /// - Configuration errors (file not found, invalid format, etc.)
291    /// - Missing context
292    /// - Missing required handlers
293    /// - Registry errors
294    ///
295    /// # Example
296    ///
297    /// ```no_run
298    /// use dynamic_cli::prelude::*;
299    ///
300    /// # #[derive(Default)]
301    /// # struct MyContext;
302    /// # impl ExecutionContext for MyContext {
303    /// #     fn as_any(&self) -> &dyn std::any::Any { self }
304    /// #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
305    /// # }
306    /// # struct MyHandler;
307    /// # impl CommandHandler for MyHandler {
308    /// #     fn execute(&self, _: &mut dyn ExecutionContext, _: &std::collections::HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
309    /// # }
310    /// # fn main() -> dynamic_cli::Result<()> {
311    /// let app = CliBuilder::new()
312    ///     .config_file("commands.yaml")
313    ///     .context(Box::new(MyContext::default()))
314    ///     .register_handler("handler", Box::new(MyHandler))
315    ///     .build()?;
316    ///
317    /// // Now app is ready to run
318    /// # Ok(())
319    /// # }
320    /// ```
321    pub fn build(mut self) -> Result<CliApp> {
322        // Load configuration if path was specified
323        let config = if let Some(config) = self.config.take() {
324            config
325        } else if let Some(path) = self.config_path.take() {
326            load_config(path)?
327        } else {
328            return Err(DynamicCliError::Config(ConfigError::InvalidSchema {
329                reason: "No configuration provided. Use config_file() or config()".to_string(),
330                path: None,
331            }));
332        };
333
334        // Validate context was provided
335        let context = self.context.take().ok_or_else(|| {
336            DynamicCliError::Config(ConfigError::InvalidSchema {
337                reason: "No execution context provided. Use context()".to_string(),
338                path: None,
339            })
340        })?;
341
342        // Create registry and register commands
343        let mut registry = CommandRegistry::new();
344
345        for command_def in &config.commands {
346            // Find handler for this command
347            let handler = self.handlers.remove(&command_def.implementation);
348
349            // Check if handler is required
350            if command_def.required && handler.is_none() {
351                return Err(DynamicCliError::Config(ConfigError::InvalidSchema {
352                    reason: format!(
353                        "Required command '{}' has no registered handler (implementation: '{}'). \
354                        Use register_handler() to register it.",
355                        command_def.name, command_def.implementation
356                    ),
357                    path: None,
358                }));
359            }
360
361            // Register command if handler exists
362            if let Some(handler) = handler {
363                registry.register(command_def.clone(), handler)?;
364            }
365        }
366
367        // Determine prompt
368        let prompt = self
369            .prompt
370            .or_else(|| Some(config.metadata.prompt.clone()))
371            .unwrap_or_else(|| "cli".to_string());
372
373        Ok(CliApp {
374            registry,
375            context,
376            prompt,
377        })
378    }
379}
380
381impl Default for CliBuilder {
382    fn default() -> Self {
383        Self::new()
384    }
385}
386
387/// Built CLI/REPL application
388///
389/// Created by `CliBuilder::build()`. Provides methods to run the application
390/// in different modes:
391/// - `run()` - Auto-detect CLI vs REPL based on arguments
392/// - `run_cli()` - Force CLI mode with specific arguments
393/// - `run_repl()` - Force REPL mode
394///
395/// # Example
396///
397/// ```no_run
398/// use dynamic_cli::prelude::*;
399///
400/// # #[derive(Default)]
401/// # struct MyContext;
402/// # impl ExecutionContext for MyContext {
403/// #     fn as_any(&self) -> &dyn std::any::Any { self }
404/// #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
405/// # }
406/// # struct MyHandler;
407/// # impl CommandHandler for MyHandler {
408/// #     fn execute(&self, _: &mut dyn ExecutionContext, _: &std::collections::HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
409/// # }
410/// # fn main() -> dynamic_cli::Result<()> {
411/// let app = CliBuilder::new()
412///     .config_file("commands.yaml")
413///     .context(Box::new(MyContext::default()))
414///     .register_handler("handler", Box::new(MyHandler))
415///     .build()?;
416///
417/// // Auto-detect mode (CLI if args provided, REPL otherwise)
418/// app.run()
419/// # }
420/// ```
421pub struct CliApp {
422    /// Command registry
423    registry: CommandRegistry,
424
425    /// Execution context
426    context: Box<dyn ExecutionContext>,
427
428    /// REPL prompt
429    prompt: String,
430}
431
432impl std::fmt::Debug for CliApp {
433    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434        f.debug_struct("CliApp")
435            .field("prompt", &self.prompt)
436            .field("registry", &"<CommandRegistry>")
437            .field("context", &"<ExecutionContext>")
438            .finish()
439    }
440}
441
442impl CliApp {
443    /// Run in CLI mode with provided arguments
444    ///
445    /// Executes a single command and exits.
446    ///
447    /// # Arguments
448    ///
449    /// * `args` - Command-line arguments (typically from `env::args().skip(1)`)
450    ///
451    /// # Returns
452    ///
453    /// - `Ok(())` on successful execution
454    /// - `Err(...)` on parse, validation, or execution errors
455    ///
456    /// # Example
457    ///
458    /// ```no_run
459    /// # use dynamic_cli::prelude::*;
460    /// # #[derive(Default)]
461    /// # struct MyContext;
462    /// # impl ExecutionContext for MyContext {
463    /// #     fn as_any(&self) -> &dyn std::any::Any { self }
464    /// #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
465    /// # }
466    /// # struct MyHandler;
467    /// # impl CommandHandler for MyHandler {
468    /// #     fn execute(&self, _: &mut dyn ExecutionContext, _: &std::collections::HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
469    /// # }
470    /// # fn main() -> dynamic_cli::Result<()> {
471    /// # let app = CliBuilder::new()
472    /// #     .config_file("commands.yaml")
473    /// #     .context(Box::new(MyContext::default()))
474    /// #     .register_handler("handler", Box::new(MyHandler))
475    /// #     .build()?;
476    /// // Run with specific arguments
477    /// app.run_cli(vec!["command".to_string(), "arg1".to_string()])
478    /// # }
479    /// ```
480    pub fn run_cli(self, args: Vec<String>) -> Result<()> {
481        let cli = CliInterface::new(self.registry, self.context);
482        cli.run(args)
483    }
484
485    /// Run in REPL mode
486    ///
487    /// Enters an interactive loop that continues until the user exits.
488    ///
489    /// # Returns
490    ///
491    /// - `Ok(())` when user exits normally
492    /// - `Err(...)` on critical errors (e.g., rustyline initialization failure)
493    ///
494    /// # Example
495    ///
496    /// ```no_run
497    /// # use dynamic_cli::prelude::*;
498    /// # #[derive(Default)]
499    /// # struct MyContext;
500    /// # impl ExecutionContext for MyContext {
501    /// #     fn as_any(&self) -> &dyn std::any::Any { self }
502    /// #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
503    /// # }
504    /// # struct MyHandler;
505    /// # impl CommandHandler for MyHandler {
506    /// #     fn execute(&self, _: &mut dyn ExecutionContext, _: &std::collections::HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
507    /// # }
508    /// # fn main() -> dynamic_cli::Result<()> {
509    /// # let app = CliBuilder::new()
510    /// #     .config_file("commands.yaml")
511    /// #     .context(Box::new(MyContext::default()))
512    /// #     .register_handler("handler", Box::new(MyHandler))
513    /// #     .build()?;
514    /// // Start interactive REPL
515    /// app.run_repl()
516    /// # }
517    /// ```
518    pub fn run_repl(self) -> Result<()> {
519        let repl = ReplInterface::new(self.registry, self.context, self.prompt)?;
520        repl.run()
521    }
522
523    /// Run with automatic mode detection
524    ///
525    /// Decides between CLI and REPL based on command-line arguments:
526    /// - If arguments provided → CLI mode
527    /// - If no arguments → REPL mode
528    ///
529    /// This is the recommended method for most applications.
530    ///
531    /// # Returns
532    ///
533    /// - `Ok(())` on successful execution
534    /// - `Err(...)` on errors
535    ///
536    /// # Example
537    ///
538    /// ```no_run
539    /// # use dynamic_cli::prelude::*;
540    /// # #[derive(Default)]
541    /// # struct MyContext;
542    /// # impl ExecutionContext for MyContext {
543    /// #     fn as_any(&self) -> &dyn std::any::Any { self }
544    /// #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
545    /// # }
546    /// # struct MyHandler;
547    /// # impl CommandHandler for MyHandler {
548    /// #     fn execute(&self, _: &mut dyn ExecutionContext, _: &std::collections::HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
549    /// # }
550    /// # fn main() -> dynamic_cli::Result<()> {
551    /// # let app = CliBuilder::new()
552    /// #     .config_file("commands.yaml")
553    /// #     .context(Box::new(MyContext::default()))
554    /// #     .register_handler("handler", Box::new(MyHandler))
555    /// #     .build()?;
556    /// // Auto-detect: CLI if args, REPL if no args
557    /// app.run()
558    /// # }
559    /// ```
560    pub fn run(self) -> Result<()> {
561        let args: Vec<String> = std::env::args().skip(1).collect();
562
563        if args.is_empty() {
564            // No arguments → REPL mode
565            self.run_repl()
566        } else {
567            // Arguments provided → CLI mode
568            self.run_cli(args)
569        }
570    }
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576    use crate::config::schema::{ArgumentDefinition, ArgumentType, CommandDefinition, Metadata};
577
578    // Test context
579    #[derive(Default)]
580    struct TestContext {
581        executed: Vec<String>,
582    }
583
584    impl ExecutionContext for TestContext {
585        fn as_any(&self) -> &dyn std::any::Any {
586            self
587        }
588
589        fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
590            self
591        }
592    }
593
594    // Test handler
595    struct TestHandler {
596        name: String,
597    }
598
599    impl CommandHandler for TestHandler {
600        fn execute(
601            &self,
602            context: &mut dyn ExecutionContext,
603            _args: &HashMap<String, String>,
604        ) -> Result<()> {
605            let ctx =
606                crate::context::downcast_mut::<TestContext>(context).expect("Failed to downcast");
607            ctx.executed.push(self.name.clone());
608            Ok(())
609        }
610    }
611
612    fn create_test_config() -> CommandsConfig {
613        CommandsConfig {
614            metadata: Metadata {
615                version: "1.0.0".to_string(),
616                prompt: "test".to_string(),
617                prompt_suffix: " > ".to_string(),
618            },
619            commands: vec![CommandDefinition {
620                name: "test".to_string(),
621                aliases: vec![],
622                description: "Test command".to_string(),
623                required: true,
624                arguments: vec![],
625                options: vec![],
626                implementation: "test_handler".to_string(),
627            }],
628            global_options: vec![],
629        }
630    }
631
632    #[test]
633    fn test_builder_creation() {
634        let builder = CliBuilder::new();
635        assert!(builder.config.is_none());
636        assert!(builder.context.is_none());
637    }
638
639    #[test]
640    fn test_builder_with_config() {
641        let config = create_test_config();
642        let builder = CliBuilder::new().config(config.clone());
643
644        assert!(builder.config.is_some());
645    }
646
647    #[test]
648    fn test_builder_with_context() {
649        let context = Box::new(TestContext::default());
650        let builder = CliBuilder::new().context(context);
651
652        assert!(builder.context.is_some());
653    }
654
655    #[test]
656    fn test_builder_with_handler() {
657        let handler = Box::new(TestHandler {
658            name: "test".to_string(),
659        });
660
661        let builder = CliBuilder::new().register_handler("test_handler", handler);
662
663        assert_eq!(builder.handlers.len(), 1);
664    }
665
666    #[test]
667    fn test_builder_with_prompt() {
668        let builder = CliBuilder::new().prompt("myapp");
669
670        assert_eq!(builder.prompt, Some("myapp".to_string()));
671    }
672
673    #[test]
674    fn test_builder_build_success() {
675        let config = create_test_config();
676        let context = Box::new(TestContext::default());
677        let handler = Box::new(TestHandler {
678            name: "test".to_string(),
679        });
680
681        let app = CliBuilder::new()
682            .config(config)
683            .context(context)
684            .register_handler("test_handler", handler)
685            .build();
686
687        assert!(app.is_ok());
688    }
689
690    #[test]
691    fn test_builder_build_missing_config() {
692        let context = Box::new(TestContext::default());
693
694        let result = CliBuilder::new().context(context).build();
695
696        assert!(result.is_err());
697        match result.unwrap_err() {
698            DynamicCliError::Config(ConfigError::InvalidSchema { reason, .. }) => {
699                assert!(reason.contains("No configuration provided"));
700            }
701            other => panic!("Expected InvalidSchema error, got: {:?}", other),
702        }
703    }
704
705    #[test]
706    fn test_builder_build_missing_context() {
707        let config = create_test_config();
708
709        let result = CliBuilder::new().config(config).build();
710
711        assert!(result.is_err());
712        match result.unwrap_err() {
713            DynamicCliError::Config(ConfigError::InvalidSchema { reason, .. }) => {
714                assert!(reason.contains("No execution context provided"));
715            }
716            other => panic!("Expected InvalidSchema error, got: {:?}", other),
717        }
718    }
719
720    #[test]
721    fn test_builder_build_missing_required_handler() {
722        let config = create_test_config();
723        let context = Box::new(TestContext::default());
724
725        let result = CliBuilder::new().config(config).context(context).build();
726
727        assert!(result.is_err());
728        match result.unwrap_err() {
729            DynamicCliError::Config(ConfigError::InvalidSchema { reason, .. }) => {
730                assert!(reason.contains("Required command"));
731                assert!(reason.contains("no registered handler"));
732            }
733            other => panic!("Expected InvalidSchema error, got: {:?}", other),
734        }
735    }
736
737    #[test]
738    fn test_builder_chaining() {
739        let config = create_test_config();
740        let context = Box::new(TestContext::default());
741        let handler = Box::new(TestHandler {
742            name: "test".to_string(),
743        });
744
745        // Test that all methods chain correctly
746        let app = CliBuilder::new()
747            .config(config)
748            .context(context)
749            .register_handler("test_handler", handler)
750            .prompt("test")
751            .build();
752
753        assert!(app.is_ok());
754    }
755
756    #[test]
757    fn test_cli_app_run_cli() {
758        let config = create_test_config();
759        let context = Box::new(TestContext::default());
760        let handler = Box::new(TestHandler {
761            name: "test".to_string(),
762        });
763
764        let app = CliBuilder::new()
765            .config(config)
766            .context(context)
767            .register_handler("test_handler", handler)
768            .build()
769            .unwrap();
770
771        // Run with test command
772        let result = app.run_cli(vec!["test".to_string()]);
773        assert!(result.is_ok());
774    }
775
776    #[test]
777    fn test_default_prompt_from_config() {
778        let config = create_test_config();
779        let context = Box::new(TestContext::default());
780        let handler = Box::new(TestHandler {
781            name: "test".to_string(),
782        });
783
784        let app = CliBuilder::new()
785            .config(config)
786            .context(context)
787            .register_handler("test_handler", handler)
788            .build()
789            .unwrap();
790
791        // Prompt should be taken from config
792        assert_eq!(app.prompt, "test");
793    }
794
795    #[test]
796    fn test_override_prompt() {
797        let config = create_test_config();
798        let context = Box::new(TestContext::default());
799        let handler = Box::new(TestHandler {
800            name: "test".to_string(),
801        });
802
803        let app = CliBuilder::new()
804            .config(config)
805            .context(context)
806            .register_handler("test_handler", handler)
807            .prompt("custom")
808            .build()
809            .unwrap();
810
811        // Prompt should be overridden
812        assert_eq!(app.prompt, "custom");
813    }
814}