Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions crates/codexbar-app/src/dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1803,6 +1803,33 @@ impl Dashboard {
cx.notify();
}

/// Two Copilot accounts through the real parser (#318): one 740 premium requests past its 3,000 allowance, in the
/// shape GitHub returns past it, and one exactly at it. Synthetic payloads only; no provider or account is read.
#[cfg(any(test, feature = "capture"))]
pub(crate) fn capture_copilot_overage(&mut self, cx: &mut Context<Self>) {
assert!(matches!(self.source, DataSource::Demo));
let now: DateTime<Utc> = "2026-10-20T12:00:00Z".parse().unwrap();
// As in `capture_muse`: refreshed at the fixed time, so the clock's demo refresh doesn't replace the fixture.
self.last_refresh = Some(now);
crate::sus::capture_time(now, cx);
let account = |user: &str, remaining: i64| {
let payload = serde_json::json!({
"copilot_plan": "business",
"quota_reset_date_utc": "2026-11-01T00:00:00.000Z",
"quota_snapshots": {"premium_interactions": {
"entitlement": 3000, "remaining": remaining, "percent_remaining": 0, "unlimited": false,
}},
});
codexbar_providers::copilot::parse_user(&payload.to_string(), user, now).unwrap()
};
let accounts = vec![account("over-allowance", -740), account("at-allowance", 0)];
self.states.clear();
self.placeholders.clear();
self.set_accounts(accounts, cx);
self.view = DashboardView::Usage;
cx.notify();
}

/// Keep geometry fixtures independent of the real weekly reset day.
#[cfg(test)]
pub(crate) fn set_alignment_demo(&mut self, cx: &mut Context<Self>) {
Expand Down
5 changes: 5 additions & 0 deletions crates/codexbar-app/src/self_capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::theme::Appearance;
mod antigravity;
mod claude_ultra;
mod claude_weekly;
mod copilot_overage;
mod far_dates;
mod gemini_status;
mod gemini_weekly;
Expand All @@ -45,6 +46,7 @@ pub fn start(cx: &mut App, handle: AnyWindowHandle, dashboard: Entity<Dashboard>
|| std::env::var_os("CODEXBAR_CAPTURE_CLAUDE_WEEKLY").is_some()
|| std::env::var_os("CODEXBAR_CAPTURE_CLAUDE_ULTRA").is_some()
|| std::env::var_os("CODEXBAR_CAPTURE_FAR_DATES").is_some()
|| std::env::var_os("CODEXBAR_CAPTURE_COPILOT_OVERAGE").is_some()
{
let hub = crate::settings_hub::SettingsHub::global(cx);
assert!(
Expand Down Expand Up @@ -264,6 +266,9 @@ async fn run(
if std::env::var_os("CODEXBAR_CAPTURE_FAR_DATES").is_some() {
saved.extend(far_dates::run(cx, handle, dashboard, dir).await);
}
if std::env::var_os("CODEXBAR_CAPTURE_COPILOT_OVERAGE").is_some() {
saved.extend(copilot_overage::run(cx, handle, dashboard, dir).await);
}
// Optional synthetic plan states. The normal live adapters never read these fixture values.
if std::env::var_os("CODEXBAR_CAPTURE_PLANS").is_some() {
cx.update(|cx| crate::prefs_hub::PrefsHub::set_appearance(cx, Appearance::Light));
Expand Down
55 changes: 55 additions & 0 deletions crates/codexbar-app/src/self_capture/copilot_overage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//! A Copilot account past its premium allowance (#318) in the real Usage table, beside one exactly at it, through the
//! production parser.
use super::{save, settle};
use crate::dashboard::Dashboard;
use crate::theme::Appearance;
use gpui_kit::test::TestWindowExt as _;
use gpui_kit::{AnyWindowHandle, AppContext as _, AsyncApp, Entity};
use std::path::Path;

/// Each row's used cell and bar, as the table labels them.
const ROWS: [(&str, &str, &str); 2] = [
(
"copilot-over-allowance",
"3,000 requests: 3,740",
"100% used, 174% at reset · limit reached",
),
(
"copilot-at-allowance",
"3,000 requests: 3,000",
"100% used, 159% at reset · limit reached",
),
];

pub(super) async fn run(
cx: &mut AsyncApp,
handle: AnyWindowHandle,
dashboard: &Entity<Dashboard>,
dir: &Path,
) -> Vec<String> {
let mut saved = Vec::new();
for (appearance, theme) in [(Appearance::Light, "light"), (Appearance::Dark, "dark")] {
cx.update(|cx| crate::prefs_hub::PrefsHub::set_appearance(cx, appearance));
cx.update(|cx| dashboard.update(cx, |d, cx| d.capture_copilot_overage(cx)));
settle(cx).await;
cx.update(|cx| {
cx.update_window(handle, |_, window, cx| {
window.render_frame(cx);
for (id, used, bar) in ROWS {
let label = |name: String| {
window
.try_find(gpui_kit::ElementId::Name(name.into()))
.and_then(|found| found.label().map(str::to_owned))
};
assert_eq!(label(format!("used-{id}")).as_deref(), Some(used), "{id}");
assert_eq!(label(format!("progress-cell-{id}")).as_deref(), Some(bar), "{id}");
}
})
})
.expect("the Copilot fixture must reach the actual table");
let name = format!("copilot-overage-usage-{theme}");
assert!(save(cx, handle, dir, &name));
saved.push(name);
}
saved
}
35 changes: 35 additions & 0 deletions crates/codexbar-app/src/ui_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8930,3 +8930,38 @@ fn far_dates_east_of_utc_show_no_date_and_leave_the_others(cx: &mut TestAppConte
}
assert!(row(cx, "Ordinary").contains("until Nov 2"));
}

/// #318: a Copilot account 740 premium requests past its 3,000 allowance reads 3,740 in the Usage table and stays
/// limit reached, beside one exactly at it, through the real parser as `capture_copilot_overage` sets up for the
/// screenshots.
#[gpui_kit::test]
fn copilot_quota_past_its_allowance_shows_the_overage_and_stays_limit_reached(cx: &mut TestAppContext) {
let settings = TempSettings::new("copilot-overage", "{}");
let (handle, dashboard) = open_dashboard(cx, &settings);
cx.update(|cx| dashboard.update(cx, |d, cx| d.capture_copilot_overage(cx)));
cx.run_until_parked();
cx.update_window(handle, |_, window, cx| window.render_frame(cx))
.unwrap();

assert_eq!(
label(cx, handle, "used-copilot-over-allowance").as_deref(),
Some("3,000 requests: 3,740")
);
assert_eq!(
label(cx, handle, "used-copilot-at-allowance").as_deref(),
Some("3,000 requests: 3,000")
);
// Both stay limit reached. The forecast follows the pace of what was used, so the account past its allowance
// projects further past it than the one at it, 19.5 days into the month.
assert_eq!(
label(cx, handle, "progress-cell-copilot-over-allowance").as_deref(),
Some("100% used, 174% at reset · limit reached")
);
assert_eq!(
label(cx, handle, "progress-cell-copilot-at-allowance").as_deref(),
Some("100% used, 159% at reset · limit reached")
);
// A used-up limit has no pill, past it or at it (#150).
assert!(label(cx, handle, "severity-tag-copilot-over-allowance").is_none());
assert!(label(cx, handle, "severity-tag-copilot-at-allowance").is_none());
}
202 changes: 195 additions & 7 deletions crates/codexbar-providers/src/copilot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,10 @@ const QUOTAS: [(&str, &str); 3] = [
("completions", "Code completions"),
];

/// A monthly quota with a real ceiling. Unlimited or zero entitlements are not limits.
/// A monthly quota with a real ceiling. Unlimited or zero entitlements are not limits, and a quota whose `remaining`
/// isn't a whole number is left out rather than shown as used up.
fn quota_metric(quota: &Value, label: &str, resets_at: DateTime<Utc>, now: DateTime<Utc>) -> Option<Metric> {
let entitlement = quota.get("entitlement").and_then(Value::as_u64).filter(|e| *e > 0)?;
if quota.get("unlimited").and_then(Value::as_bool).unwrap_or(false) {
return None;
}
let remaining = quota.get("remaining").and_then(Value::as_i64).unwrap_or(0).max(0) as u64;
let used = entitlement.saturating_sub(remaining);
let (used, entitlement) = quota_counts(quota)?;
let start = resets_at.checked_sub_months(Months::new(1)).unwrap_or(resets_at);
let pace = elapsed_pace(used as f64 / entitlement as f64, start, resets_at, now);
Some(Metric::Quota {
Expand All @@ -147,6 +143,26 @@ fn quota_metric(quota: &Value, label: &str, resets_at: DateTime<Utc>, now: DateT
})
}

/// Decode the bounded quota's used and entitled request counts before constructing a metric.
fn quota_counts(quota: &Value) -> Option<(u64, u64)> {
let entitlement = quota.get("entitlement").and_then(Value::as_u64).filter(|e| *e > 0)?;
if quota.get("unlimited").and_then(Value::as_bool).unwrap_or(false) {
return None;
}
let used = quota_used(entitlement, quota.get("remaining")?)?;
Some((used, entitlement))
}

/// What a quota has used: its entitlement less what remains. Past the entitlement GitHub reports `remaining` below
/// zero, so the overage is added rather than dropped (#318). A `remaining` above the entitlement means nothing used.
/// `None` when `remaining` isn't a whole number, or when the overage would take the count past `u64::MAX`.
fn quota_used(entitlement: u64, remaining: &Value) -> Option<u64> {
match remaining.as_u64() {
Some(left) => Some(entitlement.saturating_sub(left)),
None => entitlement.checked_add(remaining.as_i64()?.unsigned_abs()),
}
}

/// `quota_reset_date_utc` (RFC 3339), falling back to the date-only `quota_reset_date` at midnight UTC.
fn reset_date(json: &Value) -> Option<DateTime<Utc>> {
if let Some(stamp) = json.get("quota_reset_date_utc").and_then(Value::as_str)
Expand Down Expand Up @@ -1316,6 +1332,178 @@ mod tests {
assert_eq!(account.primary().unwrap().used_display(), "1");
}

/// The `key` quota `parse_user` reads from a snapshot with these raw JSON `entitlement` and `remaining` values
/// (`None` leaves the field out), beside an ordinary sibling quota that must stay.
fn quota(key: &str, entitlement: Option<&str>, remaining: Option<&str>, unlimited: bool) -> Option<Metric> {
let label_of = |key: &str| QUOTAS.iter().find(|(name, _)| *name == key).unwrap().1;
let sibling = if key == "chat" { "completions" } else { "chat" };
let fields: Vec<String> = [("entitlement", entitlement), ("remaining", remaining)]
.into_iter()
.filter_map(|(name, value)| Some(format!(r#""{name}":{}"#, value?)))
.chain([format!(r#""unlimited":{unlimited}"#)])
.collect();
let payload = format!(
r#"{{"quota_reset_date_utc":"2026-11-01T00:00:00.000Z","quota_snapshots":{{"{sibling}":{{"entitlement":50,"remaining":40,"unlimited":false}},"{key}":{{{}}}}}}}"#,
fields.join(",")
);
let account = parse_user(&payload, "dev", now()).unwrap();
assert_eq!(
used_and_limit(
account
.metrics()
.iter()
.find(|m| m.label() == label_of(sibling))
.cloned()
),
Some((10, 50)),
"the sibling quota is unaffected: {payload}"
);
account
.metrics()
.iter()
.find(|metric| metric.label() == label_of(key))
.cloned()
}

fn used_and_limit(metric: Option<Metric>) -> Option<(u64, u64)> {
match metric? {
Metric::Quota { used, limit, .. } => Some((used, limit)),
other => panic!("not a quota: {other:?}"),
}
}

const QUOTA_KEYS: [&str; 3] = ["premium_interactions", "chat", "completions"];

#[test]
fn parse_user_quota_past_its_entitlement_counts_the_overage() {
for key in QUOTA_KEYS {
// GitHub's shape past the allowance (#318): `remaining` below zero by the overage.
let over = quota(key, Some("3000"), Some("-740"), false).unwrap();
assert_eq!(used_and_limit(Some(over.clone())), Some((3740, 3000)), "{key}");
assert_eq!(over.used_display(), "3,740", "{key}");
assert_eq!(
over.used_fraction(),
Some(1.0),
"{key}: the share stays at the whole limit"
);
// One over is not the same as exactly at the limit.
assert_eq!(
used_and_limit(quota(key, Some("300"), Some("-1"), false)),
Some((301, 300)),
"{key}"
);
assert_eq!(
used_and_limit(quota(key, Some("300"), Some("0"), false)),
Some((300, 300)),
"{key}"
);
// Under the limit, and with nothing used, as before.
assert_eq!(
used_and_limit(quota(key, Some("1500"), Some("216"), false)),
Some((1284, 1500)),
"{key}"
);
assert_eq!(
used_and_limit(quota(key, Some("300"), Some("300"), false)),
Some((0, 300)),
"{key}"
);
// More left than the entitlement is nothing used, as before.
assert_eq!(
used_and_limit(quota(key, Some("300"), Some("5000"), false)),
Some((0, 300)),
"{key}"
);
}
}

#[test]
fn parse_user_quota_past_its_entitlement_is_used_up_like_one_at_it() {
let snapshot = |remaining: i64| {
let payload = serde_json::json!({
"quota_reset_date_utc": "2026-11-01T00:00:00.000Z",
"quota_snapshots": {"premium_interactions": {"entitlement": 3000, "remaining": remaining, "unlimited": false}},
});
parse_user(&payload.to_string(), "dev", now()).unwrap()
};
let at = snapshot(0).assess(now());
let over = snapshot(-740).assess(now());
assert!(at.is_exhausted() && over.is_exhausted());
assert_eq!(over.severity(), at.severity());
}

#[test]
fn parse_user_quota_used_stays_exact_at_both_ends_of_the_integers() {
let premium = |entitlement: &str, remaining: &str| {
used_and_limit(quota("premium_interactions", Some(entitlement), Some(remaining), false))
};
let max = u64::MAX.to_string();
let min = i64::MIN.to_string();
// The most negative `remaining` adds 2^63 without wrapping.
assert_eq!(premium("3000", &min), Some((3000 + (1 << 63), 3000)));
// The largest entitlement: used up, exactly at `u64::MAX` with an overage that just fits, and left out once
// the overage would pass it.
assert_eq!(premium(&max, "0"), Some((u64::MAX, u64::MAX)));
let near = (u64::MAX - 740).to_string();
assert_eq!(premium(&near, "-740"), Some((u64::MAX, u64::MAX - 740)));
assert_eq!(premium(&near, "-741"), None);
assert_eq!(premium(&max, "-1"), None);
assert_eq!(premium(&max, &min), None);
// A `remaining` past `i64::MAX` is still a whole count left, so nothing is used.
assert_eq!(premium("3000", &max), Some((0, 3000)));
assert_eq!(premium(&max, &max), Some((0, u64::MAX)));
}

#[test]
fn parse_user_quota_without_a_whole_remaining_is_left_out() {
// A count CodexBar can't read is not shown as the whole entitlement used (#318), and its sibling stays.
for key in ["premium_interactions", "completions"] {
for remaining in [
None,
Some("null"),
Some("216.5"),
Some("216.0"),
Some("-740.5"),
Some(r#""216""#),
Some("true"),
Some("-1e30"),
// One past `u64::MAX` only fits a float.
Some("18446744073709551616"),
] {
assert_eq!(quota(key, Some("3000"), remaining, false), None, "{key}: {remaining:?}");
}
}
}

#[test]
fn parse_user_quota_entitlement_limits_are_unchanged() {
for key in QUOTA_KEYS {
// No entitlement, or one that isn't a positive whole number, is no limit, whatever remains.
for entitlement in [
None,
Some("0"),
Some("-5"),
Some("300.5"),
Some(r#""300""#),
Some("null"),
] {
assert_eq!(
quota(key, entitlement, Some("-740"), false),
None,
"{key}: {entitlement:?}"
);
}
// Unlimited is no limit, even past an entitlement.
assert_eq!(quota(key, Some("3000"), Some("-740"), true), None, "{key}");
// The smallest entitlement is a limit.
assert_eq!(
used_and_limit(quota(key, Some("1"), Some("0"), false)),
Some((1, 1)),
"{key}"
);
}
}

#[test]
fn parse_user_date_only_reset_falls_back_to_midnight_utc() {
let payload = r#"{"quota_reset_date":"2026-11-01","quota_snapshots":{"premium_interactions":{"entitlement":300,"remaining":290}}}"#;
Expand Down
Loading