Add 49 unit tests for uncovered pure logic in arc-llm

Cover cli.rs formatting/parsing/resolve/apply_options, common.rs
parse_error_body/extract_system_prompt/parse_retry_after, tools.rs
args_type_name, provider.rs validate_tool_choice, and types.rs
ToolChoice::mode_str.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-04 00:14:15 -05:00
parent 9447422e18
commit 4693c7373c
5 changed files with 441 additions and 0 deletions

View file

@ -442,3 +442,196 @@ async fn test_models(provider: Option<&str>, model: Option<&str>, s: &Styles) ->
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
// --- parse_option ---
#[test]
fn parse_option_valid() {
let (k, v) = parse_option("temperature=0.7").unwrap();
assert_eq!(k, "temperature");
assert_eq!(v, "0.7");
}
#[test]
fn parse_option_value_with_equals() {
let (k, v) = parse_option("key=a=b").unwrap();
assert_eq!(k, "key");
assert_eq!(v, "a=b");
}
#[test]
fn parse_option_no_equals() {
assert!(parse_option("nope").is_err());
}
// --- format_context_window ---
#[test]
fn format_context_window_millions() {
assert_eq!(format_context_window(1_000_000), "1m");
}
#[test]
fn format_context_window_thousands() {
assert_eq!(format_context_window(128_000), "128k");
}
#[test]
fn format_context_window_small() {
assert_eq!(format_context_window(400), "400");
}
#[test]
fn format_context_window_rounds_up() {
// 1500 rounds to 2000 -> "2k"
assert_eq!(format_context_window(1500), "2k");
}
#[test]
fn format_context_window_rounds_down() {
// 1499 rounds to 1000 -> "1k"
assert_eq!(format_context_window(1499), "1k");
}
#[test]
fn format_context_window_zero() {
assert_eq!(format_context_window(0), "0");
}
// --- format_cost ---
#[test]
fn format_cost_none() {
assert_eq!(format_cost(None), "-");
}
#[test]
fn format_cost_some() {
assert_eq!(format_cost(Some(3.0)), "$3.0");
}
#[test]
fn format_cost_fractional() {
assert_eq!(format_cost(Some(15.75)), "$15.8");
}
// --- format_speed ---
#[test]
fn format_speed_none() {
assert_eq!(format_speed(None), "-");
}
#[test]
fn format_speed_some() {
assert_eq!(format_speed(Some(85.5)), "85 tok/s");
}
// --- resolve_prompt ---
#[test]
fn resolve_prompt_arg_only() {
let result = resolve_prompt(Some("hello".into()), None).unwrap();
assert_eq!(result, "hello");
}
#[test]
fn resolve_prompt_stdin_only() {
let result = resolve_prompt(None, Some("piped".into())).unwrap();
assert_eq!(result, "piped");
}
#[test]
fn resolve_prompt_both_concatenates() {
let result = resolve_prompt(Some("arg".into()), Some("stdin".into())).unwrap();
assert_eq!(result, "stdin\narg");
}
#[test]
fn resolve_prompt_neither_errors() {
assert!(resolve_prompt(None, None).is_err());
}
// --- resolve_model ---
#[test]
fn resolve_model_explicit_known() {
let (model, provider) = resolve_model(Some("claude-sonnet-4-5".into()));
assert_eq!(model, "claude-sonnet-4-5");
assert_eq!(provider, Some("anthropic".to_string()));
}
#[test]
fn resolve_model_explicit_unknown() {
let (model, provider) = resolve_model(Some("nonexistent-model-xyz".into()));
assert_eq!(model, "nonexistent-model-xyz");
assert_eq!(provider, None);
}
#[test]
fn resolve_model_none_uses_default() {
let (model, provider) = resolve_model(None);
// Should return some valid model from catalog
assert!(!model.is_empty());
assert!(provider.is_some());
}
// --- apply_options ---
#[test]
fn apply_options_temperature() {
let params = GenerateParams::new("test-model");
let result = apply_options(params, &[("temperature".into(), "0.7".into())]).unwrap();
assert_eq!(result.temperature, Some(0.7));
}
#[test]
fn apply_options_max_tokens() {
let params = GenerateParams::new("test-model");
let result = apply_options(params, &[("max_tokens".into(), "4096".into())]).unwrap();
assert_eq!(result.max_tokens, Some(4096));
}
#[test]
fn apply_options_top_p() {
let params = GenerateParams::new("test-model");
let result = apply_options(params, &[("top_p".into(), "0.9".into())]).unwrap();
assert_eq!(result.top_p, Some(0.9));
}
#[test]
fn apply_options_unknown_key_goes_to_provider_opts() {
let params = GenerateParams::new("test-model");
let result =
apply_options(params, &[("custom_key".into(), "custom_val".into())]).unwrap();
let opts = result.provider_options.unwrap();
assert_eq!(opts["custom_key"], "custom_val");
}
#[test]
fn apply_options_invalid_temperature_errors() {
let params = GenerateParams::new("test-model");
assert!(
apply_options(params, &[("temperature".into(), "not_a_number".into())]).is_err()
);
}
#[test]
fn apply_options_invalid_max_tokens_errors() {
let params = GenerateParams::new("test-model");
assert!(apply_options(params, &[("max_tokens".into(), "abc".into())]).is_err());
}
#[test]
fn apply_options_empty() {
let params = GenerateParams::new("test-model");
let result = apply_options(params, &[]).unwrap();
assert_eq!(result.temperature, None);
assert_eq!(result.max_tokens, None);
assert_eq!(result.provider_options, None);
}
}

View file

@ -285,4 +285,72 @@ mod tests {
.iter()
.all(|p| !p.api_key_env_vars().is_empty()));
}
// Mock adapter that supports all tool choices
struct MockAdapter;
#[async_trait::async_trait]
impl ProviderAdapter for MockAdapter {
fn name(&self) -> &str {
"mock"
}
async fn complete(&self, _request: &Request) -> Result<Response, SdkError> {
unimplemented!()
}
async fn stream(&self, _request: &Request) -> Result<StreamEventStream, SdkError> {
unimplemented!()
}
}
// Mock adapter that rejects "named" tool choice
struct RestrictedAdapter;
#[async_trait::async_trait]
impl ProviderAdapter for RestrictedAdapter {
fn name(&self) -> &str {
"restricted"
}
async fn complete(&self, _request: &Request) -> Result<Response, SdkError> {
unimplemented!()
}
async fn stream(&self, _request: &Request) -> Result<StreamEventStream, SdkError> {
unimplemented!()
}
fn supports_tool_choice(&self, mode: &str) -> bool {
mode != "named"
}
}
#[test]
fn validate_tool_choice_auto_accepted() {
assert!(validate_tool_choice(&MockAdapter, &ToolChoice::Auto).is_ok());
}
#[test]
fn validate_tool_choice_none_accepted() {
assert!(validate_tool_choice(&MockAdapter, &ToolChoice::None).is_ok());
}
#[test]
fn validate_tool_choice_required_accepted() {
assert!(validate_tool_choice(&MockAdapter, &ToolChoice::Required).is_ok());
}
#[test]
fn validate_tool_choice_named_rejected_by_restricted() {
let result = validate_tool_choice(&RestrictedAdapter, &ToolChoice::named("my_tool"));
assert!(result.is_err());
match result.unwrap_err() {
SdkError::UnsupportedToolChoice { message } => {
assert!(message.contains("restricted"));
assert!(message.contains("named"));
}
other => panic!("expected UnsupportedToolChoice, got {other:?}"),
}
}
#[test]
fn validate_tool_choice_named_accepted_by_default() {
assert!(validate_tool_choice(&MockAdapter, &ToolChoice::named("my_tool")).is_ok());
}
}

View file

@ -271,6 +271,7 @@ impl LineReader {
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ContentPart;
#[test]
fn is_file_path_absolute() {
@ -379,4 +380,131 @@ mod tests {
assert_eq!(info.requests_remaining, None);
assert_eq!(info.tokens_limit, Some(10000));
}
// --- parse_error_body ---
#[test]
fn parse_error_body_valid_json() {
let body = r#"{"error":{"message":"rate limited","type":"rate_limit_error"}}"#;
let (msg, code, raw) = parse_error_body(body, "type");
assert_eq!(msg, "rate limited");
assert_eq!(code.as_deref(), Some("rate_limit_error"));
assert!(raw.is_some());
}
#[test]
fn parse_error_body_missing_error_field() {
let body = r#"{"status":"fail"}"#;
let (msg, code, raw) = parse_error_body(body, "type");
assert_eq!(msg, "Unknown error");
assert_eq!(code, None);
assert!(raw.is_some());
}
#[test]
fn parse_error_body_not_json() {
let body = "Internal Server Error";
let (msg, code, raw) = parse_error_body(body, "type");
assert_eq!(msg, "Internal Server Error");
assert_eq!(code, None);
assert!(raw.is_none());
}
#[test]
fn parse_error_body_different_code_field() {
let body = r#"{"error":{"message":"bad","status":"INVALID_ARGUMENT"}}"#;
let (msg, code, _) = parse_error_body(body, "status");
assert_eq!(msg, "bad");
assert_eq!(code.as_deref(), Some("INVALID_ARGUMENT"));
}
#[test]
fn parse_error_body_no_message() {
let body = r#"{"error":{"type":"server_error"}}"#;
let (msg, code, _) = parse_error_body(body, "type");
assert_eq!(msg, "Unknown error");
assert_eq!(code.as_deref(), Some("server_error"));
}
// --- extract_system_prompt ---
#[test]
fn extract_system_prompt_no_system() {
let msgs = vec![Message::user("hello")];
let (sys, other) = extract_system_prompt(&msgs);
assert_eq!(sys, None);
assert_eq!(other.len(), 1);
}
#[test]
fn extract_system_prompt_system_only() {
let msgs = vec![Message::system("Be helpful"), Message::user("hi")];
let (sys, other) = extract_system_prompt(&msgs);
assert_eq!(sys.as_deref(), Some("Be helpful"));
assert_eq!(other.len(), 1);
assert_eq!(other[0].role, Role::User);
}
#[test]
fn extract_system_prompt_multiple_system() {
let msgs = vec![
Message::system("Rule 1"),
Message::system("Rule 2"),
Message::user("hi"),
];
let (sys, other) = extract_system_prompt(&msgs);
assert_eq!(sys.as_deref(), Some("Rule 1\nRule 2"));
assert_eq!(other.len(), 1);
}
#[test]
fn extract_system_prompt_developer_role() {
let dev = Message {
role: Role::Developer,
content: vec![ContentPart::text("dev instructions")],
name: None,
tool_call_id: None,
};
let msgs = vec![dev, Message::user("hi")];
let (sys, other) = extract_system_prompt(&msgs);
assert_eq!(sys.as_deref(), Some("dev instructions"));
assert_eq!(other.len(), 1);
}
#[test]
fn extract_system_prompt_empty() {
let msgs: Vec<Message> = vec![];
let (sys, other) = extract_system_prompt(&msgs);
assert_eq!(sys, None);
assert!(other.is_empty());
}
// --- parse_retry_after ---
#[test]
fn parse_retry_after_valid() {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("retry-after", "2.5".parse().unwrap());
assert_eq!(parse_retry_after(&headers), Some(2.5));
}
#[test]
fn parse_retry_after_missing() {
let headers = reqwest::header::HeaderMap::new();
assert_eq!(parse_retry_after(&headers), None);
}
#[test]
fn parse_retry_after_invalid() {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("retry-after", "not-a-number".parse().unwrap());
assert_eq!(parse_retry_after(&headers), None);
}
#[test]
fn parse_retry_after_integer() {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("retry-after", "5".parse().unwrap());
assert_eq!(parse_retry_after(&headers), Some(5.0));
}
}

View file

@ -466,4 +466,36 @@ mod tests {
.unwrap()
.contains("repair failed"));
}
// --- args_type_name ---
#[test]
fn args_type_name_null() {
assert_eq!(args_type_name(&serde_json::Value::Null), "null");
}
#[test]
fn args_type_name_bool() {
assert_eq!(args_type_name(&serde_json::json!(true)), "boolean");
}
#[test]
fn args_type_name_number() {
assert_eq!(args_type_name(&serde_json::json!(42)), "number");
}
#[test]
fn args_type_name_string() {
assert_eq!(args_type_name(&serde_json::json!("hello")), "string");
}
#[test]
fn args_type_name_array() {
assert_eq!(args_type_name(&serde_json::json!([1, 2])), "array");
}
#[test]
fn args_type_name_object() {
assert_eq!(args_type_name(&serde_json::json!({})), "object");
}
}

View file

@ -1263,4 +1263,24 @@ mod tests {
other => panic!("Expected StepFinish, got {other:?}"),
}
}
#[test]
fn tool_choice_mode_str_auto() {
assert_eq!(ToolChoice::Auto.mode_str(), "auto");
}
#[test]
fn tool_choice_mode_str_none() {
assert_eq!(ToolChoice::None.mode_str(), "none");
}
#[test]
fn tool_choice_mode_str_required() {
assert_eq!(ToolChoice::Required.mode_str(), "required");
}
#[test]
fn tool_choice_mode_str_named() {
assert_eq!(ToolChoice::named("get_weather").mode_str(), "named");
}
}