use crate::context::Context;
use crate::in_use::Known;
use crate::pace::Standing;
use crate::provider::ProviderId;
use crate::sessions::{Limit, Run};
use crate::state::{Account, State};
use crate::usage::{Snapshot, Window};
use serde_json::Value;
use std::collections::{BTreeMap, HashMap};
const FRESH_FOR: i64 = 15 * 60;
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct Shares {
pub five_hour: Option<f64>,
pub weekly: Option<f64>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Paces {
pub five_hour: Option<Standing>,
pub weekly: Option<Standing>,
}
#[derive(Debug, PartialEq)]
pub struct Entry {
pub label: String,
pub shares: Shares,
pub age: Option<i64>,
}
#[derive(Debug, PartialEq)]
pub struct StatusLine {
pub current: Option<String>,
pub sure: bool,
pub session: Shares,
pub pace: Paces,
pub others: Vec<Entry>,
}
const FIVE_HOUR: &[&str] = &["session", "five_hour"];
const WEEKLY: &[&str] = &["weekly_all", "seven_day"];
fn limit<'a>(windows: &'a [Window], kinds: &[&str]) -> Option<&'a Window> {
windows
.iter()
.find(|w| w.scope.is_none() && kinds.contains(&w.kind.as_str()))
}
fn shares_of(windows: &[Window], now: i64) -> Shares {
let share = |kinds| limit(windows, kinds).map(|w| w.used(now));
Shares {
five_hour: share(FIVE_HOUR),
weekly: share(WEEKLY),
}
}
fn paces_of(windows: &[Window], at: i64, now: i64) -> Paces {
let pace = |kinds| Some(limit(windows, kinds)?.pace(at, now)?.standing);
Paces {
five_hour: pace(FIVE_HOUR),
weekly: pace(WEEKLY),
}
}
enum Whose<'a> {
Proven(&'a Account),
Nobody,
Unsaid,
}
fn running(passed: &[Window], now: i64) -> impl Iterator<Item = &Window> {
passed
.iter()
.filter(move |window| window.resets_at.is_some_and(|at| at > now))
}
fn answered<'a>(
remembered: &'a HashMap<String, Snapshot>,
account: &Account,
) -> Option<&'a Snapshot> {
remembered
.get(&account.id)
.filter(|reading| reading.answered())
}
fn holds(reading: &Snapshot, passed: &[Window], now: i64) -> bool {
running(passed, now).any(|given| reading.windows.iter().any(|had| had.same_window(given)))
}
fn contradicts(reading: &Snapshot, passed: &[Window], now: i64) -> bool {
running(passed, now).any(|given| {
let theirs = || reading.windows.iter().filter(|had| had.same_limit(given));
let elsewhere =
theirs().any(|had| !had.same_window(given) && had.resets_at.is_some_and(|at| at > now));
elsewhere || (reading.lists_every_limit && theirs().next().is_none())
})
}
fn whose<'a>(
state: &'a State,
remembered: &HashMap<String, Snapshot>,
passed: &[Window],
now: i64,
) -> Whose<'a> {
if running(passed, now).next().is_none() {
return Whose::Unsaid;
}
let mut theirs = state
.accounts
.iter()
.filter(|a| a.provider() == ProviderId::Claude)
.filter(|a| {
answered(remembered, a).is_some_and(|reading| {
holds(reading, passed, now) && !contradicts(reading, passed, now)
})
});
match (theirs.next(), theirs.next()) {
(Some(account), None) => Whose::Proven(account),
_ => Whose::Nobody,
}
}
fn line(
state: &State,
in_use: &Known,
remembered: &HashMap<String, Snapshot>,
whose: &Whose,
passed: &[Window],
moved: &[Window],
now: i64,
) -> StatusLine {
let stored = in_use
.owner()
.and_then(|owner| state.account_of(ProviderId::Claude, owner));
let (current, sure) = match (whose, stored) {
(Whose::Proven(account), _) => (Some(*account), true),
(Whose::Unsaid, _) | (Whose::Nobody, None) => (stored, in_use.doubt.is_none()),
(Whose::Nobody, Some(account)) => {
let elsewhere = answered(remembered, account)
.is_some_and(|reading| contradicts(reading, passed, now));
((!elsewhere).then_some(account), false)
}
};
let others = state
.accounts
.iter()
.filter(|a| a.provider() == ProviderId::Claude)
.filter(|a| current.is_none_or(|c| c.id != a.id))
.map(|account| {
let reading = remembered.get(&account.id);
Entry {
label: account.label.clone(),
shares: reading.map_or_else(Shares::default, |r| shares_of(&r.windows, now)),
age: reading
.and_then(|r| r.observed_at)
.map(|at| now - at)
.filter(|age| *age > FRESH_FOR),
}
})
.collect();
let known = match whose {
Whose::Nobody => None,
Whose::Proven(_) | Whose::Unsaid => current.and_then(|a| remembered.get(&a.id)),
};
let folded = known.and_then(|known| crate::usage::moved(known, moved, now));
let (windows, at) = match folded.as_ref().or(known) {
Some(reading) => (
reading.windows.as_slice(),
reading.observed_at.unwrap_or(now),
),
None => (passed, now),
};
StatusLine {
current: current.map(|a| a.label.clone()),
sure,
session: shares_of(windows, now),
pace: paces_of(windows, at, now),
others,
}
}
fn window(name: &str, limit: &Limit) -> Window {
Window {
kind: name.to_owned(),
scope: None,
percent: limit.used_percentage,
resets_at: limit.resets_at,
is_active: false,
severity: None,
length_seconds: crate::usage::anthropic_window_length(name),
}
}
fn windows_of(run: &Run) -> Vec<Window> {
run.limits
.iter()
.map(|(name, limit)| window(name, limit))
.collect()
}
fn run_of(input: &Value) -> Run {
let limits = input.get("rate_limits");
Run {
limits: ["five_hour", "seven_day"]
.into_iter()
.filter_map(|name| {
let w = limits?.get(name)?;
let limit = Limit {
used_percentage: w.get("used_percentage")?.as_f64()?,
resets_at: w.get("resets_at").and_then(Value::as_i64),
};
Some((name.to_string(), limit))
})
.collect::<BTreeMap<_, _>>(),
}
}
fn moved_since(run: &Run, before: Option<&Run>) -> Vec<Window> {
let Some(before) = before else {
return Vec::new();
};
run.limits
.iter()
.filter(|(name, limit)| before.limits.get(*name) != Some(*limit))
.map(|(name, limit)| window(name, limit))
.collect()
}
pub fn read(ctx: &Context, permit: Option<crate::service::Permit>, input: &str) -> StatusLine {
let input: Value = serde_json::from_str(input).unwrap_or(Value::Null);
let state = crate::state::load(ctx).unwrap_or_default();
let in_use = crate::in_use::known(ctx, &state, ProviderId::Claude);
let now = ctx.now();
let remembered = crate::readings::load(ctx);
let run = run_of(&input);
let before = input
.get("session_id")
.and_then(Value::as_str)
.and_then(|id| match permit {
Some(permit) => crate::sessions::exchange(ctx, permit, id, &run),
None => crate::sessions::last(ctx, id),
});
let passed = windows_of(&run);
let moved = moved_since(&run, before.as_ref());
let whose = whose(&state, &remembered, &passed, now);
if let (Some(permit), Whose::Proven(account)) = (permit, &whose)
&& !moved.is_empty()
{
crate::readings::moved(ctx, permit, &account.id, &moved);
}
line(&state, &in_use, &remembered, &whose, &passed, &moved, now)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::Owner;
use crate::in_use::InUse;
use crate::service::Permit;
use crate::time::{Clock, FixedClock};
use crate::usage::{Source, Window};
use serde_json::json;
use std::sync::Arc;
const NOW: i64 = 1_789_935_000;
const HOUR: i64 = 3_600;
const DAY: i64 = 86_400;
fn state() -> State {
let account = |label: &str| Account {
last_used_at: None,
replaced_at: None,
label: label.into(),
id: format!("{label}-uuid"),
account_uuid: format!("{label}-uuid"),
email: format!("{label}@example.com"),
detail: crate::state::Detail::Claude {
organization_uuid: "o".into(),
oauth_account: json!({}),
},
parked: None,
};
State {
accounts: vec![account("work"), account("personal"), account("side")],
..State::default()
}
}
fn reading(five: f64, week: f64, observed_at: i64, resets_at: i64) -> Snapshot {
timed((five, resets_at), (week, resets_at), observed_at)
}
fn timed(five_hour: (f64, i64), weekly: (f64, i64), observed_at: i64) -> Snapshot {
let window = |kind: &str, (percent, resets_at): (f64, i64)| Window {
kind: kind.into(),
scope: None,
percent,
resets_at: Some(resets_at),
is_active: false,
severity: None,
length_seconds: None,
};
Snapshot {
windows: vec![window("session", five_hour), window("weekly_all", weekly)],
observed_at: Some(observed_at),
answered_at: Some(observed_at),
lists_every_limit: true,
source: Source::Remembered,
}
}
fn shares(five_hour: f64, weekly: f64) -> Shares {
Shares {
five_hour: Some(five_hour),
weekly: Some(weekly),
}
}
fn session(five_hour: f64, weekly: f64, resets_at: i64) -> Value {
passed((five_hour, resets_at), (weekly, resets_at))
}
fn passed(five_hour: (f64, i64), weekly: (f64, i64)) -> Value {
json!({"rate_limits": {
"five_hour": {"used_percentage": five_hour.0, "resets_at": five_hour.1},
"seven_day": {"used_percentage": weekly.0, "resets_at": weekly.1}
}})
}
fn owner(label: &str) -> Owner {
Owner {
account_uuid: format!("{label}-uuid"),
email: format!("{label}@example.com"),
organization_uuid: "o".into(),
}
}
fn known(owner: Option<Owner>) -> Known {
let Some(owner) = owner else {
return Known {
last: None,
doubt: Some(crate::in_use::Doubt::NeverEstablished),
own_record: None,
};
};
Known {
last: Some(InUse {
named: Some(crate::state::new_id(ProviderId::Claude, &owner)),
owner: Some(owner.clone()),
login: "refresh".into(),
known_at: NOW,
}),
doubt: None,
own_record: Some(owner),
}
}
fn passing_nothing(accounts: &State, in_use: &Known) -> StatusLine {
line(
accounts,
in_use,
&HashMap::new(),
&Whose::Unsaid,
&[],
&[],
NOW,
)
}
fn after_a_response(input: &Value, remembered: &HashMap<String, Snapshot>) -> StatusLine {
let accounts = state();
let run = run_of(input);
let passed = windows_of(&run);
let moved = moved_since(&run, Some(&Run::default()));
let whose = whose(&accounts, remembered, &passed, NOW);
line(
&accounts,
&known(Some(owner("work"))),
remembered,
&whose,
&passed,
&moved,
NOW,
)
}
struct Scratch(std::path::PathBuf);
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn machine(name: &str) -> (Context, Scratch) {
let root = std::env::temp_dir().join(format!(
"pitboard-statusline-{name}-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("a scratch home");
let ctx = Context::new(root.clone())
.with_pitboard_home(root.join(".pitboard"))
.with_clock(Arc::new(FixedClock::at(NOW)) as Arc<dyn Clock>);
sign_in(&ctx, "work");
(ctx, Scratch(root))
}
fn at(ctx: &Context, now: i64) -> Context {
ctx.clone()
.with_clock(Arc::new(FixedClock::at(now)) as Arc<dyn Clock>)
}
fn sign_in(ctx: &Context, label: &str) {
names(ctx, &owner(label));
}
fn names(ctx: &Context, owner: &Owner) {
std::fs::write(
ctx.home().join(".claude.json"),
json!({"oauthAccount": {
"accountUuid": owner.account_uuid,
"emailAddress": owner.email,
"organizationUuid": owner.organization_uuid,
}})
.to_string(),
)
.expect("a Claude Code config");
}
fn using(ctx: &Context, label: &str) {
let accounts = crate::state::load(ctx).expect("an account index");
let owner = accounts
.accounts
.iter()
.find(|a| a.label == label)
.expect("an enrolled account")
.owner();
stores(ctx, owner);
}
fn stores(ctx: &Context, owner: Owner) {
let mut accounts = crate::state::load(ctx).expect("an account index");
names(ctx, &owner);
let found = InUse {
login: format!("{}-refresh", owner.account_uuid),
owner: Some(owner),
known_at: ctx.now(),
named: crate::in_use::named(ctx, ProviderId::Claude),
};
accounts.identified(ProviderId::Claude, found, ctx.now());
crate::state::save(ctx, Permit::for_a_test(), &accounts).expect("an account index");
}
fn run(ctx: &Context, id: &str, input: &Value) -> StatusLine {
let mut input = input.clone();
input["session_id"] = json!(id);
read(ctx, Some(Permit::for_a_test()), &input.to_string())
}
fn open(ctx: &Context, id: &str) {
run(ctx, id, &json!({}));
}
fn recorded(ctx: &Context) -> Snapshot {
recorded_for(ctx, "work").expect("a reading")
}
fn recorded_for(ctx: &Context, label: &str) -> Option<Snapshot> {
crate::readings::load(ctx).remove(&format!("{label}-uuid"))
}
fn five_hour_alone(used: f64, resets_at: i64) -> Value {
json!({"rate_limits": {
"five_hour": {"used_percentage": used, "resets_at": resets_at}
}})
}
#[test]
fn names_the_account_in_use_and_what_every_account_has_left() {
let input = json!({"rate_limits": {
"five_hour": {"used_percentage": 46.4, "resets_at": NOW + 600},
"seven_day": {"used_percentage": 70.0, "resets_at": NOW + DAY}
}});
let remembered = HashMap::from([
(
"work-uuid".to_string(),
timed((40.0, NOW + 600), (65.0, NOW + DAY), NOW - 60),
),
(
"personal-uuid".to_string(),
reading(12.0, 40.0, NOW - 60, NOW + 2 * HOUR),
),
(
"side-uuid".to_string(),
reading(90.0, 88.0, NOW - 3 * 3_600, NOW - 1),
),
]);
let line = after_a_response(&input, &remembered);
assert_eq!(line.current.as_deref(), Some("work"));
assert!(line.sure, "its windows are work's");
assert_eq!(line.session, shares(46.4, 70.0));
assert_eq!(
line.others,
[
Entry {
label: "personal".into(),
shares: shares(12.0, 40.0),
age: None,
},
Entry {
label: "side".into(),
shares: shares(0.0, 0.0),
age: Some(3 * 3_600),
},
],
"a window past its reset counts as reset, and an old reading says how old"
);
}
#[test]
fn the_account_in_use_says_its_pace_on_each_limit() {
let input = json!({"rate_limits": {
"five_hour": {"used_percentage": 46.4, "resets_at": NOW + 600},
"seven_day": {"used_percentage": 70.0, "resets_at": NOW + 5 * DAY}
}});
let shown = after_a_response(&input, &HashMap::new());
assert_eq!(shown.pace.five_hour, Some(Standing::Under));
assert!(
matches!(shown.pace.weekly, Some(Standing::Over { .. })),
"{:?}",
shown.pace
);
assert_eq!(
passing_nothing(&state(), &known(None)).pace,
Paces::default()
);
}
#[test]
fn what_is_unknown_is_left_unknown_not_zero() {
let line = passing_nothing(&state(), &known(None));
assert_eq!(line.current, None);
assert!(!line.sure, "nobody has asked whose login is stored");
assert_eq!(line.session, Shares::default());
assert!(line.others.iter().all(|e| e.shares == Shares::default()));
assert_eq!(line.others.len(), 3);
}
#[test]
fn another_tools_accounts_are_not_on_claude_codes_line() {
let codex = |label: &str, uuid: &str| Account {
last_used_at: None,
replaced_at: None,
label: label.into(),
id: uuid.into(),
account_uuid: uuid.into(),
email: format!("{label}@example.com"),
detail: crate::state::Detail::Codex {
workspace_id: None,
plan: None,
},
parked: None,
};
let mut s = state();
s.accounts.insert(0, codex("shadow", "work-uuid"));
s.accounts.push(codex("work", "codex-work"));
let found = passing_nothing(&s, &known(Some(owner("work"))));
assert_eq!(found.current.as_deref(), Some("work"));
let others: Vec<&str> = found.others.iter().map(|e| e.label.as_str()).collect();
assert_eq!(others, ["personal", "side"]);
let codex_work = s.accounts.last().expect("codex/work").owner();
let found = passing_nothing(&s, &known(Some(codex_work)));
assert_eq!(
found.current, None,
"a Codex identity names no Claude Code account"
);
let reading = timed((20.0, NOW + 2 * HOUR), (30.0, NOW + 3 * DAY), NOW - 60);
let windows = reading.windows.clone();
let remembered = HashMap::from([("codex-work".to_string(), reading)]);
let shown = line(
&s,
&known(Some(owner("work"))),
&remembered,
&whose(&s, &remembered, &windows, NOW),
&windows,
&windows,
NOW,
);
assert_eq!(shown.current.as_deref(), Some("work"));
assert!(
!shown.sure,
"a Codex reading proves nothing of a Claude Code session"
);
let others: Vec<&str> = shown.others.iter().map(|e| e.label.as_str()).collect();
assert_eq!(others, ["personal", "side"]);
}
#[test]
fn the_account_in_use_shows_the_newer_of_the_session_and_what_is_remembered() {
let remembered = HashMap::from([(
"work-uuid".to_string(),
reading(22.0, 6.0, NOW - 60, NOW + 600),
)]);
let shown = |input: &Value, remembered: &HashMap<String, Snapshot>| {
after_a_response(input, remembered).session
};
assert_eq!(
shown(&session(20.0, 5.0, NOW + 600), &remembered),
shares(22.0, 6.0)
);
assert_eq!(
shown(&session(25.0, 7.0, NOW + 600), &remembered),
shares(25.0, 7.0)
);
let next = HashMap::from([(
"work-uuid".to_string(),
reading(1.0, 6.0, NOW - 60, NOW + 5 * 3_600),
)]);
assert_eq!(shown(&session(90.0, 5.0, NOW - 1), &next), shares(1.0, 6.0));
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn the_account_in_use_is_the_one_anthropic_last_named() {
let (ctx, _scratch) = machine("organisations");
let mut accounts = state();
accounts.upsert(Account {
last_used_at: None,
replaced_at: None,
label: "team".into(),
id: "work-uuid_team-org".into(),
account_uuid: "work-uuid".into(),
email: "work@example.com".into(),
detail: crate::state::Detail::Claude {
organization_uuid: "team-org".into(),
oauth_account: json!({}),
},
parked: None,
});
crate::state::save(&ctx, Permit::for_a_test(), &accounts).expect("an account index");
using(&ctx, "team");
let shown = run(&ctx, "pane", &json!({}));
assert_eq!(shown.current.as_deref(), Some("team"));
assert!(shown.sure);
let others: Vec<&str> = shown.others.iter().map(|e| e.label.as_str()).collect();
assert_eq!(others, ["work", "personal", "side"]);
sign_in(&ctx, "work");
let moved = run(&ctx, "pane", &json!({}));
assert_eq!(moved.current.as_deref(), Some("team"));
assert!(!moved.sure);
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn the_sessions_numbers_are_recorded_when_they_are_newer() {
let (ctx, _scratch) = machine("records");
crate::state::save(&ctx, Permit::for_a_test(), &state()).expect("an account index");
crate::readings::answered(
&ctx,
Permit::for_a_test(),
&[("work-uuid".into(), reading(22.0, 6.0, NOW - 60, NOW + 600))],
);
run(&ctx, "busy", &session(22.0, 6.0, NOW + 600));
run(&ctx, "slow", &session(18.0, 4.0, NOW + 600));
let busy = run(&ctx, "busy", &session(25.0, 7.0, NOW + 600));
assert_eq!(busy.session, shares(25.0, 7.0));
assert_eq!(shares_of(&recorded(&ctx).windows, NOW), shares(25.0, 7.0));
let slow = run(&ctx, "slow", &session(20.0, 5.0, NOW + 600));
assert_eq!(slow.session, shares(25.0, 7.0));
assert_eq!(shares_of(&recorded(&ctx).windows, NOW), shares(25.0, 7.0));
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_session_moves_the_numbers_and_leaves_what_only_anthropic_says() {
let (ctx, _scratch) = machine("leaves");
crate::state::save(&ctx, Permit::for_a_test(), &state()).expect("an account index");
let mut answered = reading(22.0, 6.0, NOW - 60, NOW + 600);
answered.windows[0].severity = Some("normal".into());
answered.windows[0].is_active = true;
crate::readings::answered(
&ctx,
Permit::for_a_test(),
&[("work-uuid".into(), answered)],
);
open(&ctx, "pane");
run(&ctx, "pane", &session(25.0, 70.0, NOW + 600));
let windows = recorded(&ctx).windows;
let said: Vec<(&str, f64, bool)> = windows
.iter()
.map(|w| (w.kind.as_str(), w.percent, w.is_active))
.collect();
assert_eq!(
said,
[("session", 25.0, true), ("weekly_all", 70.0, false)],
"under the names it had, and working against the limit it was"
);
assert_eq!(
windows[0].severity, None,
"a share Anthropic has not graded"
);
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_session_holding_a_forgotten_accounts_numbers_is_not_recorded_as_the_one_in_use() {
let (ctx, _scratch) = machine("forgotten");
crate::state::save(&ctx, Permit::for_a_test(), &state()).expect("an account index");
crate::readings::answered(
&ctx,
Permit::for_a_test(),
&[
(
"work-uuid".into(),
timed((10.0, NOW + 2 * HOUR), (30.0, NOW + 3 * DAY), NOW - 60),
),
(
"personal-uuid".into(),
timed((95.0, NOW + 4 * HOUR), (60.0, NOW + 5 * DAY), NOW - 60),
),
],
);
using(&at(&ctx, NOW - HOUR), "work");
open(&ctx, "pane");
let on_personal = run(
&ctx,
"pane",
&passed((96.0, NOW + 4 * HOUR), (61.0, NOW + 5 * DAY)),
);
assert_eq!(on_personal.current.as_deref(), Some("personal"));
let mut accounts = crate::state::load(&ctx).expect("an account index");
accounts.accounts.retain(|a| a.label != "personal");
crate::state::save(&ctx, Permit::for_a_test(), &accounts).expect("an account index");
crate::readings::forget(&ctx, Permit::for_a_test(), "personal-uuid");
let forgotten = run(
&ctx,
"pane",
&passed((97.0, NOW + 4 * HOUR), (62.0, NOW + 5 * DAY)),
);
assert_eq!(forgotten.current, None, "work is in other windows");
assert_eq!(forgotten.session, shares(97.0, 62.0));
assert_eq!(shares_of(&recorded(&ctx).windows, NOW), shares(10.0, 30.0));
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_session_seen_for_the_first_time_offers_nothing_until_its_next_response() {
let (ctx, _scratch) = machine("first");
crate::state::save(&ctx, Permit::for_a_test(), &state()).expect("an account index");
let works = timed((10.0, NOW + 2 * HOUR), (30.0, NOW + 3 * DAY), NOW - 60);
crate::readings::answered(&ctx, Permit::for_a_test(), &[("work-uuid".into(), works)]);
let first = run(
&ctx,
"pane",
&passed((50.0, NOW + 2 * HOUR), (31.0, NOW + 3 * DAY)),
);
assert_eq!(first.current.as_deref(), Some("work"));
assert_eq!(
first.session,
shares(10.0, 30.0),
"the pane shows work's own"
);
assert_eq!(shares_of(&recorded(&ctx).windows, NOW), shares(10.0, 30.0));
let next = run(
&ctx,
"pane",
&passed((51.0, NOW + 2 * HOUR), (32.0, NOW + 3 * DAY)),
);
assert_eq!(next.session, shares(51.0, 32.0));
assert_eq!(shares_of(&recorded(&ctx).windows, NOW), shares(51.0, 32.0));
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_session_on_an_account_pitboard_has_not_read_shows_its_own_numbers() {
let (ctx, _scratch) = machine("unread");
crate::state::save(&ctx, Permit::for_a_test(), &state()).expect("an account index");
let numbers = passed((12.0, NOW + 2 * HOUR), (31.0, NOW + 3 * DAY));
assert_eq!(
run(&ctx, "new", &numbers).session,
shares(12.0, 31.0),
"a session not seen before"
);
open(&ctx, "pane");
for _ in 0..2 {
assert_eq!(run(&ctx, "pane", &numbers).session, shares(12.0, 31.0));
}
assert!(crate::readings::load(&ctx).is_empty());
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_response_from_the_account_before_a_login_is_known_by_its_windows() {
let (ctx, _scratch) = machine("login");
crate::state::save(&ctx, Permit::for_a_test(), &state()).expect("an account index");
crate::readings::answered(
&ctx,
Permit::for_a_test(),
&[
(
"work-uuid".into(),
timed((10.0, NOW - 60), (30.0, NOW + 3 * DAY), NOW - HOUR),
),
(
"personal-uuid".into(),
timed((95.0, NOW + 4 * HOUR), (60.0, NOW + 5 * DAY), NOW - 60),
),
],
);
sign_in(&ctx, "personal");
open(&ctx, "pane");
let personals = passed((95.0, NOW + 4 * HOUR), (60.0, NOW + 5 * DAY));
run(&ctx, "pane", &personals);
sign_in(&ctx, "work");
run(&ctx, "pane", &personals);
run(
&ctx,
"pane",
&passed((96.0, NOW + 4 * HOUR), (61.0, NOW + 5 * DAY)),
);
assert_eq!(shares_of(&recorded(&ctx).windows, NOW), shares(0.0, 30.0));
assert_eq!(
recorded_for(&ctx, "personal").map(|reading| shares_of(&reading.windows, NOW)),
Some(shares(96.0, 61.0))
);
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_window_past_its_reset_is_not_recorded() {
let (ctx, _scratch) = machine("passed");
crate::state::save(&ctx, Permit::for_a_test(), &state()).expect("an account index");
let works = timed((50.0, NOW - 60), (30.0, NOW + 3 * DAY), NOW - HOUR);
crate::readings::answered(&ctx, Permit::for_a_test(), &[("work-uuid".into(), works)]);
open(&ctx, "pane");
run(
&ctx,
"pane",
&passed((90.0, NOW - 60), (40.0, NOW + 3 * DAY)),
);
let said: Vec<f64> = recorded(&ctx).windows.iter().map(|w| w.percent).collect();
assert_eq!(said, [50.0, 40.0]);
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_window_that_resets_stays_in_the_reading_with_nothing_used() {
let (ctx, _scratch) = machine("reset");
crate::state::save(&ctx, Permit::for_a_test(), &state()).expect("an account index");
let works = timed((40.0, NOW - 10), (30.0, NOW + 3 * DAY), NOW - HOUR);
crate::readings::answered(&ctx, Permit::for_a_test(), &[("work-uuid".into(), works)]);
let floor = crate::budget::floor_for(Some(&recorded(&ctx)));
run(
&at(&ctx, NOW - 60),
"pane",
&passed((40.0, NOW - 10), (30.0, NOW + 3 * DAY)),
);
let shown = run(
&ctx,
"pane",
&passed((41.0, NOW - 10), (31.0, NOW + 3 * DAY)),
);
assert_eq!(shown.session, shares(0.0, 31.0));
assert_eq!(shares_of(&recorded(&ctx).windows, NOW), shares(0.0, 31.0));
let offline = crate::status::gather_offline(&ctx, &state());
let row = offline
.rows
.iter()
.find(|r| r.account_uuid == "work-uuid")
.and_then(|r| r.usage.as_ref())
.expect("work's numbers");
let kinds: Vec<&str> = row.windows.iter().map(|w| w.kind.as_str()).collect();
assert_eq!(kinds, ["session", "weekly_all"], "the row is kept");
assert_eq!(crate::budget::floor_for(Some(&recorded(&ctx))), floor);
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_session_still_on_the_account_before_a_switch_goes_on_recording_it() {
let (ctx, _scratch) = machine("before-a-switch");
crate::state::save(&ctx, Permit::for_a_test(), &state()).expect("an account index");
let personals = timed((50.0, NOW + 4 * HOUR), (60.0, NOW + 5 * DAY), NOW - 60);
crate::readings::answered(
&ctx,
Permit::for_a_test(),
&[
(
"work-uuid".into(),
timed((10.0, NOW + 2 * HOUR), (30.0, NOW + 3 * DAY), NOW - 60),
),
("personal-uuid".into(), personals.clone()),
],
);
using(&at(&ctx, NOW - HOUR), "work");
run(
&ctx,
"pane",
&passed((10.0, NOW + 2 * HOUR), (30.0, NOW + 3 * DAY)),
);
using(&ctx, "personal");
for (after, five_hour, weekly) in [(10, 12.0, 31.0), (10 * 60, 14.0, 32.0)] {
let shown = run(
&at(&ctx, NOW + after),
"pane",
&passed((five_hour, NOW + 2 * HOUR), (weekly, NOW + 3 * DAY)),
);
assert_eq!(shown.current.as_deref(), Some("work"), "{after} s on");
assert!(shown.sure, "{after} s on");
assert_eq!(shown.session, shares(five_hour, weekly));
assert_eq!(
shares_of(&recorded(&ctx).windows, NOW),
shares(five_hour, weekly),
"{after} s on"
);
}
assert_eq!(
recorded_for(&ctx, "personal"),
Some(personals),
"the account switched to keeps its own"
);
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_window_no_account_holds_is_shown_for_the_session_and_filed_nowhere() {
let (ctx, _scratch) = machine("nobodys");
crate::state::save(&ctx, Permit::for_a_test(), &state()).expect("an account index");
using(&at(&ctx, NOW - HOUR), "work");
open(&ctx, "pane");
let unread = run(
&ctx,
"pane",
&passed((97.0, NOW + 4 * HOUR), (80.0, NOW + 6 * DAY)),
);
assert_eq!(unread.current.as_deref(), Some("work"));
assert!(!unread.sure);
assert_eq!(unread.session, shares(97.0, 80.0));
assert!(crate::readings::load(&ctx).is_empty());
let works = timed((10.0, NOW + 2 * HOUR), (30.0, NOW + 3 * DAY), NOW - 60);
crate::readings::answered(
&ctx,
Permit::for_a_test(),
&[("work-uuid".into(), works.clone())],
);
let elsewhere = run(
&ctx,
"pane",
&passed((98.0, NOW + 4 * HOUR), (81.0, NOW + 6 * DAY)),
);
assert_eq!(elsewhere.current, None, "work is in other windows");
assert!(!elsewhere.sure);
assert_eq!(elsewhere.session, shares(98.0, 81.0));
assert_eq!(
crate::readings::load(&ctx),
HashMap::from([("work-uuid".to_string(), works)])
);
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_session_on_a_login_nobody_enrolled_stays_unenrolled() {
let (ctx, _scratch) = machine("guest");
crate::state::save(&ctx, Permit::for_a_test(), &state()).expect("an account index");
let works = timed((10.0, NOW + 2 * HOUR), (30.0, NOW + 3 * DAY), NOW - 60);
crate::readings::answered(
&ctx,
Permit::for_a_test(),
&[("work-uuid".into(), works.clone())],
);
stores(&at(&ctx, NOW - HOUR), owner("guest"));
let opened = run(&ctx, "pane", &json!({}));
assert_eq!((opened.current, opened.sure), (None, true));
let shown = run(
&ctx,
"pane",
&passed((3.0, NOW + 4 * HOUR), (31.0, NOW + 6 * DAY)),
);
assert_eq!(shown.current, None);
assert!(
shown.sure,
"nothing puts the login Anthropic named in doubt"
);
assert_eq!(shown.session, shares(3.0, 31.0));
assert_eq!(
crate::readings::load(&ctx),
HashMap::from([("work-uuid".to_string(), works)])
);
sign_in(&ctx, "personal");
let moved = run(
&ctx,
"pane",
&passed((4.0, NOW + 4 * HOUR), (32.0, NOW + 6 * DAY)),
);
assert_eq!((moved.current, moved.sure), (None, false));
assert_eq!(moved.session, shares(4.0, 32.0));
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_new_five_hour_window_is_taken_on_the_weekly_windows_word() {
let (ctx, _scratch) = machine("next");
crate::state::save(&ctx, Permit::for_a_test(), &state()).expect("an account index");
crate::readings::answered(
&ctx,
Permit::for_a_test(),
&[
(
"work-uuid".into(),
timed((100.0, NOW - 60), (40.0, NOW + 3 * DAY), NOW - HOUR),
),
(
"personal-uuid".into(),
timed((20.0, NOW + 4 * HOUR), (50.0, NOW + 5 * DAY), NOW - 60),
),
],
);
using(&at(&ctx, NOW - HOUR), "work");
open(&ctx, "seat");
for used in [2.0, 3.0] {
let alone = run(&ctx, "seat", &five_hour_alone(used, NOW + 5 * HOUR));
assert!(!alone.sure);
}
assert_eq!(shares_of(&recorded(&ctx).windows, NOW), shares(0.0, 40.0));
using(&ctx, "personal");
run(
&ctx,
"pane",
&passed((100.0, NOW - 60), (40.0, NOW + 3 * DAY)),
);
let shown = run(
&ctx,
"pane",
&passed((2.0, NOW + 5 * HOUR), (41.0, NOW + 3 * DAY)),
);
assert_eq!(shown.current.as_deref(), Some("work"));
assert_eq!(shown.session, shares(2.0, 41.0));
assert_eq!(shares_of(&recorded(&ctx).windows, NOW), shares(2.0, 41.0));
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn two_accounts_holding_one_window_file_nothing() {
let (ctx, _scratch) = machine("coincident");
crate::state::save(&ctx, Permit::for_a_test(), &state()).expect("an account index");
let works = timed((20.0, NOW + 2 * HOUR), (30.0, NOW + 3 * DAY), NOW - 60);
let personals = timed((50.0, NOW + 2 * HOUR + 30), (60.0, NOW + 5 * DAY), NOW - 60);
crate::readings::answered(
&ctx,
Permit::for_a_test(),
&[
("work-uuid".into(), works.clone()),
("personal-uuid".into(), personals.clone()),
],
);
using(&at(&ctx, NOW - HOUR), "work");
run(&ctx, "pane", &five_hour_alone(55.0, NOW + 2 * HOUR));
let either = run(&ctx, "pane", &five_hour_alone(56.0, NOW + 2 * HOUR));
assert!(!either.sure);
assert_eq!(
crate::readings::load(&ctx),
HashMap::from([
("work-uuid".to_string(), works),
("personal-uuid".to_string(), personals),
])
);
let told = run(
&ctx,
"pane",
&passed((25.0, NOW + 2 * HOUR), (31.0, NOW + 3 * DAY)),
);
assert_eq!(told.current.as_deref(), Some("work"));
assert!(told.sure);
assert_eq!(shares_of(&recorded(&ctx).windows, NOW), shares(25.0, 31.0));
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_session_passing_a_limit_an_account_does_not_have_is_not_on_it() {
let (ctx, _scratch) = machine("seat");
crate::state::save(&ctx, Permit::for_a_test(), &state()).expect("an account index");
let limit = |kind: &str, scope: Option<&str>, percent: f64, resets_at: i64| Window {
kind: kind.into(),
scope: scope.map(str::to_owned),
percent,
resets_at: Some(resets_at),
is_active: false,
severity: None,
length_seconds: None,
};
let seats = Snapshot {
windows: vec![
limit("session", None, 20.0, NOW + 2 * HOUR + 30),
limit("weekly_scoped", Some("Fable"), 10.0, NOW + 4 * DAY),
],
observed_at: Some(NOW - 60),
answered_at: Some(NOW - 60),
lists_every_limit: true,
source: Source::Remembered,
};
crate::readings::answered(
&ctx,
Permit::for_a_test(),
&[("work-uuid".into(), seats.clone())],
);
using(&at(&ctx, NOW - HOUR), "work");
open(&ctx, "max");
for (five_hour, weekly) in [(60.0, 40.0), (61.0, 41.0)] {
let shown = run(
&ctx,
"max",
&passed((five_hour, NOW + 2 * HOUR), (weekly, NOW + 4 * DAY)),
);
assert_eq!(
shown.current, None,
"work has no weekly limit for all models"
);
assert!(!shown.sure);
assert_eq!(shown.session, shares(five_hour, weekly));
}
assert_eq!(recorded(&ctx), seats);
open(&ctx, "seat");
let own = run(&ctx, "seat", &five_hour_alone(21.0, NOW + 2 * HOUR));
assert_eq!(own.current.as_deref(), Some("work"));
assert!(own.sure);
assert_eq!(recorded(&ctx).windows[0].percent, 21.0);
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_reading_an_older_pitboard_wrote_is_matched_against_nothing() {
let (ctx, _scratch) = machine("older");
crate::state::save(&ctx, Permit::for_a_test(), &state()).expect("an account index");
let mut older = timed((10.0, NOW + 2 * HOUR), (30.0, NOW + 3 * DAY), NOW - 60);
older.answered_at = None;
older.lists_every_limit = false;
std::fs::write(
crate::home::dir(&ctx).join("usage.json"),
serde_json::to_string(&HashMap::from([("work-uuid", &older)]))
.expect("readings as JSON"),
)
.expect("readings an older Pitboard wrote");
using(&at(&ctx, NOW - HOUR), "work");
open(&ctx, "pane");
let shown = run(
&ctx,
"pane",
&passed((12.0, NOW + 2 * HOUR), (31.0, NOW + 3 * DAY)),
);
assert_eq!(shown.current.as_deref(), Some("work"));
assert!(!shown.sure);
assert_eq!(shown.session, shares(12.0, 31.0));
assert_eq!(recorded(&ctx), older);
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn the_label_is_marked_while_the_config_has_moved_since_the_last_answer() {
let (ctx, _scratch) = machine("config-moved");
crate::state::save(&ctx, Permit::for_a_test(), &state()).expect("an account index");
using(&ctx, "work");
let settled = run(&ctx, "pane", &json!({}));
assert_eq!(settled.current.as_deref(), Some("work"));
assert!(settled.sure);
sign_in(&ctx, "personal");
let moved = run(&ctx, "pane", &json!({}));
assert_eq!(moved.current.as_deref(), Some("work"));
assert!(!moved.sure);
}
}