use pretty_assertions::assert_eq;
use rho_providers::credentials::MemoryCredentialStore;
use std::{collections::BTreeMap, time::Instant};
use super::*;
use crate::usage_limits::{ProviderUsageLimits, UsageLimitWindow, UsageProviderKind};
use crate::usage_limits_cache::UsageLimitsCache;
fn sample_claude_state(observed_at_unix: i64) -> crate::claude_runtime::rate_limit::RateLimitState {
let mut state = crate::claude_runtime::rate_limit::RateLimitState::default();
state.merge_window(crate::claude_runtime::rate_limit::RateLimitObservation {
observed_at_unix,
observed_seq: 1,
observed_at_nanos: u64::try_from(observed_at_unix.max(0))
.unwrap_or(0)
.saturating_mul(1_000_000_000),
observed_nonce: "test".into(),
info: crate::claude_runtime::stream::RateLimitInfo {
status: Some("allowed".into()),
rate_limit_type: Some("five_hour".into()),
resets_at: Some(1_800_000_000),
utilization: None,
overage_status: None,
overage_resets_at: None,
is_using_overage: Some(false),
},
});
state
}
fn codex_window() -> UsageLimitWindow {
UsageLimitWindow {
label: "Weekly".into(),
remaining_percent: Some(40.0),
resets_at_unix: Some(now_unix() + 3_600),
note: None,
}
}
#[test]
fn opening_limits_does_not_queue_model_context() {
let mut app = super::super::tests::test_app();
app.begin_provider_turn_ui();
app.start_limits_command();
assert!(app.pending.steering_prompts().is_empty());
assert!(app.pending.queued_prompts().is_empty());
assert!(matches!(
app.input_ui.composer(),
super::super::ComposerMode::Limits(_)
));
assert!(
app.history
.entries()
.iter()
.all(|entry| !matches!(entry, super::super::Entry::Error(_))),
"limits overlay must not dump a transcript error, got {:?}",
app.history.entries()
);
}
#[tokio::test]
async fn cancelling_limits_query_waits_for_background_task_to_stop() {
let mut app = super::super::tests::test_app();
let task_marker = std::sync::Arc::new(());
let captured_marker = task_marker.clone();
app.pending_usage_limits.push(PendingUsageFetch {
kind: UsageProviderKind::Codex,
handle: tokio::spawn(async move {
let _marker = captured_marker;
std::future::pending::<Result<Option<ProviderUsageLimits>, UsageLimitsError>>().await
}),
});
app.cancel_limits_command().await;
assert!(app.pending_usage_limits.is_empty());
assert_eq!(std::sync::Arc::strong_count(&task_marker), 1);
}
#[test]
fn formats_reset_relative_only_within_one_day() {
assert_eq!(format_reset_at(200_000, 200_000 - 90 * 60), "in 1h 30m");
assert!(!format_reset_at(200_000, 0).starts_with("in "));
}
#[test]
fn applying_one_provider_leaves_others_checking() {
let mut overlay = LimitsOverlay {
sections: vec![
LimitsSection {
id: LimitsSectionId::Provider(UsageProviderKind::Codex),
label: UsageProviderKind::Codex.label().into(),
status: LimitsSectionStatus::Checking {
cached_at_unix: None,
},
windows: Vec::new(),
},
LimitsSection {
id: LimitsSectionId::Provider(UsageProviderKind::KimiCode),
label: UsageProviderKind::KimiCode.label().into(),
status: LimitsSectionStatus::Checking {
cached_at_unix: None,
},
windows: Vec::new(),
},
],
empty_note: None,
scroll: 0,
checking_started: Instant::now(),
};
overlay.apply_live(UsageProviderKind::Codex, vec![codex_window()]);
assert_eq!(overlay.sections[0].status, LimitsSectionStatus::Live);
assert_eq!(overlay.sections[0].windows.len(), 1);
assert!(matches!(
overlay.sections[1].status,
LimitsSectionStatus::Checking {
cached_at_unix: None
}
));
assert!(overlay.sections[1].windows.is_empty());
}
#[test]
fn live_ready_section_uses_in_memory_fetch_time_not_cache() {
let windows = vec![codex_window()];
let mut live = BTreeMap::new();
live.insert(
UsageProviderKind::Codex,
LiveUsage::Ready {
limits: ProviderUsageLimits {
provider: UsageProviderKind::Codex.label().into(),
windows: windows.clone(),
},
fetched_at_unix: 1_700,
},
);
let section = provider_section(
UsageProviderKind::Codex,
&live,
&[],
&UsageLimitsCache::default(),
);
assert_eq!(section.status, LimitsSectionStatus::Live);
assert_eq!(section.windows, windows);
}
#[test]
fn claude_section_is_present_without_oauth() {
let overlay = build_limits_overlay(
&MemoryCredentialStore::default(),
&BTreeMap::new(),
&[],
UsageLimitsCache::default(),
Some(&sample_claude_state(1_000)),
1_125,
);
assert_eq!(overlay.sections.len(), 1);
assert_eq!(overlay.sections[0].id, LimitsSectionId::ClaudeCode);
assert_eq!(overlay.sections[0].windows.len(), 1);
assert_eq!(overlay.sections[0].windows[0].label, "Five hour");
assert!(overlay.sections[0].windows[0].remaining_percent.is_none());
assert!(matches!(
overlay.sections[0].status,
LimitsSectionStatus::Observed {
observed_at_unix: 1_000
}
));
}
#[test]
fn claude_section_surfaces_utilization_without_allowed_status() {
let mut state = crate::claude_runtime::rate_limit::RateLimitState::default();
let mut five = sample_claude_state(1_000).windows.remove(0);
five.info.utilization = Some(0.25);
five.info.status = Some("allowed_warning".into());
let mut weekly = five.clone();
weekly.info.rate_limit_type = Some("seven_day".into());
weekly.info.utilization = Some(0.4);
weekly.info.status = Some("allowed".into());
weekly.observed_at_unix = 1_050;
weekly.observed_at_nanos = 1_050_000_000_000;
state.merge_window(five);
state.merge_window(weekly);
let section = claude_provider_limits(Some(&state), 1_100).expect("claude");
assert_eq!(section.windows[0].label, "Five hour");
assert_eq!(section.windows[0].remaining_percent, Some(75.0));
assert_eq!(section.windows[1].label, "Seven day");
assert_eq!(section.windows[1].remaining_percent, Some(60.0));
}
#[test]
fn missing_claude_state_does_not_invent_a_section() {
let overlay = build_limits_overlay(
&MemoryCredentialStore::default(),
&BTreeMap::new(),
&[],
UsageLimitsCache::default(),
None,
0,
);
assert!(overlay.sections.is_empty());
assert!(overlay.empty_note.is_some());
}
#[test]
fn overlay_body_uses_global_window_label_column() {
let overlay = LimitsOverlay {
sections: vec![
LimitsSection {
id: LimitsSectionId::Provider(UsageProviderKind::Codex),
label: UsageProviderKind::Codex.label().into(),
status: LimitsSectionStatus::Live,
windows: vec![UsageLimitWindow {
label: "5-hour".into(),
remaining_percent: Some(40.0),
resets_at_unix: None,
note: None,
}],
},
LimitsSection {
id: LimitsSectionId::Provider(UsageProviderKind::Xai),
label: UsageProviderKind::Xai.label().into(),
status: LimitsSectionStatus::Live,
windows: vec![UsageLimitWindow {
label: "Monthly".into(),
remaining_percent: Some(80.0),
resets_at_unix: None,
note: None,
}],
},
],
empty_note: None,
scroll: 0,
checking_started: Instant::now(),
};
let lines = overlay_body_lines(&overlay, 80, None, 10);
let texts: Vec<String> = lines
.iter()
.map(|line| {
line.spans
.iter()
.map(|span| span.content.as_ref())
.collect()
})
.collect();
let five = texts
.iter()
.find(|text| text.contains("5-hour"))
.expect("5-hour row");
let monthly = texts
.iter()
.find(|text| text.contains("Monthly"))
.expect("Monthly row");
let five_bar = five
.find('█')
.or_else(|| five.find('░'))
.expect("codex bar");
let monthly_bar = monthly
.find('█')
.or_else(|| monthly.find('░'))
.expect("xai bar");
assert_eq!(five_bar, monthly_bar);
}