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. See DD-022 for
71/// the rationale behind unifying sync and async storage in one map instead
72/// of two parallel `HashMap`s.
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, Box<dyn CommandHandler>)
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` (DD-028).
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    /// existing handlers that never heard of DD-028 are entirely
207    /// 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    /// Renamed from `register()` in v0.5.0 for symmetry with
235    /// [`register_async`][Self::register_async]. `register()` remains
236    /// available as a deprecated alias until v1.0.0 (DD-022).
237    ///
238    /// # Arguments
239    ///
240    /// * `definition` - The command definition from the configuration
241    /// * `handler` - The handler implementation for this command
242    ///
243    /// # Returns
244    ///
245    /// - `Ok(())` if registration succeeds
246    /// - `Err(RegistryError)` if:
247    ///   - A command with the same name is already registered (sync or async)
248    ///   - An alias conflicts with an existing command or alias
249    ///   - The handler's declared `expected_fault_tolerance()` (DD-028)
250    ///     contradicts `definition.continue_on_failure`
251    ///
252    /// # Errors
253    ///
254    /// - [`RegistryError::DuplicateRegistration`] if the command name already exists
255    /// - [`RegistryError::DuplicateAlias`] if an alias is already in use
256    /// - [`RegistryError::FaultToleranceMismatch`] if the handler's
257    ///   [`expected_fault_tolerance()`][crate::executor::CommandHandler::expected_fault_tolerance]
258    ///   disagrees with `definition.continue_on_failure` (DD-028)
259    ///
260    /// # Example
261    ///
262    /// ```
263    /// use dynamic_cli::registry::CommandRegistry;
264    /// use dynamic_cli::config::schema::CommandDefinition;
265    /// use dynamic_cli::executor::{CommandHandler, ParsedArgs};
266    ///
267    /// let mut registry = CommandRegistry::new();
268    ///
269    /// let definition = CommandDefinition {
270    ///     name: "simulate".to_string(),
271    ///     aliases: vec!["sim".to_string(), "run".to_string()],
272    ///     description: "Run simulation".to_string(),
273    ///     required: false,
274    ///     arguments: vec![],
275    ///     options: vec![],
276    ///     implementation: "sim_handler".to_string(),
277    ///     continue_on_failure: false,
278    ///     requires_success: false,
279    /// };
280    ///
281    /// struct SimCommand;
282    /// impl CommandHandler for SimCommand {
283    ///     fn execute(
284    ///         &self,
285    ///         _: &mut dyn dynamic_cli::context::ExecutionContext,
286    ///         _: &ParsedArgs,
287    ///     ) -> dynamic_cli::Result<()> {
288    ///         Ok(())
289    ///     }
290    /// }
291    ///
292    /// // Register the command
293    /// registry.register_sync(definition, Box::new(SimCommand))?;
294    ///
295    /// // Can now access by name or alias
296    /// assert!(registry.get_handler_sync("simulate").is_some());
297    /// assert_eq!(registry.resolve_name("sim"), Some("simulate"));
298    /// # Ok::<(), dynamic_cli::error::DynamicCliError>(())
299    /// ```
300    pub fn register_sync(
301        &mut self,
302        definition: CommandDefinition,
303        handler: Box<dyn CommandHandler>,
304    ) -> Result<()> {
305        self.check_name_available(&definition.name)?;
306        for alias in &definition.aliases {
307            self.check_name_available(alias)?;
308        }
309        Self::check_fault_tolerance(&definition, handler.expected_fault_tolerance())?;
310        self.insert_aliases(definition.clone());
311        self.commands.insert(
312            definition.name.clone(),
313            (definition, StoredHandler::Sync(handler)),
314        );
315        Ok(())
316    }
317
318    /// Deprecated alias for [`register_sync`][Self::register_sync].
319    ///
320    /// Kept for backward compatibility with pre-0.5.0 consumers (e.g.
321    /// `chrom-rs`). Scheduled for removal in v1.0.0, batched with the other
322    /// breaking changes tracked in the v1.0.0 API cleanup issue.
323    #[deprecated(
324        since = "0.5.0",
325        note = "renamed to `register_sync` for symmetry with `register_async`; \
326                will be removed in 1.0.0"
327    )]
328    pub fn register(
329        &mut self,
330        definition: CommandDefinition,
331        handler: Box<dyn CommandHandler>,
332    ) -> Result<()> {
333        self.register_sync(definition, handler)
334    }
335
336    /// Register a command with its async handler (DD-022)
337    ///
338    /// Additive counterpart of [`register_sync`][Self::register_sync] —
339    /// same conflict-detection rules (checked against both sync and async
340    /// registrations sharing the unified internal storage, plus aliases),
341    /// same alias handling.
342    ///
343    /// # Errors
344    ///
345    /// - [`RegistryError::DuplicateRegistration`] if the command name already exists
346    /// - [`RegistryError::DuplicateAlias`] if an alias is already in use
347    /// - [`RegistryError::FaultToleranceMismatch`] if the handler's
348    ///   [`expected_fault_tolerance()`][crate::executor::AsyncCommandHandler::expected_fault_tolerance]
349    ///   disagrees with `definition.continue_on_failure` (DD-028)
350    ///
351    /// # Example
352    ///
353    /// ```
354    /// use dynamic_cli::registry::CommandRegistry;
355    /// use dynamic_cli::config::schema::CommandDefinition;
356    /// use dynamic_cli::executor::{AsyncCommandHandler, ParsedArgs};
357    /// use async_trait::async_trait;
358    ///
359    /// let mut registry = CommandRegistry::new();
360    ///
361    /// let definition = CommandDefinition {
362    ///     name: "fetch".to_string(),
363    ///     aliases: vec![],
364    ///     description: "Fetch remote data".to_string(),
365    ///     required: false,
366    ///     arguments: vec![],
367    ///     options: vec![],
368    ///     implementation: "fetch_handler".to_string(),
369    ///     continue_on_failure: false,
370    ///     requires_success: false,
371    /// };
372    ///
373    /// struct FetchCommand;
374    /// #[async_trait]
375    /// impl AsyncCommandHandler for FetchCommand {
376    ///     async fn execute(
377    ///         &self,
378    ///         _: &mut dyn dynamic_cli::context::ExecutionContext,
379    ///         _: &ParsedArgs,
380    ///     ) -> dynamic_cli::Result<()> {
381    ///         Ok(())
382    ///     }
383    /// }
384    ///
385    /// registry.register_async(definition, Box::new(FetchCommand))?;
386    /// assert!(registry.get_handler_async("fetch").is_some());
387    /// # Ok::<(), dynamic_cli::error::DynamicCliError>(())
388    /// ```
389    pub fn register_async(
390        &mut self,
391        definition: CommandDefinition,
392        handler: Box<dyn AsyncCommandHandler>,
393    ) -> Result<()> {
394        self.check_name_available(&definition.name)?;
395        for alias in &definition.aliases {
396            self.check_name_available(alias)?;
397        }
398        Self::check_fault_tolerance(&definition, handler.expected_fault_tolerance())?;
399        self.insert_aliases(definition.clone());
400        self.commands.insert(
401            definition.name.clone(),
402            (definition, StoredHandler::Async(handler)),
403        );
404        Ok(())
405    }
406
407    /// Resolve a name (command or alias) to the canonical command name
408    ///
409    /// This method checks if the given name is either:
410    /// - A registered command name (returns the name itself)
411    /// - An alias (returns the canonical command name)
412    ///
413    /// # Arguments
414    ///
415    /// * `name` - The name or alias to resolve
416    ///
417    /// # Returns
418    ///
419    /// - `Some(&str)` - The canonical command name
420    /// - `None` - If the name is not registered
421    ///
422    /// # Example
423    ///
424    /// ```
425    /// use dynamic_cli::registry::CommandRegistry;
426    /// # use dynamic_cli::config::schema::CommandDefinition;
427    /// # use dynamic_cli::executor::CommandHandler;
428    /// # use std::collections::HashMap;
429    ///
430    /// let mut registry = CommandRegistry::new();
431    ///
432    /// # let definition = CommandDefinition {
433    /// #     name: "hello".to_string(),
434    /// #     aliases: vec!["hi".to_string()],
435    /// #     description: "".to_string(),
436    /// #     required: false,
437    /// #     arguments: vec![],
438    /// #     options: vec![],
439    /// #     implementation: "".to_string(),
440    /// #     continue_on_failure: false,
441    /// #     requires_success: false,
442    /// # };
443    /// # struct TestCmd;
444    /// # impl CommandHandler for TestCmd {
445    /// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
446    /// # }
447    /// # registry.register_sync(definition, Box::new(TestCmd)).unwrap();
448    /// // Resolve command name
449    /// assert_eq!(registry.resolve_name("hello"), Some("hello"));
450    ///
451    /// // Resolve alias
452    /// assert_eq!(registry.resolve_name("hi"), Some("hello"));
453    ///
454    /// // Unknown name
455    /// assert_eq!(registry.resolve_name("unknown"), None);
456    /// ```
457    pub fn resolve_name(&self, name: &str) -> Option<&str> {
458        // First check if it's a command name
459        // Return reference to the stored name, not the parameter
460        if let Some((cmd_def, _)) = self.commands.get(name) {
461            return Some(cmd_def.name.as_str());
462        }
463
464        // Then check if it's an alias
465        self.aliases.get(name).map(|s| s.as_str())
466    }
467
468    /// Get the definition of a command by name or alias
469    ///
470    /// # Arguments
471    ///
472    /// * `name` - The command name or alias
473    ///
474    /// # Returns
475    ///
476    /// - `Some(&CommandDefinition)` if the command exists
477    /// - `None` if the command is not registered
478    ///
479    /// # Example
480    ///
481    /// ```
482    /// # use dynamic_cli::registry::CommandRegistry;
483    /// # use dynamic_cli::config::schema::CommandDefinition;
484    /// # use dynamic_cli::executor::CommandHandler;
485    /// # use std::collections::HashMap;
486    /// # let mut registry = CommandRegistry::new();
487    /// # let definition = CommandDefinition {
488    /// #     name: "test".to_string(),
489    /// #     aliases: vec!["t".to_string()],
490    /// #     description: "Test command".to_string(),
491    /// #     required: false,
492    /// #     arguments: vec![],
493    /// #     options: vec![],
494    /// #     implementation: "".to_string(),
495    /// #     continue_on_failure: false,
496    /// #     requires_success: false,
497    /// # };
498    /// # struct TestCmd;
499    /// # impl CommandHandler for TestCmd {
500    /// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
501    /// # }
502    /// # registry.register_sync(definition, Box::new(TestCmd)).unwrap();
503    /// // Get by name
504    /// if let Some(def) = registry.get_definition("test") {
505    ///     assert_eq!(def.name, "test");
506    ///     assert_eq!(def.description, "Test command");
507    /// }
508    ///
509    /// // Get by alias
510    /// if let Some(def) = registry.get_definition("t") {
511    ///     assert_eq!(def.name, "test");
512    /// }
513    /// ```
514    pub fn get_definition(&self, name: &str) -> Option<&CommandDefinition> {
515        let canonical_name = self.resolve_name(name)?;
516        self.commands.get(canonical_name).map(|(def, _)| def)
517    }
518
519    /// Get the (sync) handler of a command by name or alias
520    ///
521    /// This is the primary method used during CLI/REPL dispatch to
522    /// retrieve the handler that will execute the command. Returns `None`
523    /// both when the name isn't registered at all, and when it resolves to
524    /// an *async* handler (query [`get_handler_async`][Self::get_handler_async]
525    /// instead in that case) — dispatch sites try both in sequence.
526    ///
527    /// Renamed from `get_handler()` in v0.5.0 for symmetry with
528    /// [`get_handler_async`][Self::get_handler_async]. `get_handler()`
529    /// remains available as a deprecated alias until v1.0.0 (DD-022).
530    ///
531    /// # Arguments
532    ///
533    /// * `name` - The command name or alias
534    ///
535    /// # Returns
536    ///
537    /// - `Some(&dyn CommandHandler)` if a sync handler is registered under this name
538    /// - `None` if unregistered, or if registered as an async handler
539    ///
540    /// # Example
541    ///
542    /// ```
543    /// # use dynamic_cli::registry::CommandRegistry;
544    /// # use dynamic_cli::config::schema::CommandDefinition;
545    /// # use dynamic_cli::executor::CommandHandler;
546    /// # use std::collections::HashMap;
547    /// # let mut registry = CommandRegistry::new();
548    /// # let definition = CommandDefinition {
549    /// #     name: "exec".to_string(),
550    /// #     aliases: vec!["x".to_string()],
551    /// #     description: "".to_string(),
552    /// #     required: false,
553    /// #     arguments: vec![],
554    /// #     options: vec![],
555    /// #     implementation: "".to_string(),
556    /// #     continue_on_failure: false,
557    /// #     requires_success: false,
558    /// # };
559    /// # struct ExecCmd;
560    /// # impl CommandHandler for ExecCmd {
561    /// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
562    /// # }
563    /// # registry.register_sync(definition, Box::new(ExecCmd)).unwrap();
564    /// // Get handler by name
565    /// if let Some(handler) = registry.get_handler_sync("exec") {
566    ///     // Use handler for execution
567    /// }
568    ///
569    /// // Get handler by alias
570    /// if let Some(handler) = registry.get_handler_sync("x") {
571    ///     // Same handler
572    /// }
573    /// ```
574    // The return type &dyn CommandHandler is intentional: callers receive a
575    // reference to the handler, which preserves the indirection needed for
576    // dynamic dispatch without transferring ownership.
577    pub fn get_handler_sync(&self, name: &str) -> Option<&dyn CommandHandler> {
578        let canonical = self.resolve_name(name)?;
579        match &self.commands.get(canonical)?.1 {
580            StoredHandler::Sync(h) => Some(h.as_ref()),
581            StoredHandler::Async(_) => None,
582        }
583    }
584
585    /// Deprecated alias for [`get_handler_sync`][Self::get_handler_sync].
586    /// Scheduled for removal in v1.0.0.
587    #[deprecated(
588        since = "0.5.0",
589        note = "renamed to `get_handler_sync` for symmetry with `get_handler_async`; \
590                will be removed in 1.0.0"
591    )]
592    pub fn get_handler(&self, name: &str) -> Option<&dyn CommandHandler> {
593        self.get_handler_sync(name)
594    }
595
596    /// Get the async handler of a command by name or alias (DD-022)
597    ///
598    /// Additive counterpart of [`get_handler_sync`][Self::get_handler_sync].
599    /// Returns `None` both when the name isn't registered at all, and when
600    /// it resolves to a *sync* handler.
601    ///
602    /// # Example
603    ///
604    /// ```
605    /// # use dynamic_cli::registry::CommandRegistry;
606    /// # use dynamic_cli::config::schema::CommandDefinition;
607    /// # use dynamic_cli::executor::AsyncCommandHandler;
608    /// # use std::collections::HashMap;
609    /// # use async_trait::async_trait;
610    /// # let mut registry = CommandRegistry::new();
611    /// # let definition = CommandDefinition {
612    /// #     name: "fetch".to_string(),
613    /// #     aliases: vec![],
614    /// #     description: "".to_string(),
615    /// #     required: false,
616    /// #     arguments: vec![],
617    /// #     options: vec![],
618    /// #     implementation: "".to_string(),
619    /// #     continue_on_failure: false,
620    /// #     requires_success: false,
621    /// # };
622    /// # struct FetchCmd;
623    /// # #[async_trait]
624    /// # impl AsyncCommandHandler for FetchCmd {
625    /// #     async fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
626    /// # }
627    /// # registry.register_async(definition, Box::new(FetchCmd)).unwrap();
628    /// assert!(registry.get_handler_async("fetch").is_some());
629    /// assert!(registry.get_handler_sync("fetch").is_none()); // wrong accessor
630    /// ```
631    pub fn get_handler_async(&self, name: &str) -> Option<&dyn AsyncCommandHandler> {
632        let canonical = self.resolve_name(name)?;
633        match &self.commands.get(canonical)?.1 {
634            StoredHandler::Async(h) => Some(h.as_ref()),
635            StoredHandler::Sync(_) => None,
636        }
637    }
638
639    /// List all registered command definitions
640    ///
641    /// Returns a vector of references to all command definitions in the registry.
642    /// The order is not guaranteed.
643    ///
644    /// # Returns
645    ///
646    /// Vector of command definition references
647    ///
648    /// # Example
649    ///
650    /// ```
651    /// # use dynamic_cli::registry::CommandRegistry;
652    /// # use dynamic_cli::config::schema::CommandDefinition;
653    /// # use dynamic_cli::executor::CommandHandler;
654    /// # use std::collections::HashMap;
655    /// # let mut registry = CommandRegistry::new();
656    /// # let def1 = CommandDefinition {
657    /// #     name: "cmd1".to_string(),
658    /// #     aliases: vec![],
659    /// #     description: "".to_string(),
660    /// #     required: false,
661    /// #     arguments: vec![],
662    /// #     options: vec![],
663    /// #     implementation: "".to_string(),
664    /// #     continue_on_failure: false,
665    /// #     requires_success: false,
666    /// # };
667    /// # let def2 = CommandDefinition {
668    /// #     name: "cmd2".to_string(),
669    /// #     aliases: vec![],
670    /// #     description: "".to_string(),
671    /// #     required: false,
672    /// #     arguments: vec![],
673    /// #     options: vec![],
674    /// #     implementation: "".to_string(),
675    /// #     continue_on_failure: false,
676    /// #     requires_success: false,
677    /// # };
678    /// # struct TestCmd;
679    /// # impl CommandHandler for TestCmd {
680    /// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
681    /// # }
682    /// # registry.register_sync(def1, Box::new(TestCmd)).unwrap();
683    /// # registry.register_sync(def2, Box::new(TestCmd)).unwrap();
684    /// let commands = registry.list_commands();
685    /// assert_eq!(commands.len(), 2);
686    ///
687    /// // Use for help text, command completion, etc.
688    /// for cmd in commands {
689    ///     println!("{}: {}", cmd.name, cmd.description);
690    /// }
691    /// ```
692    pub fn list_commands(&self) -> Vec<&CommandDefinition> {
693        self.commands.values().map(|(def, _)| def).collect()
694    }
695
696    /// Get the number of registered commands
697    ///
698    /// # Example
699    ///
700    /// ```
701    /// use dynamic_cli::registry::CommandRegistry;
702    ///
703    /// let registry = CommandRegistry::new();
704    /// assert_eq!(registry.len(), 0);
705    /// ```
706    pub fn len(&self) -> usize {
707        self.commands.len()
708    }
709
710    /// Check if the registry is empty
711    ///
712    /// # Example
713    ///
714    /// ```
715    /// use dynamic_cli::registry::CommandRegistry;
716    ///
717    /// let registry = CommandRegistry::new();
718    /// assert!(registry.is_empty());
719    /// ```
720    pub fn is_empty(&self) -> bool {
721        self.commands.is_empty()
722    }
723
724    /// Check if a command is registered (by name or alias)
725    ///
726    /// # Example
727    ///
728    /// ```
729    /// # use dynamic_cli::registry::CommandRegistry;
730    /// # use dynamic_cli::config::schema::CommandDefinition;
731    /// # use dynamic_cli::executor::CommandHandler;
732    /// # use std::collections::HashMap;
733    /// # let mut registry = CommandRegistry::new();
734    /// # let definition = CommandDefinition {
735    /// #     name: "test".to_string(),
736    /// #     aliases: vec!["t".to_string()],
737    /// #     description: "".to_string(),
738    /// #     required: false,
739    /// #     arguments: vec![],
740    /// #     options: vec![],
741    /// #     implementation: "".to_string(),
742    /// #     continue_on_failure: false,
743    /// #     requires_success: false,
744    /// # };
745    /// # struct TestCmd;
746    /// # impl CommandHandler for TestCmd {
747    /// #     fn execute(&self, _: &mut dyn dynamic_cli::context::ExecutionContext, _: &dynamic_cli::parser::ParsedArgs) -> dynamic_cli::Result<()> { Ok(()) }
748    /// # }
749    /// # registry.register_sync(definition, Box::new(TestCmd)).unwrap();
750    /// assert!(registry.contains("test"));
751    /// assert!(registry.contains("t"));
752    /// assert!(!registry.contains("unknown"));
753    /// ```
754    pub fn contains(&self, name: &str) -> bool {
755        self.resolve_name(name).is_some()
756    }
757}
758
759// Implement Default for convenience
760impl Default for CommandRegistry {
761    fn default() -> Self {
762        Self::new()
763    }
764}
765
766#[cfg(test)]
767mod tests {
768    use super::*;
769    use crate::parser::ParsedArgs;
770
771    // Test fixtures
772
773    struct TestHandler;
774
775    impl CommandHandler for TestHandler {
776        fn execute(
777            &self,
778            _context: &mut dyn crate::context::ExecutionContext,
779            _args: &ParsedArgs,
780        ) -> crate::error::Result<()> {
781            Ok(())
782        }
783    }
784
785    struct TestAsyncHandler;
786
787    #[async_trait::async_trait]
788    impl AsyncCommandHandler for TestAsyncHandler {
789        async fn execute(
790            &self,
791            _context: &mut dyn crate::context::ExecutionContext,
792            _args: &ParsedArgs,
793        ) -> crate::error::Result<()> {
794            Ok(())
795        }
796    }
797
798    fn create_test_definition(name: &str, aliases: Vec<&str>) -> CommandDefinition {
799        CommandDefinition {
800            name: name.to_string(),
801            aliases: aliases.iter().map(|s| s.to_string()).collect(),
802            description: format!("{} command", name),
803            required: false,
804            arguments: vec![],
805            options: vec![],
806            implementation: format!("{}_handler", name),
807            continue_on_failure: false,
808            requires_success: false,
809        }
810    }
811
812    // Basic functionality tests
813    #[test]
814    fn test_new_registry_is_empty() {
815        let registry = CommandRegistry::new();
816        assert!(registry.is_empty());
817        assert_eq!(registry.len(), 0);
818        assert_eq!(registry.list_commands().len(), 0);
819    }
820
821    #[test]
822    fn test_register_command() {
823        let mut registry = CommandRegistry::new();
824        let definition = create_test_definition("test", vec![]);
825
826        let result = registry.register_sync(definition, Box::new(TestHandler));
827
828        assert!(result.is_ok());
829        assert_eq!(registry.len(), 1);
830        assert!(!registry.is_empty());
831    }
832
833    /// Deprecated-alias coverage (DD-022 companion issue): `register()` and
834    /// `get_handler()` must keep behaving exactly like `register_sync()` /
835    /// `get_handler_sync()` until they're removed in v1.0.0. This is the
836    /// only place in the crate allowed to call them directly.
837    #[test]
838    #[allow(deprecated)]
839    fn test_deprecated_register_alias_still_works() {
840        let mut registry = CommandRegistry::new();
841        let definition = create_test_definition("legacy", vec!["old"]);
842
843        let result = registry.register(definition, Box::new(TestHandler));
844
845        assert!(result.is_ok());
846        assert!(registry.get_handler("legacy").is_some());
847        assert!(registry.get_handler("old").is_some());
848        assert_eq!(registry.resolve_name("old"), Some("legacy"));
849    }
850
851    #[test]
852    fn test_register_command_with_aliases() {
853        let mut registry = CommandRegistry::new();
854        let definition = create_test_definition("hello", vec!["hi", "greet"]);
855
856        registry
857            .register_sync(definition, Box::new(TestHandler))
858            .unwrap();
859
860        assert_eq!(registry.len(), 1);
861        assert!(registry.contains("hello"));
862        assert!(registry.contains("hi"));
863        assert!(registry.contains("greet"));
864    }
865
866    #[test]
867    fn test_register_duplicate_command_fails() {
868        let mut registry = CommandRegistry::new();
869        let def1 = create_test_definition("test", vec![]);
870        let def2 = create_test_definition("test", vec![]);
871
872        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
873        let result = registry.register_sync(def2, Box::new(TestHandler));
874
875        assert!(result.is_err());
876        match result.unwrap_err() {
877            crate::error::DynamicCliError::Registry(RegistryError::DuplicateRegistration {
878                name,
879                ..
880            }) => {
881                assert_eq!(name, "test");
882            }
883            _ => panic!("Wrong error type"),
884        }
885    }
886
887    #[test]
888    fn test_register_duplicate_alias_fails() {
889        let mut registry = CommandRegistry::new();
890        let def1 = create_test_definition("cmd1", vec!["c"]);
891        let def2 = create_test_definition("cmd2", vec!["c"]);
892
893        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
894        let result = registry.register_sync(def2, Box::new(TestHandler));
895
896        assert!(result.is_err());
897        match result.unwrap_err() {
898            crate::error::DynamicCliError::Registry(RegistryError::DuplicateAlias {
899                alias,
900                existing_command,
901                ..
902            }) => {
903                assert_eq!(alias, "c");
904                assert_eq!(existing_command, "cmd1");
905            }
906            _ => panic!("Wrong error type"),
907        }
908    }
909
910    #[test]
911    fn test_alias_conflicts_with_command_name() {
912        let mut registry = CommandRegistry::new();
913        let def1 = create_test_definition("test", vec![]);
914        let def2 = create_test_definition("other", vec!["test"]);
915
916        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
917        let result = registry.register_sync(def2, Box::new(TestHandler));
918
919        assert!(result.is_err());
920    }
921
922    #[test]
923    fn test_command_name_conflicts_with_alias() {
924        let mut registry = CommandRegistry::new();
925        let def1 = create_test_definition("cmd1", vec!["other"]);
926        let def2 = create_test_definition("other", vec![]);
927
928        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
929        let result = registry.register_sync(def2, Box::new(TestHandler));
930
931        assert!(result.is_err());
932    }
933
934    // Resolve name tests
935    #[test]
936    fn test_resolve_command_name() {
937        let mut registry = CommandRegistry::new();
938        let definition = create_test_definition("test", vec![]);
939
940        registry
941            .register_sync(definition, Box::new(TestHandler))
942            .unwrap();
943
944        assert_eq!(registry.resolve_name("test"), Some("test"));
945    }
946
947    #[test]
948    fn test_resolve_alias() {
949        let mut registry = CommandRegistry::new();
950        let definition = create_test_definition("hello", vec!["hi", "greet"]);
951
952        registry
953            .register_sync(definition, Box::new(TestHandler))
954            .unwrap();
955
956        assert_eq!(registry.resolve_name("hi"), Some("hello"));
957        assert_eq!(registry.resolve_name("greet"), Some("hello"));
958    }
959
960    #[test]
961    fn test_resolve_unknown_name() {
962        let registry = CommandRegistry::new();
963        assert_eq!(registry.resolve_name("unknown"), None);
964    }
965
966    // Get definition tests
967    #[test]
968    fn test_get_definition_by_name() {
969        let mut registry = CommandRegistry::new();
970        let definition = create_test_definition("test", vec![]);
971
972        registry
973            .register_sync(definition, Box::new(TestHandler))
974            .unwrap();
975
976        let retrieved = registry.get_definition("test");
977        assert!(retrieved.is_some());
978        assert_eq!(retrieved.unwrap().name, "test");
979    }
980
981    #[test]
982    fn test_get_definition_by_alias() {
983        let mut registry = CommandRegistry::new();
984        let definition = create_test_definition("hello", vec!["hi"]);
985
986        registry
987            .register_sync(definition, Box::new(TestHandler))
988            .unwrap();
989
990        let retrieved = registry.get_definition("hi");
991        assert!(retrieved.is_some());
992        assert_eq!(retrieved.unwrap().name, "hello");
993    }
994
995    #[test]
996    fn test_get_definition_unknown() {
997        let registry = CommandRegistry::new();
998        assert!(registry.get_definition("unknown").is_none());
999    }
1000
1001    // Get handler tests
1002    #[test]
1003    fn test_get_handler_by_name() {
1004        let mut registry = CommandRegistry::new();
1005        let definition = create_test_definition("test", vec![]);
1006
1007        registry
1008            .register_sync(definition, Box::new(TestHandler))
1009            .unwrap();
1010
1011        let handler = registry.get_handler_sync("test");
1012        assert!(handler.is_some());
1013    }
1014
1015    #[test]
1016    fn test_get_handler_sync_by_name() {
1017        let mut registry = CommandRegistry::new();
1018        let definition = create_test_definition("test", vec![]);
1019
1020        registry
1021            .register_sync(definition, Box::new(TestHandler))
1022            .unwrap();
1023
1024        let handler = registry.get_handler_sync("test");
1025        assert!(handler.is_some());
1026    }
1027
1028    #[test]
1029    fn test_get_handler_by_alias() {
1030        let mut registry = CommandRegistry::new();
1031        let definition = create_test_definition("hello", vec!["hi"]);
1032
1033        registry
1034            .register_sync(definition, Box::new(TestHandler))
1035            .unwrap();
1036
1037        let handler = registry.get_handler_sync("hi");
1038        assert!(handler.is_some());
1039    }
1040
1041    #[test]
1042    fn test_get_handler_unknown() {
1043        let registry = CommandRegistry::new();
1044        assert!(registry.get_handler_sync("unknown").is_none());
1045    }
1046
1047    // List commands tests
1048    #[test]
1049    fn test_list_commands_empty() {
1050        let registry = CommandRegistry::new();
1051        let commands = registry.list_commands();
1052        assert_eq!(commands.len(), 0);
1053    }
1054
1055    #[test]
1056    fn test_list_commands_multiple() {
1057        let mut registry = CommandRegistry::new();
1058
1059        registry
1060            .register_sync(
1061                create_test_definition("cmd1", vec![]),
1062                Box::new(TestHandler),
1063            )
1064            .unwrap();
1065        registry
1066            .register_sync(
1067                create_test_definition("cmd2", vec![]),
1068                Box::new(TestHandler),
1069            )
1070            .unwrap();
1071        registry
1072            .register_sync(
1073                create_test_definition("cmd3", vec![]),
1074                Box::new(TestHandler),
1075            )
1076            .unwrap();
1077
1078        let commands = registry.list_commands();
1079        assert_eq!(commands.len(), 3);
1080
1081        let names: Vec<&str> = commands.iter().map(|c| c.name.as_str()).collect();
1082        assert!(names.contains(&"cmd1"));
1083        assert!(names.contains(&"cmd2"));
1084        assert!(names.contains(&"cmd3"));
1085    }
1086
1087    // Integration tests
1088    #[test]
1089    fn test_complete_workflow() {
1090        let mut registry = CommandRegistry::new();
1091
1092        // Register multiple commands with aliases
1093        let def1 = create_test_definition("simulate", vec!["sim", "run"]);
1094        let def2 = create_test_definition("validate", vec!["val", "check"]);
1095        let def3 = create_test_definition("help", vec!["h", "?"]);
1096
1097        registry.register_sync(def1, Box::new(TestHandler)).unwrap();
1098        registry.register_sync(def2, Box::new(TestHandler)).unwrap();
1099        registry.register_sync(def3, Box::new(TestHandler)).unwrap();
1100
1101        // Verify registry state
1102        assert_eq!(registry.len(), 3);
1103
1104        // Verify all names resolve correctly
1105        assert_eq!(registry.resolve_name("simulate"), Some("simulate"));
1106        assert_eq!(registry.resolve_name("sim"), Some("simulate"));
1107        assert_eq!(registry.resolve_name("validate"), Some("validate"));
1108        assert_eq!(registry.resolve_name("val"), Some("validate"));
1109
1110        // Verify handlers are accessible
1111        assert!(registry.get_handler_sync("simulate").is_some());
1112        assert!(registry.get_handler_sync("sim").is_some());
1113        assert!(registry.get_handler_sync("h").is_some());
1114
1115        // Verify definitions are accessible
1116        let sim_def = registry.get_definition("sim");
1117        assert!(sim_def.is_some());
1118        assert_eq!(sim_def.unwrap().name, "simulate");
1119    }
1120
1121    #[test]
1122    fn test_default_trait() {
1123        let registry: CommandRegistry = Default::default();
1124        assert!(registry.is_empty());
1125    }
1126
1127    #[test]
1128    fn test_contains_method() {
1129        let mut registry = CommandRegistry::new();
1130        let definition = create_test_definition("test", vec!["t"]);
1131
1132        registry
1133            .register_sync(definition, Box::new(TestHandler))
1134            .unwrap();
1135
1136        assert!(registry.contains("test"));
1137        assert!(registry.contains("t"));
1138        assert!(!registry.contains("unknown"));
1139    }
1140
1141    #[test]
1142    fn test_multiple_aliases_same_command() {
1143        let mut registry = CommandRegistry::new();
1144        let definition = create_test_definition("command", vec!["c", "cmd", "com"]);
1145
1146        registry
1147            .register_sync(definition, Box::new(TestHandler))
1148            .unwrap();
1149
1150        // All aliases should resolve to the same command
1151        assert_eq!(registry.resolve_name("c"), Some("command"));
1152        assert_eq!(registry.resolve_name("cmd"), Some("command"));
1153        assert_eq!(registry.resolve_name("com"), Some("command"));
1154
1155        // All should return the same handler
1156        let handler1 = registry.get_handler_sync("c");
1157        let handler2 = registry.get_handler_sync("cmd");
1158        assert!(handler1.is_some());
1159        assert!(handler2.is_some());
1160    }
1161
1162    #[test]
1163    fn test_case_sensitivity() {
1164        let mut registry = CommandRegistry::new();
1165        let definition = create_test_definition("Test", vec![]);
1166
1167        registry
1168            .register_sync(definition, Box::new(TestHandler))
1169            .unwrap();
1170
1171        // Case matters
1172        assert!(registry.contains("Test"));
1173        assert!(!registry.contains("test"));
1174        assert!(!registry.contains("TEST"));
1175    }
1176
1177    #[test]
1178    fn test_empty_alias_list() {
1179        let mut registry = CommandRegistry::new();
1180        let definition = create_test_definition("test", vec![]);
1181
1182        let result = registry.register_sync(definition, Box::new(TestHandler));
1183
1184        assert!(result.is_ok());
1185        assert!(registry.contains("test"));
1186    }
1187
1188    // ============================================================================
1189    // AsyncCommandHandler / register_async / get_handler_async TESTS (DD-022)
1190    // ============================================================================
1191
1192    #[test]
1193    fn test_register_async_command() {
1194        let mut registry = CommandRegistry::new();
1195        let definition = create_test_definition("fetch", vec![]);
1196
1197        let result = registry.register_async(definition, Box::new(TestAsyncHandler));
1198
1199        assert!(result.is_ok());
1200        assert_eq!(registry.len(), 1);
1201    }
1202
1203    #[test]
1204    fn test_register_async_command_with_aliases() {
1205        let mut registry = CommandRegistry::new();
1206        let definition = create_test_definition("fetch", vec!["f", "get-remote"]);
1207
1208        registry
1209            .register_async(definition, Box::new(TestAsyncHandler))
1210            .unwrap();
1211
1212        assert!(registry.contains("fetch"));
1213        assert!(registry.contains("f"));
1214        assert!(registry.contains("get-remote"));
1215        assert_eq!(registry.resolve_name("f"), Some("fetch"));
1216    }
1217
1218    #[test]
1219    fn test_get_handler_async_by_name_and_alias() {
1220        let mut registry = CommandRegistry::new();
1221        let definition = create_test_definition("fetch", vec!["f"]);
1222
1223        registry
1224            .register_async(definition, Box::new(TestAsyncHandler))
1225            .unwrap();
1226
1227        assert!(registry.get_handler_async("fetch").is_some());
1228        assert!(registry.get_handler_async("f").is_some());
1229        assert!(registry.get_handler_async("unknown").is_none());
1230    }
1231
1232    /// The core cross-accessor guarantee DD-022 depends on: querying an
1233    /// async-registered command through the *sync* accessor returns `None`
1234    /// (not the wrong handler, not a panic) — dispatch sites rely on this
1235    /// to fall through from `get_handler_sync` to `get_handler_async`.
1236    #[test]
1237    fn test_sync_accessor_returns_none_for_async_command() {
1238        let mut registry = CommandRegistry::new();
1239        let definition = create_test_definition("fetch", vec![]);
1240
1241        registry
1242            .register_async(definition, Box::new(TestAsyncHandler))
1243            .unwrap();
1244
1245        assert!(registry.get_handler_sync("fetch").is_none());
1246        assert!(registry.get_handler_async("fetch").is_some());
1247    }
1248
1249    /// Symmetric case: querying a sync-registered command through the
1250    /// *async* accessor returns `None`.
1251    #[test]
1252    fn test_async_accessor_returns_none_for_sync_command() {
1253        let mut registry = CommandRegistry::new();
1254        let definition = create_test_definition("test", vec![]);
1255
1256        registry
1257            .register_sync(definition, Box::new(TestHandler))
1258            .unwrap();
1259
1260        assert!(registry.get_handler_async("test").is_none());
1261        assert!(registry.get_handler_sync("test").is_some());
1262    }
1263
1264    /// A command name already taken by a sync handler must be rejected for
1265    /// async registration — the unified storage means one name, one kind.
1266    #[test]
1267    fn test_register_async_conflicts_with_existing_sync_name() {
1268        let mut registry = CommandRegistry::new();
1269        let sync_def = create_test_definition("dual", vec![]);
1270        let async_def = create_test_definition("dual", vec![]);
1271
1272        registry
1273            .register_sync(sync_def, Box::new(TestHandler))
1274            .unwrap();
1275        let result = registry.register_async(async_def, Box::new(TestAsyncHandler));
1276
1277        assert!(result.is_err());
1278        match result.unwrap_err() {
1279            crate::error::DynamicCliError::Registry(RegistryError::DuplicateRegistration {
1280                name,
1281                ..
1282            }) => {
1283                assert_eq!(name, "dual");
1284            }
1285            other => panic!("Expected DuplicateRegistration, got: {:?}", other),
1286        }
1287    }
1288
1289    /// Symmetric case: a name already taken by an async handler must be
1290    /// rejected for sync registration.
1291    #[test]
1292    fn test_register_sync_conflicts_with_existing_async_name() {
1293        let mut registry = CommandRegistry::new();
1294        let async_def = create_test_definition("dual", vec![]);
1295        let sync_def = create_test_definition("dual", vec![]);
1296
1297        registry
1298            .register_async(async_def, Box::new(TestAsyncHandler))
1299            .unwrap();
1300        let result = registry.register_sync(sync_def, Box::new(TestHandler));
1301
1302        assert!(result.is_err());
1303    }
1304
1305    /// An async command's alias must not collide with an existing sync
1306    /// command's alias, and vice versa — conflict detection is shared
1307    /// across both kinds via `check_name_available`.
1308    #[test]
1309    fn test_async_alias_conflicts_with_sync_alias() {
1310        let mut registry = CommandRegistry::new();
1311        let sync_def = create_test_definition("cmd1", vec!["shared"]);
1312        let async_def = create_test_definition("cmd2", vec!["shared"]);
1313
1314        registry
1315            .register_sync(sync_def, Box::new(TestHandler))
1316            .unwrap();
1317        let result = registry.register_async(async_def, Box::new(TestAsyncHandler));
1318
1319        assert!(result.is_err());
1320    }
1321
1322    #[test]
1323    fn test_get_definition_works_for_async_command() {
1324        let mut registry = CommandRegistry::new();
1325        let definition = create_test_definition("fetch", vec!["f"]);
1326
1327        registry
1328            .register_async(definition, Box::new(TestAsyncHandler))
1329            .unwrap();
1330
1331        let retrieved = registry.get_definition("f");
1332        assert!(retrieved.is_some());
1333        assert_eq!(retrieved.unwrap().name, "fetch");
1334    }
1335
1336    #[test]
1337    fn test_list_commands_includes_both_sync_and_async() {
1338        let mut registry = CommandRegistry::new();
1339
1340        registry
1341            .register_sync(
1342                create_test_definition("sync-cmd", vec![]),
1343                Box::new(TestHandler),
1344            )
1345            .unwrap();
1346        registry
1347            .register_async(
1348                create_test_definition("async-cmd", vec![]),
1349                Box::new(TestAsyncHandler),
1350            )
1351            .unwrap();
1352
1353        let commands = registry.list_commands();
1354        assert_eq!(commands.len(), 2);
1355        let names: Vec<&str> = commands.iter().map(|c| c.name.as_str()).collect();
1356        assert!(names.contains(&"sync-cmd"));
1357        assert!(names.contains(&"async-cmd"));
1358    }
1359
1360    #[test]
1361    fn test_mixed_registry_workflow() {
1362        // End-to-end: a registry with both sync and async commands behaves
1363        // consistently across resolve_name / get_definition / len / contains.
1364        let mut registry = CommandRegistry::new();
1365
1366        registry
1367            .register_sync(
1368                create_test_definition("simulate", vec!["sim"]),
1369                Box::new(TestHandler),
1370            )
1371            .unwrap();
1372        registry
1373            .register_async(
1374                create_test_definition("fetch", vec!["f"]),
1375                Box::new(TestAsyncHandler),
1376            )
1377            .unwrap();
1378
1379        assert_eq!(registry.len(), 2);
1380        assert!(registry.contains("sim"));
1381        assert!(registry.contains("f"));
1382
1383        assert!(registry.get_handler_sync("simulate").is_some());
1384        assert!(registry.get_handler_async("fetch").is_some());
1385        assert!(registry.get_handler_sync("fetch").is_none());
1386        assert!(registry.get_handler_async("simulate").is_none());
1387    }
1388
1389    // ========================================================================
1390    // Fault-tolerance consistency check (DD-028, #72)
1391    // ========================================================================
1392
1393    /// Declares its failures must never be silently continued past.
1394    struct FaultIntolerantHandler;
1395
1396    impl CommandHandler for FaultIntolerantHandler {
1397        fn execute(
1398            &self,
1399            _context: &mut dyn crate::context::ExecutionContext,
1400            _args: &ParsedArgs,
1401        ) -> crate::error::Result<()> {
1402            Ok(())
1403        }
1404
1405        fn expected_fault_tolerance(&self) -> Option<bool> {
1406            Some(false)
1407        }
1408    }
1409
1410    /// Async mirror of [`FaultIntolerantHandler`].
1411    struct AsyncFaultIntolerantHandler;
1412
1413    #[async_trait::async_trait]
1414    impl AsyncCommandHandler for AsyncFaultIntolerantHandler {
1415        async fn execute(
1416            &self,
1417            _context: &mut dyn crate::context::ExecutionContext,
1418            _args: &ParsedArgs,
1419        ) -> crate::error::Result<()> {
1420            Ok(())
1421        }
1422
1423        fn expected_fault_tolerance(&self) -> Option<bool> {
1424            Some(false)
1425        }
1426    }
1427
1428    #[test]
1429    fn test_register_sync_accepts_handler_with_no_opinion() {
1430        // TestHandler never overrides expected_fault_tolerance() — the
1431        // overwhelmingly common case, and it must keep working exactly as
1432        // before DD-028, regardless of the configured continue_on_failure.
1433        let mut registry = CommandRegistry::new();
1434        let mut definition = create_test_definition("test", vec![]);
1435        definition.continue_on_failure = true;
1436
1437        let result = registry.register_sync(definition, Box::new(TestHandler));
1438
1439        assert!(result.is_ok());
1440    }
1441
1442    #[test]
1443    fn test_register_sync_accepts_matching_expectation() {
1444        let mut registry = CommandRegistry::new();
1445        let mut definition = create_test_definition("solve", vec![]);
1446        definition.continue_on_failure = false; // matches the handler's Some(false)
1447
1448        let result = registry.register_sync(definition, Box::new(FaultIntolerantHandler));
1449
1450        assert!(result.is_ok());
1451    }
1452
1453    #[test]
1454    fn test_register_sync_rejects_contradicting_expectation() {
1455        let mut registry = CommandRegistry::new();
1456        let mut definition = create_test_definition("solve", vec![]);
1457        definition.continue_on_failure = true; // contradicts the handler's Some(false)
1458
1459        let result = registry.register_sync(definition, Box::new(FaultIntolerantHandler));
1460
1461        assert!(result.is_err());
1462        match result.unwrap_err() {
1463            crate::error::DynamicCliError::Registry(RegistryError::FaultToleranceMismatch {
1464                command,
1465                expected,
1466                configured,
1467                ..
1468            }) => {
1469                assert_eq!(command, "solve");
1470                assert!(!expected);
1471                assert!(configured);
1472            }
1473            other => panic!("wrong error variant: {other:?}"),
1474        }
1475    }
1476
1477    #[test]
1478    fn test_register_sync_rejected_handler_is_not_stored() {
1479        // A rejected registration must not leave a partial trace — the
1480        // command should be registrable again with a corrected definition.
1481        let mut registry = CommandRegistry::new();
1482        let mut bad_definition = create_test_definition("solve", vec![]);
1483        bad_definition.continue_on_failure = true;
1484
1485        assert!(registry
1486            .register_sync(bad_definition, Box::new(FaultIntolerantHandler))
1487            .is_err());
1488        assert!(!registry.contains("solve"));
1489        assert_eq!(registry.len(), 0);
1490
1491        let mut good_definition = create_test_definition("solve", vec![]);
1492        good_definition.continue_on_failure = false;
1493        assert!(registry
1494            .register_sync(good_definition, Box::new(FaultIntolerantHandler))
1495            .is_ok());
1496        assert!(registry.contains("solve"));
1497    }
1498
1499    #[test]
1500    fn test_register_async_accepts_handler_with_no_opinion() {
1501        let mut registry = CommandRegistry::new();
1502        let mut definition = create_test_definition("fetch", vec![]);
1503        definition.continue_on_failure = true;
1504
1505        let result = registry.register_async(definition, Box::new(TestAsyncHandler));
1506
1507        assert!(result.is_ok());
1508    }
1509
1510    #[test]
1511    fn test_register_async_rejects_contradicting_expectation() {
1512        let mut registry = CommandRegistry::new();
1513        let mut definition = create_test_definition("solve", vec![]);
1514        definition.continue_on_failure = true; // contradicts the handler's Some(false)
1515
1516        let result = registry.register_async(definition, Box::new(AsyncFaultIntolerantHandler));
1517
1518        assert!(result.is_err());
1519        match result.unwrap_err() {
1520            crate::error::DynamicCliError::Registry(RegistryError::FaultToleranceMismatch {
1521                command,
1522                ..
1523            }) => {
1524                assert_eq!(command, "solve");
1525            }
1526            other => panic!("wrong error variant: {other:?}"),
1527        }
1528    }
1529}