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_sync_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_sync_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_sync_handler()` or
1012    /// `register_async_handler()` call is missing (DD-022).
1013    ///
1014    /// # Example
1015    ///
1016    /// ```
1017    /// use dynamic_cli::error::ExecutionError;
1018    ///
1019    /// let error = ExecutionError::handler_not_found("run", "run_handler");
1020    /// match error {
1021    ///     ExecutionError::HandlerNotFound { suggestion, .. } => {
1022    ///         assert!(suggestion.as_deref().unwrap_or("").contains("run_handler"));
1023    ///     }
1024    ///     _ => panic!("wrong variant"),
1025    /// }
1026    /// ```
1027    pub fn handler_not_found(command: &str, implementation: &str) -> Self {
1028        Self::HandlerNotFound {
1029            command: command.to_string(),
1030            implementation: implementation.to_string(),
1031            suggestion: Some(format!(
1032                "Ensure .register_sync_handler(\"{implementation}\", ...) or \
1033                 .register_async_handler(\"{implementation}\", ...) was called before running."
1034            )),
1035        }
1036    }
1037}
1038
1039impl RegistryError {
1040    /// Create a missing-handler error with an actionable suggestion
1041    ///
1042    /// The suggestion interpolates the command name so the user
1043    /// knows exactly which `.register_sync_handler()` call is missing.
1044    ///
1045    /// # Example
1046    ///
1047    /// ```
1048    /// use dynamic_cli::error::RegistryError;
1049    ///
1050    /// let error = RegistryError::missing_handler("export");
1051    /// match error {
1052    ///     RegistryError::MissingHandler { suggestion, .. } => {
1053    ///         assert!(suggestion.as_deref().unwrap_or("").contains("export"));
1054    ///     }
1055    ///     _ => panic!("wrong variant"),
1056    /// }
1057    /// ```
1058    pub fn missing_handler(command: &str) -> Self {
1059        Self::MissingHandler {
1060            command: command.to_string(),
1061            suggestion: Some(format!(
1062                "Call .register_sync_handler(\"{command}\", ...) before running."
1063            )),
1064        }
1065    }
1066}
1067
1068#[cfg(feature = "wasm-plugins")]
1069impl WasmError {
1070    /// Create a `FunctionNotFound` error for a missing mandatory export
1071    ///
1072    /// The suggestion names the exact signature expected for the missing
1073    /// export, taken from `WASM_PLUGIN_INTERFACE.md`.
1074    ///
1075    /// # Example
1076    ///
1077    /// ```
1078    /// use dynamic_cli::error::WasmError;
1079    ///
1080    /// let error = WasmError::missing_mandatory_export("dcli_alloc", "plugin.wasm");
1081    /// match error {
1082    ///     WasmError::FunctionNotFound { suggestion, .. } => {
1083    ///         assert!(suggestion.is_some());
1084    ///     }
1085    ///     _ => panic!("wrong variant"),
1086    /// }
1087    /// ```
1088    pub fn missing_mandatory_export(function: &str, module: &str) -> Self {
1089        let signature = match function {
1090            "dcli_alloc" => "fn dcli_alloc(size: i32) -> i32",
1091            "dcli_dealloc" => "fn dcli_dealloc(ptr: i32, size: i32)",
1092            "memory" => "(memory (export \"memory\") ...)",
1093            other => other,
1094        };
1095        Self::FunctionNotFound {
1096            function: function.to_string(),
1097            module: module.to_string(),
1098            suggestion: Some(format!(
1099                "Export `{signature}` from the WASM module. \
1100                 See WASM_PLUGIN_INTERFACE.md for the full contract."
1101            )),
1102        }
1103    }
1104
1105    /// Create a `GuestError` from a raw error code, without a detailed message
1106    ///
1107    /// Used when the module does not export `dcli_last_error_message`.
1108    ///
1109    /// # Example
1110    ///
1111    /// ```
1112    /// use dynamic_cli::error::WasmError;
1113    ///
1114    /// let error = WasmError::guest_error_without_message(2);
1115    /// match error {
1116    ///     WasmError::GuestError { code, message } => {
1117    ///         assert_eq!(code, 2);
1118    ///         assert!(message.is_none());
1119    ///     }
1120    ///     _ => panic!("wrong variant"),
1121    /// }
1122    /// ```
1123    pub fn guest_error_without_message(code: i32) -> Self {
1124        Self::GuestError {
1125            code,
1126            message: None,
1127        }
1128    }
1129}
1130
1131#[cfg(test)]
1132mod tests {
1133    use super::*;
1134
1135    // ── ConfigError ──────────────────────────────────────────
1136
1137    #[test]
1138    fn test_config_file_not_found_display() {
1139        let error = ConfigError::FileNotFound {
1140            path: PathBuf::from("/path/to/config.yaml"),
1141            suggestion: None,
1142        };
1143        let msg = format!("{}", error);
1144        assert!(msg.contains("not found"));
1145        assert!(msg.contains("config.yaml"));
1146    }
1147
1148    #[test]
1149    fn test_config_file_not_found_helper_has_suggestion() {
1150        let error = ConfigError::file_not_found(PathBuf::from("commands.yaml"));
1151        match error {
1152            ConfigError::FileNotFound { suggestion, .. } => {
1153                assert!(suggestion.is_some(), "helper must populate suggestion");
1154            }
1155            _ => panic!("wrong variant"),
1156        }
1157    }
1158
1159    #[test]
1160    fn test_config_unsupported_format_helper_has_suggestion() {
1161        let error = ConfigError::unsupported_format(".toml");
1162        match error {
1163            ConfigError::UnsupportedFormat {
1164                suggestion,
1165                extension,
1166                ..
1167            } => {
1168                assert_eq!(extension, ".toml");
1169                assert!(suggestion.is_some());
1170            }
1171            _ => panic!("wrong variant"),
1172        }
1173    }
1174
1175    #[test]
1176    fn test_config_duplicate_command_display() {
1177        let error = ConfigError::DuplicateCommand {
1178            name: "run".to_string(),
1179            suggestion: Some("Rename one of the conflicting commands.".to_string()),
1180        };
1181        let msg = format!("{}", error);
1182        assert!(msg.contains("run"));
1183        // suggestion must NOT appear in Display (it's rendered separately)
1184        assert!(!msg.contains("Rename"));
1185    }
1186
1187    #[test]
1188    fn test_config_unknown_type_display() {
1189        let error = ConfigError::UnknownType {
1190            type_name: "datetime".to_string(),
1191            context: "commands[0]".to_string(),
1192            suggestion: None,
1193        };
1194        let msg = format!("{}", error);
1195        assert!(msg.contains("datetime"));
1196    }
1197
1198    #[test]
1199    fn test_config_inconsistency_display() {
1200        let error = ConfigError::Inconsistency {
1201            details: "default not in choices".to_string(),
1202            suggestion: Some("hint".to_string()),
1203        };
1204        let msg = format!("{}", error);
1205        assert!(msg.contains("default not in choices"));
1206        assert!(!msg.contains("hint")); // suggestion separate from Display
1207    }
1208
1209    #[test]
1210    fn test_config_invalid_schema_display() {
1211        let error = ConfigError::InvalidSchema {
1212            reason: "missing field".to_string(),
1213            path: Some("commands[0]".to_string()),
1214            suggestion: None,
1215        };
1216        let msg = format!("{}", error);
1217        assert!(msg.contains("missing field"));
1218    }
1219
1220    // ── ParseError ───────────────────────────────────────────
1221
1222    #[test]
1223    fn test_parse_unknown_command_with_suggestions() {
1224        let available = vec!["simulate".to_string(), "validate".to_string()];
1225        let error = ParseError::unknown_command_with_suggestions("simulat", &available);
1226        match error {
1227            ParseError::UnknownCommand {
1228                command,
1229                suggestions,
1230            } => {
1231                assert_eq!(command, "simulat");
1232                assert!(suggestions.contains(&"simulate".to_string()));
1233            }
1234            _ => panic!("wrong variant"),
1235        }
1236    }
1237
1238    #[test]
1239    fn test_parse_missing_argument_helper_has_suggestion() {
1240        let error = ParseError::missing_argument("filename", "process");
1241        match error {
1242            ParseError::MissingArgument {
1243                suggestion,
1244                command,
1245                ..
1246            } => {
1247                assert_eq!(command, "process");
1248                let s = suggestion.unwrap();
1249                assert!(s.contains("process"));
1250                assert!(s.contains("--help"));
1251            }
1252            _ => panic!("wrong variant"),
1253        }
1254    }
1255
1256    #[test]
1257    fn test_parse_missing_option_helper_has_suggestion() {
1258        let error = ParseError::missing_option("output", "export");
1259        match error {
1260            ParseError::MissingOption {
1261                suggestion, option, ..
1262            } => {
1263                assert_eq!(option, "output");
1264                let s = suggestion.unwrap();
1265                assert!(s.contains("export"));
1266            }
1267            _ => panic!("wrong variant"),
1268        }
1269    }
1270
1271    #[test]
1272    fn test_parse_too_many_arguments_helper_has_suggestion() {
1273        let error = ParseError::too_many_arguments("run", 1, 3);
1274        match error {
1275            ParseError::TooManyArguments {
1276                suggestion,
1277                expected,
1278                got,
1279                ..
1280            } => {
1281                assert_eq!(expected, 1);
1282                assert_eq!(got, 3);
1283                assert!(suggestion.is_some());
1284            }
1285            _ => panic!("wrong variant"),
1286        }
1287    }
1288
1289    #[test]
1290    fn test_parse_missing_argument_suggestion_none_by_default() {
1291        // Direct construction without helper: suggestion is caller's responsibility
1292        let error = ParseError::MissingArgument {
1293            argument: "file".to_string(),
1294            command: "run".to_string(),
1295            suggestion: None,
1296        };
1297        match error {
1298            ParseError::MissingArgument { suggestion, .. } => assert!(suggestion.is_none()),
1299            _ => panic!("wrong variant"),
1300        }
1301    }
1302
1303    // ── ValidationError ──────────────────────────────────────
1304
1305    #[test]
1306    fn test_validation_out_of_range_display() {
1307        let error = ValidationError::OutOfRange {
1308            arg_name: "percentage".to_string(),
1309            value: 150.0,
1310            min: 0.0,
1311            max: 100.0,
1312            suggestion: None,
1313        };
1314        let msg = format!("{}", error);
1315        assert!(msg.contains("percentage"));
1316        assert!(msg.contains("150"));
1317        assert!(msg.contains("0"));
1318        assert!(msg.contains("100"));
1319    }
1320
1321    #[test]
1322    fn test_validation_out_of_range_suggestion_not_in_display() {
1323        let error = ValidationError::OutOfRange {
1324            arg_name: "percentage".to_string(),
1325            value: 150.0,
1326            min: 0.0,
1327            max: 100.0,
1328            suggestion: Some("Value must be between 0 and 100.".to_string()),
1329        };
1330        let msg = format!("{}", error);
1331        assert!(!msg.contains("Value must be between")); // suggestion is separate
1332    }
1333
1334    #[test]
1335    fn test_validation_file_not_found_suggestion() {
1336        let error = ValidationError::FileNotFound {
1337            path: PathBuf::from("data.csv"),
1338            arg_name: "input".to_string(),
1339            suggestion: Some("Check that the file exists.".to_string()),
1340        };
1341        match error {
1342            ValidationError::FileNotFound { suggestion, .. } => {
1343                assert!(suggestion.is_some());
1344            }
1345            _ => panic!("wrong variant"),
1346        }
1347    }
1348
1349    #[test]
1350    fn test_validation_missing_dependency_suggestion() {
1351        let error = ValidationError::MissingDependency {
1352            arg_name: "format".to_string(),
1353            required_arg: "output".to_string(),
1354            suggestion: Some("Add --output to your command.".to_string()),
1355        };
1356        let msg = format!("{}", error);
1357        assert!(msg.contains("format"));
1358        assert!(msg.contains("output"));
1359    }
1360
1361    #[test]
1362    fn test_validation_mutually_exclusive_suggestion() {
1363        let error = ValidationError::MutuallyExclusive {
1364            arg1: "--verbose".to_string(),
1365            arg2: "--quiet".to_string(),
1366            suggestion: Some("Remove one of the two conflicting options.".to_string()),
1367        };
1368        let msg = format!("{}", error);
1369        assert!(msg.contains("--verbose"));
1370        assert!(msg.contains("--quiet"));
1371    }
1372
1373    // ── ExecutionError ───────────────────────────────────────
1374
1375    #[test]
1376    fn test_execution_handler_not_found_helper_interpolates_impl() {
1377        let error = ExecutionError::handler_not_found("run", "run_handler");
1378        match error {
1379            ExecutionError::HandlerNotFound {
1380                suggestion,
1381                implementation,
1382                ..
1383            } => {
1384                assert_eq!(implementation, "run_handler");
1385                let s = suggestion.unwrap();
1386                assert!(s.contains("run_handler"));
1387                assert!(s.contains("register_sync_handler"));
1388            }
1389            _ => panic!("wrong variant"),
1390        }
1391    }
1392
1393    #[test]
1394    fn test_execution_context_downcast_failed_display() {
1395        let error = ExecutionError::ContextDowncastFailed {
1396            expected_type: "MyAppContext".to_string(),
1397            suggestion: None,
1398        };
1399        let msg = format!("{}", error);
1400        assert!(msg.contains("MyAppContext"));
1401    }
1402
1403    #[test]
1404    fn test_execution_invalid_context_state_suggestion() {
1405        let error = ExecutionError::InvalidContextState {
1406            reason: "pool not ready".to_string(),
1407            suggestion: Some("Ensure context is initialised.".to_string()),
1408        };
1409        let msg = format!("{}", error);
1410        assert!(msg.contains("pool not ready"));
1411    }
1412
1413    // ── RegistryError ────────────────────────────────────────
1414
1415    #[test]
1416    fn test_registry_missing_handler_helper_interpolates_command() {
1417        let error = RegistryError::missing_handler("export");
1418        match error {
1419            RegistryError::MissingHandler {
1420                suggestion,
1421                command,
1422            } => {
1423                assert_eq!(command, "export");
1424                let s = suggestion.unwrap();
1425                assert!(s.contains("export"));
1426                assert!(s.contains("register_sync_handler"));
1427            }
1428            _ => panic!("wrong variant"),
1429        }
1430    }
1431
1432    #[test]
1433    fn test_registry_duplicate_registration_display() {
1434        let error = RegistryError::DuplicateRegistration {
1435            name: "run".to_string(),
1436            suggestion: None,
1437        };
1438        let msg = format!("{}", error);
1439        assert!(msg.contains("run"));
1440    }
1441
1442    #[test]
1443    fn test_registry_duplicate_alias_display() {
1444        let error = RegistryError::DuplicateAlias {
1445            alias: "r".to_string(),
1446            existing_command: "run".to_string(),
1447            suggestion: Some("Choose a different alias.".to_string()),
1448        };
1449        let msg = format!("{}", error);
1450        assert!(msg.contains("run"));
1451        assert!(!msg.contains("Choose")); // suggestion separate from Display
1452    }
1453
1454    // ── WasmError ────────────────────────────────────────────
1455
1456    #[cfg(feature = "wasm-plugins")]
1457    #[test]
1458    fn test_wasm_load_failed_display() {
1459        let error = WasmError::LoadFailed {
1460            path: PathBuf::from("plugin.wasm"),
1461            source: anyhow::anyhow!("invalid magic number"),
1462            suggestion: None,
1463        };
1464        let msg = format!("{}", error);
1465        assert!(msg.contains("plugin.wasm"));
1466        assert!(msg.contains("invalid magic number"));
1467    }
1468
1469    #[cfg(feature = "wasm-plugins")]
1470    #[test]
1471    fn test_wasm_function_not_found_display() {
1472        let error = WasmError::FunctionNotFound {
1473            function: "dcli_alloc".to_string(),
1474            module: "plugin.wasm".to_string(),
1475            suggestion: None,
1476        };
1477        let msg = format!("{}", error);
1478        assert!(msg.contains("dcli_alloc"));
1479        assert!(msg.contains("plugin.wasm"));
1480    }
1481
1482    #[cfg(feature = "wasm-plugins")]
1483    #[test]
1484    fn test_wasm_missing_mandatory_export_helper_has_suggestion() {
1485        let error = WasmError::missing_mandatory_export("dcli_dealloc", "plugin.wasm");
1486        match error {
1487            WasmError::FunctionNotFound {
1488                suggestion,
1489                function,
1490                ..
1491            } => {
1492                assert_eq!(function, "dcli_dealloc");
1493                let s = suggestion.unwrap();
1494                assert!(s.contains("dcli_dealloc"));
1495                assert!(s.contains("WASM_PLUGIN_INTERFACE.md"));
1496            }
1497            _ => panic!("wrong variant"),
1498        }
1499    }
1500
1501    #[cfg(feature = "wasm-plugins")]
1502    #[test]
1503    fn test_wasm_guest_error_with_message_display() {
1504        let error = WasmError::GuestError {
1505            code: 1,
1506            message: Some("invalid argument".to_string()),
1507        };
1508        let msg = format!("{}", error);
1509        assert!(msg.contains('1'));
1510        assert!(msg.contains("invalid argument"));
1511    }
1512
1513    #[cfg(feature = "wasm-plugins")]
1514    #[test]
1515    fn test_wasm_guest_error_without_message_display() {
1516        let error = WasmError::guest_error_without_message(2);
1517        let msg = format!("{}", error);
1518        assert!(msg.contains('2'));
1519        match error {
1520            WasmError::GuestError { message, .. } => assert!(message.is_none()),
1521            _ => panic!("wrong variant"),
1522        }
1523    }
1524
1525    #[cfg(feature = "wasm-plugins")]
1526    #[test]
1527    fn test_wasm_serialization_failed_display() {
1528        let error = WasmError::SerializationFailed("unsupported map key type".to_string());
1529        let msg = format!("{}", error);
1530        assert!(msg.contains("unsupported map key type"));
1531    }
1532
1533    #[cfg(feature = "wasm-plugins")]
1534    #[test]
1535    fn test_wasm_memory_access_failed_display() {
1536        let error = WasmError::MemoryAccessFailed {
1537            reason: "write out of bounds".to_string(),
1538        };
1539        let msg = format!("{}", error);
1540        assert!(msg.contains("out of bounds"));
1541    }
1542
1543    #[cfg(feature = "wasm-plugins")]
1544    #[test]
1545    fn test_wasm_error_converts_into_dynamic_cli_error() {
1546        let wasm_err = WasmError::guest_error_without_message(1);
1547        let err: DynamicCliError = wasm_err.into();
1548        match err {
1549            DynamicCliError::Wasm(WasmError::GuestError { code, .. }) => assert_eq!(code, 1),
1550            _ => panic!("wrong variant"),
1551        }
1552    }
1553}