1use std::path::PathBuf;
7use uuid::Uuid;
8
9#[derive(Debug, thiserror::Error)]
11pub enum RustantError {
12 #[error("LLM error: {0}")]
13 Llm(#[from] LlmError),
14
15 #[error("Tool error: {0}")]
16 Tool(#[from] ToolError),
17
18 #[error("Memory error: {0}")]
19 Memory(#[from] MemoryError),
20
21 #[error("Configuration error: {0}")]
22 Config(#[from] ConfigError),
23
24 #[error("Safety error: {0}")]
25 Safety(#[from] SafetyError),
26
27 #[error("Agent error: {0}")]
28 Agent(#[from] AgentError),
29
30 #[error("Channel error: {0}")]
31 Channel(#[from] ChannelError),
32
33 #[error("Node error: {0}")]
34 Node(#[from] NodeError),
35
36 #[error("Workflow error: {0}")]
37 Workflow(#[from] WorkflowError),
38
39 #[error("Browser error: {0}")]
40 Browser(#[from] BrowserError),
41
42 #[error("Scheduler error: {0}")]
43 Scheduler(#[from] SchedulerError),
44
45 #[error("Voice error: {0}")]
46 Voice(#[from] VoiceError),
47
48 #[error("IO error: {0}")]
49 Io(#[from] std::io::Error),
50
51 #[error("Serialization error: {0}")]
52 Serialization(#[from] serde_json::Error),
53}
54
55#[derive(Debug, thiserror::Error)]
57pub enum LlmError {
58 #[error("API request failed: {message}")]
59 ApiRequest { message: String },
60
61 #[error("API response parse error: {message}")]
62 ResponseParse { message: String },
63
64 #[error("Streaming error: {message}")]
65 Streaming { message: String },
66
67 #[error("Context window exceeded: used {used} of {limit} tokens")]
68 ContextOverflow { used: usize, limit: usize },
69
70 #[error("Model not supported: {model}")]
71 UnsupportedModel { model: String },
72
73 #[error("Authentication failed for provider {provider}")]
74 AuthFailed { provider: String },
75
76 #[error("Rate limited by provider, retry after {retry_after_secs}s")]
77 RateLimited { retry_after_secs: u64 },
78
79 #[error("Request timed out after {timeout_secs}s")]
80 Timeout { timeout_secs: u64 },
81
82 #[error("Provider connection failed: {message}")]
83 Connection { message: String },
84
85 #[error("OAuth flow failed: {message}")]
86 OAuthFailed { message: String },
87}
88
89#[derive(Debug, thiserror::Error)]
91pub enum ToolError {
92 #[error("Tool not found: {name}")]
93 NotFound { name: String },
94
95 #[error("Tool already registered: {name}")]
96 AlreadyRegistered { name: String },
97
98 #[error("Invalid arguments for tool '{name}': {reason}")]
99 InvalidArguments { name: String, reason: String },
100
101 #[error("Tool '{name}' execution failed: {message}")]
102 ExecutionFailed { name: String, message: String },
103
104 #[error("Tool '{name}' timed out after {timeout_secs}s")]
105 Timeout { name: String, timeout_secs: u64 },
106
107 #[error("Tool '{name}' was cancelled")]
108 Cancelled { name: String },
109
110 #[error("Permission denied for tool '{name}': {reason}")]
111 PermissionDenied { name: String, reason: String },
112}
113
114#[derive(Debug, thiserror::Error)]
116pub enum MemoryError {
117 #[error("Context compression failed: {message}")]
118 CompressionFailed { message: String },
119
120 #[error("Memory persistence error: {message}")]
121 PersistenceError { message: String },
122
123 #[error("Memory capacity exceeded")]
124 CapacityExceeded,
125
126 #[error("Failed to load session: {message}")]
127 SessionLoadFailed { message: String },
128}
129
130#[derive(Debug, thiserror::Error)]
132pub enum ConfigError {
133 #[error("Configuration file not found: {path}")]
134 FileNotFound { path: PathBuf },
135
136 #[error("Invalid configuration: {message}")]
137 Invalid { message: String },
138
139 #[error("Missing required field: {field}")]
140 MissingField { field: String },
141
142 #[error("Environment variable not set: {var}")]
143 EnvVarMissing { var: String },
144
145 #[error("Configuration parse error: {message}")]
146 ParseError { message: String },
147}
148
149#[derive(Debug, thiserror::Error)]
151pub enum SafetyError {
152 #[error("Action denied by safety policy: {reason}")]
153 PolicyDenied { reason: String },
154
155 #[error("Path access denied: {path}")]
156 PathDenied { path: PathBuf },
157
158 #[error("Command not allowed: {command}")]
159 CommandDenied { command: String },
160
161 #[error("Network access denied for host: {host}")]
162 NetworkDenied { host: String },
163
164 #[error("Sandbox creation failed: {message}")]
165 SandboxFailed { message: String },
166
167 #[error("Approval was rejected by user")]
168 ApprovalRejected,
169}
170
171#[derive(Debug, thiserror::Error)]
173pub enum AgentError {
174 #[error("Maximum iterations ({max}) reached without completing task")]
175 MaxIterationsReached { max: usize },
176
177 #[error("Agent is already processing a task")]
178 AlreadyBusy,
179
180 #[error("Agent has been shut down")]
181 ShutDown,
182
183 #[error("Task was cancelled")]
184 Cancelled,
185
186 #[error("Invalid state transition: {from} -> {to}")]
187 InvalidStateTransition { from: String, to: String },
188
189 #[error("Budget exceeded: {message}")]
190 BudgetExceeded { message: String },
191}
192
193#[derive(Debug, thiserror::Error)]
195pub enum ChannelError {
196 #[error("Channel '{name}' connection failed: {message}")]
197 ConnectionFailed { name: String, message: String },
198
199 #[error("Channel '{name}' send failed: {message}")]
200 SendFailed { name: String, message: String },
201
202 #[error("Channel '{name}' is not connected")]
203 NotConnected { name: String },
204
205 #[error("Channel '{name}' authentication failed")]
206 AuthFailed { name: String },
207
208 #[error("Channel '{name}' rate limited")]
209 RateLimited { name: String },
210}
211
212#[derive(Debug, thiserror::Error)]
214pub enum NodeError {
215 #[error("No capable node for capability: {capability}")]
216 NoCapableNode { capability: String },
217
218 #[error("Node '{node_id}' execution failed: {message}")]
219 ExecutionFailed { node_id: String, message: String },
220
221 #[error("Node '{node_id}' is unreachable")]
222 Unreachable { node_id: String },
223
224 #[error("Consent denied for capability: {capability}")]
225 ConsentDenied { capability: String },
226
227 #[error("Node discovery failed: {message}")]
228 DiscoveryFailed { message: String },
229}
230
231#[derive(Debug, thiserror::Error)]
233pub enum WorkflowError {
234 #[error("Workflow parse error: {message}")]
235 ParseError { message: String },
236
237 #[error("Workflow validation failed: {message}")]
238 ValidationFailed { message: String },
239
240 #[error("Workflow step '{step}' failed: {message}")]
241 StepFailed { step: String, message: String },
242
243 #[error("Workflow '{name}' not found")]
244 NotFound { name: String },
245
246 #[error("Workflow run '{run_id}' not found")]
247 RunNotFound { run_id: Uuid },
248
249 #[error("Workflow approval timed out for step '{step}'")]
250 ApprovalTimeout { step: String },
251
252 #[error("Workflow cancelled")]
253 Cancelled,
254
255 #[error("Template render error: {message}")]
256 TemplateError { message: String },
257}
258
259#[derive(Debug, thiserror::Error)]
261pub enum BrowserError {
262 #[error("Navigation failed: {message}")]
263 NavigationFailed { message: String },
264
265 #[error("Element not found: {selector}")]
266 ElementNotFound { selector: String },
267
268 #[error("JavaScript evaluation failed: {message}")]
269 JsEvalFailed { message: String },
270
271 #[error("Screenshot failed: {message}")]
272 ScreenshotFailed { message: String },
273
274 #[error("Browser timeout after {timeout_secs}s")]
275 Timeout { timeout_secs: u64 },
276
277 #[error("URL blocked by security policy: {url}")]
278 UrlBlocked { url: String },
279
280 #[error("Browser session error: {message}")]
281 SessionError { message: String },
282
283 #[error("CDP protocol error: {message}")]
284 CdpError { message: String },
285
286 #[error("Page limit exceeded: maximum {max} pages")]
287 PageLimitExceeded { max: usize },
288
289 #[error("Tab not found: {tab_id}")]
290 TabNotFound { tab_id: String },
291
292 #[error("Browser not connected")]
293 NotConnected,
294}
295
296#[derive(Debug, thiserror::Error)]
298pub enum SchedulerError {
299 #[error("Invalid cron expression '{expression}': {message}")]
300 InvalidCronExpression { expression: String, message: String },
301
302 #[error("Job '{name}' not found")]
303 JobNotFound { name: String },
304
305 #[error("Job '{name}' already exists")]
306 JobAlreadyExists { name: String },
307
308 #[error("Job '{name}' is disabled")]
309 JobDisabled { name: String },
310
311 #[error("Maximum background jobs ({max}) exceeded")]
312 MaxJobsExceeded { max: usize },
313
314 #[error("Background job '{id}' not found")]
315 BackgroundJobNotFound { id: Uuid },
316
317 #[error("Webhook verification failed: {message}")]
318 WebhookVerificationFailed { message: String },
319
320 #[error("Scheduler state persistence error: {message}")]
321 PersistenceError { message: String },
322}
323
324#[derive(Debug, thiserror::Error)]
326pub enum VoiceError {
327 #[error("Audio device error: {message}")]
328 AudioDevice { message: String },
329
330 #[error("STT transcription failed: {message}")]
331 TranscriptionFailed { message: String },
332
333 #[error("TTS synthesis failed: {message}")]
334 SynthesisFailed { message: String },
335
336 #[error("Wake word detection error: {message}")]
337 WakeWordError { message: String },
338
339 #[error("Voice pipeline error: {message}")]
340 PipelineError { message: String },
341
342 #[error("Unsupported audio format: {format}")]
343 UnsupportedFormat { format: String },
344
345 #[error("Voice model not found: {model}")]
346 ModelNotFound { model: String },
347
348 #[error("Voice feature not enabled (compile with --features voice)")]
349 FeatureNotEnabled,
350
351 #[error("Voice session timeout after {timeout_secs}s")]
352 Timeout { timeout_secs: u64 },
353
354 #[error("Voice provider authentication failed: {provider}")]
355 AuthFailed { provider: String },
356
357 #[error("Audio I/O error: {message}")]
358 AudioError { message: String },
359}
360
361pub trait UserGuidance {
366 fn suggestion(&self) -> Option<String>;
368
369 fn next_steps(&self) -> Vec<String>;
371}
372
373impl UserGuidance for RustantError {
374 fn suggestion(&self) -> Option<String> {
375 match self {
376 RustantError::Llm(e) => e.suggestion(),
377 RustantError::Tool(e) => e.suggestion(),
378 RustantError::Memory(e) => e.suggestion(),
379 RustantError::Config(e) => e.suggestion(),
380 RustantError::Safety(e) => e.suggestion(),
381 RustantError::Agent(e) => e.suggestion(),
382 RustantError::Channel(e) => e.suggestion(),
383 RustantError::Node(e) => e.suggestion(),
384 RustantError::Workflow(e) => e.suggestion(),
385 RustantError::Browser(e) => e.suggestion(),
386 RustantError::Scheduler(e) => e.suggestion(),
387 RustantError::Voice(e) => e.suggestion(),
388 RustantError::Io(_) => Some("Check file permissions and disk space.".into()),
389 RustantError::Serialization(_) => {
390 Some("Data may be corrupted. Try /doctor to check.".into())
391 }
392 }
393 }
394
395 fn next_steps(&self) -> Vec<String> {
396 match self {
397 RustantError::Llm(e) => e.next_steps(),
398 RustantError::Tool(e) => e.next_steps(),
399 RustantError::Agent(e) => e.next_steps(),
400 RustantError::Node(e) => e.next_steps(),
401 RustantError::Workflow(e) => e.next_steps(),
402 RustantError::Browser(e) => e.next_steps(),
403 RustantError::Scheduler(e) => e.next_steps(),
404 RustantError::Voice(e) => e.next_steps(),
405 RustantError::Memory(e) => e.next_steps(),
406 RustantError::Config(e) => e.next_steps(),
407 RustantError::Safety(e) => e.next_steps(),
408 RustantError::Channel(e) => e.next_steps(),
409 _ => vec![],
410 }
411 }
412}
413
414impl UserGuidance for LlmError {
415 fn suggestion(&self) -> Option<String> {
416 match self {
417 LlmError::AuthFailed { provider } => Some(format!(
418 "Authentication failed for {}. Check your API key.",
419 provider
420 )),
421 LlmError::RateLimited { retry_after_secs } => Some(format!(
422 "Rate limited. Rustant will retry in {}s.",
423 retry_after_secs
424 )),
425 LlmError::Connection { .. } => {
426 Some("Cannot reach the LLM provider. Check your network.".into())
427 }
428 LlmError::Timeout { timeout_secs } => {
429 Some(format!("Request timed out after {}s.", timeout_secs))
430 }
431 LlmError::ContextOverflow { used, limit } => Some(format!(
432 "Context full ({}/{} tokens). Use /compact to free space.",
433 used, limit
434 )),
435 LlmError::UnsupportedModel { model } => Some(format!(
436 "Model '{}' is not supported by this provider.",
437 model
438 )),
439 _ => None,
440 }
441 }
442
443 fn next_steps(&self) -> Vec<String> {
444 match self {
445 LlmError::AuthFailed { .. } => vec![
446 "Run /doctor to verify API key status.".into(),
447 "Run /setup to reconfigure your provider.".into(),
448 ],
449 LlmError::RateLimited { .. } => {
450 vec!["Wait for the retry or switch models with /config model <name>.".into()]
451 }
452 LlmError::Connection { .. } => vec![
453 "Check your internet connection.".into(),
454 "Run /doctor to test LLM connectivity.".into(),
455 ],
456 LlmError::ContextOverflow { .. } => vec![
457 "Use /compact to compress conversation history.".into(),
458 "Use /pin to protect important messages before compression.".into(),
459 ],
460 _ => vec![],
461 }
462 }
463}
464
465impl UserGuidance for ToolError {
466 fn suggestion(&self) -> Option<String> {
467 match self {
468 ToolError::NotFound { name } => Some(format!(
469 "Tool '{}' is not registered. Use /tools to list available tools.",
470 name
471 )),
472 ToolError::InvalidArguments { name, reason } => {
473 Some(format!("Invalid arguments for '{}': {}", name, reason))
474 }
475 ToolError::ExecutionFailed { name, message } => {
476 if message.contains("No such file") || message.contains("not found") {
478 Some("File not found. Use file_list to browse available files.".to_string())
479 } else if message.contains("Permission denied") {
480 Some(format!(
481 "Permission denied for '{name}'. Check file permissions."
482 ))
483 } else {
484 Some(format!(
485 "Tool '{name}' failed. The agent will try to recover."
486 ))
487 }
488 }
489 ToolError::Timeout { name, timeout_secs } => Some(format!(
490 "Tool '{}' timed out after {}s. Consider breaking the task into smaller steps.",
491 name, timeout_secs
492 )),
493 ToolError::PermissionDenied { name, .. } => Some(format!(
494 "Permission denied for '{}'. Adjust with /permissions.",
495 name
496 )),
497 _ => None,
498 }
499 }
500
501 fn next_steps(&self) -> Vec<String> {
502 match self {
503 ToolError::Timeout { .. } => {
504 vec!["Try a more specific query or smaller file range.".into()]
505 }
506 ToolError::NotFound { .. } => vec!["Run /tools to see registered tools.".into()],
507 _ => vec![],
508 }
509 }
510}
511
512impl UserGuidance for MemoryError {
513 fn suggestion(&self) -> Option<String> {
514 match self {
515 MemoryError::CompressionFailed { .. } => {
516 Some("Context compression failed. Use /compact to retry manually.".into())
517 }
518 MemoryError::CapacityExceeded => {
519 Some("Memory capacity exceeded. Use /compact or start a new session.".into())
520 }
521 MemoryError::SessionLoadFailed { message } => Some(format!(
522 "Session load failed: {}. Use /sessions to list available sessions.",
523 message
524 )),
525 _ => None,
526 }
527 }
528
529 fn next_steps(&self) -> Vec<String> {
530 vec![]
531 }
532}
533
534impl UserGuidance for ConfigError {
535 fn suggestion(&self) -> Option<String> {
536 match self {
537 ConfigError::MissingField { field } => Some(format!(
538 "Missing config field '{}'. Run /setup to configure.",
539 field
540 )),
541 ConfigError::EnvVarMissing { var } => {
542 Some(format!("Set environment variable {} or run /setup.", var))
543 }
544 _ => None,
545 }
546 }
547
548 fn next_steps(&self) -> Vec<String> {
549 vec![]
550 }
551}
552
553impl UserGuidance for SafetyError {
554 fn suggestion(&self) -> Option<String> {
555 match self {
556 SafetyError::ApprovalRejected => {
557 Some("Action was denied. The agent will try an alternative approach.".into())
558 }
559 SafetyError::PathDenied { path } => Some(format!(
560 "Path '{}' is blocked by safety policy.",
561 path.display()
562 )),
563 SafetyError::CommandDenied { command } => Some(format!(
564 "Command '{}' is not in the allowed list. Adjust in config.",
565 command
566 )),
567 _ => None,
568 }
569 }
570
571 fn next_steps(&self) -> Vec<String> {
572 vec![]
573 }
574}
575
576impl UserGuidance for AgentError {
577 fn suggestion(&self) -> Option<String> {
578 match self {
579 AgentError::MaxIterationsReached { max } => Some(format!(
580 "Task exceeded {} iterations. Break it into smaller steps.",
581 max
582 )),
583 AgentError::BudgetExceeded { .. } => {
584 Some("Token budget exceeded. Start a new session or increase the budget.".into())
585 }
586 AgentError::Cancelled => Some("Task was cancelled.".into()),
587 _ => None,
588 }
589 }
590
591 fn next_steps(&self) -> Vec<String> {
592 match self {
593 AgentError::MaxIterationsReached { .. } => vec![
594 "Increase limit with /config max_iterations <n>.".into(),
595 "Break your task into smaller, focused steps.".into(),
596 ],
597 _ => vec![],
598 }
599 }
600}
601
602impl UserGuidance for ChannelError {
603 fn suggestion(&self) -> Option<String> {
604 match self {
605 ChannelError::ConnectionFailed { name, .. } => Some(format!(
606 "Channel '{}' connection failed. Check credentials.",
607 name
608 )),
609 ChannelError::AuthFailed { name } => Some(format!(
610 "Channel '{}' auth failed. Re-run channel setup.",
611 name
612 )),
613 _ => None,
614 }
615 }
616
617 fn next_steps(&self) -> Vec<String> {
618 vec![]
619 }
620}
621
622impl UserGuidance for NodeError {
623 fn suggestion(&self) -> Option<String> {
624 match self {
625 NodeError::NoCapableNode { capability } => Some(format!(
626 "No node has the '{}' capability. Check node configuration.",
627 capability
628 )),
629 NodeError::ExecutionFailed { node_id, .. } => Some(format!(
630 "Node '{}' failed. It may be overloaded or misconfigured.",
631 node_id
632 )),
633 NodeError::Unreachable { node_id } => Some(format!(
634 "Node '{}' is unreachable. Check network connectivity.",
635 node_id
636 )),
637 NodeError::ConsentDenied { capability } => Some(format!(
638 "Consent denied for '{}'. Grant permission in node settings.",
639 capability
640 )),
641 NodeError::DiscoveryFailed { .. } => {
642 Some("Node discovery failed. Check gateway configuration.".into())
643 }
644 }
645 }
646
647 fn next_steps(&self) -> Vec<String> {
648 match self {
649 NodeError::Unreachable { .. } => vec![
650 "Verify the node is running and accessible.".into(),
651 "Check firewall and network settings.".into(),
652 ],
653 _ => vec![],
654 }
655 }
656}
657
658impl UserGuidance for WorkflowError {
659 fn suggestion(&self) -> Option<String> {
660 match self {
661 WorkflowError::NotFound { name } => Some(format!(
662 "Workflow '{}' not found. Use /workflows to list available workflows.",
663 name
664 )),
665 WorkflowError::StepFailed { step, .. } => Some(format!(
666 "Workflow step '{}' failed. Check inputs and retry.",
667 step
668 )),
669 WorkflowError::ValidationFailed { message } => Some(format!(
670 "Workflow validation failed: {}. Fix the definition and retry.",
671 message
672 )),
673 WorkflowError::ApprovalTimeout { step } => Some(format!(
674 "Approval timed out for step '{}'. Re-run the workflow.",
675 step
676 )),
677 WorkflowError::Cancelled => Some("Workflow was cancelled.".into()),
678 _ => None,
679 }
680 }
681
682 fn next_steps(&self) -> Vec<String> {
683 match self {
684 WorkflowError::NotFound { .. } => {
685 vec!["Run /workflows to see available workflow templates.".into()]
686 }
687 _ => vec![],
688 }
689 }
690}
691
692impl UserGuidance for BrowserError {
693 fn suggestion(&self) -> Option<String> {
694 match self {
695 BrowserError::NotConnected => {
696 Some("Browser is not connected. Start a browser session first.".into())
697 }
698 BrowserError::Timeout { timeout_secs } => Some(format!(
699 "Browser timed out after {}s. The page may be slow to load.",
700 timeout_secs
701 )),
702 BrowserError::ElementNotFound { selector } => Some(format!(
703 "Element '{}' not found. The page structure may have changed.",
704 selector
705 )),
706 BrowserError::UrlBlocked { url } => {
707 Some(format!("URL '{}' is blocked by security policy.", url))
708 }
709 BrowserError::NavigationFailed { .. } => {
710 Some("Navigation failed. Check the URL and try again.".into())
711 }
712 _ => None,
713 }
714 }
715
716 fn next_steps(&self) -> Vec<String> {
717 match self {
718 BrowserError::NotConnected => {
719 vec!["Run 'rustant browser test' to verify browser connectivity.".into()]
720 }
721 _ => vec![],
722 }
723 }
724}
725
726impl UserGuidance for SchedulerError {
727 fn suggestion(&self) -> Option<String> {
728 match self {
729 SchedulerError::InvalidCronExpression { expression, .. } => Some(format!(
730 "Invalid cron expression '{}'. Use standard cron syntax (e.g., '0 9 * * *').",
731 expression
732 )),
733 SchedulerError::JobNotFound { name } => Some(format!(
734 "Job '{}' not found. Use 'rustant cron list' to see existing jobs.",
735 name
736 )),
737 SchedulerError::JobAlreadyExists { name } => Some(format!(
738 "Job '{}' already exists. Use a different name or remove the existing one.",
739 name
740 )),
741 SchedulerError::MaxJobsExceeded { max } => Some(format!(
742 "Maximum of {} jobs reached. Remove some before adding new ones.",
743 max
744 )),
745 _ => None,
746 }
747 }
748
749 fn next_steps(&self) -> Vec<String> {
750 match self {
751 SchedulerError::JobNotFound { .. } => {
752 vec!["Run 'rustant cron list' to see existing jobs.".into()]
753 }
754 _ => vec![],
755 }
756 }
757}
758
759impl UserGuidance for VoiceError {
760 fn suggestion(&self) -> Option<String> {
761 match self {
762 VoiceError::FeatureNotEnabled => Some(
763 "Voice features require the 'voice' feature flag. Recompile with --features voice."
764 .into(),
765 ),
766 VoiceError::AudioDevice { .. } => {
767 Some("Audio device error. Check that a microphone/speaker is connected.".into())
768 }
769 VoiceError::AuthFailed { provider } => Some(format!(
770 "Voice provider '{}' auth failed. Check API key.",
771 provider
772 )),
773 VoiceError::ModelNotFound { model } => Some(format!(
774 "Voice model '{}' not found. Check available models.",
775 model
776 )),
777 VoiceError::Timeout { timeout_secs } => Some(format!(
778 "Voice operation timed out after {}s.",
779 timeout_secs
780 )),
781 _ => None,
782 }
783 }
784
785 fn next_steps(&self) -> Vec<String> {
786 match self {
787 VoiceError::FeatureNotEnabled => {
788 vec!["Recompile: cargo build --features voice".into()]
789 }
790 VoiceError::AuthFailed { .. } => {
791 vec!["Run /doctor to verify API key status.".into()]
792 }
793 _ => vec![],
794 }
795 }
796}
797
798pub type Result<T> = std::result::Result<T, RustantError>;
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804
805 #[test]
806 fn test_error_display_llm() {
807 let err = RustantError::Llm(LlmError::ApiRequest {
808 message: "connection refused".into(),
809 });
810 assert_eq!(
811 err.to_string(),
812 "LLM error: API request failed: connection refused"
813 );
814 }
815
816 #[test]
817 fn test_error_display_tool() {
818 let err = RustantError::Tool(ToolError::NotFound {
819 name: "nonexistent".into(),
820 });
821 assert_eq!(err.to_string(), "Tool error: Tool not found: nonexistent");
822 }
823
824 #[test]
825 fn test_error_display_safety() {
826 let err = RustantError::Safety(SafetyError::PathDenied {
827 path: PathBuf::from("/etc/passwd"),
828 });
829 assert_eq!(
830 err.to_string(),
831 "Safety error: Path access denied: /etc/passwd"
832 );
833 }
834
835 #[test]
836 fn test_error_display_config() {
837 let err = RustantError::Config(ConfigError::MissingField {
838 field: "llm.api_key".into(),
839 });
840 assert_eq!(
841 err.to_string(),
842 "Configuration error: Missing required field: llm.api_key"
843 );
844 }
845
846 #[test]
847 fn test_error_display_agent() {
848 let err = RustantError::Agent(AgentError::MaxIterationsReached { max: 25 });
849 assert_eq!(
850 err.to_string(),
851 "Agent error: Maximum iterations (25) reached without completing task"
852 );
853 }
854
855 #[test]
856 fn test_error_from_io() {
857 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
858 let err: RustantError = io_err.into();
859 assert!(matches!(err, RustantError::Io(_)));
860 }
861
862 #[test]
863 fn test_error_from_serde() {
864 let serde_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
865 let err: RustantError = serde_err.into();
866 assert!(matches!(err, RustantError::Serialization(_)));
867 }
868
869 #[test]
870 fn test_tool_error_variants() {
871 let err = ToolError::InvalidArguments {
872 name: "file_read".into(),
873 reason: "path is required".into(),
874 };
875 assert_eq!(
876 err.to_string(),
877 "Invalid arguments for tool 'file_read': path is required"
878 );
879
880 let err = ToolError::Timeout {
881 name: "shell_exec".into(),
882 timeout_secs: 30,
883 };
884 assert_eq!(err.to_string(), "Tool 'shell_exec' timed out after 30s");
885 }
886
887 #[test]
888 fn test_error_display_channel() {
889 let err = RustantError::Channel(ChannelError::ConnectionFailed {
890 name: "telegram".into(),
891 message: "timeout".into(),
892 });
893 assert_eq!(
894 err.to_string(),
895 "Channel error: Channel 'telegram' connection failed: timeout"
896 );
897
898 let err = ChannelError::NotConnected {
899 name: "slack".into(),
900 };
901 assert_eq!(err.to_string(), "Channel 'slack' is not connected");
902 }
903
904 #[test]
905 fn test_error_display_node() {
906 let err = RustantError::Node(NodeError::NoCapableNode {
907 capability: "shell".into(),
908 });
909 assert_eq!(
910 err.to_string(),
911 "Node error: No capable node for capability: shell"
912 );
913
914 let err = NodeError::ConsentDenied {
915 capability: "filesystem".into(),
916 };
917 assert_eq!(err.to_string(), "Consent denied for capability: filesystem");
918 }
919
920 #[test]
921 fn test_error_display_voice() {
922 let err = RustantError::Voice(VoiceError::TranscriptionFailed {
923 message: "model not loaded".into(),
924 });
925 assert_eq!(
926 err.to_string(),
927 "Voice error: STT transcription failed: model not loaded"
928 );
929
930 let err = VoiceError::FeatureNotEnabled;
931 assert_eq!(
932 err.to_string(),
933 "Voice feature not enabled (compile with --features voice)"
934 );
935
936 let err = VoiceError::AudioDevice {
937 message: "no microphone found".into(),
938 };
939 assert_eq!(err.to_string(), "Audio device error: no microphone found");
940 }
941
942 #[test]
943 fn test_llm_error_variants() {
944 let err = LlmError::ContextOverflow {
945 used: 150_000,
946 limit: 128_000,
947 };
948 assert_eq!(
949 err.to_string(),
950 "Context window exceeded: used 150000 of 128000 tokens"
951 );
952
953 let err = LlmError::RateLimited {
954 retry_after_secs: 60,
955 };
956 assert_eq!(err.to_string(), "Rate limited by provider, retry after 60s");
957 }
958
959 #[test]
960 fn test_node_error_guidance() {
961 let err = NodeError::Unreachable {
962 node_id: "node-1".into(),
963 };
964 assert!(err.suggestion().is_some());
965 assert!(err.suggestion().unwrap().contains("node-1"));
966 assert!(!err.next_steps().is_empty());
967 }
968
969 #[test]
970 fn test_workflow_error_guidance() {
971 let err = WorkflowError::NotFound {
972 name: "deploy".into(),
973 };
974 assert!(err.suggestion().unwrap().contains("deploy"));
975 assert!(!err.next_steps().is_empty());
976 }
977
978 #[test]
979 fn test_browser_error_guidance() {
980 let err = BrowserError::NotConnected;
981 assert!(err.suggestion().is_some());
982 assert!(!err.next_steps().is_empty());
983 }
984
985 #[test]
986 fn test_scheduler_error_guidance() {
987 let err = SchedulerError::JobNotFound {
988 name: "backup".into(),
989 };
990 assert!(err.suggestion().unwrap().contains("backup"));
991 assert!(!err.next_steps().is_empty());
992 }
993
994 #[test]
995 fn test_voice_error_guidance() {
996 let err = VoiceError::FeatureNotEnabled;
997 assert!(err.suggestion().unwrap().contains("voice"));
998 assert!(!err.next_steps().is_empty());
999 }
1000
1001 #[test]
1002 fn test_rustant_error_dispatches_all_guidance() {
1003 let err = RustantError::Node(NodeError::Unreachable {
1005 node_id: "n".into(),
1006 });
1007 assert!(err.suggestion().is_some());
1008 assert!(!err.next_steps().is_empty());
1009
1010 let err = RustantError::Voice(VoiceError::FeatureNotEnabled);
1012 assert!(err.suggestion().is_some());
1013 assert!(!err.next_steps().is_empty());
1014
1015 let err = RustantError::Workflow(WorkflowError::NotFound { name: "w".into() });
1017 assert!(err.suggestion().is_some());
1018 }
1019
1020 #[test]
1021 fn test_memory_error_guidance() {
1022 let err = MemoryError::CompressionFailed {
1023 message: "out of memory".into(),
1024 };
1025 assert!(err.suggestion().is_some());
1026
1027 let err = MemoryError::CapacityExceeded;
1028 assert!(err.suggestion().is_some());
1029 }
1030
1031 #[test]
1032 fn test_config_error_guidance() {
1033 let err = ConfigError::MissingField {
1034 field: "api_key".into(),
1035 };
1036 assert!(err.suggestion().is_some());
1037
1038 let err = ConfigError::EnvVarMissing {
1039 var: "OPENAI_API_KEY".into(),
1040 };
1041 assert!(err.suggestion().is_some());
1042 }
1043
1044 #[test]
1045 fn test_safety_error_guidance() {
1046 let err = SafetyError::PathDenied {
1047 path: "/etc/passwd".into(),
1048 };
1049 assert!(err.suggestion().is_some());
1050
1051 let err = SafetyError::ApprovalRejected;
1052 assert!(err.suggestion().is_some());
1053 }
1054
1055 #[test]
1056 fn test_next_steps_delegation_memory_config_safety_channel() {
1057 let err = RustantError::Memory(MemoryError::CompressionFailed {
1059 message: "test".into(),
1060 });
1061 let _ = err.next_steps();
1063
1064 let err = RustantError::Config(ConfigError::MissingField {
1065 field: "test".into(),
1066 });
1067 let _ = err.next_steps();
1068
1069 let err = RustantError::Safety(SafetyError::ApprovalRejected);
1070 let _ = err.next_steps();
1071
1072 let err = RustantError::Channel(ChannelError::ConnectionFailed {
1073 name: "test".into(),
1074 message: "fail".into(),
1075 });
1076 let _ = err.next_steps();
1077 }
1078}