use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RateWindow {
pub used_percent: f64,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub raw_used_percent: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resets_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub window_minutes: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub window_kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub used_count: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub total_count: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub regeneration: Option<Regeneration>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub breakdown: Option<UsageBreakdown>,
}
pub mod window_kind {
pub const HOURLY: &str = "hourly";
pub const FIVE_HOUR: &str = "five_hour";
pub const DAILY: &str = "daily";
pub const WEEKLY: &str = "weekly";
pub const MONTHLY: &str = "monthly";
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UsageBreakdown {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub as_of: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub window_started_at: Option<String>,
pub rows: Vec<BreakdownRow>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BreakdownRow {
pub key: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub share_percent: Option<f64>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Regeneration {
pub mechanic: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub rate: Option<RegenerationRate>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RegenerationRate {
pub amount: f64,
pub per_minutes: i64,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ExtraWindow {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub window: Option<RateWindow>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct Usage {
#[serde(skip_serializing_if = "Option::is_none")]
pub primary: Option<RateWindow>,
#[serde(skip_serializing_if = "Option::is_none")]
pub secondary: Option<RateWindow>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tertiary: Option<RateWindow>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extra_rate_windows: Option<Vec<ExtraWindow>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Amount {
pub minor: i64,
pub exponent: u8,
pub unit: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PoolFunding {
Granted,
Purchased,
Subscription,
#[serde(other)]
Unknown,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PoolBasis {
Reported,
Derived,
#[serde(other)]
Unstated,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Pool {
pub id: String,
pub label: String,
pub funding: PoolFunding,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub remaining: Option<Amount>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub total: Option<Amount>,
pub basis: PoolBasis,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub spendable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub resets_at: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct AccountInfo {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub org_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub plan_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub subscription_renews_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub subscription_ends_at: Option<String>,
}
impl AccountInfo {
pub fn is_empty(&self) -> bool {
self.email.is_none()
&& self.org_name.is_none()
&& self.plan_type.is_none()
&& self.subscription_renews_at.is_none()
&& self.subscription_ends_at.is_none()
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CreditExpiry {
pub expires_at: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct SavedResets {
#[serde(default)]
pub available_count: u32,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub soonest_expires_at: Option<String>,
#[serde(default)]
pub credits: Vec<CreditExpiry>,
}
fn account_info_is_empty(value: &Option<AccountInfo>) -> bool {
value.as_ref().map(AccountInfo::is_empty).unwrap_or(true)
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProviderUsage {
pub provider: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub api_provider: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub account: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(skip_serializing_if = "account_info_is_empty", default)]
pub account_info: Option<AccountInfo>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub fetched_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub saved_resets: Option<SavedResets>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub spend: Option<Vec<Pool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub error_class: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub stale: Option<Stale>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Stale {
pub since: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub class: Option<String>,
}
impl ProviderUsage {
pub fn healthy(provider: &str, account: Option<String>, source: &str, usage: Usage) -> Self {
Self {
provider: provider.to_string(),
api_provider: None,
account,
source: Some(source.to_string()),
account_info: None,
fetched_at: None,
saved_resets: None,
usage: Some(usage),
spend: None,
error: None,
error_class: None,
stale: None,
}
}
pub fn degraded(provider: &str, error: impl std::fmt::Display) -> Self {
Self {
provider: provider.to_string(),
api_provider: None,
account: None,
source: None,
account_info: None,
fetched_at: None,
saved_resets: None,
usage: None,
spend: None,
error: Some(error.to_string()),
error_class: None,
stale: None,
}
}
pub fn degraded_with_class(
provider: &str,
error: impl std::fmt::Display,
error_class: impl Into<String>,
) -> Self {
Self {
error_class: Some(error_class.into()),
..Self::degraded(provider, error)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AccountIdentity {
pub provider: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub models_dev_provider_slug: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_ref: Option<AccountRef>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AccountRef {
pub value: String,
pub provenance: AccountRefProvenance,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AccountRefProvenance {
LiveClaim,
StoredLogin,
}
#[cfg(test)]
mod tests {
#[test]
fn the_account_info_key_set_is_closed_in_both_directions() {
let populated = AccountInfo {
email: Some("operator@example.com".to_string()),
org_name: Some("Example Org".to_string()),
plan_type: Some("max".to_string()),
subscription_renews_at: Some("2026-11-03".to_string()),
subscription_ends_at: Some("2026-12-01T00:00:00Z".to_string()),
};
let value = serde_json::to_value(&populated).expect("serialise");
let object = value.as_object().expect("object");
let mut keys: Vec<&str> = object.keys().map(String::as_str).collect();
keys.sort_unstable();
assert_eq!(
keys,
[
"email",
"orgName",
"planType",
"subscriptionEndsAt",
"subscriptionRenewsAt"
],
"AccountInfo's serialised key set changed. If a field was ADDED: is it \
personally identifying? If so it reaches `ck quota --redact`, whose filter \
removes fields by name and will pass it straight through into a screenshot. \
Update this test and notify that owner."
);
let empty = serde_json::to_value(AccountInfo::default()).expect("serialise");
assert_eq!(
empty.as_object().expect("object").len(),
0,
"an empty AccountInfo must emit no keys; a present-but-null field reads to a \
consumer as a resolved-and-blank identity rather than an unresolved one"
);
}
#[test]
fn subscription_dates_round_trip_and_older_payloads_still_decode() {
let info = AccountInfo {
subscription_renews_at: Some("2026-11-03".to_string()),
subscription_ends_at: Some("2026-12-01T09:30:00Z".to_string()),
..AccountInfo::default()
};
let value = serde_json::to_value(&info).expect("serialise");
assert_eq!(value["subscriptionRenewsAt"], "2026-11-03");
assert_eq!(value["subscriptionEndsAt"], "2026-12-01T09:30:00Z");
let back: AccountInfo = serde_json::from_value(value).expect("decode");
assert_eq!(back, info);
let older: AccountInfo =
serde_json::from_str(r#"{"email":"a@example.com","planType":"max"}"#)
.expect("a payload without the new fields decodes");
assert_eq!(older.subscription_renews_at, None);
assert_eq!(older.subscription_ends_at, None);
let one = AccountInfo {
subscription_renews_at: Some("2026-11-03".to_string()),
..AccountInfo::default()
};
let object = serde_json::to_value(&one).expect("serialise");
assert!(
object.get("subscriptionEndsAt").is_none(),
"an unstated end is omitted, not sent as null: {object}"
);
assert!(!one.is_empty(), "a renewal date alone is not empty");
let ends_only = AccountInfo {
subscription_ends_at: Some("2026-12-01".to_string()),
..AccountInfo::default()
};
assert!(!ends_only.is_empty(), "an end date alone is not empty");
}
#[test]
fn account_identity_full_round_trip_pins_provenance_spellings() {
let full = AccountIdentity {
provider: "claude".into(),
models_dev_provider_slug: Some("anthropic".into()),
account_ref: Some(AccountRef {
value: "ufuk@example.com".into(),
provenance: AccountRefProvenance::StoredLogin,
}),
};
let wire = serde_json::to_string(&full).unwrap();
assert!(
wire.contains("\"stored_login\""),
"frozen branch spelling: {wire}"
);
let back: AccountIdentity = serde_json::from_str(&wire).unwrap();
assert_eq!(back, full);
let live = serde_json::to_string(&AccountRefProvenance::LiveClaim).unwrap();
assert_eq!(live, "\"live_claim\"");
}
#[test]
fn a_minimal_identity_serializes_provider_only() {
let min = AccountIdentity {
provider: "kimi-for-coding".into(),
models_dev_provider_slug: None,
account_ref: None,
};
let wire = serde_json::to_string(&min).unwrap();
assert_eq!(wire, r#"{"provider":"kimi-for-coding"}"#);
let back: AccountIdentity = serde_json::from_str(&wire).unwrap();
assert_eq!(back, min);
}
#[test]
fn a_none_slug_never_becomes_the_provider_value() {
let id = AccountIdentity {
provider: "qwen-cloud".into(),
models_dev_provider_slug: None,
account_ref: None,
};
let wire = serde_json::to_string(&id).unwrap();
assert!(
!wire.contains("models_dev_provider_slug"),
"absent, not null: {wire}"
);
let back: AccountIdentity = serde_json::from_str(&wire).unwrap();
assert_eq!(back.models_dev_provider_slug, None);
assert_ne!(
back.models_dev_provider_slug.as_deref(),
Some(back.provider.as_str()),
"a decode path that mirrors provider into the slug has fabricated a join"
);
}
#[test]
fn an_unknown_provenance_refuses_to_decode() {
let err = serde_json::from_str::<AccountRef>(r#"{"value":"x","provenance":"vibes"}"#);
assert!(err.is_err(), "unknown provenance must refuse, got {err:?}");
let err2 = serde_json::from_str::<AccountRef>(r#"{"value":"x"}"#);
assert!(
err2.is_err(),
"value without provenance must refuse, got {err2:?}"
);
}
#[test]
fn a_window_without_regeneration_is_unchanged_on_the_wire() {
let window = RateWindow {
used_percent: 42.0,
raw_used_percent: None,
resets_at: Some("2026-08-17T00:00:00Z".to_string()),
window_minutes: Some(300),
used_count: None,
total_count: None,
regeneration: None,
window_kind: None,
breakdown: None,
};
let json = serde_json::to_string(&window).expect("serializes");
assert!(
!json.contains("regeneration"),
"an absent mechanic must not appear on the wire: {json}"
);
}
#[test]
fn a_pre_regeneration_payload_still_decodes() {
let json = r#"{"usedPercent":42.0,"resetsAt":"2026-08-17T00:00:00Z","windowMinutes":300}"#;
let window: RateWindow = serde_json::from_str(json).expect("older payloads must decode");
assert_eq!(window.regeneration, None);
}
#[test]
fn a_stated_cliff_refill_round_trips() {
let window = RateWindow {
used_percent: 0.67,
raw_used_percent: None,
resets_at: Some("2026-08-15T06:00:00.000Z".to_string()),
window_minutes: None,
used_count: Some(8100.0),
total_count: Some(1_207_000.0),
regeneration: Some(Regeneration {
mechanic: "cliff".to_string(),
rate: Some(RegenerationRate {
amount: 1_000_000.0,
per_minutes: 43_200,
}),
}),
window_kind: None,
breakdown: None,
};
let json = serde_json::to_string(&window).expect("serializes");
let back: RateWindow = serde_json::from_str(&json).expect("round-trips");
assert_eq!(back, window);
assert!(
json.contains("\"perMinutes\":43200"),
"camelCase on the wire: {json}"
);
}
#[test]
fn a_mechanic_without_a_rate_is_valid() {
let json = r#"{"usedPercent":10.0,"regeneration":{"mechanic":"drip"}}"#;
let window: RateWindow = serde_json::from_str(json).expect("decodes");
let regeneration = window.regeneration.expect("the mechanic is stated");
assert_eq!(regeneration.mechanic, "drip");
assert_eq!(
regeneration.rate, None,
"the mechanic stands without a rate"
);
}
#[test]
fn an_unrecognised_mechanic_decodes_rather_than_dropping_the_window() {
let json = r#"{"usedPercent":10.0,"regeneration":{"mechanic":"stepped_thaw"}}"#;
let window: RateWindow = serde_json::from_str(json).expect("a future variant must decode");
assert_eq!(
window.regeneration.expect("present").mechanic,
"stepped_thaw"
);
assert_eq!(window.used_percent, 10.0, "the rest of the window survives");
}
#[test]
fn a_fresh_entry_carries_no_stale_key() {
let entry = ProviderUsage::healthy("codex", None, "oauth", Usage::default());
let json = serde_json::to_string(&entry).expect("entry serializes");
assert!(
!json.contains("stale"),
"a fresh entry must not emit the key at all: {json}"
);
}
#[test]
fn a_preserved_reading_states_when_the_failure_began() {
let mut entry = ProviderUsage::healthy("codex", None, "oauth", Usage::default());
entry.fetched_at = Some("2026-08-13T10:00:00Z".to_string());
entry.stale = Some(Stale {
since: "2026-08-13T10:02:00Z".to_string(),
class: Some("upstream_failed".to_string()),
});
let json = serde_json::to_string(&entry).expect("entry serializes");
let back: ProviderUsage = serde_json::from_str(&json).expect("entry round-trips");
let stale = back.stale.expect("the disclosure survives the round trip");
assert_eq!(stale.since, "2026-08-13T10:02:00Z");
assert_eq!(stale.class.as_deref(), Some("upstream_failed"));
assert_ne!(
Some(stale.since.as_str()),
back.fetched_at.as_deref(),
"the two timestamps answer different questions and must not be conflated"
);
assert!(
json.contains("\"stale\""),
"the key is camelCase on the wire: {json}"
);
}
#[test]
fn a_disclosure_without_a_class_still_decodes() {
let json = r#"{"provider":"codex","stale":{"since":"2026-08-13T10:02:00Z"}}"#;
let entry: ProviderUsage = serde_json::from_str(json).expect("decodes");
let stale = entry.stale.expect("present");
assert_eq!(stale.class, None);
assert_eq!(stale.since, "2026-08-13T10:02:00Z");
}
#[test]
fn an_entry_without_the_field_decodes() {
let json = r#"{"provider":"codex","source":"oauth"}"#;
let entry: ProviderUsage = serde_json::from_str(json).expect("decodes");
assert_eq!(entry.stale, None);
assert_eq!(entry.provider, "codex");
}
use super::*;
#[test]
fn account_info_is_omitted_when_empty_and_keeps_partial_labels() {
let bare = ProviderUsage::healthy(
"codex",
None,
"oauth",
Usage {
primary: Some(RateWindow {
used_percent: 10.0,
raw_used_percent: None,
resets_at: None,
window_minutes: Some(300),
used_count: None,
total_count: None,
regeneration: None,
window_kind: None,
breakdown: None,
}),
..Default::default()
},
);
let json = serde_json::to_string(&bare).unwrap();
assert!(
!json.contains("accountInfo"),
"empty accountInfo must be omitted"
);
let mut labeled = bare.clone();
labeled.account_info = Some(AccountInfo {
email: Some("a@b.com".to_string()),
org_name: None,
plan_type: Some("pro".to_string()),
..AccountInfo::default()
});
let json = serde_json::to_string(&labeled).unwrap();
assert!(json.contains("\"email\":\"a@b.com\""));
assert!(json.contains("\"planType\":\"pro\""));
assert!(!json.contains("orgName"), "absent orgName must be omitted");
}
#[test]
fn saved_resets_use_camel_case_and_round_trip() {
let entry = ProviderUsage {
saved_resets: Some(SavedResets {
available_count: 2,
soonest_expires_at: Some("2026-07-31T20:11:35Z".to_string()),
credits: vec![CreditExpiry {
expires_at: "2026-07-31T20:11:35Z".to_string(),
}],
}),
..ProviderUsage::degraded("codex", "x")
};
let json = serde_json::to_string(&entry).unwrap();
assert!(json.contains("\"savedResets\""));
assert!(json.contains("\"availableCount\":2"));
assert!(json.contains("\"soonestExpiresAt\""));
let back: ProviderUsage = serde_json::from_str(&json).unwrap();
assert_eq!(back, entry);
}
#[test]
fn raw_used_percent_is_absent_from_unrelaxed_windows_and_camel_case_when_present() {
let unrelaxed = RateWindow {
used_percent: 41.0,
raw_used_percent: None,
resets_at: Some("2026-07-20T00:00:00Z".to_string()),
window_minutes: Some(10080),
used_count: None,
total_count: None,
regeneration: None,
window_kind: None,
breakdown: None,
};
let json = serde_json::to_string(&unrelaxed).unwrap();
assert!(
!json.contains("rawUsedPercent"),
"unrelaxed window must not carry the field"
);
let relaxed = RateWindow {
used_percent: 0.0,
raw_used_percent: Some(70.0),
resets_at: Some("2026-07-20T00:00:00Z".to_string()),
window_minutes: Some(10080),
used_count: None,
total_count: None,
regeneration: None,
window_kind: None,
breakdown: None,
};
let json = serde_json::to_string(&relaxed).unwrap();
assert!(json.contains("\"rawUsedPercent\":70.0"));
let back: RateWindow = serde_json::from_str(&json).unwrap();
assert_eq!(back, relaxed);
}
#[test]
fn healthy_entry_omits_error_and_degraded_entry_omits_usage() {
let healthy = ProviderUsage::healthy("codex", None, "oauth", Usage::default());
let json = serde_json::to_string(&healthy).unwrap();
assert!(!json.contains("error"));
let degraded = ProviderUsage::degraded("codex", "no session");
let json = serde_json::to_string(°raded).unwrap();
assert!(json.contains("\"error\":\"no session\""));
assert!(!json.contains("usage"));
}
#[test]
fn api_provider_is_camel_case_present_when_set_and_omitted_when_absent() {
let mut entry = ProviderUsage::healthy("codex", None, "oauth", Usage::default());
let json = serde_json::to_string(&entry).unwrap();
assert!(
!json.contains("apiProvider"),
"absent api_provider must be omitted"
);
entry.api_provider = Some("openai".to_string());
let json = serde_json::to_string(&entry).unwrap();
assert!(json.contains("\"apiProvider\":\"openai\""));
let back: ProviderUsage = serde_json::from_str(&json).unwrap();
assert_eq!(back, entry);
}
#[test]
fn used_count_and_total_count_are_camel_case_and_omitted_when_absent() {
let window = RateWindow {
used_percent: 25.8,
raw_used_percent: None,
resets_at: Some("2026-07-26T14:09:00Z".to_string()),
window_minutes: Some(10080),
used_count: None,
total_count: None,
regeneration: None,
window_kind: None,
breakdown: None,
};
let json = serde_json::to_string(&window).unwrap();
assert!(
!json.contains("usedCount"),
"absent used_count must be omitted"
);
assert!(
!json.contains("totalCount"),
"absent total_count must be omitted"
);
let enriched = RateWindow {
used_count: Some(10336.0),
total_count: Some(40000.0),
..window
};
let json = serde_json::to_string(&enriched).unwrap();
assert!(json.contains("\"usedCount\":10336.0"));
assert!(json.contains("\"totalCount\":40000.0"));
let back: RateWindow = serde_json::from_str(&json).unwrap();
assert_eq!(back, enriched);
}
#[test]
fn an_entry_without_a_class_serializes_as_it_did_before_the_field_existed() {
let entry = ProviderUsage::degraded("codex", "no session: nothing configured");
let json = serde_json::to_string(&entry).unwrap();
assert!(!json.contains("errorClass"), "absent class must be omitted");
assert!(json.contains("\"error\":\"no session: nothing configured\""));
let back: ProviderUsage = serde_json::from_str(&json).unwrap();
assert_eq!(back, entry);
assert_eq!(back.error_class, None);
}
#[test]
fn a_class_round_trips_under_its_camel_case_wire_name() {
let entry = ProviderUsage::degraded_with_class(
"gemini",
"credential unusable: gemini creds have no refresh_token",
"credential_unusable",
);
let json = serde_json::to_string(&entry).unwrap();
assert!(json.contains("\"errorClass\":\"credential_unusable\""));
let back: ProviderUsage = serde_json::from_str(&json).unwrap();
assert_eq!(back, entry);
}
#[test]
fn an_unknown_class_decodes_rather_than_failing() {
let json = r#"{"provider":"someprovider","error":"something new","errorClass":"a_class_from_the_future"}"#;
let entry: ProviderUsage =
serde_json::from_str(json).expect("an unrecognised class must not fail to decode");
assert_eq!(
entry.error_class.as_deref(),
Some("a_class_from_the_future")
);
assert_eq!(entry.provider, "someprovider");
assert_eq!(entry.error.as_deref(), Some("something new"));
}
#[test]
fn an_entry_without_pools_does_not_mention_them() {
let entry = ProviderUsage::healthy("codex", None, "oauth", Usage::default());
let json = serde_json::to_string(&entry).unwrap();
assert!(!json.contains("spend"), "unexpected spend key: {json}");
}
#[test]
fn pools_round_trip_with_their_basis_and_funding() {
let pool = Pool {
id: "granted_balance".to_string(),
label: "Granted".to_string(),
funding: PoolFunding::Granted,
remaining: Some(Amount {
minor: 1050,
exponent: 2,
unit: "CNY".to_string(),
}),
total: None,
basis: PoolBasis::Reported,
spendable: Some(true),
resets_at: None,
};
let mut entry = ProviderUsage::healthy("deepseek", None, "api", Usage::default());
entry.spend = Some(vec![pool.clone()]);
let json = serde_json::to_string(&entry).unwrap();
let back: ProviderUsage = serde_json::from_str(&json).unwrap();
assert_eq!(back.spend, Some(vec![pool]));
assert!(json.contains(r#""funding":"granted""#), "{json}");
assert!(json.contains(r#""basis":"reported""#), "{json}");
assert!(json.contains(r#""minor":1050"#), "{json}");
assert!(
!json.contains("10.5"),
"an amount was rendered as a decimal: {json}"
);
}
#[test]
fn a_pool_reset_round_trips_and_is_omitted_when_absent() {
let pool = Pool {
id: "individual_limit".to_string(),
label: "Credit limit".to_string(),
funding: PoolFunding::Unknown,
remaining: None,
total: None,
basis: PoolBasis::Unstated,
spendable: None,
resets_at: Some("2026-10-01T00:00:00Z".to_string()),
};
let json = serde_json::to_string(&pool).unwrap();
assert!(
json.contains(r#""resetsAt":"2026-10-01T00:00:00Z""#),
"{json}"
);
assert_eq!(serde_json::from_str::<Pool>(&json).unwrap(), pool);
let unstated = Pool {
resets_at: None,
..pool
};
let json = serde_json::to_string(&unstated).unwrap();
assert!(!json.contains("resetsAt"), "{json}");
}
#[test]
fn a_pool_without_a_reset_key_decodes_to_none() {
let json = r#"{ "id": "credits", "label": "Credits", "funding": "unknown", "basis": "reported",
"remaining": { "minor": 2402, "exponent": 2, "unit": "credit" } }"#;
let pool: Pool = serde_json::from_str(json).unwrap();
assert_eq!(pool.resets_at, None);
}
#[test]
fn an_unknown_funding_kind_does_not_discard_the_entry() {
let json = r#"{
"provider": "minimax",
"usage": { "primary": { "usedPercent": 42.0 } },
"spend": [
{ "id": "a", "label": "A", "funding": "granted", "basis": "reported" },
{ "id": "b", "label": "B", "funding": "crypto_grant", "basis": "reported" }
]
}"#;
let entry: ProviderUsage = serde_json::from_str(json).expect("entry must survive");
let pools = entry.spend.expect("pools present");
assert_eq!(pools.len(), 2, "no pool may be dropped");
assert_eq!(pools[0].funding, PoolFunding::Granted);
assert_eq!(pools[1].funding, PoolFunding::Unknown);
assert_eq!(
entry.usage.and_then(|u| u.primary).map(|w| w.used_percent),
Some(42.0)
);
}
#[test]
fn an_unknown_basis_is_unstated_rather_than_exact() {
let json = r#"{ "id": "a", "label": "A", "funding": "granted",
"basis": "sampled_hourly" }"#;
let pool: Pool = serde_json::from_str(json).expect("pool must survive");
assert_eq!(pool.basis, PoolBasis::Unstated);
assert_ne!(
pool.basis,
PoolBasis::Reported,
"an unknown basis must never read as an exact remainder"
);
}
#[test]
fn a_healthy_entry_carries_no_class() {
let entry = ProviderUsage::healthy("codex", None, "oauth", Usage::default());
assert_eq!(entry.error_class, None);
assert!(!serde_json::to_string(&entry)
.unwrap()
.contains("errorClass"));
}
fn weekly(breakdown: Option<UsageBreakdown>) -> RateWindow {
RateWindow {
used_percent: 85.0,
raw_used_percent: None,
resets_at: Some("2026-09-30T14:00:00Z".to_string()),
window_minutes: Some(10_080),
used_count: None,
total_count: None,
regeneration: None,
window_kind: None,
breakdown,
}
}
#[test]
fn a_window_naming_no_period_omits_the_kind_key() {
let json = serde_json::to_string(&weekly(None)).unwrap();
assert!(!json.contains("windowKind"), "{json}");
}
#[test]
fn a_window_from_before_the_kind_decodes_without_one() {
let json =
r#"{"usedPercent":85.0,"resetsAt":"2026-09-30T14:00:00Z","windowMinutes":10080}"#;
let window: RateWindow = serde_json::from_str(json).unwrap();
assert_eq!(window.window_kind, None);
}
#[test]
fn a_monthly_window_with_no_length_carries_its_kind() {
let window = RateWindow {
window_minutes: None,
window_kind: Some(window_kind::MONTHLY.to_string()),
..weekly(None)
};
let json = serde_json::to_string(&window).unwrap();
assert!(json.contains(r#""windowKind":"monthly""#), "{json}");
assert!(!json.contains("windowMinutes"), "{json}");
let back: RateWindow = serde_json::from_str(&json).unwrap();
assert_eq!(back, window);
}
#[test]
fn an_unknown_kind_decodes_as_itself() {
let json = r#"{"usedPercent":5.0,"windowKind":"fortnightly"}"#;
let window: RateWindow = serde_json::from_str(json).unwrap();
assert_eq!(window.window_kind.as_deref(), Some("fortnightly"));
}
#[test]
fn the_window_kind_vocabulary_is_pinned() {
assert_eq!(
[
window_kind::HOURLY,
window_kind::FIVE_HOUR,
window_kind::DAILY,
window_kind::WEEKLY,
window_kind::MONTHLY,
],
["hourly", "five_hour", "daily", "weekly", "monthly"]
);
}
#[test]
fn a_window_without_a_breakdown_omits_the_key() {
let json = serde_json::to_string(&weekly(None)).unwrap();
assert!(!json.contains("breakdown"), "{json}");
}
#[test]
fn a_window_from_before_the_field_decodes_without_one() {
let json =
r#"{"usedPercent":85.0,"resetsAt":"2026-09-30T14:00:00Z","windowMinutes":10080}"#;
let window: RateWindow = serde_json::from_str(json).unwrap();
assert_eq!(window.breakdown, None);
}
#[test]
fn a_breakdown_round_trips_keys_verbatim_and_keeps_a_missing_share_absent() {
let breakdown = UsageBreakdown {
as_of: Some("2026-09-27T08:37:37Z".to_string()),
window_started_at: Some("2026-09-23T14:00:00Z".to_string()),
rows: vec![
BreakdownRow {
key: "claude_code".to_string(),
share_percent: Some(100.0),
},
BreakdownRow {
key: "chat".to_string(),
share_percent: Some(0.0),
},
BreakdownRow {
key: "a_surface_from_the_future".to_string(),
share_percent: None,
},
],
};
let window = weekly(Some(breakdown));
let json = serde_json::to_string(&window).unwrap();
assert!(
json.contains(r#""breakdown":{"asOf":"2026-09-27T08:37:37Z","windowStartedAt":"2026-09-23T14:00:00Z","rows":[{"key":"claude_code","sharePercent":100.0},{"key":"chat","sharePercent":0.0},{"key":"a_surface_from_the_future"}]}"#),
"{json}"
);
let back: RateWindow = serde_json::from_str(&json).unwrap();
assert_eq!(back, window);
assert_eq!(back.breakdown.unwrap().rows[2].share_percent, None);
}
}