use super::*;
use crate::paths::{self, AidHomeGuard};
use crate::rate_limit::{
clear_all_rate_limits_for_agent, is_rate_limited, mark_group_rate_limited, mark_rate_limited,
};
use crate::types::AgentKind;
fn custom_agent_toml(id: &str) -> String {
format!(
"[agent]\nid = \"{id}\"\ndisplay_name = \"{id} agent\"\ncommand = \"{id}\"\n"
)
}
fn write_custom_agent(aid_home: &std::path::Path, id: &str) {
let agents_dir = aid_home.join(".aid").join("agents");
std::fs::create_dir_all(&agents_dir).unwrap();
std::fs::write(agents_dir.join(format!("{id}.toml")), custom_agent_toml(id)).unwrap();
}
fn isolated() -> tempfile::TempDir {
let temp = tempfile::tempdir().expect("tempdir");
let _ = std::fs::create_dir_all(temp.path().join(".aid"));
temp
}
#[test]
fn healthy_list_omits_status_column() {
let temp = isolated();
let _guard = AidHomeGuard::set(temp.path());
std::fs::create_dir_all(paths::aid_dir()).ok();
let rows: Vec<_> = AgentKind::ALL_BUILTIN
.iter()
.map(|k| quota_row(*k, None))
.collect();
assert!(rows.iter().all(|r| matches!(r, QuotaRow::Ok)));
}
#[test]
fn agent_level_hold_is_limited_not_partial() {
let temp = isolated();
let _guard = AidHomeGuard::set(temp.path());
std::fs::create_dir_all(paths::aid_dir()).ok();
let stated = crate::rate_limit::test_future_recovery_time();
mark_rate_limited(
&AgentKind::Codex,
None,
&format!("You've hit your usage limit. try again at {stated}."),
);
match quota_row(AgentKind::Codex, None) {
QuotaRow::Limited { detail } => {
assert!(detail.contains(&format!("resets {stated}")), "{detail}");
assert!(!detail.contains("~1h"), "must not invent a reset time");
}
other => panic!("expected Limited, got {other:?}"),
}
assert!(matches!(quota_row(AgentKind::Gemini, None), QuotaRow::Ok));
}
#[test]
fn cursor_premium_group_hold_is_partial_and_names_clear_limit() {
let temp = isolated();
let _guard = AidHomeGuard::set(temp.path());
std::fs::create_dir_all(paths::aid_dir()).ok();
mark_group_rate_limited(&AgentKind::Cursor, None,
"premium",
"ActionRequiredError: Increase limits for faster responses You're out of usage. \
Switch to Auto, or ask your admin to increase your limit to continue.",
);
assert!(
!is_rate_limited(&AgentKind::Cursor, None),
"group hold must not mark the agent"
);
match quota_row(AgentKind::Cursor, None) {
QuotaRow::Partial { detail } => {
assert!(detail.contains("premium"), "{detail}");
assert!(
detail.contains("aid config clear-limit cursor"),
"manual hold must name the clear command: {detail}"
);
assert!(!detail.contains("~1h"), "{detail}");
}
other => panic!("expected Partial, got {other:?}"),
}
clear_all_rate_limits_for_agent(&AgentKind::Cursor, None);
assert!(matches!(quota_row(AgentKind::Cursor, None), QuotaRow::Ok));
}
#[test]
fn group_hold_detail_includes_provider_message() {
let temp = isolated();
let _guard = AidHomeGuard::set(temp.path());
std::fs::create_dir_all(paths::aid_dir()).ok();
mark_group_rate_limited(&AgentKind::Cursor, None,
"premium",
"ActionRequiredError: ask your admin to increase your limit to continue.",
);
match quota_row(AgentKind::Cursor, None) {
QuotaRow::Partial { detail } => {
assert!(detail.contains("premium"), "{detail}");
assert!(
detail.contains("ActionRequiredError"),
"provider message must appear in group hold detail: {detail}"
);
}
other => panic!("expected Partial, got {other:?}"),
}
}
#[test]
fn human_ended_agent_hold_does_not_invent_reset_time() {
let temp = isolated();
let _guard = AidHomeGuard::set(temp.path());
std::fs::create_dir_all(paths::aid_dir()).ok();
mark_rate_limited(&AgentKind::OpenCode, None,
"APIError: Insufficient balance. Manage your billing here",
);
match quota_row(AgentKind::OpenCode, None) {
QuotaRow::Limited { detail } => {
assert!(
detail.contains("aid config clear-limit opencode"),
"{detail}"
);
assert!(!detail.contains("resets"), "{detail}");
assert!(!detail.contains("~1h"), "{detail}");
}
other => panic!("expected Limited, got {other:?}"),
}
}
#[test]
fn custom_agent_with_hold_shows_limited() {
let temp = isolated();
let _guard = AidHomeGuard::set(temp.path());
std::fs::create_dir_all(paths::aid_dir()).ok();
let stated = crate::rate_limit::test_future_recovery_time();
mark_rate_limited(
&AgentKind::Custom,
Some("auditor"),
&format!("try again at {stated}."),
);
match quota_row(AgentKind::Custom, Some("auditor")) {
QuotaRow::Limited { detail } => {
assert!(detail.contains(&format!("resets {stated}")), "{detail}");
}
other => panic!("expected Limited, got {other:?}"),
}
assert!(
matches!(quota_row(AgentKind::Custom, Some("other-agent")), QuotaRow::Ok),
"unrelated custom agent must not inherit the hold"
);
assert!(
matches!(quota_row(AgentKind::Codex, None), QuotaRow::Ok),
"built-in codex must not inherit custom agent hold"
);
}
#[test]
fn custom_agent_human_hold_names_agent_id_in_clear_limit_hint() {
let temp = isolated();
let _guard = AidHomeGuard::set(temp.path());
std::fs::create_dir_all(paths::aid_dir()).ok();
mark_rate_limited(
&AgentKind::Custom,
Some("auditor"),
"APIError: Insufficient balance. Manage your billing here",
);
match quota_row(AgentKind::Custom, Some("auditor")) {
QuotaRow::Limited { detail } => {
assert!(
detail.contains("clear-limit auditor"),
"hint must name the agent id 'auditor', got: {detail}"
);
assert!(
!detail.contains("clear-limit custom"),
"hint must NOT say 'clear-limit custom', got: {detail}"
);
}
other => panic!("expected Limited, got {other:?}"),
}
}
#[test]
fn show_quota_and_list_agents_cover_held_custom_agent_loop() {
let temp = isolated();
let _guard = AidHomeGuard::set(temp.path());
std::fs::create_dir_all(paths::aid_dir()).ok();
write_custom_agent(temp.path(), "held-agent");
mark_rate_limited(
&AgentKind::Custom,
Some("held-agent"),
"APIError: Insufficient balance.",
);
let quota_result = show_quota();
assert!(quota_result.is_ok(), "show_quota failed: {:?}", quota_result);
let list_result = list_agents();
assert!(list_result.is_ok(), "list_agents failed: {:?}", list_result);
}