use costroid_core::{LimitAvailability, LimitMeasure, LimitSummary, NowSummary};
pub const MAX_STEP: u8 = 8;
pub fn severity_step(fraction: f64) -> u8 {
if !fraction.is_finite() || fraction <= 0.0 {
return 0;
}
if fraction >= 1.0 {
return MAX_STEP;
}
let step = (fraction * f64::from(MAX_STEP)).round();
(step as i64).clamp(1, 7) as u8
}
#[derive(Debug, Clone)]
pub struct Constraint {
pub limit: LimitSummary,
pub fraction: f64,
}
impl Constraint {
pub fn step(&self) -> u8 {
severity_step(self.fraction)
}
}
pub fn most_constrained_available(summary: &NowSummary) -> Option<Constraint> {
summary
.limits
.iter()
.filter_map(|limit| {
available_fraction(limit).map(|fraction| Constraint {
limit: limit.clone(),
fraction,
})
})
.max_by(|a, b| a.fraction.total_cmp(&b.fraction))
}
fn available_fraction(limit: &LimitSummary) -> Option<f64> {
match &limit.availability {
LimitAvailability::Available {
measure: LimitMeasure::TokenFraction(fraction),
..
} if fraction.is_finite() => Some(fraction.max(0.0)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{DateTime, Utc};
use costroid_core::{GroupBy, LimitKind, NowSummary, PeriodRange, ProviderId};
fn ts(secs: i64) -> DateTime<Utc> {
match DateTime::from_timestamp(secs, 0) {
Some(dt) => dt,
None => panic!("invalid test timestamp {secs}"),
}
}
fn token_window(tool: ProviderId, kind: LimitKind, fraction: f64) -> LimitSummary {
LimitSummary {
tool,
plan: None,
kind,
label: None,
captured_at: ts(1_900_000_000),
availability: LimitAvailability::Available {
measure: LimitMeasure::TokenFraction(fraction),
resets_at: ts(1_900_003_600),
reset_in_seconds: 3_600,
},
}
}
fn unavailable_window(tool: ProviderId, kind: LimitKind) -> LimitSummary {
LimitSummary {
tool,
plan: None,
kind,
label: None,
captured_at: ts(0),
availability: LimitAvailability::Unavailable {
reason: "no sanctioned source".to_owned(),
},
}
}
fn unverified_window(tool: ProviderId, kind: LimitKind, fraction: f64) -> LimitSummary {
LimitSummary {
tool,
plan: None,
kind,
label: None,
captured_at: ts(1_900_000_000),
availability: LimitAvailability::Unverified {
measure: LimitMeasure::TokenFraction(fraction),
resets_at: None,
reset_in_seconds: None,
},
}
}
fn summary_with(limits: Vec<LimitSummary>) -> NowSummary {
NowSummary {
generated_at: ts(1_900_000_500),
cost_period: PeriodRange {
start: ts(1_899_000_000),
end: ts(1_901_000_000),
},
group_by: GroupBy::Model,
limits,
current_costs: Vec::new(),
providers: Vec::new(),
}
}
#[test]
fn severity_step_endpoints_and_floor() {
assert_eq!(severity_step(0.0), 0, "zero usage is idle");
assert_eq!(severity_step(-0.5), 0, "negative is clamped to idle");
assert_eq!(
severity_step(f64::NAN),
0,
"non-finite is idle, never a guess"
);
assert_eq!(severity_step(0.0001), 1, "any nonzero usage shows >= 1 dot");
assert_eq!(severity_step(1.0), 8, "at the limit is the full grid");
assert_eq!(severity_step(1.5), 8, "over the limit stays the full grid");
}
#[test]
fn severity_step_is_monotonic_nondecreasing() {
let mut last = 0u8;
let mut f = 0.0;
while f <= 1.2 {
let step = severity_step(f);
assert!(
step >= last,
"step must never decrease as fraction grows: f={f} step={step} last={last}"
);
assert!(step <= MAX_STEP);
last = step;
f += 0.01;
}
}
#[test]
fn severity_step_reserves_eight_for_at_or_over_limit() {
assert!(severity_step(0.95) <= 7);
assert!(severity_step(0.999) <= 7);
assert_eq!(severity_step(1.0), 8);
}
#[test]
fn most_constrained_picks_highest_available_fraction() {
let summary = summary_with(vec![
token_window(ProviderId::Codex, LimitKind::FiveHour, 0.40),
token_window(ProviderId::ClaudeCode, LimitKind::FiveHour, 0.92),
token_window(ProviderId::ClaudeCode, LimitKind::Weekly, 0.51),
]);
let chosen = most_constrained_available(&summary);
let Some(chosen) = chosen else {
panic!("expected a most-constrained window");
};
assert_eq!(chosen.limit.tool, ProviderId::ClaudeCode);
assert_eq!(chosen.limit.kind, LimitKind::FiveHour);
assert!((chosen.fraction - 0.92).abs() < 1e-9);
assert_eq!(chosen.step(), severity_step(0.92));
}
#[test]
fn all_degraded_windows_yield_idle_none() {
let summary = summary_with(vec![
unavailable_window(ProviderId::Cursor, LimitKind::Monthly),
unverified_window(ProviderId::ClaudeCode, LimitKind::Weekly, 0.99),
]);
assert!(
most_constrained_available(&summary).is_none(),
"an Unverified/Unavailable reading must never drive a confident tray fill"
);
}
#[test]
fn empty_summary_is_idle() {
assert!(most_constrained_available(&summary_with(Vec::new())).is_none());
}
#[test]
fn available_window_among_degraded_ones_is_still_selected() {
let summary = summary_with(vec![
unavailable_window(ProviderId::Cursor, LimitKind::Monthly),
token_window(ProviderId::Codex, LimitKind::FiveHour, 0.23),
unverified_window(ProviderId::ClaudeCode, LimitKind::Weekly, 0.99),
]);
let Some(chosen) = most_constrained_available(&summary) else {
panic!("the one Available window must be selected");
};
assert_eq!(chosen.limit.tool, ProviderId::Codex);
}
}