1#![allow(unused_assignments)]
4
5use 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
47fn 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
66fn 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
77pub trait FixSuggestion {
79 fn fix_suggestion(&self) -> Option<&str>;
80}
81
82#[derive(Error, Debug, Diagnostic)]
87#[diagnostic(url(docsrs))]
88pub enum NikaError {
89 #[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 #[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 #[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 count: usize,
178 blocked_tasks: Vec<String>,
180 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 missing: Vec<String>,
439 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 #[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 #[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 #[error("[NIKA-130] TUI error: {reason}")]
486 TuiError { reason: String },
487
488 #[error("[NIKA-135] Config error: {reason}")]
493 ConfigError { reason: String },
494
495 #[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 #[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 #[error("[{code}] {message}")]
532 ToolError { code: String, message: String },
533
534 #[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 #[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 #[error(transparent)]
575 MediaError(#[from] crate::media::error::MediaError),
576
577 #[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 #[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 #[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 #[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 #[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 pub fn code(&self) -> &'static str {
885 match self {
886 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 Self::SchemaFileNotFound { .. } => "NIKA-013",
895 Self::SchemaFileInvalid { .. } => "NIKA-014",
896 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 Self::ProviderNotConfigured { .. } => "NIKA-030",
904 Self::ProviderApiError { .. } => "NIKA-031",
905 Self::MissingApiKey { .. } => "NIKA-032",
906 Self::InvalidConfig { .. } => "NIKA-033",
907 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 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 Self::InvalidJson { .. } => "NIKA-060",
926 Self::SchemaFailed { .. } => "NIKA-061",
927 Self::SerializationError { .. } => "NIKA-062",
928 Self::UnknownAlias { .. } => "NIKA-071",
930 Self::NullValue { .. } => "NIKA-072",
931 Self::InvalidTraversal { .. } => "NIKA-073",
932 Self::TemplateParse { .. } => "NIKA-074",
933 Self::WithUnknownTask { .. } => "NIKA-080",
935 Self::WithNotUpstream { .. } => "NIKA-081",
936 Self::WithCircularDep { .. } => "NIKA-082",
937 Self::RuntimeDeadlock { .. } => "NIKA-083",
938 Self::JsonPathUnsupported { .. } => "NIKA-090",
940 Self::IoError(_) => "NIKA-093",
941 Self::JsonError(_) => "NIKA-094",
942 Self::YamlParse(_) => "NIKA-095",
943 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 Self::AgentValidationError { .. } => "NIKA-113",
956 Self::AgentExecutionError { .. } => "NIKA-115",
957 Self::ThinkingCaptureFailed { .. } => "NIKA-116",
958 Self::GuardrailViolation { .. } => "NIKA-112",
959 Self::Timeout { .. } => "NIKA-121",
961 Self::McpToolCallFailed { .. } => "NIKA-110",
962 Self::TuiError { .. } => "NIKA-130",
964 Self::ConfigError { .. } => "NIKA-135",
966 Self::StartupError { .. } => "NIKA-167",
968 Self::ToolError { .. } => "NIKA-200",
970 Self::BuiltinToolError { .. } => "NIKA-210",
972 Self::BuiltinInvalidParams { .. } => "NIKA-212",
973 Self::AssertionFailed { .. } => "NIKA-213",
974 Self::ContextLoadError { .. } => "NIKA-250",
976 Self::MediaError(e) => e.code(),
978 Self::InvalidPkgUri { .. } => "NIKA-260",
980 Self::PackageNotFound { .. } => "NIKA-261",
982
983 Self::SkillLoadError { .. } => "NIKA-270",
985 Self::ArtifactPathError { .. } => "NIKA-280",
987 Self::ArtifactWriteError { .. } => "NIKA-281",
988 Self::ArtifactSizeExceeded { .. } => "NIKA-282",
989 Self::MediaStoreLocked { .. } => "NIKA-285",
990 Self::StructuredOutputExtractionFailed { .. } => "NIKA-300",
992 Self::StructuredOutputValidationFailed { .. } => "NIKA-301",
993 Self::StructuredOutputRepairFailed { .. } => "NIKA-302",
994 Self::StructuredOutputAllLayersFailed { .. } => "NIKA-303",
995 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 Self::PolicyViolation { .. } => "NIKA-165",
1003 Self::BootFailed { .. } => "NIKA-166",
1004 Self::DecomposeTimeout { .. } => "NIKA-171",
1006 }
1007 }
1008
1009 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 | Self::FetchError { .. }
1020 | Self::StructuredOutputExtractionFailed { .. }
1022 | Self::StructuredOutputValidationFailed { .. }
1023 | Self::StructuredOutputRepairFailed { .. } => true,
1024 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 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 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 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 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 NikaError::TuiError { .. } => Some("Check terminal compatibility and size"),
1185 NikaError::ConfigError { .. } => {
1187 Some("Check ~/.config/nika/config.toml for syntax errors")
1188 }
1189 NikaError::StartupError { .. } => Some(
1191 "Check directory permissions and run 'nika init' to create required directories",
1192 ),
1193 NikaError::ToolError { .. } => {
1195 Some("Check file path and permissions. Use Read before Edit.")
1196 }
1197 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 NikaError::ContextLoadError { .. } => {
1207 Some("Check the file path exists and is readable")
1208 }
1209 NikaError::MediaError(_) => {
1211 Some("Check media content and CAS store configuration")
1212 }
1213 NikaError::InvalidPkgUri { .. } => Some(
1215 "Use format: pkg:@scope/name@version/path (e.g., pkg:@supernovae/skills@1.0.0/rust.md)",
1216 ),
1217 NikaError::PackageNotFound { .. } => Some(
1219 "Check package name and version. Run 'nika pkg list' to see installed packages.",
1220 ),
1221 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 NikaError::SkillLoadError { .. } => {
1230 Some("Ensure skill file exists and is readable. Check pkg: URI format if using packages.")
1231 }
1232 NikaError::DecomposeTimeout { .. } => {
1234 Some("Decompose expansion timed out. Try reducing max_items or check MCP server performance.")
1235 }
1236 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 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 NikaError::DuplicateTaskId { .. } => {
1267 Some("Each task must have a unique ID. Rename one of the duplicate tasks.")
1268 }
1269 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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 #[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}