diff --git a/src/providers/anthropic.rs b/src/providers/anthropic.rs index 153a777..d9d863c 100644 --- a/src/providers/anthropic.rs +++ b/src/providers/anthropic.rs @@ -1067,4 +1067,60 @@ mod tests { assert!(matches!(context_error, ProviderError::ContextWindowExceeded { .. })); assert!(context_error.to_string().contains("250000")); } + + #[test] + fn test_handle_error_response_maps_known_types() { + let provider = AnthropicProvider::new( + "test-key", + "claude-sonnet-4-20250514", + "https://api.anthropic.com", + ProviderConfig::default(), + ); + + let auth = provider.handle_error_response( + 401, + r#"{"error":{"type":"authentication_error","message":"invalid auth"}}"#, + ); + assert!(matches!(auth, ProviderError::AuthError(msg) if msg == "invalid auth")); + + let overloaded = provider.handle_error_response( + 529, + r#"{"error":{"type":"overloaded_error","message":"busy"}}"#, + ); + assert!(matches!(overloaded, ProviderError::RateLimited(msg) if msg == "API overloaded")); + + let model_not_found = provider.handle_error_response( + 400, + r#"{"error":{"type":"invalid_request_error","message":"model does not exist"}}"#, + ); + assert!(matches!( + model_not_found, + ProviderError::ModelNotFound(msg) if msg == "model does not exist" + )); + } + + #[test] + fn test_handle_error_response_falls_back_for_unrecognized_or_plain_body() { + let provider = AnthropicProvider::new( + "test-key", + "claude-sonnet-4-20250514", + "https://api.anthropic.com", + ProviderConfig::default(), + ); + + let unknown = provider.handle_error_response( + 422, + r#"{"error":{"type":"new_error_type","message":"unprocessable payload"}}"#, + ); + assert!(matches!( + unknown, + ProviderError::ApiError { message, status_code: Some(422) } if message == "unprocessable payload" + )); + + let plain = provider.handle_error_response(503, "service unavailable"); + assert!(matches!( + plain, + ProviderError::ApiError { message, status_code: Some(503) } if message == "service unavailable" + )); + } } diff --git a/src/providers/openai.rs b/src/providers/openai.rs index 15d14e0..b219e6f 100644 --- a/src/providers/openai.rs +++ b/src/providers/openai.rs @@ -1132,4 +1132,50 @@ mod tests { let context_error = ProviderError::ContextWindowExceeded { used: 200000, limit: 128000 }; assert!(context_error.to_string().contains("200000")); } + + #[test] + fn test_handle_error_response_maps_known_types() { + let provider = OpenAIProvider::openai("test-key", "gpt-4o"); + + let auth = provider.handle_error_response( + 401, + r#"{"error":{"message":"bad key","type":"invalid_api_key"}}"#, + ); + assert!(matches!(auth, ProviderError::AuthError(msg) if msg == "bad key")); + + let rate = provider.handle_error_response( + 429, + r#"{"error":{"message":"slow down","type":"rate_limit_exceeded"}}"#, + ); + assert!(matches!(rate, ProviderError::RateLimited(msg) if msg == "slow down")); + + let context = provider.handle_error_response( + 400, + r#"{"error":{"message":"context too long","type":"context_length_exceeded"}}"#, + ); + assert!(matches!( + context, + ProviderError::ContextWindowExceeded { used: 0, limit } if limit == 128_000 + )); + } + + #[test] + fn test_handle_error_response_falls_back_for_unknown_or_plain_body() { + let provider = OpenAIProvider::openai("test-key", "gpt-4o"); + + let unknown = provider.handle_error_response( + 418, + r#"{"error":{"message":"teapot","type":"something_new"}}"#, + ); + assert!(matches!( + unknown, + ProviderError::ApiError { message, status_code: Some(418) } if message == "teapot" + )); + + let plain = provider.handle_error_response(503, "upstream unavailable"); + assert!(matches!( + plain, + ProviderError::ApiError { message, status_code: Some(503) } if message == "upstream unavailable" + )); + } } diff --git a/src/tools/handlers/read_file.rs b/src/tools/handlers/read_file.rs index 590a080..413fd5d 100644 --- a/src/tools/handlers/read_file.rs +++ b/src/tools/handlers/read_file.rs @@ -345,6 +345,55 @@ mod tests { assert!(matches!(result.unwrap_err(), ToolError::InvalidInput(_))); } + #[tokio::test] + async fn test_read_file_offset_zero_rejected() { + let temp = NamedTempFile::new().unwrap(); + + let handler = ReadFileHandler; + let result = handler + .execute(serde_json::json!({ + "file_path": temp.path().to_str().unwrap(), + "offset": 0 + })) + .await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), ToolError::InvalidInput(_))); + } + + #[tokio::test] + async fn test_read_file_limit_zero_rejected() { + let temp = NamedTempFile::new().unwrap(); + + let handler = ReadFileHandler; + let result = handler + .execute(serde_json::json!({ + "file_path": temp.path().to_str().unwrap(), + "limit": 0 + })) + .await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), ToolError::InvalidInput(_))); + } + + #[tokio::test] + async fn test_read_file_offset_exceeds_length_rejected() { + let mut temp = NamedTempFile::new().unwrap(); + writeln!(temp, "line1").unwrap(); + + let handler = ReadFileHandler; + let result = handler + .execute(serde_json::json!({ + "file_path": temp.path().to_str().unwrap(), + "offset": 10 + })) + .await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), ToolError::InvalidInput(_))); + } + #[tokio::test] async fn test_read_file_crlf() { let mut temp = NamedTempFile::new().unwrap(); diff --git a/src/tools/handlers/write_file.rs b/src/tools/handlers/write_file.rs index 50551bf..bce77e6 100644 --- a/src/tools/handlers/write_file.rs +++ b/src/tools/handlers/write_file.rs @@ -234,4 +234,37 @@ mod tests { assert!(result.is_err()); // Could be IoError or PermissionDenied depending on the system } + + #[tokio::test] + async fn test_write_file_missing_content_rejected() { + let temp = tempdir().unwrap(); + let file = temp.path().join("missing-content.txt"); + + let handler = WriteFileHandler; + let result = handler + .execute(serde_json::json!({ + "file_path": file.to_str().unwrap() + })) + .await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), ToolError::InvalidInput(_))); + } + + #[tokio::test] + async fn test_write_file_invalid_content_type_rejected() { + let temp = tempdir().unwrap(); + let file = temp.path().join("bad-content-type.txt"); + + let handler = WriteFileHandler; + let result = handler + .execute(serde_json::json!({ + "file_path": file.to_str().unwrap(), + "content": 123 + })) + .await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), ToolError::InvalidInput(_))); + } }