#[cfg(test)]
mod error_scenarios {
use std::time::Duration;
use vtcode_commons::{BackoffStrategy, ErrorCategory, Retryability};
#[test]
fn category_round_trip_via_tool_error_type() {
use vtcode_core::tools::registry::ToolErrorType;
let categories = [
ErrorCategory::Network,
ErrorCategory::Timeout,
ErrorCategory::RateLimit,
ErrorCategory::Authentication,
ErrorCategory::InvalidParameters,
ErrorCategory::ToolNotFound,
ErrorCategory::ResourceNotFound,
ErrorCategory::PermissionDenied,
ErrorCategory::ExecutionError,
ErrorCategory::PolicyViolation,
];
for cat in &categories {
let tool_err: ToolErrorType = ToolErrorType::from(*cat);
let back: ErrorCategory = ErrorCategory::from(tool_err);
assert!(
!back.user_label().is_empty(),
"Round-trip for {:?} produced empty label",
cat
);
}
}
#[test]
fn category_round_trip_via_unified_error_kind() {
use vtcode_core::tools::unified_error::UnifiedErrorKind;
let categories = [
ErrorCategory::Network,
ErrorCategory::Timeout,
ErrorCategory::Authentication,
ErrorCategory::PermissionDenied,
ErrorCategory::ExecutionError,
];
for cat in &categories {
let kind: UnifiedErrorKind = UnifiedErrorKind::from(*cat);
let back: ErrorCategory = ErrorCategory::from(kind);
assert!(
!back.user_label().is_empty(),
"Round-trip for {:?} via UnifiedErrorKind produced empty label",
cat
);
}
}
#[test]
fn classify_real_world_errors() {
let cases: Vec<(&str, ErrorCategory)> = vec![
("connection refused", ErrorCategory::Network),
("request timed out after 30s", ErrorCategory::Timeout),
("429 Too Many Requests", ErrorCategory::RateLimit),
("invalid api key", ErrorCategory::Authentication),
(
"permission denied (os error 13)",
ErrorCategory::PermissionDenied,
),
("rate limit exceeded", ErrorCategory::RateLimit),
(
"server is overloaded, try again later",
ErrorCategory::ServiceUnavailable,
),
(
"service temporarily unavailable",
ErrorCategory::ServiceUnavailable,
),
];
for (msg, expected) in cases {
let actual = vtcode_commons::classify_error_message(msg);
assert_eq!(
actual, expected,
"classify_error_message({:?}) = {:?}, expected {:?}",
msg, actual, expected
);
}
}
#[test]
fn retryable_categories_have_bounded_attempts() {
let retryable = [
ErrorCategory::Network,
ErrorCategory::Timeout,
ErrorCategory::RateLimit,
ErrorCategory::ServiceUnavailable,
];
for cat in &retryable {
match cat.retryability() {
Retryability::Retryable {
max_attempts,
backoff,
} => {
assert!(
(1..=10).contains(&max_attempts),
"{:?} has unreasonable max_attempts={}",
cat,
max_attempts
);
match backoff {
BackoffStrategy::Exponential { .. } | BackoffStrategy::Fixed { .. } => {}
}
}
other => panic!("{:?} should be retryable, got {:?}", cat, other),
}
}
}
#[test]
fn non_retryable_categories_are_not_retryable() {
let non_retryable = [
ErrorCategory::Authentication,
ErrorCategory::InvalidParameters,
ErrorCategory::ToolNotFound,
ErrorCategory::PolicyViolation,
ErrorCategory::PlanModeViolation,
];
for cat in &non_retryable {
assert!(!cat.is_retryable(), "{:?} should be non-retryable", cat);
}
}
#[test]
fn actionable_categories_have_recovery_suggestions() {
let actionable = [
ErrorCategory::Network,
ErrorCategory::Timeout,
ErrorCategory::RateLimit,
ErrorCategory::Authentication,
ErrorCategory::ToolNotFound,
ErrorCategory::PermissionDenied,
ErrorCategory::ResourceNotFound,
];
for cat in &actionable {
let suggestions = cat.recovery_suggestions();
assert!(
!suggestions.is_empty(),
"{:?} should have recovery suggestions",
cat
);
for s in &suggestions {
assert!(
!s.trim().is_empty(),
"{:?} has an empty suggestion string",
cat
);
}
}
}
#[test]
fn user_labels_are_short_and_descriptive() {
let all_categories = [
ErrorCategory::Network,
ErrorCategory::Timeout,
ErrorCategory::RateLimit,
ErrorCategory::ServiceUnavailable,
ErrorCategory::CircuitOpen,
ErrorCategory::Authentication,
ErrorCategory::InvalidParameters,
ErrorCategory::ToolNotFound,
ErrorCategory::ResourceNotFound,
ErrorCategory::PermissionDenied,
ErrorCategory::PolicyViolation,
ErrorCategory::PlanModeViolation,
ErrorCategory::SandboxFailure,
ErrorCategory::ResourceExhausted,
ErrorCategory::Cancelled,
ErrorCategory::ExecutionError,
];
for cat in &all_categories {
let label = cat.user_label();
assert!(!label.is_empty(), "{:?} has empty user_label", cat);
assert!(label.len() <= 40, "{:?} label too long: {:?}", cat, label);
assert!(
label.contains(' ') || label.len() >= 6,
"{:?} label should be descriptive: {:?}",
cat,
label
);
}
}
#[test]
fn retry_policy_surfaces_circuit_open_backoff() {
let policy = vtcode_core::retry::RetryPolicy::default();
let decision = policy.decision_for_category(
ErrorCategory::CircuitOpen,
0,
Some(Duration::from_secs(7)),
);
assert!(decision.retryable);
assert_eq!(decision.delay, Some(Duration::from_secs(7)));
assert_eq!(decision.category, ErrorCategory::CircuitOpen);
}
#[test]
fn circuit_breaker_blocks_requests_after_threshold() {
let breaker = vtcode_core::tools::circuit_breaker::CircuitBreaker::new(
vtcode_core::tools::circuit_breaker::CircuitBreakerConfig {
failure_threshold: 1,
..vtcode_core::tools::circuit_breaker::CircuitBreakerConfig::default()
},
);
breaker.record_failure_category_for_tool("read_file", ErrorCategory::ExecutionError);
assert!(!breaker.allow_request_for_tool("read_file"));
let diagnostics = breaker.get_diagnostics("read_file");
assert!(diagnostics.is_open);
assert_eq!(diagnostics.tool_name, "read_file");
assert_eq!(
diagnostics.last_error_category,
Some(ErrorCategory::ExecutionError)
);
}
#[test]
fn mcp_error_codes_have_guidance() {
use vtcode_core::mcp::errors::ErrorCode;
let codes = [
ErrorCode::ToolNotFound,
ErrorCode::ToolInvocationFailed,
ErrorCode::ProviderNotFound,
ErrorCode::ProviderUnavailable,
ErrorCode::SchemaInvalid,
ErrorCode::ConfigurationError,
ErrorCode::InitializationTimeout,
];
for code in &codes {
let guidance = code.user_guidance();
assert!(
!guidance.is_empty(),
"{:?} should have non-empty user_guidance",
code
);
assert!(
guidance.len() > 10,
"{:?} guidance too short to be helpful: {:?}",
code,
guidance
);
}
}
#[test]
fn llm_retryable_errors_classified_as_retryable_categories() {
let retryable_messages = [
"rate limit exceeded",
"connection reset by peer",
"request timed out",
"503 service unavailable",
"server is overloaded",
"Too Many Requests",
];
for msg in &retryable_messages {
assert!(
vtcode_commons::is_retryable_llm_error_message(msg),
"is_retryable_llm_error_message({:?}) should be true",
msg
);
let cat = vtcode_commons::classify_error_message(msg);
assert!(
cat.is_retryable(),
"classify_error_message({:?}) = {:?} should be retryable",
msg,
cat
);
}
}
#[test]
fn non_retryable_errors_are_consistent() {
let non_retryable_messages = [
"invalid api key",
"permission denied",
"authentication failed",
];
for msg in &non_retryable_messages {
assert!(
!vtcode_commons::is_retryable_llm_error_message(msg),
"is_retryable_llm_error_message({:?}) should be false",
msg
);
let cat = vtcode_commons::classify_error_message(msg);
assert!(
!cat.is_retryable(),
"classify_error_message({:?}) = {:?} should be non-retryable",
msg,
cat
);
}
}
#[test]
fn classify_anyhow_error_with_context() {
let inner = std::io::Error::new(std::io::ErrorKind::TimedOut, "connection timed out");
let anyhow_err =
anyhow::Error::new(inner).context("request timed out while fetching resource");
let cat = vtcode_commons::classify_anyhow_error(&anyhow_err);
assert_eq!(
cat,
ErrorCategory::Timeout,
"Expected Timeout, got {:?}",
cat
);
}
#[test]
fn classify_anyhow_error_permission_denied() {
let inner = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied");
let anyhow_err = anyhow::Error::new(inner).context("permission denied while reading file");
let cat = vtcode_commons::classify_anyhow_error(&anyhow_err);
assert_eq!(
cat,
ErrorCategory::PermissionDenied,
"Expected PermissionDenied, got {:?}",
cat
);
}
}