Skip to main content

nika_engine/
error.rs

1// The #[error] attribute from thiserror uses struct fields via string interpolation,
2// but Rust's unused_assignments lint doesn't recognize this.
3#![allow(unused_assignments)]
4
5//! Nika Error Types with Error Codes
6//!
7//! Error code ranges:
8//! - NIKA-000-009: Workflow errors
9//! - NIKA-010-019: Schema/validation errors
10//! - NIKA-020-029: DAG errors
11//! - NIKA-030-039: Provider errors
12//! - NIKA-040-049: Template/binding errors
13//! - NIKA-050-059: Path/task/security errors
14//! - NIKA-060-069: Output errors
15//! - NIKA-070-079: With block validation errors
16//! - NIKA-080-089: DAG validation errors
17//! - NIKA-090-099: JSONPath/IO errors (+NIKA-096 Execution catch-all)
18//! - NIKA-100-109: MCP errors
19//! - NIKA-110-119: Agent errors
20//! - NIKA-120-129: Resilience errors
21//! - NIKA-130-139: TUI errors
22//! - NIKA-140-151: AST analysis errors (Phase 2 analyzer)
23//! - NIKA-160-164: Parse errors (Phase 1 parser — ParseErrorKind in nika-core, DO NOT REUSE)
24//! - NIKA-165-169: Startup/Policy/Boot errors (165=Policy, 166=Boot, 167=Startup)
25//!
26//! Extended ranges:
27//! - NIKA-200-209: File Tool errors (ToolErrorCode in src/tools/mod.rs)
28//! - NIKA-210-219: Builtin tool errors
29//! - NIKA-220-229: Reserved (DAG Panel - not implemented)
30//! - NIKA-230-239: Reserved (Session persistence - not implemented)
31//! - NIKA-240-249: Reserved (Animation/Export - not implemented)
32//! - NIKA-251-259: Media pipeline errors (MIME, CAS, base64, budget — src/media/error.rs)
33//! - NIKA-260-269: Package URI errors
34//! - NIKA-270-279: Skill errors
35//! - NIKA-280-285: Artifact/media errors (path validation, write, size, integrity, cleanup, lock)
36//! - NIKA-290-297: Media tool errors (tool, format, deps, timeout, args, pipeline, security)
37//! - NIKA-300-309: Structured Output errors (JSON Schema validation, extraction, repair)
38//! - NIKA-310-319: Course errors (course system, exercises, progress)
39
40use crate::mcp::types::McpErrorCode;
41use crate::serde_yaml;
42use miette::Diagnostic;
43use thiserror::Error;
44
45pub type Result<T> = std::result::Result<T, NikaError>;
46
47/// Format schema validation errors for display
48fn format_schema_errors(errors: &[crate::ast::schema_validator::SchemaError]) -> String {
49    if errors.is_empty() {
50        return "no errors".to_string();
51    }
52    if errors.len() == 1 {
53        return errors[0].message.clone();
54    }
55    format!(
56        "{} errors: {}",
57        errors.len(),
58        errors
59            .iter()
60            .map(|e| format!("[{}] {}", e.path, e.message))
61            .collect::<Vec<_>>()
62            .join("; ")
63    )
64}
65
66/// Format structured output validation errors for display
67fn format_validation_errors_short(errors: &[String]) -> String {
68    if errors.is_empty() {
69        return "no errors".to_string();
70    }
71    if errors.len() == 1 {
72        return errors[0].clone();
73    }
74    format!("{} errors: {}", errors.len(), errors.join("; "))
75}
76
77/// Trait for errors that provide fix suggestions
78pub trait FixSuggestion {
79    fn fix_suggestion(&self) -> Option<&str>;
80}
81
82/// All error variants are part of the public API.
83///
84/// Implements both `thiserror::Error` for std error compatibility
85/// and `miette::Diagnostic` for fancy terminal error display.
86#[derive(Error, Debug, Diagnostic)]
87#[diagnostic(url(docsrs))]
88pub enum NikaError {
89    // ═══════════════════════════════════════════
90    // WORKFLOW ERRORS (000-009)
91    // ═══════════════════════════════════════════
92    #[error("[NIKA-001] Failed to parse workflow: {details}")]
93    #[diagnostic(
94        code(nika::parse_error),
95        help("Check YAML syntax: indentation and quoting")
96    )]
97    ParseError { details: String },
98
99    #[error("[NIKA-002] Invalid schema version: {version}")]
100    #[diagnostic(
101        code(nika::invalid_schema_version),
102        help("Use 'nika/workflow@0.12' as the schema version")
103    )]
104    InvalidSchemaVersion { version: String },
105
106    #[error("[NIKA-003] Workflow file not found: {path}")]
107    #[diagnostic(code(nika::workflow_not_found), help("Check the file path exists"))]
108    WorkflowNotFound { path: String },
109
110    #[error("[NIKA-004] Workflow validation failed: {reason}")]
111    #[diagnostic(
112        code(nika::validation_error),
113        help("Check workflow structure matches schema")
114    )]
115    ValidationError { reason: String },
116
117    #[error("[NIKA-005] Schema validation failed: {}", format_schema_errors(.errors))]
118    #[diagnostic(
119        code(nika::schema_validation_failed),
120        help("Check YAML against schemas/nika-workflow.schema.json")
121    )]
122    SchemaValidationFailed {
123        errors: Vec<crate::ast::schema_validator::SchemaError>,
124    },
125
126    #[error("[NIKA-006] Could not determine home directory")]
127    #[diagnostic(
128        code(nika::home_directory_not_found),
129        help("Set the NIKA_HOME environment variable to specify the Nika home directory")
130    )]
131    HomeDirectoryNotFound,
132
133    // ═══════════════════════════════════════════
134    // SCHEMA ERRORS (010-019)
135    // ═══════════════════════════════════════════
136    #[error("[NIKA-013] Schema file not found for task '{task_id}': {path}")]
137    #[diagnostic(
138        code(nika::schema_file_not_found),
139        help("Ensure the schema file exists relative to the workflow file")
140    )]
141    SchemaFileNotFound { task_id: String, path: String },
142
143    #[error("[NIKA-014] Invalid JSON in schema file for task '{task_id}': {path}: {reason}")]
144    #[diagnostic(
145        code(nika::schema_file_invalid),
146        help("Ensure the schema file contains valid JSON")
147    )]
148    SchemaFileInvalid {
149        task_id: String,
150        path: String,
151        reason: String,
152    },
153
154    // ═══════════════════════════════════════════
155    // DAG ERRORS (020-029)
156    // ═══════════════════════════════════════════
157    #[error("[NIKA-020] Cycle detected in DAG: {cycle}")]
158    CycleDetected { cycle: String },
159
160    #[error("[NIKA-021] Missing dependency: task '{task_id}' depends on unknown '{dep_id}'")]
161    MissingDependency { task_id: String, dep_id: String },
162
163    #[error("[NIKA-022] Duplicate task ID: '{task_id}' appears multiple times in workflow")]
164    #[diagnostic(
165        code(nika::duplicate_task_id),
166        help("Each task must have a unique ID. Rename one of the duplicate tasks.")
167    )]
168    DuplicateTaskId { task_id: String },
169
170    #[error("[NIKA-026] Dependency chain failed: {count} task(s) blocked by failed dependencies")]
171    #[diagnostic(
172        code(nika::dependency_chain_failed),
173        help("One or more upstream tasks failed, blocking downstream tasks. Fix the failing tasks first.")
174    )]
175    DependencyChainFailed {
176        /// Number of tasks blocked
177        count: usize,
178        /// List of blocked task IDs
179        blocked_tasks: Vec<String>,
180        /// The root failure that caused the chain
181        root_failure: Option<String>,
182    },
183
184    #[error("[NIKA-027] Task '{task_id}' was cancelled due to fail_fast")]
185    #[diagnostic(
186        code(nika::task_cancelled),
187        help("Another task in the for_each batch failed with fail_fast=true, causing remaining tasks to be cancelled.")
188    )]
189    TaskCancelled { task_id: String, reason: String },
190
191    // ═══════════════════════════════════════════
192    // PROVIDER ERRORS (030-039)
193    // ═══════════════════════════════════════════
194    #[error("[NIKA-030] Provider '{provider}' not configured")]
195    #[diagnostic(
196        code(nika::provider_not_configured),
197        help("Run: nika provider list — to see available providers and their setup status")
198    )]
199    ProviderNotConfigured { provider: String },
200
201    #[error("[NIKA-031] Provider API error: {message}")]
202    #[diagnostic(
203        code(nika::provider_api_error),
204        help("Check your API key and network connection. Run: nika provider test <provider>")
205    )]
206    ProviderApiError { message: String },
207
208    #[error("[NIKA-032] Missing API key for provider '{provider}'")]
209    #[diagnostic(
210        code(nika::missing_api_key),
211        help("Run: nika provider list — to see required env vars for each provider")
212    )]
213    MissingApiKey { provider: String },
214
215    #[error("[NIKA-033] Invalid configuration: {message}")]
216    InvalidConfig { message: String },
217
218    // ═══════════════════════════════════════════
219    // TEMPLATE/BINDING ERRORS (040-049)
220    // ═══════════════════════════════════════════
221    /// Simple execution error (catch-all for genuinely misc errors)
222    #[error("[NIKA-096] Execution error: {0}")]
223    Execution(String),
224
225    #[error("[NIKA-041] Template error in '{template}': {reason}")]
226    TemplateError { template: String, reason: String },
227
228    /// [NIKA-044] Exec verb command error (spawn, cwd, shell)
229    #[error("[NIKA-044] Exec error: {reason}")]
230    #[diagnostic(
231        code(nika::exec_error),
232        help("Check the command, working directory, and shell configuration")
233    )]
234    ExecError { reason: String },
235
236    /// [NIKA-045] Fetch verb HTTP error (request, response, URL)
237    #[error("[NIKA-045] Fetch error: {reason}")]
238    #[diagnostic(
239        code(nika::fetch_error),
240        help("Check the URL, network connectivity, and response size limits")
241    )]
242    FetchError { reason: String },
243
244    /// [NIKA-046] Fetch extract mode error (readability, feed, CSS, markdown)
245    #[error("[NIKA-046] Extract error: {reason}")]
246    #[diagnostic(
247        code(nika::extract_error),
248        help("Check extract mode, selector syntax, and required features")
249    )]
250    ExtractError { reason: String },
251
252    /// [NIKA-047] Invoke parameter serialization error
253    #[error("[NIKA-047] Invoke params error: {reason}")]
254    #[diagnostic(
255        code(nika::invoke_param_error),
256        help("Check invoke params are valid JSON and template bindings resolve correctly")
257    )]
258    InvokeParamError { reason: String },
259
260    /// [NIKA-097] Workflow cancelled by user
261    #[error("[NIKA-097] Workflow cancelled: {phase}")]
262    #[diagnostic(
263        code(nika::workflow_cancelled),
264        help("The workflow was cancelled by user request or external signal")
265    )]
266    WorkflowCancelled { phase: String },
267
268    /// [NIKA-098] Task panicked during execution
269    #[error("[NIKA-098] Task panicked: {reason}")]
270    #[diagnostic(
271        code(nika::task_panicked),
272        help("A task thread panicked unexpectedly. Check for bugs in task logic.")
273    )]
274    TaskPanicked { reason: String },
275
276    #[error("[NIKA-042] Binding '{alias}' not found")]
277    BindingNotFound { alias: String },
278
279    #[error("[NIKA-043] Binding type mismatch at '{path}': expected {expected}, got {actual}")]
280    BindingTypeMismatch {
281        expected: String,
282        actual: String,
283        path: String,
284    },
285
286    // ═══════════════════════════════════════════
287    // PATH/TASK ERRORS (050-059)
288    // ═══════════════════════════════════════════
289    #[error("[NIKA-050] Invalid path syntax: {path}")]
290    InvalidPath { path: String },
291
292    #[error("[NIKA-052] Path '{path}' not found (task may not have JSON output)")]
293    PathNotFound { path: String },
294
295    #[error("[NIKA-053] Command blocked: '{command}' - {reason}")]
296    #[diagnostic(
297        code(nika::blocked_command),
298        help("Use shell: true to opt-in to shell execution, or use a different command")
299    )]
300    BlockedCommand { command: String, reason: String },
301
302    #[error("[NIKA-055] Invalid task ID '{id}': {reason}")]
303    InvalidTaskId { id: String, reason: String },
304
305    #[error("[NIKA-056] Invalid default value '{raw}': {reason}")]
306    InvalidDefault { raw: String, reason: String },
307
308    // ═══════════════════════════════════════════
309    // OUTPUT ERRORS (060-069)
310    // ═══════════════════════════════════════════
311    #[error("[NIKA-060] Invalid JSON output: {details}")]
312    InvalidJson { details: String },
313
314    #[error("[NIKA-061] Schema validation failed: {details}")]
315    SchemaFailed { details: String },
316
317    #[error("[NIKA-062] Serialization error: {details}")]
318    SerializationError { details: String },
319
320    // ═══════════════════════════════════════════
321    // BINDING VALIDATION (070-079)
322    // ═══════════════════════════════════════════
323    #[error("[NIKA-071] Unknown alias '{{{{with.{alias}}}}}' - not declared in with: block")]
324    UnknownAlias { alias: String, task_id: String },
325
326    #[error("[NIKA-072] Null value at path '{path}' (strict mode)")]
327    NullValue { path: String, alias: String },
328
329    #[error("[NIKA-073] Cannot traverse '{segment}' on {value_type} (expected object/array)")]
330    InvalidTraversal {
331        segment: String,
332        value_type: String,
333        full_path: String,
334    },
335
336    #[error("[NIKA-074] Template parse error at position {position}: {details}")]
337    TemplateParse { position: usize, details: String },
338
339    // ═══════════════════════════════════════════
340    // DAG VALIDATION (080-089)
341    // ═══════════════════════════════════════════
342    #[error("[NIKA-080] with.{alias} references unknown task '{from_task}'")]
343    WithUnknownTask {
344        alias: String,
345        from_task: String,
346        task_id: String,
347    },
348
349    #[error("[NIKA-081] with.{alias}='{from_task}' is not upstream of task '{task_id}'")]
350    WithNotUpstream {
351        alias: String,
352        from_task: String,
353        task_id: String,
354    },
355
356    #[error("[NIKA-082] with.{alias}='{from_task}' creates circular dependency with '{task_id}'")]
357    WithCircularDep {
358        alias: String,
359        from_task: String,
360        task_id: String,
361    },
362
363    // ═══════════════════════════════════════════
364    // JSONPATH / IO ERRORS (090-099)
365    // ═══════════════════════════════════════════
366    /// [NIKA-083] Runtime deadlock -- no tasks ready but workflow not complete
367    #[error("[NIKA-083] Runtime deadlock: {details}")]
368    #[diagnostic(
369        code(nika::runtime_deadlock),
370        help("Check for circular dependencies or unresolvable task graph structures")
371    )]
372    RuntimeDeadlock { details: String },
373
374    #[error("[NIKA-090] JSONPath '{path}' uses unsupported syntax (use simple paths like $.a.b or $.a[0].b)")]
375    JsonPathUnsupported { path: String },
376
377    #[error("[NIKA-093] IO error: {0}")]
378    IoError(#[from] std::io::Error),
379
380    #[error("[NIKA-094] JSON error: {0}")]
381    JsonError(#[from] serde_json::Error),
382
383    #[error("[NIKA-095] YAML parse error: {0}")]
384    #[diagnostic(
385        code(nika::yaml_parse),
386        help(
387            "Check YAML syntax: indentation must be consistent, strings with special chars need quoting"
388        )
389    )]
390    YamlParse(#[from] serde_yaml::Error),
391
392    // ═══════════════════════════════════════════
393    // MCP ERRORS (100-109)
394    // ═══════════════════════════════════════════
395    #[error("[NIKA-100] MCP server '{name}' not connected")]
396    #[diagnostic(
397        code(nika::mcp_not_connected),
398        help("Check MCP server is running and configured correctly")
399    )]
400    McpNotConnected { name: String },
401
402    #[error("[NIKA-101] MCP server '{name}' failed to start: {reason}")]
403    #[diagnostic(
404        code(nika::mcp_start_error),
405        help("Check MCP command and args in workflow config")
406    )]
407    McpStartError { name: String, reason: String },
408
409    #[error("[NIKA-102] MCP tool '{tool}' call failed{}: {reason}", error_code.map(|c| format!(" ({})", c)).unwrap_or_default())]
410    #[diagnostic(
411        code(nika::mcp_tool_error),
412        help("Check tool parameters and MCP server logs")
413    )]
414    McpToolError {
415        tool: String,
416        reason: String,
417        /// JSON-RPC error code from MCP server
418        error_code: Option<McpErrorCode>,
419    },
420
421    #[error("[NIKA-103] MCP resource '{uri}' not found")]
422    McpResourceNotFound { uri: String },
423
424    #[error("[NIKA-104] MCP protocol error: {reason}")]
425    McpProtocolError { reason: String },
426
427    #[error("[NIKA-105] MCP server '{name}' not configured in workflow")]
428    McpNotConfigured { name: String },
429
430    #[error("[NIKA-106] MCP tool '{tool}' returned invalid response: {reason}")]
431    McpInvalidResponse { tool: String, reason: String },
432
433    #[error("[NIKA-107] MCP parameter validation failed for '{tool}': {details}")]
434    McpValidationFailed {
435        tool: String,
436        details: String,
437        /// Required fields that are missing
438        missing: Vec<String>,
439        /// Suggested corrections
440        suggestions: Vec<String>,
441    },
442
443    #[error("[NIKA-108] MCP schema error for '{tool}': {reason}")]
444    McpSchemaError { tool: String, reason: String },
445
446    #[error(
447        "[NIKA-109] MCP operation timed out for '{name}' ({operation}): exceeded {timeout_secs}s"
448    )]
449    McpTimeout {
450        name: String,
451        operation: String,
452        timeout_secs: u64,
453    },
454
455    // ═══════════════════════════════════════════
456    // AGENT ERRORS (110-119)
457    // ═══════════════════════════════════════════
458    #[error("[NIKA-113] Agent validation failed: {reason}")]
459    AgentValidationError { reason: String },
460
461    #[error("[NIKA-115] Agent execution failed for task '{task_id}': {reason}")]
462    AgentExecutionError { task_id: String, reason: String },
463
464    #[error("[NIKA-116] Extended thinking capture failed: {reason}")]
465    ThinkingCaptureFailed { reason: String },
466
467    #[error("[NIKA-112] Guardrail violation in task '{task_id}': {}", violations.join(", "))]
468    GuardrailViolation {
469        task_id: String,
470        violations: Vec<String>,
471    },
472
473    // ═══════════════════════════════════════════
474    // RESILIENCE ERRORS (120-129)
475    // ═══════════════════════════════════════════
476    #[error("[NIKA-121] Operation '{operation}' timed out after {duration_ms}ms")]
477    Timeout { operation: String, duration_ms: u64 },
478
479    #[error("[NIKA-110] MCP tool call '{tool}' failed: {reason}")]
480    McpToolCallFailed { tool: String, reason: String },
481
482    // ═══════════════════════════════════════════
483    // TUI ERRORS (130-139)
484    // ═══════════════════════════════════════════
485    #[error("[NIKA-130] TUI error: {reason}")]
486    TuiError { reason: String },
487
488    // ═══════════════════════════════════════════
489    // CONFIG ERRORS (135-139) - Range reassigned to avoid NIKA-140 collision
490    // Note: NIKA-140-149 is reserved for AST analyzer errors (see ast/analyzer/errors.rs)
491    // ═══════════════════════════════════════════
492    #[error("[NIKA-135] Config error: {reason}")]
493    ConfigError { reason: String },
494
495    // ═══════════════════════════════════════════
496    // STARTUP / POLICY / BOOT ERRORS (165-169)
497    // NIKA-160-164 are reserved for ParseErrorKind in nika-core.
498    // ═══════════════════════════════════════════
499    #[error("[NIKA-165] Policy violation: {reason}")]
500    #[diagnostic(
501        code(nika::policy_violation),
502        help("Check .nika/config.toml [policy] section or use --allow flag")
503    )]
504    PolicyViolation { reason: String },
505
506    #[error("[NIKA-166] Boot sequence failed in phase '{phase}': {reason}")]
507    #[diagnostic(
508        code(nika::boot_failed),
509        help("Run 'nika doctor' to diagnose boot issues")
510    )]
511    BootFailed { phase: String, reason: String },
512
513    #[error("[NIKA-167] Startup verification failed in '{phase}': {reason}")]
514    StartupError { phase: String, reason: String },
515
516    // ═══════════════════════════════════════════
517    // RUNTIME ERRORS (170-179)
518    // ═══════════════════════════════════════════
519    #[error(
520        "[NIKA-171] Decompose expansion timed out for task '{task_id}': exceeded {timeout_secs}s"
521    )]
522    #[diagnostic(
523        code(nika::decompose_timeout),
524        help("The decompose operation took too long. Consider reducing max_depth or max_items, or check MCP server performance.")
525    )]
526    DecomposeTimeout { task_id: String, timeout_secs: u64 },
527
528    // ═══════════════════════════════════════════
529    // TOOL ERRORS (200-209)
530    // ═══════════════════════════════════════════
531    #[error("[{code}] {message}")]
532    ToolError { code: String, message: String },
533
534    // ═══════════════════════════════════════════
535    // BUILTIN TOOL ERRORS (210-219)
536    // ═══════════════════════════════════════════
537    #[error("[NIKA-210] Builtin tool '{tool}' error: {reason}")]
538    #[diagnostic(
539        code(nika::builtin_tool_error),
540        help("Check builtin tool parameters and configuration")
541    )]
542    BuiltinToolError { tool: String, reason: String },
543
544    #[error("[NIKA-212] Builtin tool '{tool}' invalid parameters: {reason}")]
545    #[diagnostic(
546        code(nika::builtin_invalid_params),
547        help("Check the parameter format matches the expected JSON schema")
548    )]
549    BuiltinInvalidParams { tool: String, reason: String },
550
551    #[error("[NIKA-213] Assertion failed in nika:assert: {message}")]
552    #[diagnostic(code(nika::assertion_failed), help("The condition evaluated to false"))]
553    AssertionFailed { message: String, condition: String },
554
555    // ═══════════════════════════════════════════
556    // CONTEXT ERROR (250)
557    // ═══════════════════════════════════════════
558    #[error("[NIKA-250] Failed to load context file '{alias}' from '{path}': {reason}")]
559    #[diagnostic(
560        code(nika::context_load_error),
561        help("Check the file path exists and is readable")
562    )]
563    ContextLoadError {
564        alias: String,
565        path: String,
566        reason: String,
567    },
568
569    // ═══════════════════════════════════════════
570    // MEDIA ERRORS (251-259)
571    // ═══════════════════════════════════════════
572    /// Media pipeline error (NIKA-251..259)
573    /// Note: miette diagnostic codes are forwarded via MediaError's own Diagnostic derive.
574    #[error(transparent)]
575    MediaError(#[from] crate::media::error::MediaError),
576
577    // ═══════════════════════════════════════════
578    // PKG URI ERRORS (260-269)
579    // ═══════════════════════════════════════════
580    #[error("[NIKA-260] Invalid pkg: URI '{uri}': {reason}")]
581    #[diagnostic(
582        code(nika::invalid_pkg_uri),
583        help("Format: pkg:@scope/name@version/path or pkg:@scope/name/path")
584    )]
585    InvalidPkgUri { uri: String, reason: String },
586
587    #[error("[NIKA-261] Package '{name}@{version}' not found in registry")]
588    #[diagnostic(
589        code(nika::package_not_found),
590        help("Install the package with: nika pkg install {name}@{version}")
591    )]
592    PackageNotFound { name: String, version: String },
593
594    // ═══════════════════════════════════════════
595    // SKILL ERRORS (270-279)
596    // ═══════════════════════════════════════════
597    #[error("[NIKA-270] Failed to load skill '{skill}': {reason}")]
598    #[diagnostic(
599        code(nika::skill_load_error),
600        help("Ensure skill file exists and is readable. Check pkg: URI format if using packages.")
601    )]
602    SkillLoadError { skill: String, reason: String },
603
604    // ═══════════════════════════════════════════
605    // ARTIFACT ERRORS (280-289)
606    // ═══════════════════════════════════════════
607    #[error("[NIKA-280] Artifact path error for '{path}': {reason}")]
608    #[diagnostic(
609        code(nika::artifact_path_error),
610        help("Check the artifact path is within the workflow directory and does not contain path traversal patterns")
611    )]
612    ArtifactPathError { path: String, reason: String },
613
614    #[error("[NIKA-281] Artifact write failed for '{path}': {reason}")]
615    #[diagnostic(
616        code(nika::artifact_write_error),
617        help("Check file permissions and disk space")
618    )]
619    ArtifactWriteError { path: String, reason: String },
620
621    #[error("[NIKA-282] Artifact size exceeds limit: {size} bytes > {max_size} bytes")]
622    #[diagnostic(
623        code(nika::artifact_size_exceeded),
624        help("Increase artifacts.max_size in workflow or reduce output size")
625    )]
626    ArtifactSizeExceeded {
627        path: String,
628        size: u64,
629        max_size: u64,
630    },
631
632    #[error("[NIKA-285] Media store is locked: {reason}")]
633    #[diagnostic(
634        code(nika::media_store_locked),
635        help("A workflow is currently running. Use --force to override or wait for completion")
636    )]
637    MediaStoreLocked { reason: String },
638
639    // ═══════════════════════════════════════════
640    // STRUCTURED OUTPUT ERRORS (300-309)
641    // ═══════════════════════════════════════════
642    #[error(
643        "[NIKA-300] Structured output extraction failed for task '{task_id}' at {layer}: {reason}"
644    )]
645    #[diagnostic(
646        code(nika::structured_output_extraction_failed),
647        help("Check the LLM response format matches the expected JSON Schema")
648    )]
649    StructuredOutputExtractionFailed {
650        task_id: String,
651        layer: String,
652        reason: String,
653    },
654
655    #[error("[NIKA-301] Structured output validation failed for task '{task_id}' at {layer} (attempt {attempt}): {}", format_validation_errors_short(.errors))]
656    #[diagnostic(
657        code(nika::structured_output_validation_failed),
658        help("Fix JSON output to match the declared schema")
659    )]
660    StructuredOutputValidationFailed {
661        task_id: String,
662        layer: String,
663        attempt: u32,
664        errors: Vec<String>,
665    },
666
667    #[error("[NIKA-302] Structured output repair failed for task '{task_id}': original errors: {original_errors:?}, repair errors: {repair_errors:?}")]
668    #[diagnostic(
669        code(nika::structured_output_repair_failed),
670        help("The LLM could not repair the output. Consider simplifying the schema or providing more context.")
671    )]
672    StructuredOutputRepairFailed {
673        task_id: String,
674        original_errors: Vec<String>,
675        repair_errors: Vec<String>,
676    },
677
678    #[error("[NIKA-303] Structured output failed after all {attempts} attempts for task '{task_id}': {}", format_validation_errors_short(.final_errors))]
679    #[diagnostic(
680        code(nika::structured_output_all_layers_failed),
681        help("All validation layers failed. Check your schema is valid and the prompt provides enough context for the LLM to generate conforming output.")
682    )]
683    StructuredOutputAllLayersFailed {
684        task_id: String,
685        attempts: u32,
686        final_errors: Vec<String>,
687    },
688
689    // ═══════════════════════════════════════════
690    // COURSE ERRORS (310-319)
691    // ═══════════════════════════════════════════
692    #[error("[NIKA-310] Course not found at '{path}'")]
693    #[diagnostic(
694        code(nika::course_not_found),
695        help("Run `nika init --course` to create a course")
696    )]
697    CourseNotFound { path: String },
698
699    #[error("[NIKA-311] Course exercise check failed for '{exercise}': {reason}")]
700    #[diagnostic(
701        code(nika::course_check_failed),
702        help("Review the exercise instructions and fix your workflow")
703    )]
704    CourseCheckFailed { exercise: String, reason: String },
705
706    #[error("[NIKA-312] Course level '{level}' is locked (complete level {prerequisite} first)")]
707    #[diagnostic(
708        code(nika::course_level_locked),
709        help("Complete all exercises in the prerequisite level before unlocking this one")
710    )]
711    CourseLevelLocked { level: String, prerequisite: u8 },
712
713    #[error("[NIKA-313] Course progress corrupted: {reason}")]
714    #[diagnostic(
715        code(nika::course_progress_corrupted),
716        help("Delete .nika/course-progress.json and restart the course")
717    )]
718    CourseProgressCorrupted { reason: String },
719
720    #[error("[NIKA-314] Course watch error: {reason}")]
721    #[diagnostic(
722        code(nika::course_watch_error),
723        help("Check file permissions and that the course directory exists")
724    )]
725    CourseWatchError { reason: String },
726}
727
728impl From<nika_core::error::CoreError> for NikaError {
729    fn from(e: nika_core::error::CoreError) -> Self {
730        match e {
731            nika_core::error::CoreError::InvalidPath { path } => NikaError::InvalidPath { path },
732            nika_core::error::CoreError::InvalidDefault { raw, reason } => {
733                NikaError::InvalidDefault { raw, reason }
734            }
735            nika_core::error::CoreError::ValidationError { reason } => {
736                NikaError::ValidationError { reason }
737            }
738        }
739    }
740}
741
742impl From<nika_event::EventError> for NikaError {
743    fn from(e: nika_event::EventError) -> Self {
744        match e {
745            nika_event::EventError::TraceWrite(io) => NikaError::IoError(io),
746            nika_event::EventError::Serialization(json) => NikaError::JsonError(json),
747        }
748    }
749}
750
751impl From<nika_mcp::McpError> for NikaError {
752    fn from(e: nika_mcp::McpError) -> Self {
753        match e {
754            nika_mcp::McpError::McpNotConnected { name } => NikaError::McpNotConnected { name },
755            nika_mcp::McpError::McpStartError { name, reason } => {
756                NikaError::McpStartError { name, reason }
757            }
758            nika_mcp::McpError::McpToolError {
759                tool,
760                reason,
761                error_code,
762            } => NikaError::McpToolError {
763                tool,
764                reason,
765                error_code,
766            },
767            nika_mcp::McpError::McpResourceNotFound { uri } => {
768                NikaError::McpResourceNotFound { uri }
769            }
770            nika_mcp::McpError::McpProtocolError { reason } => {
771                NikaError::McpProtocolError { reason }
772            }
773            nika_mcp::McpError::McpNotConfigured { name } => NikaError::McpNotConfigured { name },
774            nika_mcp::McpError::McpInvalidResponse { tool, reason } => {
775                NikaError::McpInvalidResponse { tool, reason }
776            }
777            nika_mcp::McpError::McpValidationFailed {
778                tool,
779                details,
780                missing,
781                suggestions,
782            } => NikaError::McpValidationFailed {
783                tool,
784                details,
785                missing,
786                suggestions,
787            },
788            nika_mcp::McpError::McpSchemaError { tool, reason } => {
789                NikaError::McpSchemaError { tool, reason }
790            }
791            nika_mcp::McpError::McpTimeout {
792                name,
793                operation,
794                timeout_secs,
795            } => NikaError::McpTimeout {
796                name,
797                operation,
798                timeout_secs,
799            },
800            nika_mcp::McpError::McpToolCallFailed { tool, reason } => {
801                NikaError::McpToolCallFailed { tool, reason }
802            }
803            nika_mcp::McpError::ConfigError { reason } => NikaError::ConfigError { reason },
804            nika_mcp::McpError::ValidationError { reason } => NikaError::ValidationError { reason },
805            nika_mcp::McpError::ParseError { details } => NikaError::ParseError { details },
806            nika_mcp::McpError::Io(e) => NikaError::IoError(e),
807            nika_mcp::McpError::Json(e) => NikaError::JsonError(e),
808        }
809    }
810}
811
812impl From<nika_media::tools::error::MediaToolError> for NikaError {
813    fn from(e: nika_media::tools::error::MediaToolError) -> Self {
814        match e {
815            nika_media::tools::error::MediaToolError::ToolError { tool, reason } => {
816                NikaError::BuiltinToolError {
817                    tool: format!("nika:{tool}"),
818                    reason,
819                }
820            }
821            nika_media::tools::error::MediaToolError::UnsupportedFormat { tool, mime } => {
822                NikaError::BuiltinToolError {
823                    tool: format!("nika:{tool}"),
824                    reason: format!("[NIKA-291] unsupported format '{mime}'"),
825                }
826            }
827            nika_media::tools::error::MediaToolError::DependencyMissing { tool, feature } => {
828                NikaError::BuiltinToolError {
829                    tool: format!("nika:{tool}"),
830                    reason: format!("[NIKA-292] feature '{feature}' required"),
831                }
832            }
833            nika_media::tools::error::MediaToolError::Timeout { tool } => {
834                NikaError::BuiltinToolError {
835                    tool: format!("nika:{tool}"),
836                    reason: "[NIKA-293] operation timed out".to_string(),
837                }
838            }
839            nika_media::tools::error::MediaToolError::InvalidArgs { tool, reason } => {
840                NikaError::BuiltinInvalidParams {
841                    tool: format!("nika:{tool}"),
842                    reason: format!("[NIKA-294] {reason}"),
843                }
844            }
845            nika_media::tools::error::MediaToolError::PipelineStepFailed { step, reason } => {
846                NikaError::BuiltinToolError {
847                    tool: "nika:pipeline".to_string(),
848                    reason: format!("[NIKA-295] step {step} failed: {reason}"),
849                }
850            }
851            nika_media::tools::error::MediaToolError::PipelineEmpty => {
852                NikaError::BuiltinToolError {
853                    tool: "nika:pipeline".to_string(),
854                    reason: "[NIKA-296] pipeline has no steps".to_string(),
855                }
856            }
857            nika_media::tools::error::MediaToolError::SecurityViolation { tool, reason } => {
858                NikaError::BuiltinToolError {
859                    tool: format!("nika:{tool}"),
860                    reason: format!("[NIKA-297] security violation: {reason}"),
861                }
862            }
863            nika_media::tools::error::MediaToolError::Media(me) => me.into(),
864        }
865    }
866}
867
868impl From<nika_init::error::NikaInitError> for NikaError {
869    fn from(e: nika_init::error::NikaInitError) -> Self {
870        match e {
871            nika_init::error::NikaInitError::IoError(io) => NikaError::IoError(io),
872            nika_init::error::NikaInitError::ConfigError { reason } => {
873                NikaError::ConfigError { reason }
874            }
875            nika_init::error::NikaInitError::ValidationError { reason } => {
876                NikaError::ValidationError { reason }
877            }
878        }
879    }
880}
881
882impl NikaError {
883    /// Get the error code (e.g., "NIKA-001")
884    pub fn code(&self) -> &'static str {
885        match self {
886            // Workflow errors
887            Self::ParseError { .. } => "NIKA-001",
888            Self::InvalidSchemaVersion { .. } => "NIKA-002",
889            Self::WorkflowNotFound { .. } => "NIKA-003",
890            Self::ValidationError { .. } => "NIKA-004",
891            Self::SchemaValidationFailed { .. } => "NIKA-005",
892            Self::HomeDirectoryNotFound => "NIKA-006",
893            // Schema errors
894            Self::SchemaFileNotFound { .. } => "NIKA-013",
895            Self::SchemaFileInvalid { .. } => "NIKA-014",
896            // DAG errors
897            Self::CycleDetected { .. } => "NIKA-020",
898            Self::MissingDependency { .. } => "NIKA-021",
899            Self::DuplicateTaskId { .. } => "NIKA-022",
900            Self::DependencyChainFailed { .. } => "NIKA-026",
901            Self::TaskCancelled { .. } => "NIKA-027",
902            // Provider errors
903            Self::ProviderNotConfigured { .. } => "NIKA-030",
904            Self::ProviderApiError { .. } => "NIKA-031",
905            Self::MissingApiKey { .. } => "NIKA-032",
906            Self::InvalidConfig { .. } => "NIKA-033",
907            // Binding/Template errors
908            Self::Execution(_) => "NIKA-096",
909            Self::TemplateError { .. } => "NIKA-041",
910            Self::ExecError { .. } => "NIKA-044",
911            Self::FetchError { .. } => "NIKA-045",
912            Self::ExtractError { .. } => "NIKA-046",
913            Self::InvokeParamError { .. } => "NIKA-047",
914            Self::WorkflowCancelled { .. } => "NIKA-097",
915            Self::TaskPanicked { .. } => "NIKA-098",
916            Self::BindingNotFound { .. } => "NIKA-042",
917            Self::BindingTypeMismatch { .. } => "NIKA-043",
918            // Path/Task errors
919            Self::InvalidPath { .. } => "NIKA-050",
920            Self::PathNotFound { .. } => "NIKA-052",
921            Self::BlockedCommand { .. } => "NIKA-053",
922            Self::InvalidTaskId { .. } => "NIKA-055",
923            Self::InvalidDefault { .. } => "NIKA-056",
924            // Output errors
925            Self::InvalidJson { .. } => "NIKA-060",
926            Self::SchemaFailed { .. } => "NIKA-061",
927            Self::SerializationError { .. } => "NIKA-062",
928            // With block errors
929            Self::UnknownAlias { .. } => "NIKA-071",
930            Self::NullValue { .. } => "NIKA-072",
931            Self::InvalidTraversal { .. } => "NIKA-073",
932            Self::TemplateParse { .. } => "NIKA-074",
933            // DAG validation errors
934            Self::WithUnknownTask { .. } => "NIKA-080",
935            Self::WithNotUpstream { .. } => "NIKA-081",
936            Self::WithCircularDep { .. } => "NIKA-082",
937            Self::RuntimeDeadlock { .. } => "NIKA-083",
938            // JSONPath/IO errors
939            Self::JsonPathUnsupported { .. } => "NIKA-090",
940            Self::IoError(_) => "NIKA-093",
941            Self::JsonError(_) => "NIKA-094",
942            Self::YamlParse(_) => "NIKA-095",
943            // MCP errors
944            Self::McpNotConnected { .. } => "NIKA-100",
945            Self::McpStartError { .. } => "NIKA-101",
946            Self::McpToolError { .. } => "NIKA-102",
947            Self::McpResourceNotFound { .. } => "NIKA-103",
948            Self::McpProtocolError { .. } => "NIKA-104",
949            Self::McpNotConfigured { .. } => "NIKA-105",
950            Self::McpInvalidResponse { .. } => "NIKA-106",
951            Self::McpValidationFailed { .. } => "NIKA-107",
952            Self::McpSchemaError { .. } => "NIKA-108",
953            Self::McpTimeout { .. } => "NIKA-109",
954            // Agent errors
955            Self::AgentValidationError { .. } => "NIKA-113",
956            Self::AgentExecutionError { .. } => "NIKA-115",
957            Self::ThinkingCaptureFailed { .. } => "NIKA-116",
958            Self::GuardrailViolation { .. } => "NIKA-112",
959            // Resilience errors
960            Self::Timeout { .. } => "NIKA-121",
961            Self::McpToolCallFailed { .. } => "NIKA-110",
962            // TUI errors
963            Self::TuiError { .. } => "NIKA-130",
964            // Config errors
965            Self::ConfigError { .. } => "NIKA-135",
966            // Startup errors
967            Self::StartupError { .. } => "NIKA-167",
968            // Tool errors (code is dynamic)
969            Self::ToolError { .. } => "NIKA-200",
970            // Builtin tool errors
971            Self::BuiltinToolError { .. } => "NIKA-210",
972            Self::BuiltinInvalidParams { .. } => "NIKA-212",
973            Self::AssertionFailed { .. } => "NIKA-213",
974            // Context errors
975            Self::ContextLoadError { .. } => "NIKA-250",
976            // Media errors
977            Self::MediaError(e) => e.code(),
978            // Pkg URI errors
979            Self::InvalidPkgUri { .. } => "NIKA-260",
980            // Package errors
981            Self::PackageNotFound { .. } => "NIKA-261",
982
983            // Skill errors
984            Self::SkillLoadError { .. } => "NIKA-270",
985            // Artifact errors
986            Self::ArtifactPathError { .. } => "NIKA-280",
987            Self::ArtifactWriteError { .. } => "NIKA-281",
988            Self::ArtifactSizeExceeded { .. } => "NIKA-282",
989            Self::MediaStoreLocked { .. } => "NIKA-285",
990            // Structured Output errors
991            Self::StructuredOutputExtractionFailed { .. } => "NIKA-300",
992            Self::StructuredOutputValidationFailed { .. } => "NIKA-301",
993            Self::StructuredOutputRepairFailed { .. } => "NIKA-302",
994            Self::StructuredOutputAllLayersFailed { .. } => "NIKA-303",
995            // Course errors
996            Self::CourseNotFound { .. } => "NIKA-310",
997            Self::CourseCheckFailed { .. } => "NIKA-311",
998            Self::CourseLevelLocked { .. } => "NIKA-312",
999            Self::CourseProgressCorrupted { .. } => "NIKA-313",
1000            Self::CourseWatchError { .. } => "NIKA-314",
1001            // Policy errors (renumbered from 160/161 to avoid ParseErrorKind collision)
1002            Self::PolicyViolation { .. } => "NIKA-165",
1003            Self::BootFailed { .. } => "NIKA-166",
1004            // Runtime errors
1005            Self::DecomposeTimeout { .. } => "NIKA-171",
1006        }
1007    }
1008
1009    /// Check if error is recoverable (can be retried)
1010    pub fn is_recoverable(&self) -> bool {
1011        match self {
1012            Self::McpNotConnected { .. }
1013            | Self::ProviderApiError { .. }
1014            | Self::McpToolError { .. }
1015            | Self::Timeout { .. }
1016            | Self::McpTimeout { .. }
1017            | Self::McpToolCallFailed { .. }
1018            // Fetch errors are transient HTTP issues (retryable)
1019            | Self::FetchError { .. }
1020            // Structured output errors that can be retried
1021            | Self::StructuredOutputExtractionFailed { .. }
1022            | Self::StructuredOutputValidationFailed { .. }
1023            | Self::StructuredOutputRepairFailed { .. } => true,
1024            // Delegate to MediaError's own is_recoverable (only I/O errors)
1025            Self::MediaError(e) => e.is_recoverable(),
1026            _ => false,
1027        }
1028    }
1029}
1030
1031impl FixSuggestion for NikaError {
1032    fn fix_suggestion(&self) -> Option<&str> {
1033        match self {
1034            NikaError::ParseError { .. } => Some("Check YAML syntax: indentation and quoting"),
1035            NikaError::InvalidSchemaVersion { .. } => {
1036                Some("Use 'nika/workflow@0.12' as the schema version")
1037            }
1038            NikaError::WorkflowNotFound { .. } => Some("Check the file path exists"),
1039            NikaError::ValidationError { .. } => Some("Check workflow structure matches schema"),
1040            NikaError::SchemaValidationFailed { .. } => {
1041                Some("Check YAML against schemas/nika-workflow.schema.json")
1042            }
1043            NikaError::HomeDirectoryNotFound => {
1044                Some("Set NIKA_HOME environment variable to specify Nika home directory")
1045            }
1046            NikaError::YamlParse(_) => Some("Check YAML syntax: indentation and quoting"),
1047            NikaError::SchemaFileNotFound { .. } => {
1048                Some("Check the schema file path is correct relative to the workflow file")
1049            }
1050            NikaError::SchemaFileInvalid { .. } => {
1051                Some("Ensure the schema file contains valid JSON (not YAML)")
1052            }
1053            NikaError::CycleDetected { .. } => {
1054                Some("Remove circular dependencies from your workflow")
1055            }
1056            NikaError::MissingDependency { .. } => {
1057                Some("Add the missing task or fix the dependency reference")
1058            }
1059            NikaError::ProviderNotConfigured { .. } => {
1060                Some("Add provider configuration to your workflow")
1061            }
1062            NikaError::ProviderApiError { .. } => Some("Check API key and provider availability"),
1063            NikaError::MissingApiKey { .. } => {
1064                Some("Set the API key env var (ANTHROPIC_API_KEY or OPENAI_API_KEY)")
1065            }
1066            NikaError::InvalidConfig { .. } => Some("Check configuration value is valid"),
1067            NikaError::Execution(_) => Some("Check command/URL is valid"),
1068            NikaError::ExecError { .. } => {
1069                Some("Check command syntax, working directory, and shell availability")
1070            }
1071            NikaError::FetchError { .. } => {
1072                Some("Check URL, network connectivity, and response size limits")
1073            }
1074            NikaError::ExtractError { .. } => {
1075                Some("Check extract mode name, CSS selector syntax, or enable required feature")
1076            }
1077            NikaError::InvokeParamError { .. } => {
1078                Some("Check invoke params are valid JSON and template bindings resolve correctly")
1079            }
1080            NikaError::WorkflowCancelled { .. } => {
1081                Some("The workflow was cancelled. No action needed.")
1082            }
1083            NikaError::TaskPanicked { .. } => {
1084                Some("A task panicked unexpectedly. This is likely a bug — check task logic.")
1085            }
1086            NikaError::TemplateError { .. } => Some("Use {{with.alias}} format with with: block"),
1087            NikaError::InvalidPath { .. } => Some("Use format: task_id.field.subfield"),
1088            NikaError::PathNotFound { .. } => Some("Add '?? default' or ensure task outputs JSON"),
1089            NikaError::BlockedCommand { .. } => {
1090                Some("Use shell: true to opt-in to shell execution, or use a different command")
1091            }
1092            NikaError::InvalidTaskId { .. } => {
1093                Some("Task IDs must be snake_case: lowercase letters, digits, underscores")
1094            }
1095            NikaError::InvalidDefault { .. } => {
1096                Some("Default values must be valid JSON. Strings must be quoted.")
1097            }
1098            NikaError::InvalidJson { .. } => Some("Ensure output is valid JSON"),
1099            NikaError::SchemaFailed { .. } => Some("Fix output to match declared schema"),
1100            NikaError::SerializationError { .. } => Some("Check data structure is serializable"),
1101            NikaError::UnknownAlias { .. } => {
1102                Some("Declare the alias in with: block before referencing")
1103            }
1104            NikaError::NullValue { .. } => {
1105                Some("Provide a default value or ensure non-null output")
1106            }
1107            NikaError::InvalidTraversal { .. } => {
1108                Some("Check the path - accessing field on non-object")
1109            }
1110            NikaError::TemplateParse { .. } => Some("Check template syntax: {{with.alias}}"),
1111            NikaError::WithUnknownTask { .. } => Some("Verify the task_id exists in your workflow"),
1112            NikaError::WithNotUpstream { .. } => {
1113                Some("Add depends_on: [source_task] to this task")
1114            }
1115            NikaError::WithCircularDep { .. } => Some("Remove the circular dependency"),
1116            NikaError::RuntimeDeadlock { .. } => {
1117                Some("Check for circular dependencies or unresolvable task graph structures")
1118            }
1119            NikaError::DependencyChainFailed { .. } => {
1120                Some("Fix the root task failure, then re-run the workflow")
1121            }
1122            NikaError::JsonPathUnsupported { .. } => Some("Use simple paths like $.field.subfield"),
1123            NikaError::IoError(_) => Some("Check file path and permissions"),
1124            NikaError::JsonError(_) => Some("Check JSON syntax"),
1125            // MCP errors
1126            NikaError::McpNotConnected { .. } => {
1127                Some("Check MCP server is running and configured correctly")
1128            }
1129            NikaError::McpStartError { .. } => {
1130                Some("Check MCP command and args in workflow config")
1131            }
1132            NikaError::McpToolError { .. } => Some("Check tool parameters and MCP server logs"),
1133            NikaError::McpResourceNotFound { .. } => Some("Verify the resource URI exists"),
1134            NikaError::McpProtocolError { .. } => Some("Check MCP server compatibility"),
1135            NikaError::McpNotConfigured { .. } => {
1136                Some("Add MCP server config to workflow 'mcp:' section")
1137            }
1138            NikaError::McpInvalidResponse { .. } => {
1139                Some("Check MCP server is returning valid JSON responses")
1140            }
1141            NikaError::McpValidationFailed {
1142                missing,
1143                suggestions,
1144                ..
1145            } => {
1146                if !missing.is_empty() {
1147                    Some("Add the required fields to your params")
1148                } else if !suggestions.is_empty() {
1149                    Some("Check spelling of field names")
1150                } else {
1151                    Some("Review the tool's parameter schema")
1152                }
1153            }
1154            NikaError::McpSchemaError { .. } => Some("Check MCP server's tool schema definitions"),
1155            // Binding errors (decompose)
1156            NikaError::BindingNotFound { .. } => {
1157                Some("Verify the binding alias exists in with: block or task outputs")
1158            }
1159            NikaError::BindingTypeMismatch { .. } => {
1160                Some("Check binding value type matches expected type")
1161            }
1162            // Agent errors
1163            NikaError::AgentValidationError { .. } => {
1164                Some("Check agent prompt is not empty and max_turns is valid (1-100)")
1165            }
1166            NikaError::AgentExecutionError { .. } => {
1167                Some("Check LLM provider API key and network connectivity")
1168            }
1169            NikaError::ThinkingCaptureFailed { .. } => {
1170                Some("Check Claude API response and streaming connection")
1171            }
1172            NikaError::GuardrailViolation { .. } => {
1173                Some("One or more guardrails failed. Check guardrail config or adjust on_failure action")
1174            }
1175            // Resilience errors
1176            NikaError::Timeout { .. } => Some("Increase timeout or check for slow operations"),
1177            NikaError::McpTimeout { .. } => {
1178                Some("MCP server is slow or unresponsive. Check network and server health.")
1179            }
1180            NikaError::McpToolCallFailed { .. } => {
1181                Some("Check MCP tool parameters and server logs")
1182            }
1183            // TUI errors
1184            NikaError::TuiError { .. } => Some("Check terminal compatibility and size"),
1185            // Config errors
1186            NikaError::ConfigError { .. } => {
1187                Some("Check ~/.config/nika/config.toml for syntax errors")
1188            }
1189            // Startup errors
1190            NikaError::StartupError { .. } => Some(
1191                "Check directory permissions and run 'nika init' to create required directories",
1192            ),
1193            // Tool errors
1194            NikaError::ToolError { .. } => {
1195                Some("Check file path and permissions. Use Read before Edit.")
1196            }
1197            // Builtin tool errors
1198            NikaError::BuiltinToolError { .. } => {
1199                Some("Check builtin tool parameters and configuration")
1200            }
1201            NikaError::BuiltinInvalidParams { .. } => {
1202                Some("Check the parameter format matches the expected JSON schema")
1203            }
1204            NikaError::AssertionFailed { .. } => Some("The condition evaluated to false"),
1205            // Context errors
1206            NikaError::ContextLoadError { .. } => {
1207                Some("Check the file path exists and is readable")
1208            }
1209            // Media errors
1210            NikaError::MediaError(_) => {
1211                Some("Check media content and CAS store configuration")
1212            }
1213            // Pkg URI errors
1214            NikaError::InvalidPkgUri { .. } => Some(
1215                "Use format: pkg:@scope/name@version/path (e.g., pkg:@supernovae/skills@1.0.0/rust.md)",
1216            ),
1217            // Package errors
1218            NikaError::PackageNotFound { .. } => Some(
1219                "Check package name and version. Run 'nika pkg list' to see installed packages.",
1220            ),
1221            // Policy errors
1222            NikaError::PolicyViolation { .. } => Some(
1223                "This action was blocked by security policy. Check .nika/config.toml policy section.",
1224            ),
1225            NikaError::BootFailed { .. } => {
1226                Some("Boot sequence failed. Run 'nika doctor' to diagnose.")
1227            }
1228            // Skill errors
1229            NikaError::SkillLoadError { .. } => {
1230                Some("Ensure skill file exists and is readable. Check pkg: URI format if using packages.")
1231            }
1232            // Decompose timeout
1233            NikaError::DecomposeTimeout { .. } => {
1234                Some("Decompose expansion timed out. Try reducing max_items or check MCP server performance.")
1235            }
1236            // Artifact errors
1237            NikaError::ArtifactPathError { .. } => {
1238                Some("Check the artifact path is within the workflow directory and does not contain path traversal patterns")
1239            }
1240            NikaError::ArtifactWriteError { .. } => {
1241                Some("Check file permissions and disk space")
1242            }
1243            NikaError::ArtifactSizeExceeded { .. } => {
1244                Some("Increase artifacts.max_size in workflow or reduce output size")
1245            }
1246            NikaError::MediaStoreLocked { .. } => {
1247                Some("A workflow is currently running. Use --force to override or wait for completion")
1248            }
1249            // Structured Output errors
1250            NikaError::StructuredOutputExtractionFailed { .. } => {
1251                Some("Check the LLM response format matches the expected JSON Schema")
1252            }
1253            NikaError::StructuredOutputValidationFailed { .. } => {
1254                Some("Fix JSON output to match the declared schema. Check required fields and types.")
1255            }
1256            NikaError::StructuredOutputRepairFailed { .. } => {
1257                Some("The LLM could not repair the output. Consider simplifying the schema or providing more context.")
1258            }
1259            NikaError::StructuredOutputAllLayersFailed { .. } => {
1260                Some("All validation layers failed. Check your schema is valid and the prompt provides enough context for the LLM to generate conforming output.")
1261            }
1262            NikaError::TaskCancelled { .. } => {
1263                Some("Task was cancelled. Check workflow execution logs.")
1264            }
1265            // Duplicate task ID
1266            NikaError::DuplicateTaskId { .. } => {
1267                Some("Each task must have a unique ID. Rename one of the duplicate tasks.")
1268            }
1269            // Course errors
1270            NikaError::CourseNotFound { .. } => {
1271                Some("Run `nika init --course` to create a course")
1272            }
1273            NikaError::CourseCheckFailed { .. } => {
1274                Some("Review the exercise instructions and fix your workflow")
1275            }
1276            NikaError::CourseLevelLocked { .. } => {
1277                Some("Complete all exercises in the prerequisite level before unlocking this one")
1278            }
1279            NikaError::CourseProgressCorrupted { .. } => {
1280                Some("Delete .nika/course-progress.json and restart the course")
1281            }
1282            NikaError::CourseWatchError { .. } => {
1283                Some("Check file permissions and that the course directory exists")
1284            }
1285        }
1286    }
1287}
1288
1289#[cfg(test)]
1290mod tests {
1291    use super::*;
1292    use crate::serde_yaml;
1293
1294    // ═══════════════════════════════════════════════════════════════════════════
1295    // WORKFLOW ERRORS (000-009)
1296    // ═══════════════════════════════════════════════════════════════════════════
1297
1298    #[test]
1299    fn test_parse_error_code_and_display() {
1300        let err = NikaError::ParseError {
1301            details: "unexpected token at line 5".to_string(),
1302        };
1303        assert_eq!(err.code(), "NIKA-001");
1304        let msg = err.to_string();
1305        assert!(msg.contains("[NIKA-001]"));
1306        assert!(msg.contains("unexpected token"));
1307    }
1308
1309    #[test]
1310    fn test_parse_error_fix_suggestion() {
1311        let err = NikaError::ParseError {
1312            details: "bad yaml".to_string(),
1313        };
1314        let suggestion = <NikaError as FixSuggestion>::fix_suggestion(&err);
1315        assert!(suggestion.is_some());
1316        assert!(suggestion.unwrap().contains("YAML syntax"));
1317    }
1318
1319    #[test]
1320    fn test_invalid_schema_version_error() {
1321        let err = NikaError::InvalidSchemaVersion {
1322            version: "0.1".to_string(),
1323        };
1324        assert_eq!(err.code(), "NIKA-002");
1325        let msg = err.to_string();
1326        assert!(msg.contains("[NIKA-002]"));
1327        assert!(msg.contains("0.1"));
1328    }
1329
1330    #[test]
1331    fn test_workflow_not_found_error() {
1332        let err = NikaError::WorkflowNotFound {
1333            path: "/path/to/missing.yaml".to_string(),
1334        };
1335        assert_eq!(err.code(), "NIKA-003");
1336        let msg = err.to_string();
1337        assert!(msg.contains("[NIKA-003]"));
1338        assert!(msg.contains("missing.yaml"));
1339    }
1340
1341    #[test]
1342    fn test_validation_error() {
1343        let err = NikaError::ValidationError {
1344            reason: "missing required field 'tasks'".to_string(),
1345        };
1346        assert_eq!(err.code(), "NIKA-004");
1347        let msg = err.to_string();
1348        assert!(msg.contains("[NIKA-004]"));
1349    }
1350
1351    #[test]
1352    fn test_schema_validation_failed_error_empty() {
1353        let err = NikaError::SchemaValidationFailed { errors: vec![] };
1354        assert_eq!(err.code(), "NIKA-005");
1355        let msg = err.to_string();
1356        assert!(msg.contains("[NIKA-005]"));
1357        assert!(msg.contains("no errors"));
1358    }
1359
1360    // ═══════════════════════════════════════════════════════════════════════════
1361    // SCHEMA ERRORS (010-019)
1362    // ═══════════════════════════════════════════════════════════════════════════
1363
1364    #[test]
1365    fn test_schema_file_not_found_error() {
1366        let err = NikaError::SchemaFileNotFound {
1367            task_id: "extract".to_string(),
1368            path: "./schemas/user.json".to_string(),
1369        };
1370        assert_eq!(err.code(), "NIKA-013");
1371        let msg = err.to_string();
1372        assert!(msg.contains("[NIKA-013]"));
1373        assert!(msg.contains("extract"));
1374        assert!(msg.contains("./schemas/user.json"));
1375    }
1376
1377    #[test]
1378    fn test_schema_file_invalid_error() {
1379        let err = NikaError::SchemaFileInvalid {
1380            task_id: "generate".to_string(),
1381            path: "./schemas/broken.json".to_string(),
1382            reason: "expected value at line 1".to_string(),
1383        };
1384        assert_eq!(err.code(), "NIKA-014");
1385        let msg = err.to_string();
1386        assert!(msg.contains("[NIKA-014]"));
1387        assert!(msg.contains("generate"));
1388        assert!(msg.contains("broken.json"));
1389        assert!(msg.contains("expected value"));
1390    }
1391
1392    // ═══════════════════════════════════════════════════════════════════════════
1393    // DAG ERRORS (020-029)
1394    // ═══════════════════════════════════════════════════════════════════════════
1395
1396    #[test]
1397    fn test_cycle_detected_error() {
1398        let err = NikaError::CycleDetected {
1399            cycle: "task1 -> task2 -> task1".to_string(),
1400        };
1401        assert_eq!(err.code(), "NIKA-020");
1402        let msg = err.to_string();
1403        assert!(msg.contains("[NIKA-020]"));
1404        assert!(msg.contains("task1"));
1405    }
1406
1407    #[test]
1408    fn test_missing_dependency_error() {
1409        let err = NikaError::MissingDependency {
1410            task_id: "step2".to_string(),
1411            dep_id: "step1".to_string(),
1412        };
1413        assert_eq!(err.code(), "NIKA-021");
1414        let msg = err.to_string();
1415        assert!(msg.contains("[NIKA-021]"));
1416        assert!(msg.contains("step2"));
1417        assert!(msg.contains("step1"));
1418    }
1419
1420    // ═══════════════════════════════════════════════════════════════════════════
1421    // PROVIDER ERRORS (030-039)
1422    // ═══════════════════════════════════════════════════════════════════════════
1423
1424    #[test]
1425    fn test_provider_not_configured_error() {
1426        let err = NikaError::ProviderNotConfigured {
1427            provider: "openai".to_string(),
1428        };
1429        assert_eq!(err.code(), "NIKA-030");
1430        let msg = err.to_string();
1431        assert!(msg.contains("[NIKA-030]"));
1432    }
1433
1434    #[test]
1435    fn test_provider_api_error() {
1436        let err = NikaError::ProviderApiError {
1437            message: "Rate limit exceeded".to_string(),
1438        };
1439        assert_eq!(err.code(), "NIKA-031");
1440        let msg = err.to_string();
1441        assert!(msg.contains("[NIKA-031]"));
1442    }
1443
1444    #[test]
1445    fn test_missing_api_key_error() {
1446        let err = NikaError::MissingApiKey {
1447            provider: "anthropic".to_string(),
1448        };
1449        assert_eq!(err.code(), "NIKA-032");
1450        let msg = err.to_string();
1451        assert!(msg.contains("[NIKA-032]"));
1452        assert!(msg.contains("anthropic"));
1453    }
1454
1455    #[test]
1456    fn test_invalid_config_error() {
1457        let err = NikaError::InvalidConfig {
1458            message: "port must be > 0".to_string(),
1459        };
1460        assert_eq!(err.code(), "NIKA-033");
1461        let msg = err.to_string();
1462        assert!(msg.contains("[NIKA-033]"));
1463    }
1464
1465    // ═══════════════════════════════════════════════════════════════════════════
1466    // TEMPLATE/BINDING ERRORS (040-049)
1467    // ═══════════════════════════════════════════════════════════════════════════
1468
1469    #[test]
1470    fn test_execution_error() {
1471        let err = NikaError::Execution("command not found".to_string());
1472        assert_eq!(err.code(), "NIKA-096");
1473        let msg = err.to_string();
1474        assert!(msg.contains("[NIKA-096]"));
1475        assert!(msg.contains("Execution error"));
1476    }
1477
1478    #[test]
1479    fn test_exec_error() {
1480        let err = NikaError::ExecError {
1481            reason: "Failed to spawn command: not found".to_string(),
1482        };
1483        assert_eq!(err.code(), "NIKA-044");
1484        let msg = err.to_string();
1485        assert!(msg.contains("[NIKA-044]"));
1486        assert!(msg.contains("Exec error"));
1487        assert!(!err.is_recoverable());
1488    }
1489
1490    #[test]
1491    fn test_fetch_error() {
1492        let err = NikaError::FetchError {
1493            reason: "HTTP server error: 503".to_string(),
1494        };
1495        assert_eq!(err.code(), "NIKA-045");
1496        let msg = err.to_string();
1497        assert!(msg.contains("[NIKA-045]"));
1498        assert!(msg.contains("Fetch error"));
1499        assert!(err.is_recoverable());
1500    }
1501
1502    #[test]
1503    fn test_extract_error() {
1504        let err = NikaError::ExtractError {
1505            reason: "Invalid CSS selector: >>>".to_string(),
1506        };
1507        assert_eq!(err.code(), "NIKA-046");
1508        let msg = err.to_string();
1509        assert!(msg.contains("[NIKA-046]"));
1510        assert!(msg.contains("Extract error"));
1511        assert!(!err.is_recoverable());
1512    }
1513
1514    #[test]
1515    fn test_task_panicked() {
1516        let err = NikaError::TaskPanicked {
1517            reason: "index out of bounds".to_string(),
1518        };
1519        assert_eq!(err.code(), "NIKA-098");
1520        let msg = err.to_string();
1521        assert!(msg.contains("[NIKA-098]"));
1522        assert!(msg.contains("Task panicked"));
1523        assert!(!err.is_recoverable());
1524    }
1525
1526    #[test]
1527    fn test_template_error_with_path() {
1528        let err = NikaError::TemplateError {
1529            template: "{{with.result}}".to_string(),
1530            reason: "alias not in with block".to_string(),
1531        };
1532        assert_eq!(err.code(), "NIKA-041");
1533        let msg = err.to_string();
1534        assert!(msg.contains("[NIKA-041]"));
1535        assert!(msg.contains("result"));
1536    }
1537
1538    #[test]
1539    fn test_binding_not_found_error() {
1540        let err = NikaError::BindingNotFound {
1541            alias: "entity_data".to_string(),
1542        };
1543        assert_eq!(err.code(), "NIKA-042");
1544        let msg = err.to_string();
1545        assert!(msg.contains("[NIKA-042]"));
1546        assert!(msg.contains("entity_data"));
1547    }
1548
1549    #[test]
1550    fn test_binding_type_mismatch_error() {
1551        let err = NikaError::BindingTypeMismatch {
1552            expected: "string".to_string(),
1553            actual: "array".to_string(),
1554            path: "use.field.subfield".to_string(),
1555        };
1556        assert_eq!(err.code(), "NIKA-043");
1557        let msg = err.to_string();
1558        assert!(msg.contains("[NIKA-043]"));
1559        assert!(msg.contains("string"));
1560        assert!(msg.contains("array"));
1561    }
1562
1563    // ═══════════════════════════════════════════════════════════════════════════
1564    // PATH/TASK ERRORS (050-059)
1565    // ═══════════════════════════════════════════════════════════════════════════
1566
1567    #[test]
1568    fn test_invalid_path_error() {
1569        let err = NikaError::InvalidPath {
1570            path: "task1..field".to_string(),
1571        };
1572        assert_eq!(err.code(), "NIKA-050");
1573        let msg = err.to_string();
1574        assert!(msg.contains("[NIKA-050]"));
1575    }
1576
1577    #[test]
1578    fn test_path_not_found_error() {
1579        let err = NikaError::PathNotFound {
1580            path: "task.deeply.nested.field".to_string(),
1581        };
1582        assert_eq!(err.code(), "NIKA-052");
1583        let msg = err.to_string();
1584        assert!(msg.contains("[NIKA-052]"));
1585    }
1586
1587    #[test]
1588    fn test_invalid_task_id_error() {
1589        let err = NikaError::InvalidTaskId {
1590            id: "Invalid-Task-ID".to_string(),
1591            reason: "contains uppercase or hyphens".to_string(),
1592        };
1593        assert_eq!(err.code(), "NIKA-055");
1594        let msg = err.to_string();
1595        assert!(msg.contains("[NIKA-055]"));
1596    }
1597
1598    #[test]
1599    fn test_invalid_default_error() {
1600        let err = NikaError::InvalidDefault {
1601            raw: "not_quoted_string".to_string(),
1602            reason: "strings must be quoted".to_string(),
1603        };
1604        assert_eq!(err.code(), "NIKA-056");
1605        let msg = err.to_string();
1606        assert!(msg.contains("[NIKA-056]"));
1607    }
1608
1609    #[test]
1610    fn test_blocked_command_error() {
1611        let err = NikaError::BlockedCommand {
1612            command: "rm -rf /".to_string(),
1613            reason: "Destructive command blocked by security policy".to_string(),
1614        };
1615        assert_eq!(err.code(), "NIKA-053");
1616        let msg = err.to_string();
1617        assert!(msg.contains("[NIKA-053]"));
1618        assert!(msg.contains("rm -rf /"));
1619        assert!(msg.contains("blocked"));
1620    }
1621
1622    // ═══════════════════════════════════════════════════════════════════════════
1623    // OUTPUT ERRORS (060-069)
1624    // ═══════════════════════════════════════════════════════════════════════════
1625
1626    #[test]
1627    fn test_invalid_json_error() {
1628        let err = NikaError::InvalidJson {
1629            details: "trailing comma in object".to_string(),
1630        };
1631        assert_eq!(err.code(), "NIKA-060");
1632        let msg = err.to_string();
1633        assert!(msg.contains("[NIKA-060]"));
1634    }
1635
1636    #[test]
1637    fn test_schema_failed_error() {
1638        let err = NikaError::SchemaFailed {
1639            details: "missing required property 'id'".to_string(),
1640        };
1641        assert_eq!(err.code(), "NIKA-061");
1642        let msg = err.to_string();
1643        assert!(msg.contains("[NIKA-061]"));
1644    }
1645
1646    // ═══════════════════════════════════════════════════════════════════════════
1647    // WITH BLOCK VALIDATION (070-079)
1648    // ═══════════════════════════════════════════════════════════════════════════
1649
1650    #[test]
1651    fn test_unknown_alias_error() {
1652        let err = NikaError::UnknownAlias {
1653            alias: "undefined".to_string(),
1654            task_id: "current_task".to_string(),
1655        };
1656        assert_eq!(err.code(), "NIKA-071");
1657        let msg = err.to_string();
1658        assert!(msg.contains("[NIKA-071]"));
1659        assert!(msg.contains("undefined"));
1660    }
1661
1662    #[test]
1663    fn test_null_value_error() {
1664        let err = NikaError::NullValue {
1665            path: "task.field".to_string(),
1666            alias: "myalias".to_string(),
1667        };
1668        assert_eq!(err.code(), "NIKA-072");
1669        let msg = err.to_string();
1670        assert!(msg.contains("[NIKA-072]"));
1671    }
1672
1673    #[test]
1674    fn test_invalid_traversal_error() {
1675        let err = NikaError::InvalidTraversal {
1676            segment: "field".to_string(),
1677            value_type: "string".to_string(),
1678            full_path: "task.value.field".to_string(),
1679        };
1680        assert_eq!(err.code(), "NIKA-073");
1681        let msg = err.to_string();
1682        assert!(msg.contains("[NIKA-073]"));
1683        assert!(msg.contains("string"));
1684    }
1685
1686    #[test]
1687    fn test_template_parse_error() {
1688        let err = NikaError::TemplateParse {
1689            position: 10,
1690            details: "unexpected closing brace".to_string(),
1691        };
1692        assert_eq!(err.code(), "NIKA-074");
1693        let msg = err.to_string();
1694        assert!(msg.contains("[NIKA-074]"));
1695        assert!(msg.contains("10"));
1696    }
1697
1698    // ═══════════════════════════════════════════════════════════════════════════
1699    // DAG VALIDATION (080-089)
1700    // ═══════════════════════════════════════════════════════════════════════════
1701
1702    #[test]
1703    fn test_with_unknown_task_error() {
1704        let err = NikaError::WithUnknownTask {
1705            alias: "ctx".to_string(),
1706            from_task: "undefined".to_string(),
1707            task_id: "current".to_string(),
1708        };
1709        assert_eq!(err.code(), "NIKA-080");
1710        let msg = err.to_string();
1711        assert!(msg.contains("[NIKA-080]"));
1712        assert!(msg.contains("undefined"));
1713    }
1714
1715    #[test]
1716    fn test_with_not_upstream_error() {
1717        let err = NikaError::WithNotUpstream {
1718            alias: "ctx".to_string(),
1719            from_task: "task2".to_string(),
1720            task_id: "task1".to_string(),
1721        };
1722        assert_eq!(err.code(), "NIKA-081");
1723        let msg = err.to_string();
1724        assert!(msg.contains("[NIKA-081]"));
1725    }
1726
1727    #[test]
1728    fn test_with_circular_dep_error() {
1729        let err = NikaError::WithCircularDep {
1730            alias: "ctx".to_string(),
1731            from_task: "task1".to_string(),
1732            task_id: "task2".to_string(),
1733        };
1734        assert_eq!(err.code(), "NIKA-082");
1735        let msg = err.to_string();
1736        assert!(msg.contains("[NIKA-082]"));
1737        assert!(msg.contains("circular"));
1738    }
1739
1740    // ═══════════════════════════════════════════════════════════════════════════
1741    // JSONPATH / IO ERRORS (090-099)
1742    // ═══════════════════════════════════════════════════════════════════════════
1743
1744    #[test]
1745    fn test_jsonpath_unsupported_error() {
1746        let err = NikaError::JsonPathUnsupported {
1747            path: "$.deeply[*].nested.path".to_string(),
1748        };
1749        assert_eq!(err.code(), "NIKA-090");
1750        let msg = err.to_string();
1751        assert!(msg.contains("[NIKA-090]"));
1752    }
1753
1754    #[test]
1755    fn test_io_error_from_std() {
1756        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
1757        let err: NikaError = io_err.into();
1758        assert_eq!(err.code(), "NIKA-093");
1759        let msg = err.to_string();
1760        assert!(msg.contains("[NIKA-093]"));
1761    }
1762
1763    #[test]
1764    fn test_json_error_from_serde() {
1765        let json_str = "{invalid json";
1766        let json_err: serde_json::Result<serde_json::Value> = serde_json::from_str(json_str);
1767        if let Err(e) = json_err {
1768            let err: NikaError = e.into();
1769            assert_eq!(err.code(), "NIKA-094");
1770            let msg = err.to_string();
1771            assert!(msg.contains("[NIKA-094]"));
1772        }
1773    }
1774
1775    #[test]
1776    fn test_yaml_parse_error_from_serde() {
1777        let yaml_str = "invalid: yaml: syntax:";
1778        // Use serde_json::Value as target since serde-saphyr doesn't export Value type
1779        let yaml_err = serde_yaml::from_str::<serde_json::Value>(yaml_str);
1780        if let Err(e) = yaml_err {
1781            let err: NikaError = e.into();
1782            assert_eq!(err.code(), "NIKA-095");
1783            let msg = err.to_string();
1784            assert!(msg.contains("[NIKA-095]"));
1785        }
1786    }
1787
1788    // ═══════════════════════════════════════════════════════════════════════════
1789    // MCP ERRORS (100-109)
1790    // ═══════════════════════════════════════════════════════════════════════════
1791
1792    #[test]
1793    fn test_mcp_not_connected_error() {
1794        let err = NikaError::McpNotConnected {
1795            name: "novanet".to_string(),
1796        };
1797        assert_eq!(err.code(), "NIKA-100");
1798        let msg = err.to_string();
1799        assert!(msg.contains("[NIKA-100]"));
1800        assert!(msg.contains("novanet"));
1801    }
1802
1803    #[test]
1804    fn test_mcp_start_error() {
1805        let err = NikaError::McpStartError {
1806            name: "novanet".to_string(),
1807            reason: "port already in use".to_string(),
1808        };
1809        assert_eq!(err.code(), "NIKA-101");
1810        let msg = err.to_string();
1811        assert!(msg.contains("[NIKA-101]"));
1812    }
1813
1814    #[test]
1815    fn test_mcp_tool_error_without_code() {
1816        let err = NikaError::McpToolError {
1817            tool: "novanet_context".to_string(),
1818            reason: "invalid parameters".to_string(),
1819            error_code: None,
1820        };
1821        assert_eq!(err.code(), "NIKA-102");
1822        let msg = err.to_string();
1823        assert!(msg.contains("[NIKA-102]"));
1824        assert!(msg.contains("novanet_context"));
1825    }
1826
1827    #[test]
1828    fn test_mcp_tool_error_with_code() {
1829        let err = NikaError::McpToolError {
1830            tool: "novanet_describe".to_string(),
1831            reason: "entity not found".to_string(),
1832            error_code: Some(McpErrorCode::InvalidRequest),
1833        };
1834        assert_eq!(err.code(), "NIKA-102");
1835        let msg = err.to_string();
1836        assert!(msg.contains("[NIKA-102]"));
1837        // Error code description should be included in display
1838        // McpErrorCode::InvalidRequest displays as "The JSON sent is not a valid Request object (-32600)"
1839        assert!(msg.contains("Request") || msg.contains("-32600"));
1840    }
1841
1842    #[test]
1843    fn test_mcp_resource_not_found_error() {
1844        let err = NikaError::McpResourceNotFound {
1845            uri: "novanet://entity/qr-code".to_string(),
1846        };
1847        assert_eq!(err.code(), "NIKA-103");
1848        let msg = err.to_string();
1849        assert!(msg.contains("[NIKA-103]"));
1850    }
1851
1852    #[test]
1853    fn test_mcp_protocol_error() {
1854        let err = NikaError::McpProtocolError {
1855            reason: "JSON-RPC version mismatch".to_string(),
1856        };
1857        assert_eq!(err.code(), "NIKA-104");
1858        let msg = err.to_string();
1859        assert!(msg.contains("[NIKA-104]"));
1860    }
1861
1862    #[test]
1863    fn test_mcp_not_configured_error() {
1864        let err = NikaError::McpNotConfigured {
1865            name: "novanet".to_string(),
1866        };
1867        assert_eq!(err.code(), "NIKA-105");
1868        let msg = err.to_string();
1869        assert!(msg.contains("[NIKA-105]"));
1870    }
1871
1872    #[test]
1873    fn test_mcp_invalid_response_error() {
1874        let err = NikaError::McpInvalidResponse {
1875            tool: "novanet_search".to_string(),
1876            reason: "missing 'result' field".to_string(),
1877        };
1878        assert_eq!(err.code(), "NIKA-106");
1879        let msg = err.to_string();
1880        assert!(msg.contains("[NIKA-106]"));
1881    }
1882
1883    #[test]
1884    fn test_mcp_validation_failed_error() {
1885        let err = NikaError::McpValidationFailed {
1886            tool: "novanet_context".to_string(),
1887            details: "parameter validation failed".to_string(),
1888            missing: vec!["focus_key".to_string(), "locale".to_string()],
1889            suggestions: vec!["Check parameter names".to_string()],
1890        };
1891        assert_eq!(err.code(), "NIKA-107");
1892        let msg = err.to_string();
1893        assert!(msg.contains("[NIKA-107]"));
1894    }
1895
1896    #[test]
1897    fn test_mcp_schema_error() {
1898        let err = NikaError::McpSchemaError {
1899            tool: "novanet_context".to_string(),
1900            reason: "invalid property type in schema".to_string(),
1901        };
1902        assert_eq!(err.code(), "NIKA-108");
1903        let msg = err.to_string();
1904        assert!(msg.contains("[NIKA-108]"));
1905    }
1906
1907    #[test]
1908    fn test_mcp_timeout_error() {
1909        let err = NikaError::McpTimeout {
1910            name: "novanet".to_string(),
1911            operation: "novanet_context".to_string(),
1912            timeout_secs: 30,
1913        };
1914        assert_eq!(err.code(), "NIKA-109");
1915        let msg = err.to_string();
1916        assert!(msg.contains("[NIKA-109]"));
1917        assert!(msg.contains("30"));
1918    }
1919
1920    // ═══════════════════════════════════════════════════════════════════════════
1921    // AGENT ERRORS (110-119)
1922    // ═══════════════════════════════════════════════════════════════════════════
1923
1924    #[test]
1925    fn test_agent_validation_error() {
1926        let err = NikaError::AgentValidationError {
1927            reason: "empty prompt".to_string(),
1928        };
1929        assert_eq!(err.code(), "NIKA-113");
1930        let msg = err.to_string();
1931        assert!(msg.contains("[NIKA-113]"));
1932    }
1933
1934    #[test]
1935    fn test_agent_execution_error() {
1936        let err = NikaError::AgentExecutionError {
1937            task_id: "agent_task".to_string(),
1938            reason: "provider unreachable".to_string(),
1939        };
1940        assert_eq!(err.code(), "NIKA-115");
1941        let msg = err.to_string();
1942        assert!(msg.contains("[NIKA-115]"));
1943    }
1944
1945    #[test]
1946    fn test_thinking_capture_failed_error() {
1947        let err = NikaError::ThinkingCaptureFailed {
1948            reason: "streaming connection lost".to_string(),
1949        };
1950        assert_eq!(err.code(), "NIKA-116");
1951        let msg = err.to_string();
1952        assert!(msg.contains("[NIKA-116]"));
1953    }
1954
1955    // ═══════════════════════════════════════════════════════════════════════════
1956    // RESILIENCE ERRORS (120-129)
1957    // ═══════════════════════════════════════════════════════════════════════════
1958
1959    #[test]
1960    fn test_timeout_error() {
1961        let err = NikaError::Timeout {
1962            operation: "fetch_data".to_string(),
1963            duration_ms: 5000,
1964        };
1965        assert_eq!(err.code(), "NIKA-121");
1966        let msg = err.to_string();
1967        assert!(msg.contains("[NIKA-121]"));
1968        assert!(msg.contains("5000"));
1969    }
1970
1971    #[test]
1972    fn test_mcp_tool_call_failed_error() {
1973        let err = NikaError::McpToolCallFailed {
1974            tool: "novanet_audit".to_string(),
1975            reason: "malformed response".to_string(),
1976        };
1977        assert_eq!(err.code(), "NIKA-110");
1978        let msg = err.to_string();
1979        assert!(msg.contains("[NIKA-110]"));
1980    }
1981
1982    // ═══════════════════════════════════════════════════════════════════════════
1983    // TUI ERRORS (130-139)
1984    // ═══════════════════════════════════════════════════════════════════════════
1985
1986    #[test]
1987    fn test_tui_error() {
1988        let err = NikaError::TuiError {
1989            reason: "terminal size too small".to_string(),
1990        };
1991        assert_eq!(err.code(), "NIKA-130");
1992        let msg = err.to_string();
1993        assert!(msg.contains("[NIKA-130]"));
1994    }
1995
1996    // ═══════════════════════════════════════════════════════════════════════════
1997    // CONFIG ERRORS (135-139)
1998    // ═══════════════════════════════════════════════════════════════════════════
1999
2000    #[test]
2001    fn test_config_error() {
2002        let err = NikaError::ConfigError {
2003            reason: "invalid TOML syntax".to_string(),
2004        };
2005        assert_eq!(err.code(), "NIKA-135");
2006        let msg = err.to_string();
2007        assert!(msg.contains("[NIKA-135]"));
2008    }
2009
2010    // ═══════════════════════════════════════════════════════════════════════════
2011    // TOOL ERRORS (200-219)
2012    // ═══════════════════════════════════════════════════════════════════════════
2013
2014    #[test]
2015    fn test_tool_error() {
2016        let err = NikaError::ToolError {
2017            code: "TOOL-001".to_string(),
2018            message: "File not found".to_string(),
2019        };
2020        assert_eq!(err.code(), "NIKA-200");
2021        let msg = err.to_string();
2022        assert!(msg.contains("TOOL-001"));
2023        assert!(msg.contains("File not found"));
2024    }
2025
2026    // ═══════════════════════════════════════════════════════════════════════════
2027    // FIX SUGGESTION TRAIT TESTS
2028    // ═══════════════════════════════════════════════════════════════════════════
2029
2030    #[test]
2031    fn test_fix_suggestion_for_all_recoverable_errors() {
2032        let err = NikaError::Timeout {
2033            operation: "slow_op".to_string(),
2034            duration_ms: 5000,
2035        };
2036        let suggestion = <NikaError as FixSuggestion>::fix_suggestion(&err);
2037        assert!(suggestion.is_some());
2038        assert!(suggestion.unwrap().contains("timeout"));
2039    }
2040
2041    #[test]
2042    fn test_fix_suggestion_for_mcp_validation_with_missing_fields() {
2043        let err = NikaError::McpValidationFailed {
2044            tool: "test_tool".to_string(),
2045            details: "missing required fields".to_string(),
2046            missing: vec!["field1".to_string()],
2047            suggestions: vec![],
2048        };
2049        let suggestion = <NikaError as FixSuggestion>::fix_suggestion(&err);
2050        assert!(suggestion.is_some());
2051        assert!(suggestion.unwrap().contains("required fields"));
2052    }
2053
2054    #[test]
2055    fn test_fix_suggestion_for_mcp_validation_with_suggestions() {
2056        let err = NikaError::McpValidationFailed {
2057            tool: "test_tool".to_string(),
2058            details: "field mismatch".to_string(),
2059            missing: vec![],
2060            suggestions: vec!["Did you mean 'entity'?".to_string()],
2061        };
2062        let suggestion = <NikaError as FixSuggestion>::fix_suggestion(&err);
2063        assert!(suggestion.is_some());
2064        assert!(suggestion.unwrap().contains("spelling"));
2065    }
2066
2067    #[test]
2068    fn test_fix_suggestion_for_mcp_validation_default() {
2069        let err = NikaError::McpValidationFailed {
2070            tool: "test_tool".to_string(),
2071            details: "unknown issue".to_string(),
2072            missing: vec![],
2073            suggestions: vec![],
2074        };
2075        let suggestion = <NikaError as FixSuggestion>::fix_suggestion(&err);
2076        assert!(suggestion.is_some());
2077        assert!(suggestion.unwrap().contains("parameter schema"));
2078    }
2079
2080    // ═══════════════════════════════════════════════════════════════════════════
2081    // IS_RECOVERABLE TESTS
2082    // ═══════════════════════════════════════════════════════════════════════════
2083
2084    #[test]
2085    fn test_is_recoverable_mcp_not_connected() {
2086        let err = NikaError::McpNotConnected { name: "x".into() };
2087        assert!(err.is_recoverable());
2088    }
2089
2090    #[test]
2091    fn test_is_recoverable_provider_api_error() {
2092        let err = NikaError::ProviderApiError {
2093            message: "x".into(),
2094        };
2095        assert!(err.is_recoverable());
2096    }
2097
2098    #[test]
2099    fn test_is_recoverable_mcp_tool_error() {
2100        let err = NikaError::McpToolError {
2101            tool: "x".into(),
2102            reason: "y".into(),
2103            error_code: None,
2104        };
2105        assert!(err.is_recoverable());
2106    }
2107
2108    #[test]
2109    fn test_is_recoverable_timeout() {
2110        let err = NikaError::Timeout {
2111            operation: "x".into(),
2112            duration_ms: 1000,
2113        };
2114        assert!(err.is_recoverable());
2115    }
2116
2117    #[test]
2118    fn test_is_recoverable_mcp_timeout() {
2119        let err = NikaError::McpTimeout {
2120            name: "x".into(),
2121            operation: "y".into(),
2122            timeout_secs: 30,
2123        };
2124        assert!(err.is_recoverable());
2125    }
2126
2127    #[test]
2128    fn test_is_recoverable_mcp_tool_call_failed() {
2129        let err = NikaError::McpToolCallFailed {
2130            tool: "x".into(),
2131            reason: "y".into(),
2132        };
2133        assert!(err.is_recoverable());
2134    }
2135
2136    #[test]
2137    fn test_is_not_recoverable_parse_error() {
2138        let err = NikaError::ParseError {
2139            details: "x".into(),
2140        };
2141        assert!(!err.is_recoverable());
2142    }
2143
2144    #[test]
2145    fn test_is_not_recoverable_validation_error() {
2146        let err = NikaError::ValidationError { reason: "x".into() };
2147        assert!(!err.is_recoverable());
2148    }
2149
2150    #[test]
2151    fn test_is_not_recoverable_cycle_detected() {
2152        let err = NikaError::CycleDetected { cycle: "x".into() };
2153        assert!(!err.is_recoverable());
2154    }
2155
2156    // ═══════════════════════════════════════════════════════════════════════════
2157    // ERROR CODE CONSISTENCY TESTS
2158    // ═══════════════════════════════════════════════════════════════════════════
2159
2160    #[test]
2161    fn test_all_workflow_errors_have_correct_codes() {
2162        assert_eq!(
2163            NikaError::ParseError {
2164                details: "x".into()
2165            }
2166            .code(),
2167            "NIKA-001"
2168        );
2169        assert_eq!(
2170            NikaError::InvalidSchemaVersion {
2171                version: "x".into()
2172            }
2173            .code(),
2174            "NIKA-002"
2175        );
2176        assert_eq!(
2177            NikaError::WorkflowNotFound { path: "x".into() }.code(),
2178            "NIKA-003"
2179        );
2180        assert_eq!(
2181            NikaError::ValidationError { reason: "x".into() }.code(),
2182            "NIKA-004"
2183        );
2184    }
2185
2186    #[test]
2187    fn test_all_dag_errors_have_correct_codes() {
2188        assert_eq!(
2189            NikaError::CycleDetected { cycle: "x".into() }.code(),
2190            "NIKA-020"
2191        );
2192        assert_eq!(
2193            NikaError::MissingDependency {
2194                task_id: "x".into(),
2195                dep_id: "y".into()
2196            }
2197            .code(),
2198            "NIKA-021"
2199        );
2200    }
2201
2202    #[test]
2203    fn test_all_provider_errors_have_correct_codes() {
2204        assert_eq!(
2205            NikaError::ProviderNotConfigured {
2206                provider: "x".into()
2207            }
2208            .code(),
2209            "NIKA-030"
2210        );
2211        assert_eq!(
2212            NikaError::ProviderApiError {
2213                message: "x".into()
2214            }
2215            .code(),
2216            "NIKA-031"
2217        );
2218        assert_eq!(
2219            NikaError::MissingApiKey {
2220                provider: "x".into()
2221            }
2222            .code(),
2223            "NIKA-032"
2224        );
2225    }
2226
2227    #[test]
2228    fn test_all_binding_errors_have_correct_codes() {
2229        assert_eq!(
2230            NikaError::BindingNotFound { alias: "x".into() }.code(),
2231            "NIKA-042"
2232        );
2233        assert_eq!(
2234            NikaError::BindingTypeMismatch {
2235                expected: "x".into(),
2236                actual: "y".into(),
2237                path: "z".into()
2238            }
2239            .code(),
2240            "NIKA-043"
2241        );
2242    }
2243
2244    // ═══════════════════════════════════════════════════════════════════════════
2245    // STRUCTURED OUTPUT ERRORS (300-309)
2246    // ═══════════════════════════════════════════════════════════════════════════
2247
2248    #[test]
2249    fn test_structured_output_extraction_failed_error() {
2250        let err = NikaError::StructuredOutputExtractionFailed {
2251            task_id: "generate_json".to_string(),
2252            layer: "rig_extractor".to_string(),
2253            reason: "Failed to parse JSON from response".to_string(),
2254        };
2255        assert_eq!(err.code(), "NIKA-300");
2256        let msg = err.to_string();
2257        assert!(msg.contains("[NIKA-300]"));
2258        assert!(msg.contains("generate_json"));
2259        assert!(msg.contains("rig_extractor"));
2260        assert!(msg.contains("Failed to parse JSON"));
2261    }
2262
2263    #[test]
2264    fn test_structured_output_validation_failed_error() {
2265        let err = NikaError::StructuredOutputValidationFailed {
2266            task_id: "validate_output".to_string(),
2267            layer: "extract_validate".to_string(),
2268            attempt: 2,
2269            errors: vec![
2270                "missing required field 'id'".to_string(),
2271                "invalid type for 'count': expected integer".to_string(),
2272            ],
2273        };
2274        assert_eq!(err.code(), "NIKA-301");
2275        let msg = err.to_string();
2276        assert!(msg.contains("[NIKA-301]"));
2277        assert!(msg.contains("validate_output"));
2278        assert!(msg.contains("extract_validate"));
2279        assert!(msg.contains("attempt 2"));
2280        assert!(msg.contains("2 errors"));
2281    }
2282
2283    #[test]
2284    fn test_structured_output_validation_failed_single_error() {
2285        let err = NikaError::StructuredOutputValidationFailed {
2286            task_id: "single_error".to_string(),
2287            layer: "retry_with_feedback".to_string(),
2288            attempt: 1,
2289            errors: vec!["missing required field 'name'".to_string()],
2290        };
2291        assert_eq!(err.code(), "NIKA-301");
2292        let msg = err.to_string();
2293        assert!(msg.contains("[NIKA-301]"));
2294        assert!(msg.contains("missing required field 'name'"));
2295        // Should not contain "errors:" prefix for single error
2296        assert!(!msg.contains("1 errors:"));
2297    }
2298
2299    #[test]
2300    fn test_structured_output_repair_failed_error() {
2301        let err = NikaError::StructuredOutputRepairFailed {
2302            task_id: "repair_task".to_string(),
2303            original_errors: vec!["invalid JSON syntax".to_string()],
2304            repair_errors: vec!["repair produced invalid output".to_string()],
2305        };
2306        assert_eq!(err.code(), "NIKA-302");
2307        let msg = err.to_string();
2308        assert!(msg.contains("[NIKA-302]"));
2309        assert!(msg.contains("repair_task"));
2310        assert!(msg.contains("original errors"));
2311        assert!(msg.contains("repair errors"));
2312    }
2313
2314    #[test]
2315    fn test_structured_output_all_layers_failed_error() {
2316        let err = NikaError::StructuredOutputAllLayersFailed {
2317            task_id: "final_failure".to_string(),
2318            attempts: 4,
2319            final_errors: vec!["schema validation failed".to_string()],
2320        };
2321        assert_eq!(err.code(), "NIKA-303");
2322        let msg = err.to_string();
2323        assert!(msg.contains("[NIKA-303]"));
2324        assert!(msg.contains("final_failure"));
2325        assert!(msg.contains("4 attempts"));
2326    }
2327
2328    #[test]
2329    fn test_all_structured_output_errors_have_correct_codes() {
2330        assert_eq!(
2331            NikaError::StructuredOutputExtractionFailed {
2332                task_id: "x".into(),
2333                layer: "y".into(),
2334                reason: "z".into()
2335            }
2336            .code(),
2337            "NIKA-300"
2338        );
2339        assert_eq!(
2340            NikaError::StructuredOutputValidationFailed {
2341                task_id: "x".into(),
2342                layer: "y".into(),
2343                attempt: 1,
2344                errors: vec![]
2345            }
2346            .code(),
2347            "NIKA-301"
2348        );
2349        assert_eq!(
2350            NikaError::StructuredOutputRepairFailed {
2351                task_id: "x".into(),
2352                original_errors: vec![],
2353                repair_errors: vec![]
2354            }
2355            .code(),
2356            "NIKA-302"
2357        );
2358        assert_eq!(
2359            NikaError::StructuredOutputAllLayersFailed {
2360                task_id: "x".into(),
2361                attempts: 1,
2362                final_errors: vec![]
2363            }
2364            .code(),
2365            "NIKA-303"
2366        );
2367    }
2368
2369    #[test]
2370    fn test_structured_output_errors_fix_suggestions() {
2371        let extraction_err = NikaError::StructuredOutputExtractionFailed {
2372            task_id: "t".into(),
2373            layer: "l".into(),
2374            reason: "r".into(),
2375        };
2376        let suggestion = <NikaError as FixSuggestion>::fix_suggestion(&extraction_err);
2377        assert!(suggestion.is_some());
2378        assert!(suggestion.unwrap().contains("JSON Schema"));
2379
2380        let validation_err = NikaError::StructuredOutputValidationFailed {
2381            task_id: "t".into(),
2382            layer: "l".into(),
2383            attempt: 1,
2384            errors: vec![],
2385        };
2386        let suggestion = <NikaError as FixSuggestion>::fix_suggestion(&validation_err);
2387        assert!(suggestion.is_some());
2388        assert!(suggestion.unwrap().contains("schema"));
2389
2390        let repair_err = NikaError::StructuredOutputRepairFailed {
2391            task_id: "t".into(),
2392            original_errors: vec![],
2393            repair_errors: vec![],
2394        };
2395        let suggestion = <NikaError as FixSuggestion>::fix_suggestion(&repair_err);
2396        assert!(suggestion.is_some());
2397        assert!(suggestion.unwrap().contains("simplifying"));
2398
2399        let all_failed_err = NikaError::StructuredOutputAllLayersFailed {
2400            task_id: "t".into(),
2401            attempts: 1,
2402            final_errors: vec![],
2403        };
2404        let suggestion = <NikaError as FixSuggestion>::fix_suggestion(&all_failed_err);
2405        assert!(suggestion.is_some());
2406        assert!(suggestion.unwrap().contains("validation layers"));
2407    }
2408
2409    #[test]
2410    fn test_structured_output_is_recoverable() {
2411        // Extraction, validation, and repair errors are recoverable
2412        let extraction = NikaError::StructuredOutputExtractionFailed {
2413            task_id: "x".into(),
2414            layer: "y".into(),
2415            reason: "z".into(),
2416        };
2417        assert!(extraction.is_recoverable());
2418
2419        let validation = NikaError::StructuredOutputValidationFailed {
2420            task_id: "x".into(),
2421            layer: "y".into(),
2422            attempt: 1,
2423            errors: vec![],
2424        };
2425        assert!(validation.is_recoverable());
2426
2427        let repair = NikaError::StructuredOutputRepairFailed {
2428            task_id: "x".into(),
2429            original_errors: vec![],
2430            repair_errors: vec![],
2431        };
2432        assert!(repair.is_recoverable());
2433
2434        // All layers failed is NOT recoverable (final failure)
2435        let all_failed = NikaError::StructuredOutputAllLayersFailed {
2436            task_id: "x".into(),
2437            attempts: 4,
2438            final_errors: vec![],
2439        };
2440        assert!(!all_failed.is_recoverable());
2441    }
2442
2443    // ═══════════════════════════════════════════════════════════════════════════
2444    // COURSE ERRORS (310-319)
2445    // ═══════════════════════════════════════════════════════════════════════════
2446
2447    #[test]
2448    fn test_course_not_found_code_and_display() {
2449        let err = NikaError::CourseNotFound {
2450            path: "/tmp/course".to_string(),
2451        };
2452        assert_eq!(err.code(), "NIKA-310");
2453        assert!(err.to_string().contains("NIKA-310"));
2454        assert!(err.to_string().contains("/tmp/course"));
2455        assert!(!err.is_recoverable());
2456    }
2457
2458    #[test]
2459    fn test_course_check_failed_code_and_display() {
2460        let err = NikaError::CourseCheckFailed {
2461            exercise: "01_hello".to_string(),
2462            reason: "missing infer: verb".to_string(),
2463        };
2464        assert_eq!(err.code(), "NIKA-311");
2465        assert!(err.to_string().contains("NIKA-311"));
2466        assert!(err.to_string().contains("01_hello"));
2467        assert!(err.to_string().contains("missing infer: verb"));
2468        assert!(!err.is_recoverable());
2469    }
2470
2471    #[test]
2472    fn test_course_level_locked_code_and_display() {
2473        let err = NikaError::CourseLevelLocked {
2474            level: "intermediate".to_string(),
2475            prerequisite: 1,
2476        };
2477        assert_eq!(err.code(), "NIKA-312");
2478        assert!(err.to_string().contains("NIKA-312"));
2479        assert!(err.to_string().contains("intermediate"));
2480        assert!(err.to_string().contains("level 1"));
2481        assert!(!err.is_recoverable());
2482    }
2483
2484    #[test]
2485    fn test_course_progress_corrupted_code_and_display() {
2486        let err = NikaError::CourseProgressCorrupted {
2487            reason: "invalid JSON".to_string(),
2488        };
2489        assert_eq!(err.code(), "NIKA-313");
2490        assert!(err.to_string().contains("NIKA-313"));
2491        assert!(err.to_string().contains("invalid JSON"));
2492        assert!(!err.is_recoverable());
2493    }
2494
2495    #[test]
2496    fn test_course_watch_error_code_and_display() {
2497        let err = NikaError::CourseWatchError {
2498            reason: "notify failed".to_string(),
2499        };
2500        assert_eq!(err.code(), "NIKA-314");
2501        assert!(err.to_string().contains("NIKA-314"));
2502        assert!(err.to_string().contains("notify failed"));
2503        assert!(!err.is_recoverable());
2504    }
2505
2506    #[test]
2507    fn test_course_errors_fix_suggestions() {
2508        let not_found = NikaError::CourseNotFound {
2509            path: "/tmp".to_string(),
2510        };
2511        assert!(not_found.fix_suggestion().is_some());
2512
2513        let check_failed = NikaError::CourseCheckFailed {
2514            exercise: "ex".to_string(),
2515            reason: "r".to_string(),
2516        };
2517        assert!(check_failed.fix_suggestion().is_some());
2518
2519        let locked = NikaError::CourseLevelLocked {
2520            level: "l".to_string(),
2521            prerequisite: 1,
2522        };
2523        assert!(locked.fix_suggestion().is_some());
2524
2525        let corrupted = NikaError::CourseProgressCorrupted {
2526            reason: "r".to_string(),
2527        };
2528        assert!(corrupted.fix_suggestion().is_some());
2529
2530        let watch = NikaError::CourseWatchError {
2531            reason: "r".to_string(),
2532        };
2533        assert!(watch.fix_suggestion().is_some());
2534    }
2535
2536    #[test]
2537    fn test_format_validation_errors_short_empty() {
2538        let result = format_validation_errors_short(&[]);
2539        assert_eq!(result, "no errors");
2540    }
2541
2542    #[test]
2543    fn test_format_validation_errors_short_single() {
2544        let result = format_validation_errors_short(&["missing field".to_string()]);
2545        assert_eq!(result, "missing field");
2546    }
2547
2548    #[test]
2549    fn test_format_validation_errors_short_multiple() {
2550        let result = format_validation_errors_short(&[
2551            "error 1".to_string(),
2552            "error 2".to_string(),
2553            "error 3".to_string(),
2554        ]);
2555        assert!(result.contains("3 errors:"));
2556        assert!(result.contains("error 1"));
2557        assert!(result.contains("error 2"));
2558        assert!(result.contains("error 3"));
2559    }
2560}