Skip to main content

dynamic_cli/error/
types.rs

1//! Error types for dynamic-cli
2//!
3//! Defines all possible error types with context and clear messages.
4//!
5//! Each error variant carries an optional `suggestion` field that surfaces
6//! an actionable hint to the end user. Suggestions are rendered by
7//! [`crate::error::display::format_error`] and are never part of the
8//! `Display` string itself, keeping machine-readable messages stable.
9
10use std::path::PathBuf;
11use thiserror::Error;
12
13/// Main error for the dynamic-cli framework
14///
15/// Encompasses all possible error categories. Uses `thiserror`
16/// to automatically generate `Display` and `Error` implementations.
17#[derive(Debug, Error)]
18pub enum DynamicCliError {
19    /// Errors related to the configuration file
20    #[error(transparent)]
21    Config(#[from] ConfigError),
22
23    /// Command parsing errors
24    #[error(transparent)]
25    Parse(#[from] ParseError),
26
27    /// Validation errors
28    #[error(transparent)]
29    Validation(#[from] ValidationError),
30
31    /// Execution errors
32    #[error(transparent)]
33    Execution(#[from] ExecutionError),
34
35    /// Registry errors
36    #[error(transparent)]
37    Registry(#[from] RegistryError),
38
39    /// WASM plugin errors (only available with the `wasm-plugins` feature)
40    #[cfg(feature = "wasm-plugins")]
41    #[error(transparent)]
42    Wasm(#[from] WasmError),
43
44    /// I/O errors
45    #[error("I/O error: {0}")]
46    Io(#[from] std::io::Error),
47}
48
49// ═══════════════════════════════════════════════════════════
50// CONFIGURATION ERRORS
51// ═══════════════════════════════════════════════════════════
52
53/// Errors related to loading and parsing the configuration file
54///
55/// These errors occur when loading the `commands.yaml` or `commands.json`
56/// file and its structural validation.
57#[derive(Debug, Error)]
58pub enum ConfigError {
59    /// Configuration file not found
60    ///
61    /// # Example
62    ///
63    /// ```
64    /// use dynamic_cli::error::ConfigError;
65    /// use std::path::PathBuf;
66    ///
67    /// let error = ConfigError::FileNotFound {
68    ///     path: PathBuf::from("missing.yaml"),
69    ///     suggestion: Some("Verify the path and file permissions.".to_string()),
70    /// };
71    /// let msg = format!("{}", error);
72    /// assert!(msg.contains("missing.yaml"));
73    /// ```
74    #[error("Configuration file not found: {path:?}")]
75    FileNotFound {
76        path: PathBuf,
77        /// Actionable hint surfaced to the user (not part of the Display string)
78        suggestion: Option<String>,
79    },
80
81    /// Unsupported file extension
82    ///
83    /// Only `.yaml`, `.yml` and `.json` are supported.
84    ///
85    /// # Example
86    ///
87    /// ```
88    /// use dynamic_cli::error::ConfigError;
89    ///
90    /// let error = ConfigError::UnsupportedFormat {
91    ///     extension: ".toml".to_string(),
92    ///     suggestion: Some("Rename the file with a .yaml, .yml or .json extension.".to_string()),
93    /// };
94    /// let msg = format!("{}", error);
95    /// assert!(msg.contains(".toml"));
96    /// ```
97    #[error("Unsupported file format: '{extension}'. Supported: .yaml, .yml, .json")]
98    UnsupportedFormat {
99        extension: String,
100        /// Actionable hint surfaced to the user (not part of the Display string)
101        suggestion: Option<String>,
102    },
103
104    /// YAML parsing error
105    #[error("Failed to parse YAML configuration at line {line:?}, column {column:?}: {source}")]
106    YamlParse {
107        #[source]
108        source: serde_yaml::Error,
109        /// Position in the file (if available)
110        line: Option<usize>,
111        column: Option<usize>,
112    },
113
114    /// JSON parsing error
115    #[error("Failed to parse JSON configuration at line {line}, column {column}: {source}")]
116    JsonParse {
117        #[source]
118        source: serde_json::Error,
119        /// Position in the file
120        line: usize,
121        column: usize,
122    },
123
124    /// Invalid configuration schema
125    ///
126    /// The file structure doesn't match the expected format.
127    ///
128    /// # Example
129    ///
130    /// ```
131    /// use dynamic_cli::error::ConfigError;
132    ///
133    /// let error = ConfigError::InvalidSchema {
134    ///     reason: "Missing required field 'name'".to_string(),
135    ///     path: Some("commands[0]".to_string()),
136    ///     suggestion: Some("Add a 'name' field to each command entry.".to_string()),
137    /// };
138    /// let msg = format!("{}", error);
139    /// assert!(msg.contains("Missing required field"));
140    /// ```
141    #[error("Invalid configuration schema: {reason} (at {path:?})")]
142    InvalidSchema {
143        reason: String,
144        /// Path in the config (e.g., "commands[0].options[2].type")
145        path: Option<String>,
146        /// Actionable hint surfaced to the user (not part of the Display string)
147        suggestion: Option<String>,
148    },
149
150    /// Duplicate command (same name or alias)
151    ///
152    /// # Example
153    ///
154    /// ```
155    /// use dynamic_cli::error::ConfigError;
156    ///
157    /// let error = ConfigError::DuplicateCommand {
158    ///     name: "run".to_string(),
159    ///     suggestion: Some("Rename one of the conflicting commands or aliases.".to_string()),
160    /// };
161    /// let msg = format!("{}", error);
162    /// assert!(msg.contains("run"));
163    /// ```
164    #[error("Duplicate command name or alias: '{name}'")]
165    DuplicateCommand {
166        name: String,
167        /// Actionable hint surfaced to the user (not part of the Display string)
168        suggestion: Option<String>,
169    },
170
171    /// Unknown argument type
172    ///
173    /// # Example
174    ///
175    /// ```
176    /// use dynamic_cli::error::ConfigError;
177    ///
178    /// let error = ConfigError::UnknownType {
179    ///     type_name: "datetime".to_string(),
180    ///     context: "commands[1].options[0]".to_string(),
181    ///     suggestion: Some(
182    ///         "Supported types: string, integer, float, boolean, file.".to_string()
183    ///     ),
184    /// };
185    /// let msg = format!("{}", error);
186    /// assert!(msg.contains("datetime"));
187    /// ```
188    #[error("Unknown argument type: '{type_name}' in {context}")]
189    UnknownType {
190        type_name: String,
191        context: String,
192        /// Actionable hint surfaced to the user (not part of the Display string)
193        suggestion: Option<String>,
194    },
195
196    /// Inconsistent configuration
197    ///
198    /// For example, a default value that's not in the allowed choices.
199    ///
200    /// # Example
201    ///
202    /// ```
203    /// use dynamic_cli::error::ConfigError;
204    ///
205    /// let error = ConfigError::Inconsistency {
206    ///     details: "Default value 'fast' is not in choices: slow, medium".to_string(),
207    ///     suggestion: Some("Ensure the default value matches one of the allowed choices.".to_string()),
208    /// };
209    /// let msg = format!("{}", error);
210    /// assert!(msg.contains("Default value"));
211    /// ```
212    #[error("Configuration inconsistency: {details}")]
213    Inconsistency {
214        details: String,
215        /// Actionable hint surfaced to the user (not part of the Display string)
216        suggestion: Option<String>,
217    },
218}
219
220// ═══════════════════════════════════════════════════════════
221// PARSING ERRORS
222// ═══════════════════════════════════════════════════════════
223
224/// Errors when parsing user commands
225///
226/// These errors occur when analyzing arguments provided
227/// by the user in CLI or REPL mode.
228#[derive(Debug, Error)]
229pub enum ParseError {
230    /// Unknown command
231    ///
232    /// The user typed a command that doesn't exist.
233    /// Includes suggestions based on Levenshtein distance.
234    #[error("Unknown command: '{command}'. Type 'help' for available commands.")]
235    UnknownCommand {
236        command: String,
237        /// Similar command suggestions (from Levenshtein distance)
238        suggestions: Vec<String>,
239    },
240
241    /// Missing required positional argument
242    ///
243    /// # Example
244    ///
245    /// ```
246    /// use dynamic_cli::error::ParseError;
247    ///
248    /// let error = ParseError::MissingArgument {
249    ///     argument: "filename".to_string(),
250    ///     command: "process".to_string(),
251    ///     suggestion: Some("Run --help process to see required arguments.".to_string()),
252    /// };
253    /// let msg = format!("{}", error);
254    /// assert!(msg.contains("filename"));
255    /// ```
256    #[error("Missing required argument: {argument} for command '{command}'")]
257    MissingArgument {
258        argument: String,
259        command: String,
260        /// Actionable hint surfaced to the user (not part of the Display string)
261        suggestion: Option<String>,
262    },
263
264    /// Missing required option
265    ///
266    /// # Example
267    ///
268    /// ```
269    /// use dynamic_cli::error::ParseError;
270    ///
271    /// let error = ParseError::MissingOption {
272    ///     option: "output".to_string(),
273    ///     command: "export".to_string(),
274    ///     suggestion: Some("Run --help export to see required options.".to_string()),
275    /// };
276    /// let msg = format!("{}", error);
277    /// assert!(msg.contains("output"));
278    /// ```
279    #[error("Missing required option: --{option} for command '{command}'")]
280    MissingOption {
281        option: String,
282        command: String,
283        /// Actionable hint surfaced to the user (not part of the Display string)
284        suggestion: Option<String>,
285    },
286
287    /// Too many positional arguments
288    ///
289    /// # Example
290    ///
291    /// ```
292    /// use dynamic_cli::error::ParseError;
293    ///
294    /// let error = ParseError::TooManyArguments {
295    ///     command: "run".to_string(),
296    ///     expected: 1,
297    ///     got: 3,
298    ///     suggestion: Some("Run --help run for the expected usage.".to_string()),
299    /// };
300    /// let msg = format!("{}", error);
301    /// assert!(msg.contains("run"));
302    /// ```
303    #[error("Too many arguments for command '{command}'. Expected {expected}, got {got}")]
304    TooManyArguments {
305        command: String,
306        expected: usize,
307        got: usize,
308        /// Actionable hint surfaced to the user (not part of the Display string)
309        suggestion: Option<String>,
310    },
311
312    /// Unknown option
313    ///
314    /// Includes similar option suggestions.
315    #[error("Unknown option: {flag} for command '{command}'")]
316    UnknownOption {
317        flag: String,
318        command: String,
319        /// Similar option suggestions (from Levenshtein distance)
320        suggestions: Vec<String>,
321    },
322
323    /// Type parsing error
324    ///
325    /// The user provided a value that can't be converted
326    /// to the expected type (e.g., "abc" for an integer).
327    #[error("Failed to parse {arg_name} as {expected_type}: '{value}'{}", 
328        .details.as_ref().map(|d| format!(" ({})", d)).unwrap_or_default())]
329    TypeParseError {
330        arg_name: String,
331        expected_type: String,
332        value: String,
333        /// Error details (e.g., "not a valid integer")
334        details: Option<String>,
335    },
336
337    /// Value not in allowed choices
338    #[error("Invalid value for {arg_name}: '{value}'. Must be one of: {}", 
339        .choices.join(", "))]
340    InvalidChoice {
341        arg_name: String,
342        value: String,
343        choices: Vec<String>,
344    },
345
346    /// Invalid command syntax
347    #[error("Invalid command syntax: {details}{}", 
348        .hint.as_ref().map(|h| format!("\nHint: {}", h)).unwrap_or_default())]
349    InvalidSyntax {
350        details: String,
351        /// Example of correct syntax
352        hint: Option<String>,
353    },
354}
355
356// ═══════════════════════════════════════════════════════════
357// VALIDATION ERRORS
358// ═══════════════════════════════════════════════════════════
359
360/// Errors during argument validation
361///
362/// These errors occur after parsing, during validation
363/// of constraints defined in the configuration.
364#[derive(Debug, Error)]
365pub enum ValidationError {
366    /// Required file doesn't exist
367    ///
368    /// # Example
369    ///
370    /// ```
371    /// use dynamic_cli::error::ValidationError;
372    /// use std::path::PathBuf;
373    ///
374    /// let error = ValidationError::FileNotFound {
375    ///     path: PathBuf::from("data.csv"),
376    ///     arg_name: "input".to_string(),
377    ///     suggestion: Some("Check that the file exists and the path is correct.".to_string()),
378    /// };
379    /// let msg = format!("{}", error);
380    /// assert!(msg.contains("data.csv"));
381    /// ```
382    #[error("File not found for argument '{arg_name}': {path:?}")]
383    FileNotFound {
384        path: PathBuf,
385        arg_name: String,
386        /// Actionable hint surfaced to the user (not part of the Display string)
387        suggestion: Option<String>,
388    },
389
390    /// Invalid file extension
391    #[error("Invalid file extension for {arg_name}: {path:?}. Expected: {}", 
392        .expected.join(", "))]
393    InvalidExtension {
394        arg_name: String,
395        path: PathBuf,
396        expected: Vec<String>,
397    },
398
399    /// Value out of allowed range
400    ///
401    /// # Example
402    ///
403    /// ```
404    /// use dynamic_cli::error::ValidationError;
405    ///
406    /// let error = ValidationError::OutOfRange {
407    ///     arg_name: "percentage".to_string(),
408    ///     value: 150.0,
409    ///     min: 0.0,
410    ///     max: 100.0,
411    ///     suggestion: Some("Value must be between 0 and 100.".to_string()),
412    /// };
413    /// let msg = format!("{}", error);
414    /// assert!(msg.contains("150"));
415    /// ```
416    #[error("{arg_name} must be between {min} and {max}, got {value}")]
417    OutOfRange {
418        arg_name: String,
419        value: f64,
420        min: f64,
421        max: f64,
422        /// Actionable hint surfaced to the user (not part of the Display string)
423        suggestion: Option<String>,
424    },
425
426    /// Custom constraint not met
427    ///
428    /// # Example
429    ///
430    /// ```
431    /// use dynamic_cli::error::ValidationError;
432    ///
433    /// let error = ValidationError::CustomConstraint {
434    ///     arg_name: "email".to_string(),
435    ///     reason: "not a valid email address".to_string(),
436    ///     suggestion: Some("Provide a valid email address (e.g. user@example.com).".to_string()),
437    /// };
438    /// let msg = format!("{}", error);
439    /// assert!(msg.contains("email"));
440    /// ```
441    #[error("Validation failed for {arg_name}: {reason}")]
442    CustomConstraint {
443        arg_name: String,
444        reason: String,
445        /// Actionable hint surfaced to the user (not part of the Display string)
446        suggestion: Option<String>,
447    },
448
449    /// Dependency between arguments not satisfied
450    ///
451    /// Some arguments require the presence of other arguments.
452    ///
453    /// # Example
454    ///
455    /// ```
456    /// use dynamic_cli::error::ValidationError;
457    ///
458    /// let error = ValidationError::MissingDependency {
459    ///     arg_name: "output-format".to_string(),
460    ///     required_arg: "output".to_string(),
461    ///     suggestion: Some("Add --output to your command.".to_string()),
462    /// };
463    /// let msg = format!("{}", error);
464    /// assert!(msg.contains("output-format"));
465    /// ```
466    #[error("{arg_name} requires {required_arg} to be specified")]
467    MissingDependency {
468        arg_name: String,
469        required_arg: String,
470        /// Actionable hint surfaced to the user (not part of the Display string)
471        suggestion: Option<String>,
472    },
473
474    /// Mutually exclusive arguments
475    ///
476    /// Some arguments cannot be used together.
477    ///
478    /// # Example
479    ///
480    /// ```
481    /// use dynamic_cli::error::ValidationError;
482    ///
483    /// let error = ValidationError::MutuallyExclusive {
484    ///     arg1: "--verbose".to_string(),
485    ///     arg2: "--quiet".to_string(),
486    ///     suggestion: Some("Remove one of the two conflicting options.".to_string()),
487    /// };
488    /// let msg = format!("{}", error);
489    /// assert!(msg.contains("--verbose"));
490    /// ```
491    #[error("Options {arg1} and {arg2} cannot be used together")]
492    MutuallyExclusive {
493        arg1: String,
494        arg2: String,
495        /// Actionable hint surfaced to the user (not part of the Display string)
496        suggestion: Option<String>,
497    },
498}
499
500// ═══════════════════════════════════════════════════════════
501// EXECUTION ERRORS
502// ═══════════════════════════════════════════════════════════
503
504/// Errors during command execution
505///
506/// These errors occur during user code execution.
507#[derive(Debug, Error)]
508pub enum ExecutionError {
509    /// Command handler not found
510    ///
511    /// The implementation name in the config doesn't match any
512    /// registered handler.
513    ///
514    /// # Example
515    ///
516    /// ```
517    /// use dynamic_cli::error::ExecutionError;
518    ///
519    /// let error = ExecutionError::HandlerNotFound {
520    ///     command: "run".to_string(),
521    ///     implementation: "run_handler".to_string(),
522    ///     suggestion: Some(
523    ///         "Ensure .register_handler(\"run_handler\", ...) was called before running."
524    ///             .to_string()
525    ///     ),
526    /// };
527    /// let msg = format!("{}", error);
528    /// assert!(msg.contains("run"));
529    /// ```
530    #[error("No handler registered for command '{command}' (implementation: '{implementation}')")]
531    HandlerNotFound {
532        command: String,
533        implementation: String,
534        /// Actionable hint surfaced to the user (not part of the Display string)
535        suggestion: Option<String>,
536    },
537
538    /// Error during context downcasting
539    ///
540    /// The handler tried to downcast the context to an incorrect type.
541    ///
542    /// # Example
543    ///
544    /// ```
545    /// use dynamic_cli::error::ExecutionError;
546    ///
547    /// let error = ExecutionError::ContextDowncastFailed {
548    ///     expected_type: "MyAppContext".to_string(),
549    ///     suggestion: Some(
550    ///         "Check that the context type passed to the handler matches the expected type."
551    ///             .to_string()
552    ///     ),
553    /// };
554    /// let msg = format!("{}", error);
555    /// assert!(msg.contains("MyAppContext"));
556    /// ```
557    #[error("Failed to downcast execution context to expected type: {expected_type}")]
558    ContextDowncastFailed {
559        expected_type: String,
560        /// Actionable hint surfaced to the user (not part of the Display string)
561        suggestion: Option<String>,
562    },
563
564    /// Invalid context state for this operation
565    ///
566    /// # Example
567    ///
568    /// ```
569    /// use dynamic_cli::error::ExecutionError;
570    ///
571    /// let error = ExecutionError::InvalidContextState {
572    ///     reason: "connection pool not initialised".to_string(),
573    ///     suggestion: Some("Ensure the context is fully initialised before running commands.".to_string()),
574    /// };
575    /// let msg = format!("{}", error);
576    /// assert!(msg.contains("connection pool"));
577    /// ```
578    #[error("Invalid context state: {reason}")]
579    InvalidContextState {
580        reason: String,
581        /// Actionable hint surfaced to the user (not part of the Display string)
582        suggestion: Option<String>,
583    },
584
585    /// Error in command implementation
586    ///
587    /// Wraps errors from user code.
588    #[error("Command execution failed: {0}")]
589    CommandFailed(#[source] anyhow::Error),
590
591    /// Command interrupted by user
592    ///
593    /// User pressed Ctrl+C during execution.
594    #[error("Command interrupted by user")]
595    Interrupted,
596}
597
598// ═══════════════════════════════════════════════════════════
599// REGISTRY ERRORS
600// ═══════════════════════════════════════════════════════════
601
602/// Errors related to the command registry
603///
604/// These errors occur when registering commands
605/// and handlers in the registry.
606#[derive(Debug, Error)]
607pub enum RegistryError {
608    /// Attempt to register an already existing command
609    ///
610    /// # Example
611    ///
612    /// ```
613    /// use dynamic_cli::error::RegistryError;
614    ///
615    /// let error = RegistryError::DuplicateRegistration {
616    ///     name: "run".to_string(),
617    ///     suggestion: Some("Command names must be unique across the registry.".to_string()),
618    /// };
619    /// let msg = format!("{}", error);
620    /// assert!(msg.contains("run"));
621    /// ```
622    #[error("Command '{name}' is already registered")]
623    DuplicateRegistration {
624        name: String,
625        /// Actionable hint surfaced to the user (not part of the Display string)
626        suggestion: Option<String>,
627    },
628
629    /// Alias already used by another command
630    ///
631    /// # Example
632    ///
633    /// ```
634    /// use dynamic_cli::error::RegistryError;
635    ///
636    /// let error = RegistryError::DuplicateAlias {
637    ///     alias: "r".to_string(),
638    ///     existing_command: "run".to_string(),
639    ///     suggestion: Some("Choose a different alias for one of the commands.".to_string()),
640    /// };
641    /// let msg = format!("{}", error);
642    /// assert!(msg.contains("run"));
643    /// ```
644    #[error("Alias '{alias}' is already used by command '{existing_command}'")]
645    DuplicateAlias {
646        alias: String,
647        existing_command: String,
648        /// Actionable hint surfaced to the user (not part of the Display string)
649        suggestion: Option<String>,
650    },
651
652    /// Missing handler for a definition
653    ///
654    /// A command is defined in the config but no handler
655    /// has been registered for it.
656    ///
657    /// # Example
658    ///
659    /// ```
660    /// use dynamic_cli::error::RegistryError;
661    ///
662    /// let error = RegistryError::MissingHandler {
663    ///     command: "export".to_string(),
664    ///     suggestion: Some(
665    ///         "Call .register_handler(\"export\", ...) before running.".to_string()
666    ///     ),
667    /// };
668    /// let msg = format!("{}", error);
669    /// assert!(msg.contains("export"));
670    /// ```
671    #[error("No handler provided for command '{command}'")]
672    MissingHandler {
673        command: String,
674        /// Actionable hint surfaced to the user (not part of the Display string)
675        suggestion: Option<String>,
676    },
677}
678
679// ═══════════════════════════════════════════════════════════
680// WASM PLUGIN ERRORS (feature = "wasm-plugins")
681// ═══════════════════════════════════════════════════════════
682
683/// Errors related to loading and executing WASM plugins
684///
685/// Only available when the `wasm-plugins` feature is enabled.
686/// These errors occur when loading a `.wasm` module, validating its
687/// mandatory exports, or invoking a guest function across the host/guest
688/// boundary. See `WASM_PLUGIN_INTERFACE.md` for the full ABI contract.
689#[cfg(feature = "wasm-plugins")]
690#[derive(Debug, Error)]
691pub enum WasmError {
692    /// The `.wasm` module could not be loaded or instantiated
693    ///
694    /// # Example
695    ///
696    /// ```
697    /// use dynamic_cli::error::WasmError;
698    /// use std::path::PathBuf;
699    ///
700    /// let error = WasmError::LoadFailed {
701    ///     path: PathBuf::from("plugin.wasm"),
702    ///     source: anyhow::anyhow!("invalid magic number"),
703    ///     suggestion: Some("Verify the file is a valid WASM binary.".to_string()),
704    /// };
705    /// let msg = format!("{}", error);
706    /// assert!(msg.contains("plugin.wasm"));
707    /// ```
708    #[error("Failed to load WASM module: {path:?}: {source}")]
709    LoadFailed {
710        path: PathBuf,
711        #[source]
712        source: anyhow::Error,
713        /// Actionable hint surfaced to the user (not part of the Display string)
714        suggestion: Option<String>,
715    },
716
717    /// A mandatory or mapped export is missing from the module
718    ///
719    /// Mandatory exports are `memory`, `dcli_alloc`, `dcli_dealloc`, and the
720    /// mapped business function. See `WASM_PLUGIN_INTERFACE.md`.
721    ///
722    /// # Example
723    ///
724    /// ```
725    /// use dynamic_cli::error::WasmError;
726    ///
727    /// let error = WasmError::FunctionNotFound {
728    ///     function: "dcli_dealloc".to_string(),
729    ///     module: "plugin.wasm".to_string(),
730    ///     suggestion: Some(
731    ///         "Export `dcli_dealloc(ptr: i32, size: i32)` from the WASM module.".to_string()
732    ///     ),
733    /// };
734    /// let msg = format!("{}", error);
735    /// assert!(msg.contains("dcli_dealloc"));
736    /// ```
737    #[error("WASM function '{function}' not found in module '{module}'")]
738    FunctionNotFound {
739        function: String,
740        module: String,
741        /// Actionable hint surfaced to the user (not part of the Display string)
742        suggestion: Option<String>,
743    },
744
745    /// The guest function returned a non-zero error code
746    ///
747    /// `message` is populated from `dcli_last_error_message()` when the
748    /// module exports it; otherwise `None`.
749    ///
750    /// # Example
751    ///
752    /// ```
753    /// use dynamic_cli::error::WasmError;
754    ///
755    /// let error = WasmError::GuestError {
756    ///     code: 1,
757    ///     message: Some("invalid argument".to_string()),
758    /// };
759    /// let msg = format!("{}", error);
760    /// assert!(msg.contains('1'));
761    /// ```
762    #[error("WASM guest returned error code {code}{}",
763        .message.as_ref().map(|m| format!(": {m}")).unwrap_or_default())]
764    GuestError {
765        code: i32,
766        /// Detailed message from `dcli_last_error_message()`, if exported
767        message: Option<String>,
768    },
769
770    /// Failed to serialize handler arguments before crossing the WASM boundary
771    ///
772    /// # Example
773    ///
774    /// ```
775    /// use dynamic_cli::error::WasmError;
776    ///
777    /// let error = WasmError::SerializationFailed("unsupported map key type".to_string());
778    /// let msg = format!("{}", error);
779    /// assert!(msg.contains("unsupported map key type"));
780    /// ```
781    #[error("Failed to serialize arguments for WASM call: {0}")]
782    SerializationFailed(String),
783
784    /// Failed to read from or write to the guest's linear memory
785    ///
786    /// # Example
787    ///
788    /// ```
789    /// use dynamic_cli::error::WasmError;
790    ///
791    /// let error = WasmError::MemoryAccessFailed {
792    ///     reason: "write out of bounds".to_string(),
793    /// };
794    /// let msg = format!("{}", error);
795    /// assert!(msg.contains("out of bounds"));
796    /// ```
797    #[error("Failed to access WASM guest memory: {reason}")]
798    MemoryAccessFailed { reason: String },
799}
800
801// ═══════════════════════════════════════════════════════════
802// HELPERS FOR CREATING CONTEXTUAL ERRORS
803// ═══════════════════════════════════════════════════════════
804
805impl ParseError {
806    /// Create an unknown command error with Levenshtein suggestions
807    ///
808    /// Automatically computes similar command names from the available list.
809    ///
810    /// # Arguments
811    ///
812    /// * `command` - The command typed by the user
813    /// * `available` - List of available commands
814    ///
815    /// # Example
816    ///
817    /// ```
818    /// use dynamic_cli::error::ParseError;
819    ///
820    /// let available = vec!["simulate".to_string(), "validate".to_string()];
821    /// let error = ParseError::unknown_command_with_suggestions("simulat", &available);
822    /// match error {
823    ///     ParseError::UnknownCommand { suggestions, .. } => {
824    ///         assert!(suggestions.contains(&"simulate".to_string()));
825    ///     }
826    ///     _ => panic!("wrong variant"),
827    /// }
828    /// ```
829    pub fn unknown_command_with_suggestions(command: &str, available: &[String]) -> Self {
830        let suggestions = crate::error::find_similar_strings(command, available, 3);
831        Self::UnknownCommand {
832            command: command.to_string(),
833            suggestions,
834        }
835    }
836
837    /// Create an unknown option error with Levenshtein suggestions
838    ///
839    /// # Example
840    ///
841    /// ```
842    /// use dynamic_cli::error::ParseError;
843    ///
844    /// let available = vec!["--verbose".to_string(), "--output".to_string()];
845    /// let error = ParseError::unknown_option_with_suggestions("--verbos", "run", &available);
846    /// match error {
847    ///     ParseError::UnknownOption { suggestions, .. } => {
848    ///         assert!(suggestions.contains(&"--verbose".to_string()));
849    ///     }
850    ///     _ => panic!("wrong variant"),
851    /// }
852    /// ```
853    pub fn unknown_option_with_suggestions(
854        flag: &str,
855        command: &str,
856        available: &[String],
857    ) -> Self {
858        let suggestions = crate::error::find_similar_strings(flag, available, 2);
859        Self::UnknownOption {
860            flag: flag.to_string(),
861            command: command.to_string(),
862            suggestions,
863        }
864    }
865
866    /// Create a missing argument error with a help hint
867    ///
868    /// The suggestion automatically refers the user to `--help <command>`.
869    ///
870    /// # Example
871    ///
872    /// ```
873    /// use dynamic_cli::error::ParseError;
874    ///
875    /// let error = ParseError::missing_argument("filename", "process");
876    /// match error {
877    ///     ParseError::MissingArgument { suggestion, .. } => {
878    ///         assert!(suggestion.is_some());
879    ///     }
880    ///     _ => panic!("wrong variant"),
881    /// }
882    /// ```
883    pub fn missing_argument(argument: &str, command: &str) -> Self {
884        Self::MissingArgument {
885            argument: argument.to_string(),
886            command: command.to_string(),
887            suggestion: Some(format!("Run --help {command} to see required arguments.")),
888        }
889    }
890
891    /// Create a missing option error with a help hint
892    ///
893    /// The suggestion automatically refers the user to `--help <command>`.
894    ///
895    /// # Example
896    ///
897    /// ```
898    /// use dynamic_cli::error::ParseError;
899    ///
900    /// let error = ParseError::missing_option("output", "export");
901    /// match error {
902    ///     ParseError::MissingOption { suggestion, .. } => {
903    ///         assert!(suggestion.is_some());
904    ///     }
905    ///     _ => panic!("wrong variant"),
906    /// }
907    /// ```
908    pub fn missing_option(option: &str, command: &str) -> Self {
909        Self::MissingOption {
910            option: option.to_string(),
911            command: command.to_string(),
912            suggestion: Some(format!("Run --help {command} to see required options.")),
913        }
914    }
915
916    /// Create a too-many-arguments error with a help hint
917    ///
918    /// # Example
919    ///
920    /// ```
921    /// use dynamic_cli::error::ParseError;
922    ///
923    /// let error = ParseError::too_many_arguments("run", 1, 3);
924    /// match error {
925    ///     ParseError::TooManyArguments { suggestion, .. } => {
926    ///         assert!(suggestion.is_some());
927    ///     }
928    ///     _ => panic!("wrong variant"),
929    /// }
930    /// ```
931    pub fn too_many_arguments(command: &str, expected: usize, got: usize) -> Self {
932        Self::TooManyArguments {
933            command: command.to_string(),
934            expected,
935            got,
936            suggestion: Some(format!("Run --help {command} for the expected usage.")),
937        }
938    }
939}
940
941impl ConfigError {
942    /// Create a file-not-found error with a standard suggestion
943    ///
944    /// # Example
945    ///
946    /// ```
947    /// use dynamic_cli::error::ConfigError;
948    /// use std::path::PathBuf;
949    ///
950    /// let error = ConfigError::file_not_found(PathBuf::from("commands.yaml"));
951    /// match error {
952    ///     ConfigError::FileNotFound { suggestion, .. } => {
953    ///         assert!(suggestion.is_some());
954    ///     }
955    ///     _ => panic!("wrong variant"),
956    /// }
957    /// ```
958    pub fn file_not_found(path: PathBuf) -> Self {
959        Self::FileNotFound {
960            path,
961            suggestion: Some("Verify the path and file permissions.".to_string()),
962        }
963    }
964
965    /// Create an unsupported-format error with a standard suggestion
966    ///
967    /// # Example
968    ///
969    /// ```
970    /// use dynamic_cli::error::ConfigError;
971    ///
972    /// let error = ConfigError::unsupported_format(".toml");
973    /// match error {
974    ///     ConfigError::UnsupportedFormat { suggestion, .. } => {
975    ///         assert!(suggestion.is_some());
976    ///     }
977    ///     _ => panic!("wrong variant"),
978    /// }
979    /// ```
980    pub fn unsupported_format(extension: &str) -> Self {
981        Self::UnsupportedFormat {
982            extension: extension.to_string(),
983            suggestion: Some("Rename the file with a .yaml, .yml or .json extension.".to_string()),
984        }
985    }
986
987    /// Create a YAML parse error with position extracted from the serde error
988    pub fn yaml_parse_with_location(source: serde_yaml::Error) -> Self {
989        let location = source.location();
990        Self::YamlParse {
991            source,
992            line: location.as_ref().map(|l| l.line()),
993            column: location.map(|l| l.column()),
994        }
995    }
996
997    /// Create a JSON parse error with position extracted from the serde error
998    pub fn json_parse_with_location(source: serde_json::Error) -> Self {
999        Self::JsonParse {
1000            line: source.line(),
1001            column: source.column(),
1002            source,
1003        }
1004    }
1005}
1006
1007impl ExecutionError {
1008    /// Create a handler-not-found error with an actionable suggestion
1009    ///
1010    /// The suggestion interpolates the implementation name so the user
1011    /// knows exactly which `.register_handler()` call is missing.
1012    ///
1013    /// # Example
1014    ///
1015    /// ```
1016    /// use dynamic_cli::error::ExecutionError;
1017    ///
1018    /// let error = ExecutionError::handler_not_found("run", "run_handler");
1019    /// match error {
1020    ///     ExecutionError::HandlerNotFound { suggestion, .. } => {
1021    ///         assert!(suggestion.as_deref().unwrap_or("").contains("run_handler"));
1022    ///     }
1023    ///     _ => panic!("wrong variant"),
1024    /// }
1025    /// ```
1026    pub fn handler_not_found(command: &str, implementation: &str) -> Self {
1027        Self::HandlerNotFound {
1028            command: command.to_string(),
1029            implementation: implementation.to_string(),
1030            suggestion: Some(format!(
1031                "Ensure .register_handler(\"{implementation}\", ...) was called before running."
1032            )),
1033        }
1034    }
1035}
1036
1037impl RegistryError {
1038    /// Create a missing-handler error with an actionable suggestion
1039    ///
1040    /// The suggestion interpolates the command name so the user
1041    /// knows exactly which `.register_handler()` call is missing.
1042    ///
1043    /// # Example
1044    ///
1045    /// ```
1046    /// use dynamic_cli::error::RegistryError;
1047    ///
1048    /// let error = RegistryError::missing_handler("export");
1049    /// match error {
1050    ///     RegistryError::MissingHandler { suggestion, .. } => {
1051    ///         assert!(suggestion.as_deref().unwrap_or("").contains("export"));
1052    ///     }
1053    ///     _ => panic!("wrong variant"),
1054    /// }
1055    /// ```
1056    pub fn missing_handler(command: &str) -> Self {
1057        Self::MissingHandler {
1058            command: command.to_string(),
1059            suggestion: Some(format!(
1060                "Call .register_handler(\"{command}\", ...) before running."
1061            )),
1062        }
1063    }
1064}
1065
1066#[cfg(feature = "wasm-plugins")]
1067impl WasmError {
1068    /// Create a `FunctionNotFound` error for a missing mandatory export
1069    ///
1070    /// The suggestion names the exact signature expected for the missing
1071    /// export, taken from `WASM_PLUGIN_INTERFACE.md`.
1072    ///
1073    /// # Example
1074    ///
1075    /// ```
1076    /// use dynamic_cli::error::WasmError;
1077    ///
1078    /// let error = WasmError::missing_mandatory_export("dcli_alloc", "plugin.wasm");
1079    /// match error {
1080    ///     WasmError::FunctionNotFound { suggestion, .. } => {
1081    ///         assert!(suggestion.is_some());
1082    ///     }
1083    ///     _ => panic!("wrong variant"),
1084    /// }
1085    /// ```
1086    pub fn missing_mandatory_export(function: &str, module: &str) -> Self {
1087        let signature = match function {
1088            "dcli_alloc" => "fn dcli_alloc(size: i32) -> i32",
1089            "dcli_dealloc" => "fn dcli_dealloc(ptr: i32, size: i32)",
1090            "memory" => "(memory (export \"memory\") ...)",
1091            other => other,
1092        };
1093        Self::FunctionNotFound {
1094            function: function.to_string(),
1095            module: module.to_string(),
1096            suggestion: Some(format!(
1097                "Export `{signature}` from the WASM module. \
1098                 See WASM_PLUGIN_INTERFACE.md for the full contract."
1099            )),
1100        }
1101    }
1102
1103    /// Create a `GuestError` from a raw error code, without a detailed message
1104    ///
1105    /// Used when the module does not export `dcli_last_error_message`.
1106    ///
1107    /// # Example
1108    ///
1109    /// ```
1110    /// use dynamic_cli::error::WasmError;
1111    ///
1112    /// let error = WasmError::guest_error_without_message(2);
1113    /// match error {
1114    ///     WasmError::GuestError { code, message } => {
1115    ///         assert_eq!(code, 2);
1116    ///         assert!(message.is_none());
1117    ///     }
1118    ///     _ => panic!("wrong variant"),
1119    /// }
1120    /// ```
1121    pub fn guest_error_without_message(code: i32) -> Self {
1122        Self::GuestError {
1123            code,
1124            message: None,
1125        }
1126    }
1127}
1128
1129#[cfg(test)]
1130mod tests {
1131    use super::*;
1132
1133    // ── ConfigError ──────────────────────────────────────────
1134
1135    #[test]
1136    fn test_config_file_not_found_display() {
1137        let error = ConfigError::FileNotFound {
1138            path: PathBuf::from("/path/to/config.yaml"),
1139            suggestion: None,
1140        };
1141        let msg = format!("{}", error);
1142        assert!(msg.contains("not found"));
1143        assert!(msg.contains("config.yaml"));
1144    }
1145
1146    #[test]
1147    fn test_config_file_not_found_helper_has_suggestion() {
1148        let error = ConfigError::file_not_found(PathBuf::from("commands.yaml"));
1149        match error {
1150            ConfigError::FileNotFound { suggestion, .. } => {
1151                assert!(suggestion.is_some(), "helper must populate suggestion");
1152            }
1153            _ => panic!("wrong variant"),
1154        }
1155    }
1156
1157    #[test]
1158    fn test_config_unsupported_format_helper_has_suggestion() {
1159        let error = ConfigError::unsupported_format(".toml");
1160        match error {
1161            ConfigError::UnsupportedFormat {
1162                suggestion,
1163                extension,
1164                ..
1165            } => {
1166                assert_eq!(extension, ".toml");
1167                assert!(suggestion.is_some());
1168            }
1169            _ => panic!("wrong variant"),
1170        }
1171    }
1172
1173    #[test]
1174    fn test_config_duplicate_command_display() {
1175        let error = ConfigError::DuplicateCommand {
1176            name: "run".to_string(),
1177            suggestion: Some("Rename one of the conflicting commands.".to_string()),
1178        };
1179        let msg = format!("{}", error);
1180        assert!(msg.contains("run"));
1181        // suggestion must NOT appear in Display (it's rendered separately)
1182        assert!(!msg.contains("Rename"));
1183    }
1184
1185    #[test]
1186    fn test_config_unknown_type_display() {
1187        let error = ConfigError::UnknownType {
1188            type_name: "datetime".to_string(),
1189            context: "commands[0]".to_string(),
1190            suggestion: None,
1191        };
1192        let msg = format!("{}", error);
1193        assert!(msg.contains("datetime"));
1194    }
1195
1196    #[test]
1197    fn test_config_inconsistency_display() {
1198        let error = ConfigError::Inconsistency {
1199            details: "default not in choices".to_string(),
1200            suggestion: Some("hint".to_string()),
1201        };
1202        let msg = format!("{}", error);
1203        assert!(msg.contains("default not in choices"));
1204        assert!(!msg.contains("hint")); // suggestion separate from Display
1205    }
1206
1207    #[test]
1208    fn test_config_invalid_schema_display() {
1209        let error = ConfigError::InvalidSchema {
1210            reason: "missing field".to_string(),
1211            path: Some("commands[0]".to_string()),
1212            suggestion: None,
1213        };
1214        let msg = format!("{}", error);
1215        assert!(msg.contains("missing field"));
1216    }
1217
1218    // ── ParseError ───────────────────────────────────────────
1219
1220    #[test]
1221    fn test_parse_unknown_command_with_suggestions() {
1222        let available = vec!["simulate".to_string(), "validate".to_string()];
1223        let error = ParseError::unknown_command_with_suggestions("simulat", &available);
1224        match error {
1225            ParseError::UnknownCommand {
1226                command,
1227                suggestions,
1228            } => {
1229                assert_eq!(command, "simulat");
1230                assert!(suggestions.contains(&"simulate".to_string()));
1231            }
1232            _ => panic!("wrong variant"),
1233        }
1234    }
1235
1236    #[test]
1237    fn test_parse_missing_argument_helper_has_suggestion() {
1238        let error = ParseError::missing_argument("filename", "process");
1239        match error {
1240            ParseError::MissingArgument {
1241                suggestion,
1242                command,
1243                ..
1244            } => {
1245                assert_eq!(command, "process");
1246                let s = suggestion.unwrap();
1247                assert!(s.contains("process"));
1248                assert!(s.contains("--help"));
1249            }
1250            _ => panic!("wrong variant"),
1251        }
1252    }
1253
1254    #[test]
1255    fn test_parse_missing_option_helper_has_suggestion() {
1256        let error = ParseError::missing_option("output", "export");
1257        match error {
1258            ParseError::MissingOption {
1259                suggestion, option, ..
1260            } => {
1261                assert_eq!(option, "output");
1262                let s = suggestion.unwrap();
1263                assert!(s.contains("export"));
1264            }
1265            _ => panic!("wrong variant"),
1266        }
1267    }
1268
1269    #[test]
1270    fn test_parse_too_many_arguments_helper_has_suggestion() {
1271        let error = ParseError::too_many_arguments("run", 1, 3);
1272        match error {
1273            ParseError::TooManyArguments {
1274                suggestion,
1275                expected,
1276                got,
1277                ..
1278            } => {
1279                assert_eq!(expected, 1);
1280                assert_eq!(got, 3);
1281                assert!(suggestion.is_some());
1282            }
1283            _ => panic!("wrong variant"),
1284        }
1285    }
1286
1287    #[test]
1288    fn test_parse_missing_argument_suggestion_none_by_default() {
1289        // Direct construction without helper: suggestion is caller's responsibility
1290        let error = ParseError::MissingArgument {
1291            argument: "file".to_string(),
1292            command: "run".to_string(),
1293            suggestion: None,
1294        };
1295        match error {
1296            ParseError::MissingArgument { suggestion, .. } => assert!(suggestion.is_none()),
1297            _ => panic!("wrong variant"),
1298        }
1299    }
1300
1301    // ── ValidationError ──────────────────────────────────────
1302
1303    #[test]
1304    fn test_validation_out_of_range_display() {
1305        let error = ValidationError::OutOfRange {
1306            arg_name: "percentage".to_string(),
1307            value: 150.0,
1308            min: 0.0,
1309            max: 100.0,
1310            suggestion: None,
1311        };
1312        let msg = format!("{}", error);
1313        assert!(msg.contains("percentage"));
1314        assert!(msg.contains("150"));
1315        assert!(msg.contains("0"));
1316        assert!(msg.contains("100"));
1317    }
1318
1319    #[test]
1320    fn test_validation_out_of_range_suggestion_not_in_display() {
1321        let error = ValidationError::OutOfRange {
1322            arg_name: "percentage".to_string(),
1323            value: 150.0,
1324            min: 0.0,
1325            max: 100.0,
1326            suggestion: Some("Value must be between 0 and 100.".to_string()),
1327        };
1328        let msg = format!("{}", error);
1329        assert!(!msg.contains("Value must be between")); // suggestion is separate
1330    }
1331
1332    #[test]
1333    fn test_validation_file_not_found_suggestion() {
1334        let error = ValidationError::FileNotFound {
1335            path: PathBuf::from("data.csv"),
1336            arg_name: "input".to_string(),
1337            suggestion: Some("Check that the file exists.".to_string()),
1338        };
1339        match error {
1340            ValidationError::FileNotFound { suggestion, .. } => {
1341                assert!(suggestion.is_some());
1342            }
1343            _ => panic!("wrong variant"),
1344        }
1345    }
1346
1347    #[test]
1348    fn test_validation_missing_dependency_suggestion() {
1349        let error = ValidationError::MissingDependency {
1350            arg_name: "format".to_string(),
1351            required_arg: "output".to_string(),
1352            suggestion: Some("Add --output to your command.".to_string()),
1353        };
1354        let msg = format!("{}", error);
1355        assert!(msg.contains("format"));
1356        assert!(msg.contains("output"));
1357    }
1358
1359    #[test]
1360    fn test_validation_mutually_exclusive_suggestion() {
1361        let error = ValidationError::MutuallyExclusive {
1362            arg1: "--verbose".to_string(),
1363            arg2: "--quiet".to_string(),
1364            suggestion: Some("Remove one of the two conflicting options.".to_string()),
1365        };
1366        let msg = format!("{}", error);
1367        assert!(msg.contains("--verbose"));
1368        assert!(msg.contains("--quiet"));
1369    }
1370
1371    // ── ExecutionError ───────────────────────────────────────
1372
1373    #[test]
1374    fn test_execution_handler_not_found_helper_interpolates_impl() {
1375        let error = ExecutionError::handler_not_found("run", "run_handler");
1376        match error {
1377            ExecutionError::HandlerNotFound {
1378                suggestion,
1379                implementation,
1380                ..
1381            } => {
1382                assert_eq!(implementation, "run_handler");
1383                let s = suggestion.unwrap();
1384                assert!(s.contains("run_handler"));
1385                assert!(s.contains("register_handler"));
1386            }
1387            _ => panic!("wrong variant"),
1388        }
1389    }
1390
1391    #[test]
1392    fn test_execution_context_downcast_failed_display() {
1393        let error = ExecutionError::ContextDowncastFailed {
1394            expected_type: "MyAppContext".to_string(),
1395            suggestion: None,
1396        };
1397        let msg = format!("{}", error);
1398        assert!(msg.contains("MyAppContext"));
1399    }
1400
1401    #[test]
1402    fn test_execution_invalid_context_state_suggestion() {
1403        let error = ExecutionError::InvalidContextState {
1404            reason: "pool not ready".to_string(),
1405            suggestion: Some("Ensure context is initialised.".to_string()),
1406        };
1407        let msg = format!("{}", error);
1408        assert!(msg.contains("pool not ready"));
1409    }
1410
1411    // ── RegistryError ────────────────────────────────────────
1412
1413    #[test]
1414    fn test_registry_missing_handler_helper_interpolates_command() {
1415        let error = RegistryError::missing_handler("export");
1416        match error {
1417            RegistryError::MissingHandler {
1418                suggestion,
1419                command,
1420            } => {
1421                assert_eq!(command, "export");
1422                let s = suggestion.unwrap();
1423                assert!(s.contains("export"));
1424                assert!(s.contains("register_handler"));
1425            }
1426            _ => panic!("wrong variant"),
1427        }
1428    }
1429
1430    #[test]
1431    fn test_registry_duplicate_registration_display() {
1432        let error = RegistryError::DuplicateRegistration {
1433            name: "run".to_string(),
1434            suggestion: None,
1435        };
1436        let msg = format!("{}", error);
1437        assert!(msg.contains("run"));
1438    }
1439
1440    #[test]
1441    fn test_registry_duplicate_alias_display() {
1442        let error = RegistryError::DuplicateAlias {
1443            alias: "r".to_string(),
1444            existing_command: "run".to_string(),
1445            suggestion: Some("Choose a different alias.".to_string()),
1446        };
1447        let msg = format!("{}", error);
1448        assert!(msg.contains("run"));
1449        assert!(!msg.contains("Choose")); // suggestion separate from Display
1450    }
1451
1452    // ── WasmError ────────────────────────────────────────────
1453
1454    #[cfg(feature = "wasm-plugins")]
1455    #[test]
1456    fn test_wasm_load_failed_display() {
1457        let error = WasmError::LoadFailed {
1458            path: PathBuf::from("plugin.wasm"),
1459            source: anyhow::anyhow!("invalid magic number"),
1460            suggestion: None,
1461        };
1462        let msg = format!("{}", error);
1463        assert!(msg.contains("plugin.wasm"));
1464        assert!(msg.contains("invalid magic number"));
1465    }
1466
1467    #[cfg(feature = "wasm-plugins")]
1468    #[test]
1469    fn test_wasm_function_not_found_display() {
1470        let error = WasmError::FunctionNotFound {
1471            function: "dcli_alloc".to_string(),
1472            module: "plugin.wasm".to_string(),
1473            suggestion: None,
1474        };
1475        let msg = format!("{}", error);
1476        assert!(msg.contains("dcli_alloc"));
1477        assert!(msg.contains("plugin.wasm"));
1478    }
1479
1480    #[cfg(feature = "wasm-plugins")]
1481    #[test]
1482    fn test_wasm_missing_mandatory_export_helper_has_suggestion() {
1483        let error = WasmError::missing_mandatory_export("dcli_dealloc", "plugin.wasm");
1484        match error {
1485            WasmError::FunctionNotFound {
1486                suggestion,
1487                function,
1488                ..
1489            } => {
1490                assert_eq!(function, "dcli_dealloc");
1491                let s = suggestion.unwrap();
1492                assert!(s.contains("dcli_dealloc"));
1493                assert!(s.contains("WASM_PLUGIN_INTERFACE.md"));
1494            }
1495            _ => panic!("wrong variant"),
1496        }
1497    }
1498
1499    #[cfg(feature = "wasm-plugins")]
1500    #[test]
1501    fn test_wasm_guest_error_with_message_display() {
1502        let error = WasmError::GuestError {
1503            code: 1,
1504            message: Some("invalid argument".to_string()),
1505        };
1506        let msg = format!("{}", error);
1507        assert!(msg.contains('1'));
1508        assert!(msg.contains("invalid argument"));
1509    }
1510
1511    #[cfg(feature = "wasm-plugins")]
1512    #[test]
1513    fn test_wasm_guest_error_without_message_display() {
1514        let error = WasmError::guest_error_without_message(2);
1515        let msg = format!("{}", error);
1516        assert!(msg.contains('2'));
1517        match error {
1518            WasmError::GuestError { message, .. } => assert!(message.is_none()),
1519            _ => panic!("wrong variant"),
1520        }
1521    }
1522
1523    #[cfg(feature = "wasm-plugins")]
1524    #[test]
1525    fn test_wasm_serialization_failed_display() {
1526        let error = WasmError::SerializationFailed("unsupported map key type".to_string());
1527        let msg = format!("{}", error);
1528        assert!(msg.contains("unsupported map key type"));
1529    }
1530
1531    #[cfg(feature = "wasm-plugins")]
1532    #[test]
1533    fn test_wasm_memory_access_failed_display() {
1534        let error = WasmError::MemoryAccessFailed {
1535            reason: "write out of bounds".to_string(),
1536        };
1537        let msg = format!("{}", error);
1538        assert!(msg.contains("out of bounds"));
1539    }
1540
1541    #[cfg(feature = "wasm-plugins")]
1542    #[test]
1543    fn test_wasm_error_converts_into_dynamic_cli_error() {
1544        let wasm_err = WasmError::guest_error_without_message(1);
1545        let err: DynamicCliError = wasm_err.into();
1546        match err {
1547            DynamicCliError::Wasm(WasmError::GuestError { code, .. }) => assert_eq!(code, 1),
1548            _ => panic!("wrong variant"),
1549        }
1550    }
1551}