// Tests for Claude stream-json event parsing.
// Covers rate-limit marking and detail capping with metadata.full.
// Loaded by `claude_events.rs` under `#[cfg(test)]`.
use super::*;
use crate::{paths, rate_limit};
// modelUsage copied verbatim from a real two-model result in ~/.aid/logs/t-73b769ce.jsonl.
const CAPTURED_RESULT: &str = r#"{"type":"result","modelUsage":{"claude-opus-5-5":{"inputTokens":146,"outputTokens":58023,"cacheReadInputTokens":9931249,"cacheCreationInputTokens":188174,"webSearchRequests":0,"costUSD":4.6526858,"contextWindow":1000000,"maxOutputTokens":128000,"thinkingTokens":25274,"canonicalModel":"claude-opus-5-5","provider":"firstParty","costBasis":"list"},"claude-haiku-4-5-20251001":{"inputTokens":65614,"outputTokens":835,"cacheReadInputTokens":0,"cacheCreationInputTokens":0,"webSearchRequests":1,"costUSD":0.079789,"contextWindow":200000,"maxOutputTokens":32000,"thinkingTokens":0,"canonicalModel":"claude-haiku-4-5","provider":"firstParty","costBasis":"list"}}}"#;
#[test]
fn marks_claude_rate_limits_from_error_and_user_events() {
let temp = tempfile::tempdir().unwrap();
let _aid_home = paths::AidHomeGuard::set(temp.path());
rate_limit::clear_rate_limit(&AgentKind::Claude, None);
let task_id = TaskId("t-claude-rate".to_string());
let event = parse_event_line(
&task_id,
r#"{"type":"error","message":"rate limit exceeded"}"#,
)
.unwrap();
assert_eq!(event.event_kind, EventKind::Error);
assert!(
rate_limit::get_rate_limit_info(&AgentKind::Claude, None).is_some(),
"generic 429 still writes a Transient marker"
);
assert!(
!rate_limit::is_rate_limited(&AgentKind::Claude, None),
"Transient is not Held"
);
rate_limit::clear_rate_limit(&AgentKind::Claude, None);
// A `tool_result` block flagged `is_error` is the *tool* failing, and its
// text is whatever the model asked for. It is still an Error event; it is
// not testimony about the provider. Marking here wrote
// `~/.aid/rate-limit-claude` on 2026-08-07 off an agent's own message.
let event = parse_event_line(
&task_id,
r#"{"type":"user","message":{"content":[{"content":"HTTP 429 too many requests","is_error":true}]}}"#,
)
.unwrap();
assert_eq!(event.event_kind, EventKind::Error);
assert!(!rate_limit::is_rate_limited(&AgentKind::Claude, None));
}
#[test]
fn caps_long_assistant_text_and_keeps_full_in_metadata() {
let task_id = TaskId("t-claude-cap".to_string());
let long_text = format!("Reasoning {}", "w".repeat(120));
let line = serde_json::json!({
"type": "assistant",
"message": { "content": [{ "type": "text", "text": long_text }] }
})
.to_string();
let event = parse_event_line(&task_id, &line).unwrap();
assert!(event.detail.len() <= crate::agent::truncate::EVENT_DETAIL_MAX);
assert!(event.detail.ends_with("..."));
let metadata = event.metadata.expect("metadata with full text");
assert_eq!(metadata["full"].as_str(), Some(long_text.as_str()));
}
#[test]
fn short_assistant_text_has_no_full_metadata() {
let task_id = TaskId("t-claude-short".to_string());
let line = serde_json::json!({
"type": "assistant",
"message": { "content": [{ "type": "text", "text": "brief note" }] }
})
.to_string();
let event = parse_event_line(&task_id, &line).unwrap();
assert_eq!(event.detail, "brief note");
assert!(event.metadata.is_none());
}
#[test]
fn picks_claude_model_with_highest_cost_from_captured_result() {
let event = parse_event_line(&TaskId("t-multi-model".to_string()), CAPTURED_RESULT).unwrap();
assert_eq!(event.metadata.unwrap()["model"], "claude-opus-5-5");
}
#[test]
fn picks_claude_model_by_tokens_when_cost_missing_or_tied() {
let mut result: Value = serde_json::from_str(CAPTURED_RESULT).unwrap();
result["modelUsage"]["claude-haiku-4-5-20251001"]["costUSD"] =
result["modelUsage"]["claude-opus-5-5"]["costUSD"].clone();
assert_eq!(extract_result_model(&result).as_deref(), Some("claude-opus-5-5"));
result["modelUsage"]["claude-haiku-4-5-20251001"].as_object_mut().unwrap().remove("costUSD");
assert_eq!(extract_result_model(&result).as_deref(), Some("claude-opus-5-5"));
result["modelUsage"]["claude-opus-5-5"].as_object_mut().unwrap().remove("costUSD");
assert_eq!(extract_result_model(&result).as_deref(), Some("claude-opus-5-5"));
for key in ["inputTokens", "outputTokens", "cacheReadInputTokens", "cacheCreationInputTokens"] {
result["modelUsage"]["claude-haiku-4-5-20251001"][key] =
result["modelUsage"]["claude-opus-5-5"][key].clone();
}
assert_eq!(extract_result_model(&result).as_deref(), Some("claude-haiku-4-5-20251001"));
}
#[test]
fn regression_picks_larger_claude_model_with_huge_token_counts() {
let result = json!({
"modelUsage": {
"a-large": {"inputTokens": i64::MAX, "outputTokens": i64::MAX},
"z-small": {"inputTokens": i64::MAX, "outputTokens": 1}
}
});
assert_eq!(extract_result_model(&result).as_deref(), Some("a-large"));
}