use super::*;
const BASE_UTC: i64 = 1_779_027_600;
#[test]
fn short_label_drops_claude_prefix_and_keeps_max_multiplier() {
assert_eq!(
PlanTier::Max(Some(5)).short_label().as_deref(),
Some("Max 5x")
);
assert_eq!(PlanTier::Max(None).short_label().as_deref(), Some("Max"));
assert_eq!(PlanTier::Pro.short_label().as_deref(), Some("Pro"));
assert_eq!(PlanTier::Unknown.short_label(), None);
}
#[test]
fn parses_z_suffix() {
assert_eq!(iso_to_epoch_secs("2026-05-17T14:20:00Z"), Some(BASE_UTC));
assert_eq!(
iso_to_epoch_secs("2026-05-17T14:20:00.121699Z"),
Some(BASE_UTC)
);
}
#[test]
fn parses_colon_offset() {
assert_eq!(
iso_to_epoch_secs("2026-05-17T14:20:00+00:00"),
Some(BASE_UTC)
);
assert_eq!(
iso_to_epoch_secs("2026-05-17T14:20:00.121699+00:00"),
Some(BASE_UTC)
);
assert_eq!(
iso_to_epoch_secs("2026-05-17T14:20:00+05:30"),
Some(BASE_UTC - (5 * 3600 + 30 * 60))
);
assert_eq!(
iso_to_epoch_secs("2026-05-17T14:20:00-05:30"),
Some(BASE_UTC + (5 * 3600 + 30 * 60))
);
}
#[test]
fn parses_colonless_offset() {
assert_eq!(
iso_to_epoch_secs("2026-05-17T14:20:00+0000"),
Some(BASE_UTC)
);
assert_eq!(
iso_to_epoch_secs("2026-05-17T14:20:00.121699+0000"),
Some(BASE_UTC)
);
assert_eq!(
iso_to_epoch_secs("2026-05-17T14:20:00+0530"),
Some(BASE_UTC - (5 * 3600 + 30 * 60))
);
assert_eq!(
iso_to_epoch_secs("2026-05-17T14:20:00-0530"),
Some(BASE_UTC + (5 * 3600 + 30 * 60))
);
}
#[test]
fn parses_bare_hour_offset() {
assert_eq!(iso_to_epoch_secs("2026-05-17T14:20:00+00"), Some(BASE_UTC));
assert_eq!(
iso_to_epoch_secs("2026-05-17T14:20:00+05"),
Some(BASE_UTC - 5 * 3600)
);
assert_eq!(
iso_to_epoch_secs("2026-05-17T14:20:00-05"),
Some(BASE_UTC + 5 * 3600)
);
}
#[test]
fn colon_and_colonless_agree() {
assert_eq!(
iso_to_epoch_secs("2026-05-17T14:20:00+0530"),
iso_to_epoch_secs("2026-05-17T14:20:00+05:30")
);
assert_eq!(
iso_to_epoch_secs("2026-05-17T14:20:00-0800"),
iso_to_epoch_secs("2026-05-17T14:20:00-08:00")
);
}
#[test]
fn rejects_malformed() {
assert_eq!(iso_to_epoch_secs("2026-05-17"), None);
assert_eq!(iso_to_epoch_secs("2026/05/17T14:20:00Z"), None);
assert_eq!(iso_to_epoch_secs("2026-05-17T14:20:00X"), None);
assert_eq!(iso_to_epoch_secs("2026-05-17T14:20:00+ab:cd"), None);
assert_eq!(iso_to_epoch_secs("2026-05-17T14:20:00+5"), None);
assert_eq!(iso_to_epoch_secs("2026-05-17T14:20:00+12345"), None);
assert_eq!(iso_to_epoch_secs("2026-05-17T14:20:00+05:3"), None);
}
#[test]
fn epoch_to_iso_round_trips() {
assert_eq!(epoch_secs_to_iso(BASE_UTC), "2026-05-17T14:20:00+00:00");
for secs in [0, BASE_UTC, 951_867_122, 4_102_444_799] {
assert_eq!(
iso_to_epoch_secs(&epoch_secs_to_iso(secs)),
Some(secs),
"round trip failed for {secs}"
);
}
assert_eq!(epoch_secs_to_iso(-1), "1970-01-01T00:00:00+00:00");
}
#[test]
fn retry_after_parses_delta_seconds_and_http_date() {
assert_eq!(parse_retry_after("5"), Some(Duration::from_secs(5)));
assert_eq!(parse_retry_after(" 30 "), Some(Duration::from_secs(30)));
assert_eq!(parse_retry_after("0"), Some(Duration::ZERO));
assert_eq!(parse_retry_after(""), None);
assert_eq!(parse_retry_after("-5"), None);
assert_eq!(parse_retry_after("1.5"), None);
let date = "Wed, 21 Oct 2015 07:28:00 GMT";
assert_eq!(
parse_retry_after_at(date, 1_445_412_480),
Some(Duration::ZERO)
);
assert_eq!(
parse_retry_after_at(date, 1_445_412_480 - 90),
Some(Duration::from_secs(90)),
"a future HTTP-date yields the delay until it"
);
assert_eq!(
parse_retry_after_at(date, 1_445_412_480 + 120),
Some(Duration::ZERO),
"a past HTTP-date saturates to zero"
);
assert_eq!(
parse_retry_after_at("Wed, 21 Foo 2015 07:28:00 GMT", 0),
None
);
assert_eq!(parse_retry_after_at("21 Oct 2015 07:28:00 GMT", 0), None);
assert_eq!(
parse_retry_after_at("Wed, 21 Oct 2015 07:28:00 PST", 0),
None
);
}
#[test]
fn reserve_slot_spaces_requests() {
let now = 1_000_000u64;
let (next, wait) = reserve_slot(0, now);
assert_eq!(wait, 0);
assert_eq!(next, now + REQUEST_SPACING_MS);
let (next2, wait2) = reserve_slot(next, now);
assert_eq!(wait2, REQUEST_SPACING_MS);
assert_eq!(next2, next + REQUEST_SPACING_MS);
}
#[test]
fn ideal_pace_tracks_elapsed_window_fraction() {
let win = |reset_secs: i64| UsageWindow {
utilization: 0.0,
resets_at: Some(epoch_secs_to_iso(reset_secs)),
};
let p = ideal_pace_pct(LABEL_5H, &win(BASE_UTC + 9_000), BASE_UTC).unwrap();
assert!(
(p - 50.0).abs() < 1e-6,
"half-elapsed 5h window paces at 50%, got {p}"
);
let p = ideal_pace_pct(LABEL_7D, &win(BASE_UTC + 7 * 86_400), BASE_UTC).unwrap();
assert!(
p.abs() < 1e-6,
"a freshly-opened window paces at 0%, got {p}"
);
let p = ideal_pace_pct(LABEL_5H, &win(BASE_UTC - 1), BASE_UTC).unwrap();
assert!(
(p - 100.0).abs() < 1e-6,
"a lapsed window paces at 100%, got {p}"
);
let no_reset = UsageWindow {
utilization: 0.0,
resets_at: None,
};
assert_eq!(ideal_pace_pct(LABEL_5H, &no_reset, BASE_UTC), None);
assert_eq!(
ideal_pace_pct("extra", &win(BASE_UTC + 100), BASE_UTC),
None
);
}
#[test]
fn window_avg_pace_is_util_over_elapsed_days() {
let win = |util: f64, reset_secs: i64| UsageWindow {
utilization: util,
resets_at: Some(epoch_secs_to_iso(reset_secs)),
};
let duration = 7 * 86_400;
let reset = BASE_UTC + duration - 12 * 3600;
let pace = window_avg_pace_per_day(LABEL_7D, &win(21.0, reset), BASE_UTC, 3600).unwrap();
assert!((pace - 42.0).abs() < 1e-6, "12h/21% → 42 %/d, got {pace}");
let reset = BASE_UTC + duration - 1800;
assert_eq!(
window_avg_pace_per_day(LABEL_7D, &win(5.0, reset), BASE_UTC, 3600),
None
);
let no_reset = UsageWindow {
utilization: 21.0,
resets_at: None,
};
assert_eq!(
window_avg_pace_per_day(LABEL_7D, &no_reset, BASE_UTC, 3600),
None
);
assert_eq!(
window_avg_pace_per_day("extra", &win(21.0, BASE_UTC + 100), BASE_UTC, 3600),
None
);
}
#[test]
fn window_duration_parses_provider_labels() {
assert_eq!(window_duration_secs(LABEL_5H), Some(5 * 3600));
assert_eq!(window_duration_secs(LABEL_7D), Some(7 * 86_400));
assert_eq!(window_duration_secs("7d fable"), Some(7 * 86_400));
assert_eq!(window_duration_secs("7d opus"), Some(7 * 86_400));
assert_eq!(window_duration_secs("5h"), Some(5 * 3600));
assert_eq!(window_duration_secs("30d"), Some(30 * 86_400));
assert_eq!(window_duration_secs("14d"), Some(14 * 86_400));
assert_eq!(window_duration_secs("balance"), None);
assert_eq!(window_duration_secs("d"), None);
assert_eq!(window_duration_secs("0d"), None);
assert_eq!(window_duration_secs(""), None);
}
#[test]
fn take_profile_fetch_honors_ttl_force_and_expiry() {
let t0 = 1_000_000_000_000u64;
assert!(
take_profile_fetch("ttl-first", false, t0),
"first load pulls /profile"
);
assert!(
!take_profile_fetch("ttl-first", false, t0 + 60_000),
"within the TTL the cached plan is reused — no per-tick re-pull"
);
assert!(take_profile_fetch("ttl-force", false, t0));
assert!(
take_profile_fetch("ttl-force", true, t0 + 60_000),
"force (401 retry) re-pulls /profile despite a fresh TTL"
);
assert!(take_profile_fetch("ttl-stale", false, t0));
assert!(
take_profile_fetch("ttl-stale", false, t0 + PROFILE_TTL_MS + 1),
"a plan past the TTL is re-pulled"
);
assert!(take_profile_fetch("ttl-expire", false, t0));
assert!(!take_profile_fetch("ttl-expire", false, t0 + 60_000));
expire_profile_ttl("ttl-expire");
assert!(
take_profile_fetch("ttl-expire", false, t0 + 120_000),
"expiring the TTL forces a re-pull"
);
}
#[test]
fn limits_drive_windows_and_pick_up_new_models() {
let json = r#"{
"five_hour": {"utilization": 99, "resets_at": "2026-07-02T14:50:00+00:00"},
"seven_day": {"utilization": 99, "resets_at": "2026-07-06T23:59:59+00:00"},
"seven_day_opus": null,
"limits": [
{"kind": "session", "percent": 3, "resets_at": "2026-07-02T14:50:00+00:00"},
{"kind": "weekly_all", "percent": 9, "resets_at": "2026-07-06T23:59:59+00:00"},
{"kind": "weekly_scoped", "percent": 14, "resets_at": "2026-07-07T00:00:00+00:00",
"scope": {"model": {"display_name": "Fable"}}}
],
"spend": {"enabled": true, "percent": 32,
"used": {"amount_minor": 320, "currency": "USD", "exponent": 2},
"limit": {"amount_minor": 1000, "currency": "USD", "exponent": 2}}
}"#;
let raw: RawUsage = serde_json::from_str(json).unwrap();
let w = windows_from_raw(&raw);
assert_eq!(w.five_hour.map(|w| w.utilization), Some(3.0));
assert_eq!(w.seven_day.map(|w| w.utilization), Some(9.0));
assert_eq!(w.weekly_scoped.len(), 1);
assert_eq!(w.weekly_scoped[0].label, "7d fable");
assert_eq!(w.weekly_scoped[0].window.utilization, 14.0);
assert_eq!(
window_duration_secs(&w.weekly_scoped[0].label),
Some(7 * 86_400)
);
let spend = SpendInfo::from_raw(raw.spend.as_ref().unwrap());
assert!(spend.is_visible());
assert_eq!(spend.used, Some(3.20));
assert_eq!(spend.limit, Some(10.0));
assert_eq!(spend.percent, Some(32.0));
assert_eq!(spend.currency.as_deref(), Some("USD"));
}
#[test]
fn windows_fall_back_to_legacy_fields_when_limits_absent() {
let json = r#"{
"five_hour": {"utilization": 7, "resets_at": "2026-07-02T14:50:00+00:00"},
"seven_day": {"utilization": 11, "resets_at": "2026-07-06T23:59:59+00:00"},
"limits": [],
"spend": {"enabled": false, "limit": null}
}"#;
let raw: RawUsage = serde_json::from_str(json).unwrap();
let w = windows_from_raw(&raw);
assert_eq!(w.five_hour.map(|w| w.utilization), Some(7.0));
assert_eq!(w.seven_day.map(|w| w.utilization), Some(11.0));
assert!(w.weekly_scoped.is_empty());
assert!(!SpendInfo::from_raw(raw.spend.as_ref().unwrap()).is_visible());
}
#[test]
fn spend_money_conversion_and_visibility() {
let parse = |json: &str| SpendInfo::from_raw(&serde_json::from_str::<RawSpend>(json).unwrap());
let s = parse(
r#"{"enabled": true, "percent": 50,
"used": {"amount_minor": 1500, "currency": "EUR", "exponent": 3},
"limit": {"amount_minor": 3000, "currency": "EUR", "exponent": 3}}"#,
);
assert_eq!(s.used, Some(1.5));
assert_eq!(s.limit, Some(3.0));
assert_eq!(s.currency.as_deref(), Some("EUR"));
assert!(s.is_visible());
let s = parse(r#"{"enabled": true, "used": {"amount_minor": 250, "currency": "USD"}}"#);
assert_eq!(s.used, Some(2.5));
assert_eq!(s.limit, None);
assert!(s.is_visible());
assert!(parse(r#"{"enabled": false, "limit": {"amount_minor": 1000}}"#).is_visible());
let s = parse(r#"{"enabled": false}"#);
assert!(!s.is_visible());
assert_eq!(s.used, None);
}
#[test]
fn blind_fields_parse_defensively() {
assert_eq!(json_to_dollars(&serde_json::json!(12.5)), Some(12.5));
assert_eq!(
json_to_dollars(&serde_json::json!({"amount_minor": 1234, "exponent": 2})),
Some(12.34)
);
assert_eq!(
json_to_dollars(&serde_json::json!({"amount": "7.5"})),
Some(7.5)
);
assert_eq!(json_to_dollars(&serde_json::json!("garbage")), None);
assert_eq!(json_to_dollars(&serde_json::json!({})), None);
let json = r#"{
"five_hour": {"utilization": 40, "resets_at": "2026-07-02T14:50:00+00:00",
"used_dollars": {"amount_minor": 320, "exponent": 2},
"limit_dollars": 10},
"limits": [
{"kind": "session", "percent": 40, "resets_at": "2026-07-02T14:50:00+00:00"},
{"kind": "weekly_scoped", "percent": 5, "resets_at": "2026-07-07T00:00:00+00:00",
"scope": {"surface": {"display_name": "Code"}}}
],
"extra_usage": {"is_enabled": true, "currency": "USD",
"daily": {"used_credits": 1.5, "utilization": 15},
"weekly": 999}
}"#;
let raw: RawUsage = serde_json::from_str(json).unwrap();
let w = windows_from_raw(&raw);
let d = w.window_dollars.iter().find(|d| d.label == "5h").unwrap();
assert_eq!(d.used, Some(3.20));
assert_eq!(d.limit, Some(10.0));
assert_eq!(w.weekly_scoped.len(), 1);
assert_eq!(w.weekly_scoped[0].label, "7d code");
let extra = raw.extra_usage.as_ref().unwrap();
let daily = ExtraPeriod::from_value(extra.daily.as_ref().unwrap()).unwrap();
assert_eq!(daily.utilization, Some(15.0));
assert_eq!(daily.used_credits, Some(1.5));
assert!(ExtraPeriod::from_value(extra.weekly.as_ref().unwrap()).is_none());
}