use super::*;
#[test]
fn bar_spans_places_marker_without_changing_width() {
let fill = Style::default();
let total =
|spans: &[Span<'_>]| -> usize { spans.iter().map(|s| s.content.chars().count()).sum() };
let marker_col = |spans: &[Span<'_>]| -> Option<usize> {
let mut col = 0;
for s in spans {
if s.content == "│" {
return Some(col);
}
col += s.content.chars().count();
}
None
};
let plain = bar_spans(4, 10, fill, None);
assert_eq!(total(&plain), 10);
assert_eq!(marker_col(&plain), None);
let under = bar_spans(4, 10, fill, Some(7));
assert_eq!(
total(&under),
10,
"width unchanged with a marker over the empty run"
);
assert_eq!(marker_col(&under), Some(7));
let ahead = bar_spans(8, 10, fill, Some(3));
assert_eq!(
total(&ahead),
10,
"width unchanged with a marker over the filled run"
);
assert_eq!(marker_col(&ahead), Some(3));
let boundary = bar_spans(4, 10, fill, Some(4));
assert_eq!(total(&boundary), 10);
assert_eq!(marker_col(&boundary), Some(4));
let oob = bar_spans(4, 10, fill, Some(10));
assert_eq!(marker_col(&oob), None);
assert_eq!(total(&oob), 10);
}
#[test]
fn stats_from_bars_keeps_api_labels_and_source_order() {
let now = crate::usage::now_epoch_secs();
let bars = vec![
tp_bar(
"time limit",
0.0,
now + 30 * 86_400,
Some(0.0),
Some(1000.0),
),
tp_bar("tokens limit", 1.0, now + 4 * 3600, None, None),
];
let stats = stats_from_bars(&bars, true, true);
assert_eq!(stats[0].label, "time limit", "API label kept verbatim");
assert_eq!(stats[1].label, "tokens limit");
assert_eq!(stats[0].amount, "0 / 1000");
assert!(!stats[0].trailing.contains('/'));
assert!(stats[0].trailing.contains("resets in"));
assert!(stats[1].amount.is_empty());
assert!(stats[1].trailing.contains("resets in"));
}
#[test]
fn stats_from_bars_does_not_rename_duplicate_labels() {
let now = crate::usage::now_epoch_secs();
let bars = vec![
tp_bar("tokens limit", 0.0, now + 4 * 3600, None, None),
tp_bar("tokens limit", 12.0, now + 6 * 86_400, None, None),
];
let stats = stats_from_bars(&bars, true, true);
assert_eq!(stats[0].label, "tokens limit");
assert_eq!(stats[1].label, "tokens limit");
assert_eq!(stats[0].pct, 0.0);
assert_eq!(stats[1].pct, 12.0);
}
#[test]
fn stats_from_bars_fills_pace_for_windowed_labels() {
let approx = |a: Option<f64>, b: f64| a.is_some_and(|v| (v - b).abs() < 0.1);
let now = crate::usage::now_epoch_secs();
let bars = vec![
tp_bar("5h", 20.0, now + 3600, None, None),
tp_bar("7d", 35.0, now + 3 * 86_400 + 43_200, None, None),
tp_bar("30d", 30.0, now + 15 * 86_400, None, None),
];
let stats = stats_from_bars(&bars, true, true);
assert_eq!(stats[0].rate_unit, "h");
assert!(approx(stats[0].burn_rate, 5.0), "5h shows %/h average pace");
assert!(approx(stats[0].pace_pct, 80.0), "5h ideal-pace marker");
assert_eq!(stats[1].rate_unit, "d");
assert!(
approx(stats[1].burn_rate, 10.0),
"7d shows %/d average pace"
);
assert!(
approx(stats[2].burn_rate, 2.0),
"30d window now resolves a pace"
);
let bare = stats_from_bars(&bars, false, false);
assert!(bare.iter().all(|s| s.burn_rate.is_none()));
assert!(bare.iter().all(|s| s.pace_pct.is_none()));
let other = stats_from_bars(
&[tp_bar("balance", 50.0, now + 3600, None, None)],
true,
true,
);
assert!(other[0].burn_rate.is_none() && other[0].pace_pct.is_none());
}
fn tp_bar(
label: &str,
pct: f64,
reset_secs: i64,
used: Option<f64>,
total: Option<f64>,
) -> crate::providers::UsageBar {
crate::providers::UsageBar {
label: label.to_string(),
pct,
resets_at: Some(crate::usage::epoch_secs_to_iso(reset_secs)),
used,
total,
}
}
#[test]
fn empty_msg_credless_profile_is_terminal() {
let profile = crate::testutil::blank_profile("a");
assert_eq!(
oauth_empty_msg(&profile),
"no credentials — capture or sign in"
);
}
#[test]
fn empty_msg_failed_fetch_is_terminal() {
let mut profile = crate::testutil::blank_profile("a");
profile.credentials = Some(crate::profile::ClaudeCredentials {
claude_ai_oauth: Some(crate::profile::OAuthToken {
access_token: "at".into(),
refresh_token: None,
expires_at: None,
scopes: None,
subscription_type: None,
}),
});
profile.fetch_status = Some(FetchStatus::Failed);
assert_eq!(oauth_empty_msg(&profile), "no usage available");
}
#[test]
fn empty_msg_pending_fetch_loads() {
let mut profile = crate::testutil::blank_profile("a");
profile.credentials = Some(crate::profile::ClaudeCredentials {
claude_ai_oauth: Some(crate::profile::OAuthToken {
access_token: "at".into(),
refresh_token: None,
expires_at: None,
scopes: None,
subscription_type: None,
}),
});
assert_eq!(oauth_empty_msg(&profile), "loading");
}
#[test]
fn extra_bar_dedups_against_spend_and_scales_cents() {
let with = |extra: Option<crate::usage::ExtraUsage>, spend: Option<crate::usage::SpendInfo>| {
let mut profile = crate::testutil::blank_profile("a");
profile.usage = Some(crate::usage::UsageInfo {
plan: None,
five_hour: None,
seven_day: None,
weekly_scoped: Vec::new(),
window_dollars: Vec::new(),
extra_usage: extra,
spend,
});
collect_stats(&profile)
};
let extra = crate::usage::ExtraUsage {
is_enabled: true,
monthly_limit: Some(5000.0),
used_credits: Some(487.0),
utilization: Some(9.74),
currency: Some("USD".to_string()),
..Default::default()
};
let spend = crate::usage::SpendInfo {
enabled: true,
used: Some(4.87),
limit: Some(50.0),
percent: Some(10.0),
currency: Some("USD".to_string()),
};
let both = with(Some(extra.clone()), Some(spend));
assert!(both.iter().any(|s| s.label == "spend"));
assert!(
!both.iter().any(|s| s.label == "extra"),
"extra suppressed while spend is visible"
);
let legacy = with(Some(extra), None);
let bar = legacy.iter().find(|s| s.label == "extra").unwrap();
assert_eq!(bar.trailing, "$4.87 / $50.00");
assert!(bar.amount.is_empty());
}