1use std::borrow::Cow;
22use std::fmt;
23use std::time::Duration;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
28pub enum ErrorCategory {
29 Network,
32 Timeout,
34 RateLimit,
36 ServiceUnavailable,
38 CircuitOpen,
40
41 Authentication,
44 InvalidParameters,
46 ToolNotFound,
48 ResourceNotFound,
50 PermissionDenied,
52 PolicyViolation,
54 PlanModeViolation,
56 SandboxFailure,
58 ResourceExhausted,
60 Cancelled,
62 ExecutionError,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum Retryability {
69 Retryable {
71 max_attempts: u32,
73 backoff: BackoffStrategy,
75 },
76 NonRetryable,
78 RequiresIntervention,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum BackoffStrategy {
85 Exponential { base: Duration, max: Duration },
87 Fixed(Duration),
89}
90
91impl ErrorCategory {
92 #[inline]
94 pub const fn is_retryable(&self) -> bool {
95 matches!(
96 self,
97 ErrorCategory::Network
98 | ErrorCategory::Timeout
99 | ErrorCategory::RateLimit
100 | ErrorCategory::ServiceUnavailable
101 | ErrorCategory::CircuitOpen
102 )
103 }
104
105 #[inline]
107 pub const fn should_trip_circuit_breaker(&self) -> bool {
108 matches!(
109 self,
110 ErrorCategory::Network
111 | ErrorCategory::Timeout
112 | ErrorCategory::RateLimit
113 | ErrorCategory::ServiceUnavailable
114 | ErrorCategory::ExecutionError
115 )
116 }
117
118 #[inline]
121 pub const fn is_llm_mistake(&self) -> bool {
122 matches!(self, ErrorCategory::InvalidParameters)
123 }
124
125 #[inline]
127 pub const fn is_permanent(&self) -> bool {
128 matches!(
129 self,
130 ErrorCategory::Authentication
131 | ErrorCategory::PolicyViolation
132 | ErrorCategory::PlanModeViolation
133 | ErrorCategory::ResourceExhausted
134 )
135 }
136
137 pub fn retryability(&self) -> Retryability {
139 match self {
140 ErrorCategory::Network | ErrorCategory::ServiceUnavailable => Retryability::Retryable {
141 max_attempts: 3,
142 backoff: BackoffStrategy::Exponential {
143 base: Duration::from_millis(500),
144 max: Duration::from_secs(10),
145 },
146 },
147 ErrorCategory::Timeout => Retryability::Retryable {
148 max_attempts: 2,
149 backoff: BackoffStrategy::Exponential {
150 base: Duration::from_millis(1000),
151 max: Duration::from_secs(15),
152 },
153 },
154 ErrorCategory::RateLimit => Retryability::Retryable {
155 max_attempts: 3,
156 backoff: BackoffStrategy::Exponential {
157 base: Duration::from_secs(1),
158 max: Duration::from_secs(30),
159 },
160 },
161 ErrorCategory::CircuitOpen => Retryability::Retryable {
162 max_attempts: 1,
163 backoff: BackoffStrategy::Fixed(Duration::from_secs(10)),
164 },
165 ErrorCategory::PermissionDenied => Retryability::RequiresIntervention,
166 _ => Retryability::NonRetryable,
167 }
168 }
169
170 pub fn recovery_suggestions(&self) -> Vec<Cow<'static, str>> {
173 match self {
174 ErrorCategory::Network => vec![
175 Cow::Borrowed("Check network connectivity"),
176 Cow::Borrowed("Retry the operation after a brief delay"),
177 Cow::Borrowed("Verify external service availability"),
178 ],
179 ErrorCategory::Timeout => vec![
180 Cow::Borrowed("Increase timeout values if appropriate"),
181 Cow::Borrowed("Break large operations into smaller chunks"),
182 Cow::Borrowed("Check system resources and performance"),
183 ],
184 ErrorCategory::RateLimit => vec![
185 Cow::Borrowed("Wait before retrying the request"),
186 Cow::Borrowed("Reduce request frequency"),
187 Cow::Borrowed("Check provider rate limit documentation"),
188 ],
189 ErrorCategory::ServiceUnavailable => vec![
190 Cow::Borrowed("The service is temporarily unavailable"),
191 Cow::Borrowed("Retry after a brief delay"),
192 Cow::Borrowed("Check service status page if available"),
193 ],
194 ErrorCategory::CircuitOpen => vec![
195 Cow::Borrowed("This tool has been temporarily disabled due to repeated failures"),
196 Cow::Borrowed("Wait for the circuit breaker cooldown period"),
197 Cow::Borrowed("Try an alternative approach"),
198 ],
199 ErrorCategory::Authentication => vec![
200 Cow::Borrowed("Verify your API key or credentials"),
201 Cow::Borrowed("Check that your account is active and has sufficient permissions"),
202 Cow::Borrowed("Ensure environment variables for API keys are set correctly"),
203 ],
204 ErrorCategory::InvalidParameters => vec![
205 Cow::Borrowed("Check parameter names and types against the tool schema"),
206 Cow::Borrowed("Ensure required parameters are provided"),
207 Cow::Borrowed("Verify parameter values are within acceptable ranges"),
208 ],
209 ErrorCategory::ToolNotFound => vec![
210 Cow::Borrowed("Verify the tool name is spelled correctly"),
211 Cow::Borrowed("Check if the tool is available in the current context"),
212 ],
213 ErrorCategory::ResourceNotFound => vec![
214 Cow::Borrowed("Verify file paths and resource locations"),
215 Cow::Borrowed("Check if files exist and are accessible"),
216 Cow::Borrowed("Use list_dir to explore available resources"),
217 ],
218 ErrorCategory::PermissionDenied => vec![
219 Cow::Borrowed("Check file permissions and access rights"),
220 Cow::Borrowed("Ensure workspace boundaries are respected"),
221 ],
222 ErrorCategory::PolicyViolation => vec![
223 Cow::Borrowed("Review workspace policies and restrictions"),
224 Cow::Borrowed("Use alternative tools that comply with policies"),
225 ],
226 ErrorCategory::PlanModeViolation => vec![
227 Cow::Borrowed("This operation is not allowed in plan/read-only mode"),
228 Cow::Borrowed("Exit plan mode to perform mutating operations"),
229 ],
230 ErrorCategory::SandboxFailure => vec![
231 Cow::Borrowed("The sandbox denied this operation"),
232 Cow::Borrowed("Check sandbox configuration and permissions"),
233 ],
234 ErrorCategory::ResourceExhausted => vec![
235 Cow::Borrowed("Check your account usage limits and billing status"),
236 Cow::Borrowed("Review resource consumption and optimize if possible"),
237 ],
238 ErrorCategory::Cancelled => vec![Cow::Borrowed("The operation was cancelled")],
239 ErrorCategory::ExecutionError => vec![
240 Cow::Borrowed("Review error details for specific issues"),
241 Cow::Borrowed("Check tool documentation for known limitations"),
242 ],
243 }
244 }
245
246 pub const fn user_label(&self) -> &'static str {
248 match self {
249 ErrorCategory::Network => "Network error",
250 ErrorCategory::Timeout => "Request timed out",
251 ErrorCategory::RateLimit => "Rate limit exceeded",
252 ErrorCategory::ServiceUnavailable => "Service temporarily unavailable",
253 ErrorCategory::CircuitOpen => "Tool temporarily disabled",
254 ErrorCategory::Authentication => "Authentication failed",
255 ErrorCategory::InvalidParameters => "Invalid parameters",
256 ErrorCategory::ToolNotFound => "Tool not found",
257 ErrorCategory::ResourceNotFound => "Resource not found",
258 ErrorCategory::PermissionDenied => "Permission denied",
259 ErrorCategory::PolicyViolation => "Blocked by policy",
260 ErrorCategory::PlanModeViolation => "Not allowed in plan mode",
261 ErrorCategory::SandboxFailure => "Sandbox denied",
262 ErrorCategory::ResourceExhausted => "Resource limit reached",
263 ErrorCategory::Cancelled => "Operation cancelled",
264 ErrorCategory::ExecutionError => "Execution failed",
265 }
266 }
267}
268
269impl fmt::Display for ErrorCategory {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 f.write_str(self.user_label())
272 }
273}
274
275pub fn classify_anyhow_error(err: &anyhow::Error) -> ErrorCategory {
285 let msg = err.to_string().to_ascii_lowercase();
286 classify_error_message(&msg)
287}
288
289pub fn classify_error_message(msg: &str) -> ErrorCategory {
294 let msg = if msg.as_bytes().iter().any(|b| b.is_ascii_uppercase()) {
295 Cow::Owned(msg.to_ascii_lowercase())
296 } else {
297 Cow::Borrowed(msg)
298 };
299
300 if contains_any(
302 &msg,
303 &[
304 "policy violation",
305 "denied by policy",
306 "tool permission denied",
307 "safety validation failed",
308 "not allowed in plan mode",
309 "only available when plan mode is active",
310 "workspace boundary",
311 "blocked by policy",
312 ],
313 ) {
314 return ErrorCategory::PolicyViolation;
315 }
316
317 if contains_any(
319 &msg,
320 &["plan mode", "read-only mode", "plan_mode_violation"],
321 ) {
322 return ErrorCategory::PlanModeViolation;
323 }
324
325 if contains_any(
327 &msg,
328 &[
329 "invalid api key",
330 "authentication failed",
331 "unauthorized",
332 "401",
333 "invalid credentials",
334 ],
335 ) {
336 return ErrorCategory::Authentication;
337 }
338
339 if contains_any(
341 &msg,
342 &[
343 "weekly usage limit",
344 "daily usage limit",
345 "monthly spending limit",
346 "insufficient credits",
347 "quota exceeded",
348 "billing",
349 "payment required",
350 ],
351 ) {
352 return ErrorCategory::ResourceExhausted;
353 }
354
355 if contains_any(
357 &msg,
358 &[
359 "invalid argument",
360 "invalid parameters",
361 "invalid type",
362 "malformed",
363 "failed to parse arguments",
364 "failed to parse argument",
365 "missing required",
366 "at least one item is required",
367 "is required for",
368 "schema validation",
369 "argument validation failed",
370 "unknown field",
371 "unknown variant",
372 "expected struct",
373 "expected enum",
374 "type mismatch",
375 "must be an absolute path",
376 "not parseable",
377 "parseable as",
378 ],
379 ) {
380 return ErrorCategory::InvalidParameters;
381 }
382
383 if contains_any(
385 &msg,
386 &[
387 "tool not found",
388 "unknown tool",
389 "unsupported tool",
390 "no such tool",
391 ],
392 ) {
393 return ErrorCategory::ToolNotFound;
394 }
395
396 if contains_any(
398 &msg,
399 &[
400 "no such file",
401 "no such directory",
402 "file not found",
403 "directory not found",
404 "resource not found",
405 "path not found",
406 "does not exist",
407 "enoent",
408 ],
409 ) {
410 return ErrorCategory::ResourceNotFound;
411 }
412
413 if contains_any(
415 &msg,
416 &[
417 "permission denied",
418 "access denied",
419 "operation not permitted",
420 "eacces",
421 "eperm",
422 "forbidden",
423 "403",
424 ],
425 ) {
426 return ErrorCategory::PermissionDenied;
427 }
428
429 if contains_any(&msg, &["cancelled", "interrupted", "canceled"]) {
431 return ErrorCategory::Cancelled;
432 }
433
434 if contains_any(&msg, &["circuit breaker", "circuit open"]) {
436 return ErrorCategory::CircuitOpen;
437 }
438
439 if contains_any(&msg, &["sandbox denied", "sandbox failure"]) {
441 return ErrorCategory::SandboxFailure;
442 }
443
444 if contains_any(&msg, &["rate limit", "too many requests", "429", "throttl"]) {
446 return ErrorCategory::RateLimit;
447 }
448
449 if contains_any(&msg, &["timeout", "timed out", "deadline exceeded"]) {
451 return ErrorCategory::Timeout;
452 }
453
454 if contains_any(
456 &msg,
457 &[
458 "invalid response format: missing choices",
459 "invalid response format: missing message",
460 "missing choices in response",
461 "missing message in choice",
462 "no choices in response",
463 "invalid response from ",
464 "empty response body",
465 "response did not contain",
466 "unexpected response format",
467 "failed to parse response",
468 ],
469 ) {
470 return ErrorCategory::ServiceUnavailable;
471 }
472
473 if contains_any(
475 &msg,
476 &[
477 "service unavailable",
478 "temporarily unavailable",
479 "internal server error",
480 "bad gateway",
481 "gateway timeout",
482 "overloaded",
483 "500",
484 "502",
485 "503",
486 "504",
487 ],
488 ) {
489 return ErrorCategory::ServiceUnavailable;
490 }
491
492 if contains_any(
494 &msg,
495 &[
496 "network",
497 "connection reset",
498 "connection refused",
499 "broken pipe",
500 "dns",
501 "name resolution",
502 "try again",
503 "retry later",
504 "upstream connect error",
505 "tls handshake",
506 "socket hang up",
507 "econnreset",
508 "etimedout",
509 ],
510 ) {
511 return ErrorCategory::Network;
512 }
513
514 if contains_any(&msg, &["out of memory", "disk full", "no space left"]) {
516 return ErrorCategory::ResourceExhausted;
517 }
518
519 ErrorCategory::ExecutionError
521}
522
523pub fn is_retryable_llm_error_message(msg: &str) -> bool {
528 let category = classify_error_message(msg);
529 category.is_retryable()
530}
531
532#[inline]
533fn contains_any(message: &str, markers: &[&str]) -> bool {
534 markers.iter().any(|marker| message.contains(marker))
535}
536
537impl From<&crate::llm::LLMError> for ErrorCategory {
542 fn from(err: &crate::llm::LLMError) -> Self {
543 match err {
544 crate::llm::LLMError::Authentication { .. } => ErrorCategory::Authentication,
545 crate::llm::LLMError::RateLimit { metadata } => {
546 classify_llm_metadata(metadata.as_deref(), ErrorCategory::RateLimit)
547 }
548 crate::llm::LLMError::InvalidRequest { .. } => ErrorCategory::InvalidParameters,
549 crate::llm::LLMError::Network { .. } => ErrorCategory::Network,
550 crate::llm::LLMError::Provider { message, metadata } => {
551 let metadata_category =
552 classify_llm_metadata(metadata.as_deref(), ErrorCategory::ExecutionError);
553 if metadata_category != ErrorCategory::ExecutionError {
554 return metadata_category;
555 }
556
557 if let Some(meta) = metadata
559 && let Some(status) = meta.status
560 {
561 return match status {
562 401 => ErrorCategory::Authentication,
563 403 => ErrorCategory::PermissionDenied,
564 404 => ErrorCategory::ResourceNotFound,
565 429 => ErrorCategory::RateLimit,
566 400 => ErrorCategory::InvalidParameters,
567 500 | 502 | 503 | 504 => ErrorCategory::ServiceUnavailable,
568 408 => ErrorCategory::Timeout,
569 _ => classify_error_message(message),
570 };
571 }
572 classify_error_message(message)
574 }
575 }
576 }
577}
578
579fn classify_llm_metadata(
580 metadata: Option<&crate::llm::LLMErrorMetadata>,
581 fallback: ErrorCategory,
582) -> ErrorCategory {
583 let Some(metadata) = metadata else {
584 return fallback;
585 };
586
587 let mut hint = String::new();
588 if let Some(code) = &metadata.code {
589 hint.push_str(code);
590 hint.push(' ');
591 }
592 if let Some(message) = &metadata.message {
593 hint.push_str(message);
594 hint.push(' ');
595 }
596 if let Some(status) = metadata.status {
597 use std::fmt::Write;
598 let _ = write!(&mut hint, "{status}");
599 }
600
601 let classified = classify_error_message(&hint);
602 if classified == ErrorCategory::ExecutionError {
603 fallback
604 } else {
605 classified
606 }
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612
613 #[test]
616 fn policy_violation_takes_priority_over_permission() {
617 assert_eq!(
618 classify_error_message("tool permission denied by policy"),
619 ErrorCategory::PolicyViolation
620 );
621 }
622
623 #[test]
624 fn rate_limit_classified_correctly() {
625 assert_eq!(
626 classify_error_message("provider returned 429 Too Many Requests"),
627 ErrorCategory::RateLimit
628 );
629 assert_eq!(
630 classify_error_message("rate limit exceeded"),
631 ErrorCategory::RateLimit
632 );
633 }
634
635 #[test]
636 fn service_unavailable_is_classified() {
637 assert_eq!(
638 classify_error_message("503 service unavailable"),
639 ErrorCategory::ServiceUnavailable
640 );
641 }
642
643 #[test]
644 fn authentication_errors() {
645 assert_eq!(
646 classify_error_message("invalid api key provided"),
647 ErrorCategory::Authentication
648 );
649 assert_eq!(
650 classify_error_message("401 unauthorized"),
651 ErrorCategory::Authentication
652 );
653 }
654
655 #[test]
656 fn billing_errors_are_resource_exhausted() {
657 assert_eq!(
658 classify_error_message("you have reached your weekly usage limit"),
659 ErrorCategory::ResourceExhausted
660 );
661 assert_eq!(
662 classify_error_message("quota exceeded for this model"),
663 ErrorCategory::ResourceExhausted
664 );
665 }
666
667 #[test]
668 fn timeout_errors() {
669 assert_eq!(
670 classify_error_message("connection timeout"),
671 ErrorCategory::Timeout
672 );
673 assert_eq!(
674 classify_error_message("request timed out after 30s"),
675 ErrorCategory::Timeout
676 );
677 }
678
679 #[test]
680 fn network_errors() {
681 assert_eq!(
682 classify_error_message("connection reset by peer"),
683 ErrorCategory::Network
684 );
685 assert_eq!(
686 classify_error_message("dns name resolution failed"),
687 ErrorCategory::Network
688 );
689 }
690
691 #[test]
692 fn tool_not_found() {
693 assert_eq!(
694 classify_error_message("unknown tool: ask_questions"),
695 ErrorCategory::ToolNotFound
696 );
697 }
698
699 #[test]
700 fn resource_not_found() {
701 assert_eq!(
702 classify_error_message("no such file or directory: /tmp/missing"),
703 ErrorCategory::ResourceNotFound
704 );
705 assert_eq!(
706 classify_error_message("Path 'vtcode-core/src/agent' does not exist"),
707 ErrorCategory::ResourceNotFound
708 );
709 }
710
711 #[test]
712 fn permission_denied() {
713 assert_eq!(
714 classify_error_message("permission denied: /etc/shadow"),
715 ErrorCategory::PermissionDenied
716 );
717 }
718
719 #[test]
720 fn cancelled_operations() {
721 assert_eq!(
722 classify_error_message("operation cancelled by user"),
723 ErrorCategory::Cancelled
724 );
725 }
726
727 #[test]
728 fn plan_mode_violation() {
729 assert_eq!(
730 classify_error_message("not allowed in plan mode"),
731 ErrorCategory::PolicyViolation
732 );
733 }
734
735 #[test]
736 fn sandbox_failure() {
737 assert_eq!(
738 classify_error_message("sandbox denied this operation"),
739 ErrorCategory::SandboxFailure
740 );
741 }
742
743 #[test]
744 fn unknown_error_is_execution_error() {
745 assert_eq!(
746 classify_error_message("something went wrong"),
747 ErrorCategory::ExecutionError
748 );
749 }
750
751 #[test]
752 fn invalid_parameters() {
753 assert_eq!(
754 classify_error_message("invalid argument: missing path field"),
755 ErrorCategory::InvalidParameters
756 );
757 assert_eq!(
758 classify_error_message(
759 "Failed to parse arguments for read_file handler: invalid type: boolean `false`"
760 ),
761 ErrorCategory::InvalidParameters
762 );
763 assert_eq!(
764 classify_error_message("at least one item is required for 'create'"),
765 ErrorCategory::InvalidParameters
766 );
767 assert_eq!(
768 classify_error_message(
769 "structural pattern preflight failed: pattern is not parseable as Rust syntax"
770 ),
771 ErrorCategory::InvalidParameters
772 );
773 }
774
775 #[test]
778 fn retryable_categories() {
779 assert!(ErrorCategory::Network.is_retryable());
780 assert!(ErrorCategory::Timeout.is_retryable());
781 assert!(ErrorCategory::RateLimit.is_retryable());
782 assert!(ErrorCategory::ServiceUnavailable.is_retryable());
783 assert!(ErrorCategory::CircuitOpen.is_retryable());
784 }
785
786 #[test]
787 fn non_retryable_categories() {
788 assert!(!ErrorCategory::Authentication.is_retryable());
789 assert!(!ErrorCategory::InvalidParameters.is_retryable());
790 assert!(!ErrorCategory::PolicyViolation.is_retryable());
791 assert!(!ErrorCategory::ResourceExhausted.is_retryable());
792 assert!(!ErrorCategory::Cancelled.is_retryable());
793 }
794
795 #[test]
796 fn permanent_error_detection() {
797 assert!(ErrorCategory::Authentication.is_permanent());
798 assert!(ErrorCategory::PolicyViolation.is_permanent());
799 assert!(!ErrorCategory::Network.is_permanent());
800 assert!(!ErrorCategory::Timeout.is_permanent());
801 }
802
803 #[test]
804 fn llm_mistake_detection() {
805 assert!(ErrorCategory::InvalidParameters.is_llm_mistake());
806 assert!(!ErrorCategory::Network.is_llm_mistake());
807 assert!(!ErrorCategory::Timeout.is_llm_mistake());
808 }
809
810 #[test]
813 fn llm_error_authentication_converts() {
814 let err = crate::llm::LLMError::Authentication {
815 message: "bad key".to_string(),
816 metadata: None,
817 };
818 assert_eq!(ErrorCategory::from(&err), ErrorCategory::Authentication);
819 }
820
821 #[test]
822 fn llm_error_rate_limit_converts() {
823 let err = crate::llm::LLMError::RateLimit { metadata: None };
824 assert_eq!(ErrorCategory::from(&err), ErrorCategory::RateLimit);
825 }
826
827 #[test]
828 fn llm_error_quota_exhaustion_converts() {
829 let err = crate::llm::LLMError::RateLimit {
830 metadata: Some(crate::llm::LLMErrorMetadata::new(
831 "openai",
832 Some(429),
833 Some("insufficient_quota".to_string()),
834 None,
835 None,
836 None,
837 Some("quota exceeded".to_string()),
838 )),
839 };
840
841 assert_eq!(ErrorCategory::from(&err), ErrorCategory::ResourceExhausted);
842 }
843
844 #[test]
845 fn llm_error_network_converts() {
846 let err = crate::llm::LLMError::Network {
847 message: "connection refused".to_string(),
848 metadata: None,
849 };
850 assert_eq!(ErrorCategory::from(&err), ErrorCategory::Network);
851 }
852
853 #[test]
854 fn llm_error_provider_with_status_code() {
855 use crate::llm::LLMErrorMetadata;
856 let err = crate::llm::LLMError::Provider {
857 message: "error".to_string(),
858 metadata: Some(LLMErrorMetadata::new(
859 "openai",
860 Some(503),
861 None,
862 None,
863 None,
864 None,
865 None,
866 )),
867 };
868 assert_eq!(ErrorCategory::from(&err), ErrorCategory::ServiceUnavailable);
869 }
870
871 #[test]
872 fn minimax_invalid_response_is_service_unavailable() {
873 assert_eq!(
874 classify_error_message("Invalid response from MiniMax: missing choices"),
875 ErrorCategory::ServiceUnavailable
876 );
877 assert_eq!(
878 classify_error_message("Invalid response format: missing message"),
879 ErrorCategory::ServiceUnavailable
880 );
881 }
882
883 #[test]
886 fn retryable_llm_messages() {
887 assert!(is_retryable_llm_error_message("429 too many requests"));
888 assert!(is_retryable_llm_error_message("500 internal server error"));
889 assert!(is_retryable_llm_error_message("connection timeout"));
890 assert!(is_retryable_llm_error_message("network error"));
891 }
892
893 #[test]
894 fn non_retryable_llm_messages() {
895 assert!(!is_retryable_llm_error_message("invalid api key"));
896 assert!(!is_retryable_llm_error_message(
897 "weekly usage limit reached"
898 ));
899 assert!(!is_retryable_llm_error_message("permission denied"));
900 }
901
902 #[test]
905 fn recovery_suggestions_non_empty() {
906 for cat in [
907 ErrorCategory::Network,
908 ErrorCategory::Timeout,
909 ErrorCategory::RateLimit,
910 ErrorCategory::Authentication,
911 ErrorCategory::InvalidParameters,
912 ErrorCategory::ToolNotFound,
913 ErrorCategory::ResourceNotFound,
914 ErrorCategory::PermissionDenied,
915 ErrorCategory::PolicyViolation,
916 ErrorCategory::ExecutionError,
917 ] {
918 assert!(
919 !cat.recovery_suggestions().is_empty(),
920 "Missing recovery suggestions for {:?}",
921 cat
922 );
923 }
924 }
925
926 #[test]
929 fn user_labels_are_non_empty() {
930 assert!(!ErrorCategory::Network.user_label().is_empty());
931 assert!(!ErrorCategory::ExecutionError.user_label().is_empty());
932 }
933
934 #[test]
937 fn display_matches_user_label() {
938 assert_eq!(
939 format!("{}", ErrorCategory::RateLimit),
940 ErrorCategory::RateLimit.user_label()
941 );
942 }
943}