use super::*;
use crate::agent::stream_completion::record_quota_exhaustion;
use crate::quota_channel::Channel;
use crate::rate_limit::{self, get_rate_limit_info, mark_rate_limited, refusal_on_channel};
use crate::types::AgentKind;
#[test]
fn droid_default_can_launch_with_only_core_held_refusal_regression() {
let temp = tempfile::tempdir().expect("home");
let _home = crate::paths::AidHomeGuard::set(temp.path());
let droid = AgentKind::Droid;
rate_limit::mark_rate_limited_for_message(
&droid,
None,
"You've reached your monthly Droid Core usage limit (resets in 2 days).",
);
assert_eq!(
crate::agent::model_group::model_group(droid, None),
Some("standard")
);
assert!(rate_limit::dispatch_blocking_hold_for_model(&droid, None, None).is_none());
assert!(rate_limit::dispatch_blocking_hold_for_model(&droid, None, Some("glm-5.2")).is_some());
}
#[test]
fn monthly_droid_core_provider_error_holds_until_reset_refusal_regression() {
let message = "402 You've reached your monthly Droid Core usage limit (resets in 2 days).";
assert!(
crate::rate_limit_signatures::match_quota_signature_for_agent(message, AgentKind::Droid).is_some()
);
for channel in [Channel::CliStream, Channel::CliStderr] {
let temp = tempfile::tempdir().expect("home");
let _home = crate::paths::AidHomeGuard::set(temp.path());
let event = serde_json::json!({"type": "error", "message": message}).to_string();
if channel == Channel::CliStderr {
assert_eq!(refusal_on_channel(&event, AgentKind::Droid, channel), None);
continue;
}
let refusal = refusal_on_channel(&event, AgentKind::Droid, channel).expect("refusal");
rate_limit::mark_rate_limited_for_message(&AgentKind::Droid, None, &refusal);
let marker = rate_limit::group_marker_path(&AgentKind::Droid, None, "core");
let body = std::fs::read_to_string(marker).expect("Core marker");
let reset = body
.lines()
.find_map(|line| line.strip_prefix("recovery_at: "))
.expect("reset");
let at = rate_limit::parse_recovery_datetime(reset).expect("date");
assert!((47..=48).contains(&(at - Local::now().naive_local()).num_hours()));
assert!(rate_limit::is_group_rate_limited(&AgentKind::Droid, None, "core"));
assert!(!rate_limit::is_group_rate_limited(
&AgentKind::Droid,
None,
"standard"
));
let prose = serde_json::json!({"type": "assistant_message", "content": message}).to_string();
assert_eq!(
refusal_on_channel(&prose, AgentKind::Droid, Channel::CliStream),
None
);
}
}
#[test]
fn claude_seven_day_hold_lasts_until_structured_reset_refusal_regression() {
let temp = tempfile::tempdir().expect("home");
let _home = crate::paths::AidHomeGuard::set(temp.path());
let reset = Utc::now().timestamp().div_euclid(60) * 60 + 7 * 24 * 3600;
let event = serde_json::json!({
"type": "rate_limit_event",
"rate_limit_info": {"status": "rejected", "rateLimitType": "seven_day", "resetsAt": reset}
})
.to_string();
assert!(record_quota_exhaustion(&event, AgentKind::Claude, None, None).recorded());
let info = get_rate_limit_info(&AgentKind::Claude, None).expect("hold");
let expected = format_recovery(
DateTime::from_timestamp(reset, 0)
.expect("timestamp")
.with_timezone(&Local)
.naive_local(),
);
assert_eq!(info.recovery_at.as_deref(), Some(expected.as_str()));
assert_eq!(
rate_limit::parse_recovery_datetime(&expected),
Some(
DateTime::from_timestamp(reset, 0)
.expect("timestamp")
.with_timezone(&Local)
.naive_local()
)
);
assert!(rate_limit::is_rate_limited(&AgentKind::Claude, None));
}
#[test]
fn expired_claude_seven_day_reset_releases_hold_refusal_regression() {
let temp = tempfile::tempdir().expect("home");
let _home = crate::paths::AidHomeGuard::set(temp.path());
let event = serde_json::json!({
"type": "rate_limit_event",
"rate_limit_info": {"status": "rejected", "rateLimitType": "seven_day",
"resetsAt": Utc::now().timestamp() - 60}
})
.to_string();
assert!(record_quota_exhaustion(&event, AgentKind::Claude, None, None).recorded());
assert!(!rate_limit::is_rate_limited(&AgentKind::Claude, None));
}
#[test]
fn unrelated_429_with_unix_trace_does_not_create_a_dated_hold() {
let message = "HTTP 429 downloading an artifact; trace|1893456000";
assert_eq!(parse_recovery_time(message), None);
let temp = tempfile::tempdir().expect("temp dir");
let _home = crate::paths::AidHomeGuard::set(temp.path());
let event = serde_json::json!({"type": "error", "message": message}).to_string();
let refusal = refusal_on_channel(&event, AgentKind::Cursor, Channel::CliStream)
.expect("429 in error envelope");
mark_rate_limited(&AgentKind::Cursor, None, &refusal);
let info = get_rate_limit_info(&AgentKind::Cursor, None).expect("marker");
assert_eq!(info.recovery_at, None, "{info:?}");
assert!(!rate_limit::is_rate_limited(&AgentKind::Cursor, None));
}
#[test]
fn unwrapped_claude_prose_cannot_create_a_hold() {
let temp = tempfile::tempdir().expect("temp dir");
let _home = crate::paths::AidHomeGuard::set(temp.path());
for line in [
"Fixture says You've hit your limit",
"Fixture says You've hit your limit|1893456000",
"HTTP 429 downloading an artifact; trace|1893456000",
"You've hit your limit",
] {
assert_eq!(
refusal_on_channel(line, AgentKind::Claude, Channel::CliStream),
None,
"{line}"
);
assert!(!record_quota_exhaustion(line, AgentKind::Claude, None, None).recorded());
}
assert_eq!(
refusal_on_channel("You've hit your limit", AgentKind::Claude, Channel::CliStderr),
None
);
assert!(get_rate_limit_info(&AgentKind::Claude, None).is_none());
}
#[test]
fn rejected_claude_event_uses_its_nearby_unix_reset() {
let temp = tempfile::tempdir().expect("temp dir");
let _home = crate::paths::AidHomeGuard::set(temp.path());
let reset = Utc::now().timestamp() + 7200;
let event = serde_json::json!({
"type": "rate_limit_event",
"rate_limit_info": {"status": "rejected", "resetsAt": reset}
}).to_string();
assert!(record_quota_exhaustion(&event, AgentKind::Claude, None, None).recorded());
let info = get_rate_limit_info(&AgentKind::Claude, None).expect("marker");
let expected = format_recovery(
DateTime::from_timestamp(reset, 0).expect("timestamp").with_timezone(&Local).naive_local()
);
assert_eq!(info.recovery_at.as_deref(), Some(expected.as_str()));
assert!(rate_limit::is_rate_limited(&AgentKind::Claude, None));
}
#[test]
fn far_future_claude_reset_falls_back_to_signature_window() {
let temp = tempfile::tempdir().expect("temp dir");
let _home = crate::paths::AidHomeGuard::set(temp.path());
let reset = Utc::now().timestamp() + 30 * 24 * 3600;
let event = serde_json::json!({
"type": "rate_limit_event",
"rate_limit_info": {"status": "rejected", "resetsAt": reset}
}).to_string();
assert!(record_quota_exhaustion(&event, AgentKind::Claude, None, None).recorded());
let info = get_rate_limit_info(&AgentKind::Claude, None).expect("marker");
let at = rate_limit::parse_recovery_datetime(
info.recovery_at.as_deref().expect("bounded fallback")
).expect("parse fallback");
let minutes = (at - Local::now().naive_local()).num_minutes();
assert!((298..=300).contains(&minutes), "expected five-hour fallback, got {minutes}");
}
#[test]
fn plain_claude_stdout_still_sets_the_auth_marker() {
let line = "Invalid API key ยท Please run /login";
let found = crate::auth_marker::auth_failure_line(line, AgentKind::Claude, Channel::CliStream);
assert!(found.is_some_and(|text| text.contains("/login")));
assert_eq!(refusal_on_channel(line, AgentKind::Claude, Channel::CliStream), None);
}