Skip to main content

dynamic_cli/registry/
command_registry.rs

1//! Command registry implementation
2//!
3//! This module provides the central registry for storing and retrieving
4//! command definitions and their associated handlers.
5//!
6//! # Architecture
7//!
8//! The registry maintains two main data structures:
9//! - A map of command names to their definitions and handlers
10//! - A map of aliases to canonical command names
11//!
12//! This design allows O(1) lookup by both command name and alias.
13//!
14//! # Example
15//!
16//! ```
17//! use dynamic_cli::registry::CommandRegistry;
18//! use dynamic_cli::config::schema::CommandDefinition;
19//! use dynamic_cli::executor::{CommandHandler, ParsedArgs};
20//!
21//! // Create a registry
22//! let mut registry = CommandRegistry::new();
23//!
24//! // Define a command
25//! let definition = CommandDefinition {
26//!     name: "hello".to_string(),
27//!     aliases: vec!["hi".to_string(), "greet".to_string()],
28//!     description: "Say hello".to_string(),
29//!     required: false,
30//!     arguments: vec![],
31//!     options: vec![],
32//!     implementation: "hello_handler".to_string(),
33//!     continue_on_failure: false,
34//!     requires_success: false,
35//! };
36//!
37//! // Create a handler
38//! struct HelloCommand;
39//! impl CommandHandler for HelloCommand {
40//!     fn execute(
41//!         &self,
42//!         _ctx: &mut dyn dynamic_cli::context::ExecutionContext,
43//!         _args: &ParsedArgs,
44//!     ) -> dynamic_cli::Result<()> {
45//!         println!("Hello!");
46//!         Ok(())
47//!     }
48//! }
49//!
50//! // Register the command
51//! registry.register_sync(definition, Box::new(HelloCommand))?;
52//!
53//! // Retrieve by name
54//! assert!(registry.get_handler_sync("hello").is_some());
55//!
56//! // Retrieve by alias
57//! assert_eq!(registry.resolve_name("hi"), Some("hello"));
58//! # Ok::<(), dynamic_cli::error::DynamicCliError>(())
59//! ```
60
61use crate::config::schema::CommandDefinition;
62use crate::error::{RegistryError, Result};
63use crate::executor::{AsyncCommandHandler, CommandHandler};
64use std::collections::HashMap;
65
66/// Internal storage for a single registered command's handler.
67///
68/// Private — never leaks into the public API. `get_handler_sync()` /
69/// `get_handler_async()` return `None` when queried against the wrong
70/// variant, so callers never need to know this enum exists. Sync and
71/// async handlers share one map rather than two parallel `HashMap`s, so a
72/// single lookup detects a name already taken by either kind.
73enum StoredHandler {
74    Sync(Box<dyn CommandHandler>),
75    Async(Box<dyn AsyncCommandHandler>),
76}
77/// Central registry for commands and their handlers
78///
79/// The registry stores all registered commands along with their definitions
80/// and handlers. It provides efficient lookup by both command name and alias.
81///
82/// # Thread Safety
83///
84/// The registry is designed to be constructed once during application startup
85/// and then shared immutably across the application. For multi-threaded access,
86/// wrap it in `Arc<CommandRegistry>`.
87///
88/// # Example
89///
90/// ```
91/// use dynamic_cli::registry::CommandRegistry;
92/// use dynamic_cli::config::schema::CommandDefinition;
93/// use dynamic_cli::executor::CommandHandler;
94/// use std::collections::HashMap;
95///
96/// let mut registry = CommandRegistry::new();
97///
98/// // Register commands during initialization
99/// # let definition = CommandDefinition {
100/// #     name: "test".to_string(),
101/// #     aliases: vec![],
102/// #     description: "Test".to_string(),
103/// #     required: false,
104/// #     arguments: vec![],
105/// #     options: vec![],
106/// #     implementation: "test_handler".to_string(),
107/// #     continue_on_failure: false,
108/// #     requires_success: false,
109/// # };
110/// # struct TestCommand;
111/// # impl CommandHandler for TestCommand {
112/// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
113/// # }
114/// registry.register_sync(definition, Box::new(TestCommand))?;
115///
116/// // Use throughout the application
117/// if let Some(handler) = registry.get_handler_sync("test") {
118///     // Execute the command
119/// }
120/// # Ok::<(), dynamic_cli::error::DynamicCliError>(())
121/// ```
122pub struct CommandRegistry {
123    /// Map of command names to their data
124    /// Key: canonical command name
125    /// Value: `(CommandDefinition, StoredHandler)`
126    commands: HashMap<String, (CommandDefinition, StoredHandler)>,
127
128    /// Map of aliases to canonical command names
129    /// Key: alias
130    /// Value: canonical command name
131    ///
132    /// This allows O(1) resolution of aliases to command names.
133    aliases: HashMap<String, String>,
134}
135
136impl CommandRegistry {
137    /// Create a new empty registry
138    ///
139    /// # Example
140    ///
141    /// ```
142    /// use dynamic_cli::registry::CommandRegistry;
143    ///
144    /// let registry = CommandRegistry::new();
145    /// assert_eq!(registry.list_commands().len(), 0);
146    /// ```
147    pub fn new() -> Self {
148        Self {
149            commands: HashMap::new(),
150            aliases: HashMap::new(),
151        }
152    }
153
154    /// Checks that `name` is free to use as a command name or alias.
155    ///
156    /// Shared by [`register_sync`][Self::register_sync] and
157    /// [`register_async`][Self::register_async] — a name can never belong
158    /// to both a sync and an async handler, nor be duplicated as a command
159    /// or an alias. Checked against the single unified `commands` map, so
160    /// this one call covers both storage kinds.
161    ///
162    /// # Errors
163    ///
164    /// - [`RegistryError::DuplicateRegistration`] if `name` is already a
165    ///   registered command (sync or async).
166    /// - [`RegistryError::DuplicateAlias`] if `name` is already registered
167    ///   as an alias of another command.
168    fn check_name_available(&self, name: &str) -> Result<()> {
169        if self.commands.contains_key(name) {
170            return Err(RegistryError::DuplicateRegistration {
171                name: name.to_string(),
172                suggestion: None,
173            }
174            .into());
175        }
176
177        if let Some(existing_cmd) = self.aliases.get(name) {
178            return Err(RegistryError::DuplicateAlias {
179                alias: name.to_string(),
180                existing_command: existing_cmd.clone(),
181                suggestion: None,
182            }
183            .into());
184        }
185
186        Ok(())
187    }
188
189    /// Registers every alias declared in `definition` as pointing to
190    /// `definition.name`. Called by both `register_sync` and
191    /// `register_async` after `check_name_available` has confirmed there is
192    /// no conflict.
193    fn insert_aliases(&mut self, definition: CommandDefinition) {
194        for alias in &definition.aliases {
195            self.aliases.insert(alias.clone(), definition.name.clone());
196        }
197    }
198
199    /// Checks a handler's declared [`expected_fault_tolerance()`][ceft]
200    /// against `definition.continue_on_failure`.
201    ///
202    /// Called by both `register_sync` and `register_async` after
203    /// `check_name_available` has confirmed there is no conflict, and
204    /// before the definition/handler pair is actually stored. Does nothing
205    /// when the handler expresses no opinion (`None`, the default) —
206    /// handlers that do not override `expected_fault_tolerance()` are
207    /// entirely unaffected.
208    ///
209    /// [ceft]: crate::executor::CommandHandler::expected_fault_tolerance
210    ///
211    /// # Errors
212    ///
213    /// [`RegistryError::FaultToleranceMismatch`] if the handler declared
214    /// `Some(expected)` and `expected != definition.continue_on_failure`.
215    fn check_fault_tolerance(definition: &CommandDefinition, expected: Option<bool>) -> Result<()> {
216        if let Some(expected) = expected {
217            if expected != definition.continue_on_failure {
218                return Err(RegistryError::fault_tolerance_mismatch(
219                    &definition.name,
220                    expected,
221                    definition.continue_on_failure,
222                )
223                .into());
224            }
225        }
226        Ok(())
227    }
228
229    /// Register a command with its (sync) handler
230    ///
231    /// This method registers a command definition along with its handler.
232    /// It also registers all aliases for the command.
233    ///
234    /// # Arguments
235    ///
236    /// * `definition` - The command definition from the configuration
237    /// * `handler` - The handler implementation for this command
238    ///
239    /// # Returns
240    ///
241    /// - `Ok(())` if registration succeeds
242    /// - `Err(RegistryError)` if:
243    ///   - A command with the same name is already registered (sync or async)
244    ///   - An alias conflicts with an existing command or alias
245    ///   - The handler's declared `expected_fault_tolerance()`
246    ///     contradicts `definition.continue_on_failure`
247    ///
248    /// # Errors
249    ///
250    /// - [`RegistryError::DuplicateRegistration`] if the command name already exists
251    /// - [`RegistryError::DuplicateAlias`] if an alias is already in use
252    /// - [`RegistryError::FaultToleranceMismatch`] if the handler's
253    ///   [`expected_fault_tolerance()`][crate::executor::CommandHandler::expected_fault_tolerance]
254    ///   disagrees with `definition.continue_on_failure`
255    ///
256    /// # Example
257    ///
258    /// ```
259    /// use dynamic_cli::registry::CommandRegistry;
260    /// use dynamic_cli::config::schema::CommandDefinition;
261    /// use dynamic_cli::executor::{CommandHandler, ParsedArgs};
262    ///
263    /// let mut registry = CommandRegistry::new();
264    ///
265    /// let definition = CommandDefinition {
266    ///     name: "simulate".to_string(),
267    ///     aliases: vec!["sim".to_string(), "run".to_string()],
268    ///     description: "Run simulation".to_string(),
269    ///     required: false,
270    ///     arguments: vec![],
271    ///     options: vec![],
272    ///     implementation: "sim_handler".to_string(),
273    ///     continue_on_failure: false,
274    ///     requires_success: false,
275    /// };
276    ///
277    /// struct SimCommand;
278    /// impl CommandHandler for SimCommand {
279    ///     fn execute(
280    ///         &self,
281    ///         _: &mut dyn dynamic_cli::context::ExecutionContext,
282    ///         _: &ParsedArgs,
283    ///     ) -> dynamic_cli::Result<()> {
284    ///         Ok(())
285    ///     }
286    /// }
287    ///
288    /// // Register the command
289    /// registry.register_sync(definition, Box::new(SimCommand))?;
290    ///
291    /// // Can now access by name or alias
292    /// assert!(registry.get_handler_sync("simulate").is_some());
293    /// assert_eq!(registry.resolve_name("sim"), Some("simulate"));
294    /// # Ok::<(), dynamic_cli::error::DynamicCliError>(())
295    /// ```
296    pub fn register_sync(
297        &mut self,
298        definition: CommandDefinition,
299        handler: Box<dyn CommandHandler>,
300    ) -> Result<()> {
301        self.check_name_available(&definition.name)?;
302        for alias in &definition.aliases {
303            self.check_name_available(alias)?;
304        }
305        Self::check_fault_tolerance(&definition, handler.expected_fault_tolerance())?;
306        self.insert_aliases(definition.clone());
307        self.commands.insert(
308            definition.name.clone(),
309            (definition, StoredHandler::Sync(handler)),
310        );
311        Ok(())
312    }
313
314    /// Register a command with its async handler
315    ///
316    /// Additive counterpart of [`register_sync`][Self::register_sync] —
317    /// same conflict-detection rules (checked against both sync and async
318    /// registrations sharing the unified internal storage, plus aliases),
319    /// same alias handling.
320    ///
321    /// # Errors
322    ///
323    /// - [`RegistryError::DuplicateRegistration`] if the command name already exists
324    /// - [`RegistryError::DuplicateAlias`] if an alias is already in use
325    /// - [`RegistryError::FaultToleranceMismatch`] if the handler's
326    ///   [`expected_fault_tolerance()`][crate::executor::AsyncCommandHandler::expected_fault_tolerance]
327    ///   disagrees with `definition.continue_on_failure`
328    ///
329    /// # Example
330    ///
331    /// ```
332    /// use dynamic_cli::registry::CommandRegistry;
333    /// use dynamic_cli::config::schema::CommandDefinition;
334    /// use dynamic_cli::executor::{AsyncCommandHandler, ParsedArgs};
335    /// use async_trait::async_trait;
336    ///
337    /// let mut registry = CommandRegistry::new();
338    ///
339    /// let definition = CommandDefinition {
340    ///     name: "fetch".to_string(),
341    ///     aliases: vec![],
342    ///     description: "Fetch remote data".to_string(),
343    ///     required: false,
344    ///     arguments: vec![],
345    ///     options: vec![],
346    ///     implementation: "fetch_handler".to_string(),
347    ///     continue_on_failure: false,
348    ///     requires_success: false,
349    /// };
350    ///
351    /// struct FetchCommand;
352    /// #[async_trait]
353    /// impl AsyncCommandHandler for FetchCommand {
354    ///     async fn execute(
355    ///         &self,
356    ///         _: &mut dyn dynamic_cli::context::ExecutionContext,
357    ///         _: &ParsedArgs,
358    ///     ) -> dynamic_cli::Result<()> {
359    ///         Ok(())
360    ///     }
361    /// }
362    ///
363    /// registry.register_async(definition, Box::new(FetchCommand))?;
364    /// assert!(registry.get_handler_async("fetch").is_some());
365    /// # Ok::<(), dynamic_cli::error::DynamicCliError>(())
366    /// ```
367    pub fn register_async(
368        &mut self,
369        definition: CommandDefinition,
370        handler: Box<dyn AsyncCommandHandler>,
371    ) -> Result<()> {
372        self.check_name_available(&definition.name)?;
373        for alias in &definition.aliases {
374            self.check_name_available(alias)?;
375        }
376        Self::check_fault_tolerance(&definition, handler.expected_fault_tolerance())?;
377        self.insert_aliases(definition.clone());
378        self.commands.insert(
379            definition.name.clone(),
380            (definition, StoredHandler::Async(handler)),
381        );
382        Ok(())
383    }
384
385    /// Resolve a name (command or alias) to the canonical command name
386    ///
387    /// This method checks if the given name is either:
388    /// - A registered command name (returns the name itself)
389    /// - An alias (returns the canonical command name)
390    ///
391    /// # Arguments
392    ///
393    /// * `name` - The name or alias to resolve
394    ///
395    /// # Returns
396    ///
397    /// - `Some(&str)` - The canonical command name
398    /// - `None` - If the name is not registered
399    ///
400    /// # Example
401    ///
402    /// ```
403    /// use dynamic_cli::registry::CommandRegistry;
404    /// # use dynamic_cli::config::schema::CommandDefinition;
405    /// # use dynamic_cli::executor::CommandHandler;
406    /// # use std::collections::HashMap;
407    ///
408    /// let mut registry = CommandRegistry::new();
409    ///
410    /// # let definition = CommandDefinition {
411    /// #     name: "hello".to_string(),
412    /// #     aliases: vec!["hi".to_string()],
413    /// #     description: "".to_string(),
414    /// #     required: false,
415    /// #     arguments: vec![],
416    /// #     options: vec![],
417    /// #     implementation: "".to_string(),
418    /// #     continue_on_failure: false,
419    /// #     requires_success: false,
420    /// # };
421    /// # struct TestCmd;
422    /// # impl CommandHandler for TestCmd {
423    /// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
424    /// # }
425    /// # registry.register_sync(definition, Box::new(TestCmd)).unwrap();
426    /// // Resolve command name
427    /// assert_eq!(registry.resolve_name("hello"), Some("hello"));
428    ///
429    /// // Resolve alias
430    /// assert_eq!(registry.resolve_name("hi"), Some("hello"));
431    ///
432    /// // Unknown name
433    /// assert_eq!(registry.resolve_name("unknown"), None);
434    /// ```
435    pub fn resolve_name(&self, name: &str) -> Option<&str> {
436        // First check if it's a command name
437        // Return reference to the stored name, not the parameter
438        if let Some((cmd_def, _)) = self.commands.get(name) {
439            return Some(cmd_def.name.as_str());
440        }
441
442        // Then check if it's an alias
443        self.aliases.get(name).map(|s| s.as_str())
444    }
445
446    /// Get the definition of a command by name or alias
447    ///
448    /// # Arguments
449    ///
450    /// * `name` - The command name or alias
451    ///
452    /// # Returns
453    ///
454    /// - `Some(&CommandDefinition)` if the command exists
455    /// - `None` if the command is not registered
456    ///
457    /// # Example
458    ///
459    /// ```
460    /// # use dynamic_cli::registry::CommandRegistry;
461    /// # use dynamic_cli::config::schema::CommandDefinition;
462    /// # use dynamic_cli::executor::CommandHandler;
463    /// # use std::collections::HashMap;
464    /// # let mut registry = CommandRegistry::new();
465    /// # let definition = CommandDefinition {
466    /// #     name: "test".to_string(),
467    /// #     aliases: vec!["t".to_string()],
468    /// #     description: "Test command".to_string(),
469    /// #     required: false,
470    /// #     arguments: vec![],
471    /// #     options: vec![],
472    /// #     implementation: "".to_string(),
473    /// #     continue_on_failure: false,
474    /// #     requires_success: false,
475    /// # };
476    /// # struct TestCmd;
477    /// # impl CommandHandler for TestCmd {
478    /// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
479    /// # }
480    /// # registry.register_sync(definition, Box::new(TestCmd)).unwrap();
481    /// // Get by name
482    /// if let Some(def) = registry.get_definition("test") {
483    ///     assert_eq!(def.name, "test");
484    ///     assert_eq!(def.description, "Test command");
485    /// }
486    ///
487    /// // Get by alias
488    /// if let Some(def) = registry.get_definition("t") {
489    ///     assert_eq!(def.name, "test");
490    /// }
491    /// ```
492    pub fn get_definition(&self, name: &str) -> Option<&CommandDefinition> {
493        let canonical_name = self.resolve_name(name)?;
494        self.commands.get(canonical_name).map(|(def, _)| def)
495    }
496
497    /// Get the (sync) handler of a command by name or alias
498    ///
499    /// This is the primary method used during CLI/REPL dispatch to
500    /// retrieve the handler that will execute the command. Returns `None`
501    /// both when the name isn't registered at all, and when it resolves to
502    /// an *async* handler (query [`get_handler_async`][Self::get_handler_async]
503    /// instead in that case) — dispatch sites try both in sequence.
504    ///
505    /// # Arguments
506    ///
507    /// * `name` - The command name or alias
508    ///
509    /// # Returns
510    ///
511    /// - `Some(&dyn CommandHandler)` if a sync handler is registered under this name
512    /// - `None` if unregistered, or if registered as an async handler
513    ///
514    /// # Example
515    ///
516    /// ```
517    /// # use dynamic_cli::registry::CommandRegistry;
518    /// # use dynamic_cli::config::schema::CommandDefinition;
519    /// # use dynamic_cli::executor::CommandHandler;
520    /// # use std::collections::HashMap;
521    /// # let mut registry = CommandRegistry::new();
522    /// # let definition = CommandDefinition {
523    /// #     name: "exec".to_string(),
524    /// #     aliases: vec!["x".to_string()],
525    /// #     description: "".to_string(),
526    /// #     required: false,
527    /// #     arguments: vec![],
528    /// #     options: vec![],
529    /// #     implementation: "".to_string(),
530    /// #     continue_on_failure: false,
531    /// #     requires_success: false,
532    /// # };
533    /// # struct ExecCmd;
534    /// # impl CommandHandler for ExecCmd {
535    /// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
536    /// # }
537    /// # registry.register_sync(definition, Box::new(ExecCmd)).unwrap();
538    /// // Get handler by name
539    /// if let Some(handler) = registry.get_handler_sync("exec") {
540    ///     // Use handler for execution
541    /// }
542    ///
543    /// // Get handler by alias
544    /// if let Some(handler) = registry.get_handler_sync("x") {
545    ///     // Same handler
546    /// }
547    /// ```
548    // The return type &dyn CommandHandler is intentional: callers receive a
549    // reference to the handler, which preserves the indirection needed for
550    // dynamic dispatch without transferring ownership.
551    pub fn get_handler_sync(&self, name: &str) -> Option<&dyn CommandHandler> {
552        let canonical = self.resolve_name(name)?;
553        match &self.commands.get(canonical)?.1 {
554            StoredHandler::Sync(h) => Some(h.as_ref()),
555            StoredHandler::Async(_) => None,
556        }
557    }
558
559    /// Get the async handler of a command by name or alias
560    ///
561    /// Additive counterpart of [`get_handler_sync`][Self::get_handler_sync].
562    /// Returns `None` both when the name isn't registered at all, and when
563    /// it resolves to a *sync* handler.
564    ///
565    /// # Example
566    ///
567    /// ```
568    /// # use dynamic_cli::registry::CommandRegistry;
569    /// # use dynamic_cli::config::schema::CommandDefinition;
570    /// # use dynamic_cli::executor::AsyncCommandHandler;
571    /// # use std::collections::HashMap;
572    /// # use async_trait::async_trait;
573    /// # let mut registry = CommandRegistry::new();
574    /// # let definition = CommandDefinition {
575    /// #     name: "fetch".to_string(),
576    /// #     aliases: vec![],
577    /// #     description: "".to_string(),
578    /// #     required: false,
579    /// #     arguments: vec![],
580    /// #     options: vec![],
581    /// #     implementation: "".to_string(),
582    /// #     continue_on_failure: false,
583    /// #     requires_success: false,
584    /// # };
585    /// # struct FetchCmd;
586    /// # #[async_trait]
587    /// # impl AsyncCommandHandler for FetchCmd {
588    /// #     async fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
589    /// # }
590    /// # registry.register_async(definition, Box::new(FetchCmd)).unwrap();
591    /// assert!(registry.get_handler_async("fetch").is_some());
592    /// assert!(registry.get_handler_sync("fetch").is_none()); // wrong accessor
593    /// ```
594    pub fn get_handler_async(&self, name: &str) -> Option<&dyn AsyncCommandHandler> {
595        let canonical = self.resolve_name(name)?;
596        match &self.commands.get(canonical)?.1 {
597            StoredHandler::Async(h) => Some(h.as_ref()),
598            StoredHandler::Sync(_) => None,
599        }
600    }
601
602    /// List all registered command definitions
603    ///
604    /// Returns a vector of references to all command definitions in the registry.
605    /// The order is not guaranteed.
606    ///
607    /// # Returns
608    ///
609    /// Vector of command definition references
610    ///
611    /// # Example
612    ///
613    /// ```
614    /// # use dynamic_cli::registry::CommandRegistry;
615    /// # use dynamic_cli::config::schema::CommandDefinition;
616    /// # use dynamic_cli::executor::CommandHandler;
617    /// # use std::collections::HashMap;
618    /// # let mut registry = CommandRegistry::new();
619    /// # let def1 = CommandDefinition {
620    /// #     name: "cmd1".to_string(),
621    /// #     aliases: vec![],
622    /// #     description: "".to_string(),
623    /// #     required: false,
624    /// #     arguments: vec![],
625    /// #     options: vec![],
626    /// #     implementation: "".to_string(),
627    /// #     continue_on_failure: false,
628    /// #     requires_success: false,
629    /// # };
630    /// # let def2 = CommandDefinition {
631    /// #     name: "cmd2".to_string(),
632    /// #     aliases: vec![],
633    /// #     description: "".to_string(),
634    /// #     required: false,
635    /// #     arguments: vec![],
636    /// #     options: vec![],
637    /// #     implementation: "".to_string(),
638    /// #     continue_on_failure: false,
639    /// #     requires_success: false,
640    /// # };
641    /// # struct TestCmd;
642    /// # impl CommandHandler for TestCmd {
643    /// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
644    /// # }
645    /// # registry.register_sync(def1, Box::new(TestCmd)).unwrap();
646    /// # registry.register_sync(def2, Box::new(TestCmd)).unwrap();
647    /// let commands = registry.list_commands();
648    /// assert_eq!(commands.len(), 2);
649    ///
650    /// // Use for help text, command completion, etc.
651    /// for cmd in commands {
652    ///     println!("{}: {}", cmd.name, cmd.description);
653    /// }
654    /// ```
655    pub fn list_commands(&self) -> Vec<&CommandDefinition> {
656        self.commands.values().map(|(def, _)| def).collect()
657    }
658
659    /// Get the number of registered commands
660    ///
661    /// # Example
662    ///
663    /// ```
664    /// use dynamic_cli::registry::CommandRegistry;
665    ///
666    /// let registry = CommandRegistry::new();
667    /// assert_eq!(registry.len(), 0);
668    /// ```
669    pub fn len(&self) -> usize {
670        self.commands.len()
671    }
672
673    /// Check if the registry is empty
674    ///
675    /// # Example
676    ///
677    /// ```
678    /// use dynamic_cli::registry::CommandRegistry;
679    ///
680    /// let registry = CommandRegistry::new();
681    /// assert!(registry.is_empty());
682    /// ```
683    pub fn is_empty(&self) -> bool {
684        self.commands.is_empty()
685    }
686
687    /// Check if a command is registered (by name or alias)
688    ///
689    /// # Example
690    ///
691    /// ```
692    /// # use dynamic_cli::registry::CommandRegistry;
693    /// # use dynamic_cli::config::schema::CommandDefinition;
694    /// # use dynamic_cli::executor::CommandHandler;
695    /// # use std::collections::HashMap;
696    /// # let mut registry = CommandRegistry::new();
697    /// # let definition = CommandDefinition {
698    /// #     name: "test".to_string(),
699    /// #     aliases: vec!["t".to_string()],
700    /// #     description: "".to_string(),
701    /// #     required: false,
702    /// #     arguments: vec![],
703    /// #     options: vec![],
704    /// #     implementation: "".to_string(),
705    /// #     continue_on_failure: false,
706    /// #     requires_success: false,
707    /// # };
708    /// # struct TestCmd;
709    /// # impl CommandHandler for TestCmd {
710    /// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
711    /// # }
712    /// # registry.register_sync(definition, Box::new(TestCmd)).unwrap();
713    /// assert!(registry.contains("test"));
714    /// assert!(registry.contains("t"));
715    /// assert!(!registry.contains("unknown"));
716    /// ```
717    pub fn contains(&self, name: &str) -> bool {
718        self.resolve_name(name).is_some()
719    }
720}
721
722// Implement Default for convenience
723impl Default for CommandRegistry {
724    fn default() -> Self {
725        Self::new()
726    }
727}
728
729#[cfg(test)]
730mod tests {
731    use super::*;
732    use crate::parser::ParsedArgs;
733
734    // Test fixtures
735
736    struct TestHandler;
737
738    impl CommandHandler for TestHandler {
739        fn execute(
740            &self,
741            _context: &mut dyn crate::context::ExecutionContext,
742            _args: &ParsedArgs,
743        ) -> crate::error::Result<()> {
744            Ok(())
745        }
746    }
747
748    struct TestAsyncHandler;
749
750    #[async_trait::async_trait]
751    impl AsyncCommandHandler for TestAsyncHandler {
752        async fn execute(
753            &self,
754            _context: &mut dyn crate::context::ExecutionContext,
755            _args: &ParsedArgs,
756        ) -> crate::error::Result<()> {
757            Ok(())
758        }
759    }
760
761    fn create_test_definition(name: &str, aliases: Vec<&str>) -> CommandDefinition {
762        CommandDefinition {
763            name: name.to_string(),
764            aliases: aliases.iter().map(|s| s.to_string()).collect(),
765            description: format!("{} command", name),
766            required: false,
767            arguments: vec![],
768            options: vec![],
769            implementation: format!("{}_handler", name),
770            continue_on_failure: false,
771            requires_success: false,
772        }
773    }
774
775    // Basic functionality tests
776    #[test]
777    fn test_new_registry_is_empty() {
778        let registry = CommandRegistry::new();
779        assert!(registry.is_empty());
780        assert_eq!(registry.len(), 0);
781        assert_eq!(registry.list_commands().len(), 0);
782    }
783
784    #[test]
785    fn test_register_command() {
786        let mut registry = CommandRegistry::new();
787        let definition = create_test_definition("test", vec![]);
788
789        let result = registry.register_sync(definition, Box::new(TestHandler));
790
791        assert!(result.is_ok());
792        assert_eq!(registry.len(), 1);
793        assert!(!registry.is_empty());
794    }
795
796    #[test]
797    fn test_register_command_with_aliases() {
798        let mut registry = CommandRegistry::new();
799        let definition = create_test_definition("hello", vec!["hi", "greet"]);
800
801        registry
802            .register_sync(definition, Box::new(TestHandler))
803            .unwrap();
804
805        assert_eq!(registry.len(), 1);
806        assert!(registry.contains("hello"));
807        assert!(registry.contains("hi"));
808        assert!(registry.contains("greet"));
809    }
810
811    #[test]
812    fn test_register_duplicate_command_fails() {
813        let mut registry = CommandRegistry::new();
814        let def1 = create_test_definition("test", vec![]);
815        let def2 = create_test_definition("test", vec![]);
816
817        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
818        let result = registry.register_sync(def2, Box::new(TestHandler));
819
820        assert!(result.is_err());
821        match result.unwrap_err() {
822            crate::error::DynamicCliError::Registry(RegistryError::DuplicateRegistration {
823                name,
824                ..
825            }) => {
826                assert_eq!(name, "test");
827            }
828            _ => panic!("Wrong error type"),
829        }
830    }
831
832    #[test]
833    fn test_register_duplicate_alias_fails() {
834        let mut registry = CommandRegistry::new();
835        let def1 = create_test_definition("cmd1", vec!["c"]);
836        let def2 = create_test_definition("cmd2", vec!["c"]);
837
838        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
839        let result = registry.register_sync(def2, Box::new(TestHandler));
840
841        assert!(result.is_err());
842        match result.unwrap_err() {
843            crate::error::DynamicCliError::Registry(RegistryError::DuplicateAlias {
844                alias,
845                existing_command,
846                ..
847            }) => {
848                assert_eq!(alias, "c");
849                assert_eq!(existing_command, "cmd1");
850            }
851            _ => panic!("Wrong error type"),
852        }
853    }
854
855    #[test]
856    fn test_alias_conflicts_with_command_name() {
857        let mut registry = CommandRegistry::new();
858        let def1 = create_test_definition("test", vec![]);
859        let def2 = create_test_definition("other", vec!["test"]);
860
861        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
862        let result = registry.register_sync(def2, Box::new(TestHandler));
863
864        assert!(result.is_err());
865    }
866
867    #[test]
868    fn test_command_name_conflicts_with_alias() {
869        let mut registry = CommandRegistry::new();
870        let def1 = create_test_definition("cmd1", vec!["other"]);
871        let def2 = create_test_definition("other", vec![]);
872
873        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
874        let result = registry.register_sync(def2, Box::new(TestHandler));
875
876        assert!(result.is_err());
877    }
878
879    // Resolve name tests
880    #[test]
881    fn test_resolve_command_name() {
882        let mut registry = CommandRegistry::new();
883        let definition = create_test_definition("test", vec![]);
884
885        registry
886            .register_sync(definition, Box::new(TestHandler))
887            .unwrap();
888
889        assert_eq!(registry.resolve_name("test"), Some("test"));
890    }
891
892    #[test]
893    fn test_resolve_alias() {
894        let mut registry = CommandRegistry::new();
895        let definition = create_test_definition("hello", vec!["hi", "greet"]);
896
897        registry
898            .register_sync(definition, Box::new(TestHandler))
899            .unwrap();
900
901        assert_eq!(registry.resolve_name("hi"), Some("hello"));
902        assert_eq!(registry.resolve_name("greet"), Some("hello"));
903    }
904
905    #[test]
906    fn test_resolve_unknown_name() {
907        let registry = CommandRegistry::new();
908        assert_eq!(registry.resolve_name("unknown"), None);
909    }
910
911    // Get definition tests
912    #[test]
913    fn test_get_definition_by_name() {
914        let mut registry = CommandRegistry::new();
915        let definition = create_test_definition("test", vec![]);
916
917        registry
918            .register_sync(definition, Box::new(TestHandler))
919            .unwrap();
920
921        let retrieved = registry.get_definition("test");
922        assert!(retrieved.is_some());
923        assert_eq!(retrieved.unwrap().name, "test");
924    }
925
926    #[test]
927    fn test_get_definition_by_alias() {
928        let mut registry = CommandRegistry::new();
929        let definition = create_test_definition("hello", vec!["hi"]);
930
931        registry
932            .register_sync(definition, Box::new(TestHandler))
933            .unwrap();
934
935        let retrieved = registry.get_definition("hi");
936        assert!(retrieved.is_some());
937        assert_eq!(retrieved.unwrap().name, "hello");
938    }
939
940    #[test]
941    fn test_get_definition_unknown() {
942        let registry = CommandRegistry::new();
943        assert!(registry.get_definition("unknown").is_none());
944    }
945
946    // Get handler tests
947    #[test]
948    fn test_get_handler_by_name() {
949        let mut registry = CommandRegistry::new();
950        let definition = create_test_definition("test", vec![]);
951
952        registry
953            .register_sync(definition, Box::new(TestHandler))
954            .unwrap();
955
956        let handler = registry.get_handler_sync("test");
957        assert!(handler.is_some());
958    }
959
960    #[test]
961    fn test_get_handler_sync_by_name() {
962        let mut registry = CommandRegistry::new();
963        let definition = create_test_definition("test", vec![]);
964
965        registry
966            .register_sync(definition, Box::new(TestHandler))
967            .unwrap();
968
969        let handler = registry.get_handler_sync("test");
970        assert!(handler.is_some());
971    }
972
973    #[test]
974    fn test_get_handler_by_alias() {
975        let mut registry = CommandRegistry::new();
976        let definition = create_test_definition("hello", vec!["hi"]);
977
978        registry
979            .register_sync(definition, Box::new(TestHandler))
980            .unwrap();
981
982        let handler = registry.get_handler_sync("hi");
983        assert!(handler.is_some());
984    }
985
986    #[test]
987    fn test_get_handler_unknown() {
988        let registry = CommandRegistry::new();
989        assert!(registry.get_handler_sync("unknown").is_none());
990    }
991
992    // List commands tests
993    #[test]
994    fn test_list_commands_empty() {
995        let registry = CommandRegistry::new();
996        let commands = registry.list_commands();
997        assert_eq!(commands.len(), 0);
998    }
999
1000    #[test]
1001    fn test_list_commands_multiple() {
1002        let mut registry = CommandRegistry::new();
1003
1004        registry
1005            .register_sync(
1006                create_test_definition("cmd1", vec![]),
1007                Box::new(TestHandler),
1008            )
1009            .unwrap();
1010        registry
1011            .register_sync(
1012                create_test_definition("cmd2", vec![]),
1013                Box::new(TestHandler),
1014            )
1015            .unwrap();
1016        registry
1017            .register_sync(
1018                create_test_definition("cmd3", vec![]),
1019                Box::new(TestHandler),
1020            )
1021            .unwrap();
1022
1023        let commands = registry.list_commands();
1024        assert_eq!(commands.len(), 3);
1025
1026        let names: Vec<&str> = commands.iter().map(|c| c.name.as_str()).collect();
1027        assert!(names.contains(&"cmd1"));
1028        assert!(names.contains(&"cmd2"));
1029        assert!(names.contains(&"cmd3"));
1030    }
1031
1032    // Integration tests
1033    #[test]
1034    fn test_complete_workflow() {
1035        let mut registry = CommandRegistry::new();
1036
1037        // Register multiple commands with aliases
1038        let def1 = create_test_definition("simulate", vec!["sim", "run"]);
1039        let def2 = create_test_definition("validate", vec!["val", "check"]);
1040        let def3 = create_test_definition("help", vec!["h", "?"]);
1041
1042        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
1043        registry.register_sync(def2, Box::new(TestHandler)).unwrap();
1044        registry.register_sync(def3, Box::new(TestHandler)).unwrap();
1045
1046        // Verify registry state
1047        assert_eq!(registry.len(), 3);
1048
1049        // Verify all names resolve correctly
1050        assert_eq!(registry.resolve_name("simulate"), Some("simulate"));
1051        assert_eq!(registry.resolve_name("sim"), Some("simulate"));
1052        assert_eq!(registry.resolve_name("validate"), Some("validate"));
1053        assert_eq!(registry.resolve_name("val"), Some("validate"));
1054
1055        // Verify handlers are accessible
1056        assert!(registry.get_handler_sync("simulate").is_some());
1057        assert!(registry.get_handler_sync("sim").is_some());
1058        assert!(registry.get_handler_sync("h").is_some());
1059
1060        // Verify definitions are accessible
1061        let sim_def = registry.get_definition("sim");
1062        assert!(sim_def.is_some());
1063        assert_eq!(sim_def.unwrap().name, "simulate");
1064    }
1065
1066    #[test]
1067    fn test_default_trait() {
1068        let registry: CommandRegistry = Default::default();
1069        assert!(registry.is_empty());
1070    }
1071
1072    #[test]
1073    fn test_contains_method() {
1074        let mut registry = CommandRegistry::new();
1075        let definition = create_test_definition("test", vec!["t"]);
1076
1077        registry
1078            .register_sync(definition, Box::new(TestHandler))
1079            .unwrap();
1080
1081        assert!(registry.contains("test"));
1082        assert!(registry.contains("t"));
1083        assert!(!registry.contains("unknown"));
1084    }
1085
1086    #[test]
1087    fn test_multiple_aliases_same_command() {
1088        let mut registry = CommandRegistry::new();
1089        let definition = create_test_definition("command", vec!["c", "cmd", "com"]);
1090
1091        registry
1092            .register_sync(definition, Box::new(TestHandler))
1093            .unwrap();
1094
1095        // All aliases should resolve to the same command
1096        assert_eq!(registry.resolve_name("c"), Some("command"));
1097        assert_eq!(registry.resolve_name("cmd"), Some("command"));
1098        assert_eq!(registry.resolve_name("com"), Some("command"));
1099
1100        // All should return the same handler
1101        let handler1 = registry.get_handler_sync("c");
1102        let handler2 = registry.get_handler_sync("cmd");
1103        assert!(handler1.is_some());
1104        assert!(handler2.is_some());
1105    }
1106
1107    #[test]
1108    fn test_case_sensitivity() {
1109        let mut registry = CommandRegistry::new();
1110        let definition = create_test_definition("Test", vec![]);
1111
1112        registry
1113            .register_sync(definition, Box::new(TestHandler))
1114            .unwrap();
1115
1116        // Case matters
1117        assert!(registry.contains("Test"));
1118        assert!(!registry.contains("test"));
1119        assert!(!registry.contains("TEST"));
1120    }
1121
1122    #[test]
1123    fn test_empty_alias_list() {
1124        let mut registry = CommandRegistry::new();
1125        let definition = create_test_definition("test", vec![]);
1126
1127        let result = registry.register_sync(definition, Box::new(TestHandler));
1128
1129        assert!(result.is_ok());
1130        assert!(registry.contains("test"));
1131    }
1132
1133    // ============================================================================
1134    // AsyncCommandHandler / register_async / get_handler_async TESTS — DA-022 (#8)
1135    // ============================================================================
1136
1137    #[test]
1138    fn test_register_async_command() {
1139        let mut registry = CommandRegistry::new();
1140        let definition = create_test_definition("fetch", vec![]);
1141
1142        let result = registry.register_async(definition, Box::new(TestAsyncHandler));
1143
1144        assert!(result.is_ok());
1145        assert_eq!(registry.len(), 1);
1146    }
1147
1148    #[test]
1149    fn test_register_async_command_with_aliases() {
1150        let mut registry = CommandRegistry::new();
1151        let definition = create_test_definition("fetch", vec!["f", "get-remote"]);
1152
1153        registry
1154            .register_async(definition, Box::new(TestAsyncHandler))
1155            .unwrap();
1156
1157        assert!(registry.contains("fetch"));
1158        assert!(registry.contains("f"));
1159        assert!(registry.contains("get-remote"));
1160        assert_eq!(registry.resolve_name("f"), Some("fetch"));
1161    }
1162
1163    #[test]
1164    fn test_get_handler_async_by_name_and_alias() {
1165        let mut registry = CommandRegistry::new();
1166        let definition = create_test_definition("fetch", vec!["f"]);
1167
1168        registry
1169            .register_async(definition, Box::new(TestAsyncHandler))
1170            .unwrap();
1171
1172        assert!(registry.get_handler_async("fetch").is_some());
1173        assert!(registry.get_handler_async("f").is_some());
1174        assert!(registry.get_handler_async("unknown").is_none());
1175    }
1176
1177    /// The core cross-accessor guarantee of the unified storage: querying an
1178    /// async-registered command through the *sync* accessor returns `None`
1179    /// (not the wrong handler, not a panic) — dispatch sites rely on this
1180    /// to fall through from `get_handler_sync` to `get_handler_async`.
1181    #[test]
1182    fn test_sync_accessor_returns_none_for_async_command() {
1183        let mut registry = CommandRegistry::new();
1184        let definition = create_test_definition("fetch", vec![]);
1185
1186        registry
1187            .register_async(definition, Box::new(TestAsyncHandler))
1188            .unwrap();
1189
1190        assert!(registry.get_handler_sync("fetch").is_none());
1191        assert!(registry.get_handler_async("fetch").is_some());
1192    }
1193
1194    /// Symmetric case: querying a sync-registered command through the
1195    /// *async* accessor returns `None`.
1196    #[test]
1197    fn test_async_accessor_returns_none_for_sync_command() {
1198        let mut registry = CommandRegistry::new();
1199        let definition = create_test_definition("test", vec![]);
1200
1201        registry
1202            .register_sync(definition, Box::new(TestHandler))
1203            .unwrap();
1204
1205        assert!(registry.get_handler_async("test").is_none());
1206        assert!(registry.get_handler_sync("test").is_some());
1207    }
1208
1209    /// A command name already taken by a sync handler must be rejected for
1210    /// async registration — the unified storage means one name, one kind.
1211    #[test]
1212    fn test_register_async_conflicts_with_existing_sync_name() {
1213        let mut registry = CommandRegistry::new();
1214        let sync_def = create_test_definition("dual", vec![]);
1215        let async_def = create_test_definition("dual", vec![]);
1216
1217        registry
1218            .register_sync(sync_def, Box::new(TestHandler))
1219            .unwrap();
1220        let result = registry.register_async(async_def, Box::new(TestAsyncHandler));
1221
1222        assert!(result.is_err());
1223        match result.unwrap_err() {
1224            crate::error::DynamicCliError::Registry(RegistryError::DuplicateRegistration {
1225                name,
1226                ..
1227            }) => {
1228                assert_eq!(name, "dual");
1229            }
1230            other => panic!("Expected DuplicateRegistration, got: {:?}", other),
1231        }
1232    }
1233
1234    /// Symmetric case: a name already taken by an async handler must be
1235    /// rejected for sync registration.
1236    #[test]
1237    fn test_register_sync_conflicts_with_existing_async_name() {
1238        let mut registry = CommandRegistry::new();
1239        let async_def = create_test_definition("dual", vec![]);
1240        let sync_def = create_test_definition("dual", vec![]);
1241
1242        registry
1243            .register_async(async_def, Box::new(TestAsyncHandler))
1244            .unwrap();
1245        let result = registry.register_sync(sync_def, Box::new(TestHandler));
1246
1247        assert!(result.is_err());
1248    }
1249
1250    /// An async command's alias must not collide with an existing sync
1251    /// command's alias, and vice versa — conflict detection is shared
1252    /// across both kinds via `check_name_available`.
1253    #[test]
1254    fn test_async_alias_conflicts_with_sync_alias() {
1255        let mut registry = CommandRegistry::new();
1256        let sync_def = create_test_definition("cmd1", vec!["shared"]);
1257        let async_def = create_test_definition("cmd2", vec!["shared"]);
1258
1259        registry
1260            .register_sync(sync_def, Box::new(TestHandler))
1261            .unwrap();
1262        let result = registry.register_async(async_def, Box::new(TestAsyncHandler));
1263
1264        assert!(result.is_err());
1265    }
1266
1267    #[test]
1268    fn test_get_definition_works_for_async_command() {
1269        let mut registry = CommandRegistry::new();
1270        let definition = create_test_definition("fetch", vec!["f"]);
1271
1272        registry
1273            .register_async(definition, Box::new(TestAsyncHandler))
1274            .unwrap();
1275
1276        let retrieved = registry.get_definition("f");
1277        assert!(retrieved.is_some());
1278        assert_eq!(retrieved.unwrap().name, "fetch");
1279    }
1280
1281    #[test]
1282    fn test_list_commands_includes_both_sync_and_async() {
1283        let mut registry = CommandRegistry::new();
1284
1285        registry
1286            .register_sync(
1287                create_test_definition("sync-cmd", vec![]),
1288                Box::new(TestHandler),
1289            )
1290            .unwrap();
1291        registry
1292            .register_async(
1293                create_test_definition("async-cmd", vec![]),
1294                Box::new(TestAsyncHandler),
1295            )
1296            .unwrap();
1297
1298        let commands = registry.list_commands();
1299        assert_eq!(commands.len(), 2);
1300        let names: Vec<&str> = commands.iter().map(|c| c.name.as_str()).collect();
1301        assert!(names.contains(&"sync-cmd"));
1302        assert!(names.contains(&"async-cmd"));
1303    }
1304
1305    #[test]
1306    fn test_mixed_registry_workflow() {
1307        // End-to-end: a registry with both sync and async commands behaves
1308        // consistently across resolve_name / get_definition / len / contains.
1309        let mut registry = CommandRegistry::new();
1310
1311        registry
1312            .register_sync(
1313                create_test_definition("simulate", vec!["sim"]),
1314                Box::new(TestHandler),
1315            )
1316            .unwrap();
1317        registry
1318            .register_async(
1319                create_test_definition("fetch", vec!["f"]),
1320                Box::new(TestAsyncHandler),
1321            )
1322            .unwrap();
1323
1324        assert_eq!(registry.len(), 2);
1325        assert!(registry.contains("sim"));
1326        assert!(registry.contains("f"));
1327
1328        assert!(registry.get_handler_sync("simulate").is_some());
1329        assert!(registry.get_handler_async("fetch").is_some());
1330        assert!(registry.get_handler_sync("fetch").is_none());
1331        assert!(registry.get_handler_async("simulate").is_none());
1332    }
1333
1334    // ========================================================================
1335    // Fault-tolerance consistency check — DD-028 (#64), #72
1336    // ========================================================================
1337
1338    /// Declares its failures must never be silently continued past.
1339    struct FaultIntolerantHandler;
1340
1341    impl CommandHandler for FaultIntolerantHandler {
1342        fn execute(
1343            &self,
1344            _context: &mut dyn crate::context::ExecutionContext,
1345            _args: &ParsedArgs,
1346        ) -> crate::error::Result<()> {
1347            Ok(())
1348        }
1349
1350        fn expected_fault_tolerance(&self) -> Option<bool> {
1351            Some(false)
1352        }
1353    }
1354
1355    /// Async mirror of [`FaultIntolerantHandler`].
1356    struct AsyncFaultIntolerantHandler;
1357
1358    #[async_trait::async_trait]
1359    impl AsyncCommandHandler for AsyncFaultIntolerantHandler {
1360        async fn execute(
1361            &self,
1362            _context: &mut dyn crate::context::ExecutionContext,
1363            _args: &ParsedArgs,
1364        ) -> crate::error::Result<()> {
1365            Ok(())
1366        }
1367
1368        fn expected_fault_tolerance(&self) -> Option<bool> {
1369            Some(false)
1370        }
1371    }
1372
1373    #[test]
1374    fn test_register_sync_accepts_handler_with_no_opinion() {
1375        // TestHandler never overrides expected_fault_tolerance() — the
1376        // overwhelmingly common case, and it must keep working exactly as
1377        // before DD-028 (#64), regardless of the configured continue_on_failure.
1378        let mut registry = CommandRegistry::new();
1379        let mut definition = create_test_definition("test", vec![]);
1380        definition.continue_on_failure = true;
1381
1382        let result = registry.register_sync(definition, Box::new(TestHandler));
1383
1384        assert!(result.is_ok());
1385    }
1386
1387    #[test]
1388    fn test_register_sync_accepts_matching_expectation() {
1389        let mut registry = CommandRegistry::new();
1390        let mut definition = create_test_definition("solve", vec![]);
1391        definition.continue_on_failure = false; // matches the handler's Some(false)
1392
1393        let result = registry.register_sync(definition, Box::new(FaultIntolerantHandler));
1394
1395        assert!(result.is_ok());
1396    }
1397
1398    #[test]
1399    fn test_register_sync_rejects_contradicting_expectation() {
1400        let mut registry = CommandRegistry::new();
1401        let mut definition = create_test_definition("solve", vec![]);
1402        definition.continue_on_failure = true; // contradicts the handler's Some(false)
1403
1404        let result = registry.register_sync(definition, Box::new(FaultIntolerantHandler));
1405
1406        assert!(result.is_err());
1407        match result.unwrap_err() {
1408            crate::error::DynamicCliError::Registry(RegistryError::FaultToleranceMismatch {
1409                command,
1410                expected,
1411                configured,
1412                ..
1413            }) => {
1414                assert_eq!(command, "solve");
1415                assert!(!expected);
1416                assert!(configured);
1417            }
1418            other => panic!("wrong error variant: {other:?}"),
1419        }
1420    }
1421
1422    #[test]
1423    fn test_register_sync_rejected_handler_is_not_stored() {
1424        // A rejected registration must not leave a partial trace — the
1425        // command should be registrable again with a corrected definition.
1426        let mut registry = CommandRegistry::new();
1427        let mut bad_definition = create_test_definition("solve", vec![]);
1428        bad_definition.continue_on_failure = true;
1429
1430        assert!(registry
1431            .register_sync(bad_definition, Box::new(FaultIntolerantHandler))
1432            .is_err());
1433        assert!(!registry.contains("solve"));
1434        assert_eq!(registry.len(), 0);
1435
1436        let mut good_definition = create_test_definition("solve", vec![]);
1437        good_definition.continue_on_failure = false;
1438        assert!(registry
1439            .register_sync(good_definition, Box::new(FaultIntolerantHandler))
1440            .is_ok());
1441        assert!(registry.contains("solve"));
1442    }
1443
1444    #[test]
1445    fn test_register_async_accepts_handler_with_no_opinion() {
1446        let mut registry = CommandRegistry::new();
1447        let mut definition = create_test_definition("fetch", vec![]);
1448        definition.continue_on_failure = true;
1449
1450        let result = registry.register_async(definition, Box::new(TestAsyncHandler));
1451
1452        assert!(result.is_ok());
1453    }
1454
1455    #[test]
1456    fn test_register_async_rejects_contradicting_expectation() {
1457        let mut registry = CommandRegistry::new();
1458        let mut definition = create_test_definition("solve", vec![]);
1459        definition.continue_on_failure = true; // contradicts the handler's Some(false)
1460
1461        let result = registry.register_async(definition, Box::new(AsyncFaultIntolerantHandler));
1462
1463        assert!(result.is_err());
1464        match result.unwrap_err() {
1465            crate::error::DynamicCliError::Registry(RegistryError::FaultToleranceMismatch {
1466                command,
1467                ..
1468            }) => {
1469                assert_eq!(command, "solve");
1470            }
1471            other => panic!("wrong error variant: {other:?}"),
1472        }
1473    }
1474}