use unilang::data::{ ErrorCode, ErrorData };
use super::types::AccountQuota;
use super::format::token_exp_label;
pub(super) fn read_token( credential_store : &std::path::Path, name : &str ) -> Result< String, String >
{
let path = credential_store.join( format!( "{name}.credentials.json" ) );
claude_profile_core::account::read_access_token_from_file( &path )
}
fn inject_synthetic_if_new( results : &mut Vec< AccountQuota >, row : AccountQuota )
{
if !results.iter().any( |r| r.name == row.name )
{
results.insert( 0, row );
}
}
#[ allow( clippy::too_many_lines ) ]
pub( crate ) fn fetch_quota_for_list(
accounts : &[ crate::account::Account ],
credential_store : &std::path::Path,
live_creds_file : &std::path::Path,
stagger : bool,
trace : bool,
solo : bool,
) -> Vec< AccountQuota >
{
let live_token : Option< String > = std::fs::read_to_string( live_creds_file )
.ok()
.and_then( |s| crate::account::parse_string_field( &s, "accessToken" ) );
let occupied_elsewhere = crate::account::other_machines_active( credential_store );
let now_secs = std::time::SystemTime::now().duration_since( std::time::UNIX_EPOCH ).unwrap_or_default().as_secs();
let mut results = Vec::with_capacity( accounts.len() );
for acct in accounts
{
if stagger
{
let nanos = u64::from(
std::time::SystemTime::now()
.duration_since( std::time::UNIX_EPOCH )
.unwrap_or_default()
.subsec_nanos()
);
std::thread::sleep( core::time::Duration::from_millis( 200 + nanos % 1301 ) ); }
let owner = claude_profile_core::account::read_owner( credential_store, &acct.name );
if !claude_profile_core::account::is_owned( &owner )
{
if trace { eprintln!( "[trace] fetch {} skipped (reason: not owned)", acct.name ); }
let ( host, role ) = read_profile_metadata( credential_store, &acct.name );
let renewal_at = read_renewal_at( credential_store, &acct.name );
let ( result, cached, cache_age_secs ) = match read_cached_quota( credential_store, &acct.name, now_secs )
{
Some( ( data, age ) ) => ( Ok( data ), true, Some( age ) ),
None => ( Err( "not owned".to_string() ), false, None ),
};
results.push( AccountQuota
{
name : acct.name.clone(),
is_current : false,
is_active : acct.is_active,
is_occupied_elsewhere : occupied_elsewhere.contains( &acct.name ),
expires_at_ms : acct.expires_at_ms,
result,
account : None,
host,
role,
renewal_at,
cached,
cache_age_secs,
is_owned : false,
owner : owner.clone(),
} );
continue;
}
let is_current = live_token.as_ref().is_some_and( |live|
{
read_token( credential_store, &acct.name )
.is_ok_and( |stored| stored == *live )
} );
if solo && !is_current
{
let aq = approximate_quota( acct, credential_store, is_current, occupied_elsewhere.contains( &acct.name ), now_secs );
let age_hint = aq.cache_age_secs.unwrap_or( 0 );
if trace
{
eprintln!( "[trace] fetch {} solo-skip: approximated (age: {}s)", acct.name, age_hint );
}
results.push( aq );
continue;
}
if !is_current && occupied_elsewhere.contains( &acct.name )
{
if trace { eprintln!( "[trace] fetch {} skipped (reason: occupied elsewhere)", acct.name ); }
let aq = approximate_quota( acct, credential_store, is_current, true, now_secs );
results.push( aq );
continue;
}
let ( result, account ) = if acct.expires_at_ms / 1000 <= now_secs
{
if trace { eprintln!( "[trace] {} token expired (local) — skipping API calls", acct.name ); }
( Err( "token expired (local)".to_string() ), None )
}
else
{
if trace
{
let creds_path = credential_store.join( format!( "{}.credentials.json", acct.name ) );
eprintln!( "[trace] {} reading {}", acct.name, creds_path.display() );
}
match read_token( credential_store, &acct.name )
{
Ok( token ) =>
{
let token_for_account = token.clone();
let account_handle = std::thread::spawn( move ||
{
claude_quota::fetch_oauth_account( &token_for_account )
} );
if trace
{
let prefix = if token.len() >= 20 { &token[ ..20 ] } else { &token };
eprintln!(
"[trace] {} GET {} token={}... exp={}",
acct.name,
claude_quota::OAUTH_USAGE_URL,
prefix,
token_exp_label( acct.expires_at_ms ),
);
}
let r = claude_quota::fetch_oauth_usage( &token ).map_err( |e| e.to_string() );
let account_data = account_handle.join().ok().and_then( core::result::Result::ok );
let r = if account_data.as_ref().is_some_and( |a| a.billing_type == "none" ) && r.is_err() { Err( "no subscription".to_string() ) } else { r };
if trace
{
match &r
{
Ok( _ ) => eprintln!( "[trace] {} result: OK", acct.name ),
Err( e ) => eprintln!( "[trace] {} result: Err({})", acct.name, e ),
}
}
( r, account_data )
}
Err( e ) =>
{
if trace { eprintln!( "[trace] {} cannot read token: {}", acct.name, e ); }
( Err( e ), None )
}
}
};
let ( host, role ) = read_profile_metadata( credential_store, &acct.name );
let renewal_at = read_renewal_at( credential_store, &acct.name );
let ( result, cached, cache_age_secs ) = match result
{
Ok( ref data ) =>
{
let h5 = data.five_hour.as_ref().map( |p| ( p.utilization, p.resets_at.as_deref() ) );
let d7 = data.seven_day.as_ref().map( |p| ( p.utilization, p.resets_at.as_deref() ) );
let sn = data.seven_day_sonnet.as_ref().map( |p| ( p.utilization, p.resets_at.as_deref() ) );
claude_profile_core::account::write_quota_cache( credential_store, &acct.name, h5, d7, sn );
{
let now_secs = std::time::SystemTime::now()
.duration_since( std::time::UNIX_EPOCH )
.map_or( 0, |d| d.as_secs() );
let hh5 = data.five_hour.as_ref().map( |p| ( p.utilization, p.resets_at.as_deref().unwrap_or( "" ) ) );
let hd7 = data.seven_day.as_ref().map( |p| ( p.utilization, p.resets_at.as_deref().unwrap_or( "" ) ) );
let hsn = data.seven_day_sonnet.as_ref().map( |p| ( p.utilization, p.resets_at.as_deref().unwrap_or( "" ) ) );
claude_profile_core::account::write_history_entry( credential_store, &acct.name, now_secs, hh5, hd7, hsn );
}
( result, false, None )
}
Err( ref e ) if !e.contains( "401" ) && !e.contains( "403" ) =>
{
match read_cached_quota( credential_store, &acct.name, now_secs )
{
Some( ( data, age ) ) => ( Ok( data ), true, Some( age ) ),
None => ( result, false, None ),
}
}
Err( _ ) => ( result, false, None ),
};
results.push( AccountQuota
{
name : acct.name.clone(),
is_current,
is_active : acct.is_active,
is_occupied_elsewhere : occupied_elsewhere.contains( &acct.name ),
expires_at_ms : acct.expires_at_ms,
result,
account,
host,
role,
renewal_at,
cached,
cache_age_secs,
is_owned : true,
owner,
} );
}
inject_synthetic_row_if_needed( &mut results, live_token, live_creds_file );
results
}
pub( crate ) fn fetch_all_quota(
credential_store : &std::path::Path,
live_creds_file : &std::path::Path,
stagger : bool,
trace : bool,
solo : bool,
) -> Result< Vec< AccountQuota >, ErrorData >
{
let accounts = crate::account::list( credential_store )
.map_err( |e| ErrorData::new(
ErrorCode::InternalError,
format!( "cannot read credential store: {e}" ),
) )?;
Ok( fetch_quota_for_list( &accounts, credential_store, live_creds_file, stagger, trace, solo ) )
}
fn inject_synthetic_row_if_needed(
results : &mut Vec< AccountQuota >,
live_token : Option< String >,
live_creds_file : &std::path::Path,
)
{
if results.iter().any( |r| r.is_current ) { return; }
let Some( token ) = live_token else { return; };
let synthetic_name = live_creds_file.parent()
.and_then( |p| p.parent() )
.map( |home| home.join( ".claude.json" ) )
.and_then( |p| std::fs::read_to_string( p ).ok() )
.and_then( |s| crate::account::parse_string_field( &s, "emailAddress" ) )
.filter( |e| !e.is_empty() )
.unwrap_or_else( || "(current session)".to_string() );
let expires_at_ms = parse_u64_field( live_creds_file, "expiresAt" ).unwrap_or( 0 );
let result = claude_quota::fetch_oauth_usage( &token ).map_err( |e| e.to_string() );
let account = claude_quota::fetch_oauth_account( &token ).ok();
inject_synthetic_if_new( results, AccountQuota
{
name : synthetic_name,
is_current : true,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms,
result,
account,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
} );
}
fn cache_age_from_fetched_at( fetched_at : &str ) -> u64
{
let now = std::time::SystemTime::now().duration_since( std::time::UNIX_EPOCH ).unwrap_or_default().as_secs();
let then = claude_profile_core::account::parse_iso_utc_secs( fetched_at ).unwrap_or( now );
now.saturating_sub( then )
}
pub( crate ) fn read_cached_quota(
credential_store : &std::path::Path,
name : &str,
now_secs : u64,
) -> Option< ( claude_quota::OauthUsageData, u64 /* cache_age_secs */ ) >
{
let entry = claude_profile_core::account::read_quota_cache( credential_store, name )?;
let age = cache_age_from_fetched_at( &entry.fetched_at );
let mut data = claude_quota::OauthUsageData
{
five_hour : entry.five_hour.map( |( u, r )| claude_quota::PeriodUsage { utilization : u, resets_at : r } ),
seven_day : entry.seven_day.map( |( u, r )| claude_quota::PeriodUsage { utilization : u, resets_at : r } ),
seven_day_sonnet : entry.seven_day_sonnet.map( |( u, r )| claude_quota::PeriodUsage { utilization : u, resets_at : r } ),
};
let history = claude_profile_core::account::read_history( credential_store, name );
if history.len() >= 2
{
if let Some( ref mut fh ) = data.five_hour
{
let pts : Vec< ( u64, f64 ) > = history.iter()
.filter_map( |m| m.h5.as_ref().map( |( u, _ )| ( m.t, *u ) ) )
.collect();
let resets = history.iter().rev()
.find_map( |m| m.h5.as_ref().map( |( _, r )| r.as_str() ) )
.and_then( claude_quota::iso_to_unix_secs );
if let Some( v ) = super::approx::approximate_utilization( &pts, resets, 18_000, now_secs )
{
fh.utilization = v;
}
}
if let Some( ref mut d7 ) = data.seven_day
{
let pts : Vec< ( u64, f64 ) > = history.iter()
.filter_map( |m| m.d7.as_ref().map( |( u, _ )| ( m.t, *u ) ) )
.collect();
let resets = history.iter().rev()
.find_map( |m| m.d7.as_ref().map( |( _, r )| r.as_str() ) )
.and_then( claude_quota::iso_to_unix_secs );
if let Some( v ) = super::approx::approximate_utilization( &pts, resets, 604_800, now_secs )
{
d7.utilization = v;
}
}
if let Some( ref mut sn ) = data.seven_day_sonnet
{
let pts : Vec< ( u64, f64 ) > = history.iter()
.filter_map( |m| m.sn.as_ref().map( |( u, _ )| ( m.t, *u ) ) )
.collect();
let resets = history.iter().rev()
.find_map( |m| m.sn.as_ref().map( |( _, r )| r.as_str() ) )
.and_then( claude_quota::iso_to_unix_secs );
if let Some( v ) = super::approx::approximate_utilization( &pts, resets, 604_800, now_secs )
{
sn.utilization = v;
}
}
}
Some( ( data, age ) )
}
fn approximate_quota(
acct : &crate::account::Account,
credential_store : &std::path::Path,
is_current : bool,
is_occupied_elsewhere : bool,
now_secs : u64,
) -> AccountQuota
{
let ( host, role ) = read_profile_metadata( credential_store, &acct.name );
let renewal_at = read_renewal_at( credential_store, &acct.name );
let owner = claude_profile_core::account::read_owner( credential_store, &acct.name );
let ( result, cached, cache_age_secs ) = match read_cached_quota( credential_store, &acct.name, now_secs )
{
Some( ( data, age ) ) => ( Ok( data ), true, Some( age ) ),
None => ( Err( "no cache".to_string() ), false, None ),
};
AccountQuota
{
name : acct.name.clone(),
is_current,
is_active : acct.is_active,
is_occupied_elsewhere,
expires_at_ms : acct.expires_at_ms,
result,
account : None,
host,
role,
renewal_at,
cached,
cache_age_secs,
is_owned : true,
owner,
}
}
fn read_profile_metadata( credential_store : &std::path::Path, name : &str ) -> ( String, String )
{
let path = credential_store.join( format!( "{name}.json" ) );
let Ok( text ) = std::fs::read_to_string( &path ) else { return ( String::new(), String::new() ) };
let host = crate::account::parse_string_field( &text, "host" ).unwrap_or_default();
let role = crate::account::parse_string_field( &text, "role" ).unwrap_or_default();
( host, role )
}
fn read_renewal_at( credential_store : &std::path::Path, name : &str ) -> Option< String >
{
let path = credential_store.join( format!( "{name}.json" ) );
let text = std::fs::read_to_string( &path ).ok()?;
crate::account::parse_string_field( &text, "_renewal_at" )
}
pub( crate ) fn parse_u64_from_str( s : &str, key : &str ) -> Option< u64 >
{
let needle = format!( "\"{key}\":" );
let start = s.find( &needle )? + needle.len();
let rest = s[ start.. ].trim_start();
let end = rest.find( |c : char| !c.is_ascii_digit() ).unwrap_or( rest.len() );
rest[ ..end ].parse().ok()
}
fn parse_u64_field( path : &std::path::Path, key : &str ) -> Option< u64 >
{
let s = std::fs::read_to_string( path ).ok()?;
parse_u64_from_str( &s, key )
}
#[ cfg( test ) ]
mod tests
{
use super::{ inject_synthetic_if_new, parse_u64_from_str, fetch_quota_for_list, read_cached_quota };
use crate::usage::types::AccountQuota;
use crate::usage::test_support::FAR_FUTURE_MS;
#[ doc = "bug_reproducer(BUG-233)" ]
#[ test ]
fn test_class_b_expired_token_predicate()
{
let now_secs : u64 = 1_748_000_000;
let past_ms : u64 = ( now_secs - 1 ) * 1_000;
assert!(
past_ms / 1_000 <= now_secs,
"Class B: past token (past_ms={past_ms}) must satisfy expired predicate vs now_secs={now_secs}",
);
let future_ms : u64 = ( now_secs + 3_600 ) * 1_000;
assert!(
future_ms / 1_000 > now_secs,
"Class B: future token (future_ms={future_ms}) must NOT satisfy expired predicate vs now_secs={now_secs}",
);
}
#[ doc = "bug_reproducer(BUG-233)" ]
#[ test ]
fn test_class_a_billing_none_override_predicate()
{
let cancelled = Some( claude_quota::OauthAccountData
{
tagged_id : String::new(),
uuid : String::new(),
email_address : String::new(),
full_name : String::new(),
display_name : String::new(),
billing_type : "none".to_string(),
has_max : false,
capabilities : vec![],
rate_limit_tier : String::new(),
org_created_at : "2024-01-01T00:00:00Z".to_string(),
memberships : vec![],
} );
assert!(
cancelled.as_ref().is_some_and( |a| a.billing_type == "none" ),
"Class A: billing_type==\"none\" must trigger result override to Err(\"no subscription\")",
);
let active = Some( claude_quota::OauthAccountData
{
tagged_id : String::new(),
uuid : String::new(),
email_address : String::new(),
full_name : String::new(),
display_name : String::new(),
billing_type : "stripe_subscription".to_string(),
has_max : false,
capabilities : vec![],
rate_limit_tier : String::new(),
org_created_at : "2024-01-01T00:00:00Z".to_string(),
memberships : vec![],
} );
assert!(
!active.as_ref().is_some_and( |a| a.billing_type == "none" ),
"Class A: active subscription must NOT trigger result override",
);
let failed : Option< claude_quota::OauthAccountData > = None;
assert!(
!failed.as_ref().is_some_and( |a| a.billing_type == "none" ),
"Class A: account=None must NOT trigger result override (billing_type unknown)",
);
}
#[ doc = "bug_reproducer(BUG-234)" ]
#[ test ]
fn mre_bug234_result_trace_after_billing_type_override()
{
let src = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/fetch.rs" ) );
let override_pos = src.find( r#"a.billing_type == "none" ) && r.is_err() { Err( "no subscription""# )
.expect( "BUG-234: Class A billing_type override not found in fetch.rs" );
let trace_pos = src.find( r#"eprintln!( "[trace] {} result: OK""# )
.expect( "BUG-234: result: OK trace line not found in fetch.rs" );
assert!(
override_pos < trace_pos,
"BUG-234: result trace emitted before Class A override — \
for billing_type=\"none\" accounts trace shows raw result, not stored result; \
override_pos={override_pos}, trace_pos={trace_pos}",
);
}
#[ doc = "bug_reproducer(BUG-170)" ]
#[ test ]
fn test_parse_u64_from_str_mre_bug170_extracts_expires_at()
{
let flat = r#"{"accessToken":"sk-ant-oat01-XXXX","expiresAt":9999999999999}"#;
assert_eq!(
parse_u64_from_str( flat, "expiresAt" ),
Some( 9_999_999_999_999_u64 ),
"parse_u64_from_str must extract expiresAt from flat credentials JSON",
);
let nested =
r#"{"claudeAiOauth":{"accessToken":"sk-ant-oat01-XXXX","expiresAt":1779487948931}}"#;
assert_eq!(
parse_u64_from_str( nested, "expiresAt" ),
Some( 1_779_487_948_931_u64 ),
"parse_u64_from_str must extract expiresAt from nested claudeAiOauth credentials JSON",
);
let no_key = r#"{"accessToken":"sk-ant-oat01-XXXX"}"#;
assert!(
parse_u64_from_str( no_key, "expiresAt" ).is_none(),
"parse_u64_from_str must return None when expiresAt key is absent",
);
}
#[ doc = "bug_reproducer(BUG-218)" ]
#[ test ]
fn test_mre_bug218_fetch_all_quota_no_duplicate_synthetic_row()
{
let stored_row = AccountQuota
{
name : "i6@wbox.pro".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Err( "missing accessToken".to_string() ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
};
let mut results = vec![ stored_row ];
let synthetic = AccountQuota
{
name : "i6@wbox.pro".to_string(),
is_current : true,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Err( "missing accessToken".to_string() ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
};
inject_synthetic_if_new( &mut results, synthetic );
let i6_count = results.iter().filter( |r| r.name == "i6@wbox.pro" ).count();
assert_eq!(
i6_count, 1,
"BUG-218: inject_synthetic creates duplicate — missing collision guard; count={i6_count}",
);
assert_eq!(
results.len(), 1,
"BUG-218: quota table must have exactly 1 row for 1 stored account; len={}",
results.len(),
);
}
#[ doc = "bug_reproducer(BUG-296)" ]
#[ test ]
fn mre_bug296_cached_non_expired_401_no_refresh()
{
let src = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/fetch.rs" ) );
let guard_pos = src.find( r#"!e.contains( "401" ) && !e.contains( "403" )"# )
.expect(
"BUG-296: auth-error guard not found in fetch.rs — \
401/403 errors must bypass cache fallback via match guard",
);
assert!(
src[ guard_pos.. ].contains( "read_cached_quota( credential_store" ),
"BUG-296: read_cached_quota call not found after auth-error guard in fetch.rs — \
guard must precede read_cached_quota in the cache fallback arm",
);
assert!(
src.contains( "Err( _ ) => ( result, false, None )" ),
"BUG-296: catch-all Err arm missing in cache fallback match — \
auth errors must propagate as Err with cached=false",
);
}
#[ doc = "bug_reproducer(BUG-236)" ]
#[ test ]
fn mre_bug236_ok_result_not_overridden_when_billing_type_none()
{
let account_data = Some( claude_quota::OauthAccountData
{
tagged_id : String::new(),
uuid : String::new(),
email_address : String::new(),
full_name : String::new(),
display_name : String::new(),
billing_type : "none".to_string(),
has_max : false,
capabilities : vec![],
rate_limit_tier : String::new(),
org_created_at : "2024-01-01T00:00:00Z".to_string(),
memberships : vec![],
} );
let r : Result< claude_quota::OauthUsageData, String > = Ok( claude_quota::OauthUsageData
{
five_hour : None,
seven_day : None,
seven_day_sonnet : None,
} );
let would_override = account_data.as_ref().is_some_and( |a| a.billing_type == "none" ) && r.is_err();
assert!(
!would_override,
"BUG-236: billing_type=\"none\" + r=Ok must NOT trigger override — active non-stripe \
accounts have billing_type=\"none\" but a valid usage response",
);
}
#[ doc = "bug_reproducer(BUG-236)" ]
#[ test ]
fn mre_bug236_err_result_overridden_when_billing_type_none()
{
let account_data = Some( claude_quota::OauthAccountData
{
tagged_id : String::new(),
uuid : String::new(),
email_address : String::new(),
full_name : String::new(),
display_name : String::new(),
billing_type : "none".to_string(),
has_max : false,
capabilities : vec![],
rate_limit_tier : String::new(),
org_created_at : "2024-01-01T00:00:00Z".to_string(),
memberships : vec![],
} );
let r : Result< claude_quota::OauthUsageData, String > = Err( "HTTP 429".to_string() );
let would_override = account_data.as_ref().is_some_and( |a| a.billing_type == "none" ) && r.is_err();
assert!(
would_override,
"BUG-236: billing_type=\"none\" + r=Err must trigger override — cancelled account \
signals agree (billing=no-sub AND usage=err)",
);
}
#[ test ]
fn ft04_non_owned_uses_cache_not_http()
{
let store = tempfile::TempDir::new().unwrap();
let meta = serde_json::json!(
{
"owner" : "other@remote",
"cache" :
{
"fetched_at" : "2026-06-14T10:00:00Z",
"status" : "ok",
"five_hour" : { "left_pct" : 70.0 }
}
} );
std::fs::write(
store.path().join( "alice@test.com.json" ),
serde_json::to_string_pretty( &meta ).map( | s | s + "\n" ).unwrap(),
).unwrap();
std::fs::write(
store.path().join( "alice@test.com.credentials.json" ),
r#"{"accessToken":"tok","expiresAt":9999999999999}"#,
).unwrap();
let accounts = vec![ crate::account::Account
{
name : "alice@test.com".to_string(),
subscription_type : "pro".to_string(),
rate_limit_tier : String::new(),
expires_at_ms : u64::MAX / 2,
is_active : false,
email : String::new(),
display_name : String::new(),
role : String::new(),
billing : String::new(),
model : String::new(),
tagged_id : String::new(),
uuid : String::new(),
capabilities : Vec::new(),
organization_uuid : String::new(),
organization_name : String::new(),
organization_role : String::new(),
workspace_uuid : String::new(),
workspace_name : String::new(),
profile_host : String::new(),
profile_role : String::new(),
} ];
let absent_live = store.path().join( ".absent_credentials.json" );
let results = fetch_quota_for_list( &accounts, store.path(), &absent_live, false, false, false );
assert_eq!( results.len(), 1, "FT-04: must return exactly 1 AccountQuota for 1 account" );
let aq = &results[ 0 ];
assert!(
!aq.is_owned,
"FT-04: G1 gate must set is_owned=false for non-owned account; got: {:?}", aq.is_owned,
);
assert!(
aq.cached,
"FT-04: G1 gate must read cache (cached=true) for non-owned account; got: {:?}", aq.cached,
);
assert!(
aq.result.is_ok(),
"FT-04: G1 gate must return Ok(cache_data) when cache present; got: {:?}", aq.result,
);
}
#[ test ]
fn cc7_non_owned_no_cache()
{
let store = tempfile::TempDir::new().unwrap();
let meta = serde_json::json!( { "owner" : "other@remote" } );
std::fs::write(
store.path().join( "alice@test.com.json" ),
serde_json::to_string_pretty( &meta ).map( | s | s + "\n" ).unwrap(),
).unwrap();
std::fs::write(
store.path().join( "alice@test.com.credentials.json" ),
r#"{"accessToken":"tok","expiresAt":9999999999999}"#,
).unwrap();
let accounts = vec![ crate::account::Account
{
name : "alice@test.com".to_string(),
subscription_type : "pro".to_string(),
rate_limit_tier : String::new(),
expires_at_ms : u64::MAX / 2,
is_active : false,
email : String::new(),
display_name : String::new(),
role : String::new(),
billing : String::new(),
model : String::new(),
tagged_id : String::new(),
uuid : String::new(),
capabilities : Vec::new(),
organization_uuid : String::new(),
organization_name : String::new(),
organization_role : String::new(),
workspace_uuid : String::new(),
workspace_name : String::new(),
profile_host : String::new(),
profile_role : String::new(),
} ];
let absent_live = store.path().join( ".absent_credentials.json" );
let results = fetch_quota_for_list( &accounts, store.path(), &absent_live, false, false, false );
assert_eq!( results.len(), 1, "CC-7: must return exactly 1 AccountQuota" );
let aq = &results[ 0 ];
assert!( !aq.is_owned, "CC-7: G1 gate must set is_owned=false" );
assert!( !aq.cached, "CC-7: no cache → cached must be false" );
assert!(
aq.result.is_err(),
"CC-7: no cache → result must be Err; got: {:?}", aq.result,
);
let err = aq.result.as_ref().unwrap_err();
assert!(
err.contains( "not owned" ),
"CC-7: error message must contain 'not owned'; got: {err}",
);
}
#[ test ]
fn ft03_history_skips_cached_fallback()
{
let store = tempfile::TempDir::new().unwrap();
let meta = serde_json::json!(
{
"cache" :
{
"fetched_at" : "2026-06-01T10:00:00Z",
"status" : "ok",
"five_hour" : { "left_pct" : 60.0 },
"history" :
[
{ "t" : 1_748_000_000_u64, "h5" : [ 60.0, "" ], "d7" : null, "sn" : null }
]
}
} );
std::fs::write(
store.path().join( "alice@test.com.json" ),
serde_json::to_string_pretty( &meta ).unwrap() + "\n",
).unwrap();
std::fs::write(
store.path().join( "alice@test.com.credentials.json" ),
r#"{"accessToken":"tok","expiresAt":1}"#,
).unwrap();
let accounts = vec![ crate::account::Account
{
name : "alice@test.com".to_string(),
subscription_type : "pro".to_string(),
rate_limit_tier : String::new(),
expires_at_ms : 1,
is_active : false,
email : String::new(),
display_name : String::new(),
role : String::new(),
billing : String::new(),
model : String::new(),
tagged_id : String::new(),
uuid : String::new(),
capabilities : Vec::new(),
organization_uuid : String::new(),
organization_name : String::new(),
organization_role : String::new(),
workspace_uuid : String::new(),
workspace_name : String::new(),
profile_host : String::new(),
profile_role : String::new(),
} ];
let absent_live = store.path().join( ".absent_credentials.json" );
let results = fetch_quota_for_list( &accounts, store.path(), &absent_live, false, false, false );
assert_eq!( results.len(), 1 );
assert!( results[ 0 ].cached, "FT-03: result must be cached" );
let text = std::fs::read_to_string( store.path().join( "alice@test.com.json" ) ).unwrap();
let json : serde_json::Value = serde_json::from_str( &text ).unwrap();
let history_len = json[ "cache" ][ "history" ].as_array().map_or( 0, Vec::len );
assert_eq!(
history_len, 1,
"FT-03: cache-fallback must not append new history entry; got {history_len} entries",
);
}
#[ test ]
fn ft05_approx_independent_periods_absent_sn_unaffected()
{
let store = tempfile::TempDir::new().unwrap();
let meta = serde_json::json!(
{
"cache" :
{
"fetched_at" : "2026-06-01T10:00:00Z",
"status" : "ok",
"five_hour" : { "left_pct" : 50.0 },
"seven_day_sonnet" : { "left_pct" : 50.0 },
"history" :
[
{ "t" : 1_748_000_000_u64, "h5" : [ 70.0, "" ], "d7" : null, "sn" : null },
{ "t" : 1_748_003_600_u64, "h5" : [ 70.0, "" ], "d7" : null, "sn" : null }
]
}
} );
std::fs::write(
store.path().join( "alice@test.com.json" ),
serde_json::to_string_pretty( &meta ).unwrap() + "\n",
).unwrap();
std::fs::write(
store.path().join( "alice@test.com.credentials.json" ),
r#"{"accessToken":"tok","expiresAt":1}"#,
).unwrap();
let accounts = vec![ crate::account::Account
{
name : "alice@test.com".to_string(),
subscription_type : "pro".to_string(),
rate_limit_tier : String::new(),
expires_at_ms : 1,
is_active : false,
email : String::new(),
display_name : String::new(),
role : String::new(),
billing : String::new(),
model : String::new(),
tagged_id : String::new(),
uuid : String::new(),
capabilities : Vec::new(),
organization_uuid : String::new(),
organization_name : String::new(),
organization_role : String::new(),
workspace_uuid : String::new(),
workspace_name : String::new(),
profile_host : String::new(),
profile_role : String::new(),
} ];
let absent_live = store.path().join( ".absent_credentials.json" );
let results = fetch_quota_for_list( &accounts, store.path(), &absent_live, false, false, false );
assert_eq!( results.len(), 1 );
let aq = &results[ 0 ];
assert!( aq.cached, "FT-05: must be cached result" );
let data = aq.result.as_ref().expect( "FT-05: result must be Ok(cached)" );
let h5 = data.five_hour.as_ref().expect( "FT-05: five_hour must be Some" );
let sn = data.seven_day_sonnet.as_ref().expect( "FT-05: seven_day_sonnet must be Some" );
assert!(
( h5.utilization - 70.0 ).abs() < 1e-6,
"FT-05: h5.utilization must be 70.0 (approximated from 2 identical history points); got: {}", h5.utilization,
);
assert!(
( sn.utilization - 50.0 ).abs() < 1e-6,
"FT-05: sn.utilization must remain 50.0 (no sn history; absent period unaffected); got: {}", sn.utilization,
);
}
#[ test ]
fn test_read_cached_quota_absent_returns_none()
{
let store = tempfile::TempDir::new().unwrap();
let now_secs = 1_750_000_000_u64;
let result = read_cached_quota( store.path(), "nonexistent@test.com", now_secs );
assert!( result.is_none(), "FT-14: absent cache must return None" );
}
#[ test ]
fn test_read_cached_quota_no_history_returns_raw()
{
let store = tempfile::TempDir::new().unwrap();
let now_secs = 1_750_000_000_u64;
let meta = serde_json::json!(
{
"cache" :
{
"fetched_at" : "2026-06-21T10:00:00Z",
"status" : "ok",
"five_hour" : { "left_pct" : 42.0 }
}
} );
std::fs::write(
store.path().join( "alice@test.com.json" ),
serde_json::to_string_pretty( &meta ).unwrap() + "\n",
).unwrap();
let ( data, _ ) = read_cached_quota( store.path(), "alice@test.com", now_secs )
.expect( "FT-15: cache present → must return Some" );
let h5 = data.five_hour.expect( "FT-15: five_hour must be Some" );
assert!(
( h5.utilization - 42.0 ).abs() < 1e-6,
"FT-15: no history → raw cache value 42.0 unchanged; got {}", h5.utilization,
);
}
#[ test ]
fn test_read_cached_quota_one_history_returns_raw()
{
let store = tempfile::TempDir::new().unwrap();
let now_secs = 1_750_000_000_u64;
let meta = serde_json::json!(
{
"cache" :
{
"fetched_at" : "2026-06-21T10:00:00Z",
"status" : "ok",
"five_hour" : { "left_pct" : 42.0 },
"history" :
[
{ "t" : 1_749_990_000_u64, "h5" : [ 42.0, "" ], "d7" : null, "sn" : null }
]
}
} );
std::fs::write(
store.path().join( "alice@test.com.json" ),
serde_json::to_string_pretty( &meta ).unwrap() + "\n",
).unwrap();
let ( data, _ ) = read_cached_quota( store.path(), "alice@test.com", now_secs )
.expect( "FT-16: cache present → must return Some" );
let h5 = data.five_hour.expect( "FT-16: five_hour must be Some" );
assert!(
( h5.utilization - 42.0 ).abs() < 1e-6,
"FT-16: 1 history entry (< 2) → raw cache 42.0 unchanged; got {}", h5.utilization,
);
}
#[ test ]
fn test_read_cached_quota_applies_approximation()
{
let store = tempfile::TempDir::new().unwrap();
let t0 = 1_749_990_000_u64;
let t1 = t0 + 3_600;
let t2 = t0 + 7_200;
let now_secs = t2 + 3_600; let meta = serde_json::json!(
{
"cache" :
{
"fetched_at" : "2025-06-15T12:20:00Z",
"status" : "ok",
"five_hour" : { "left_pct" : 42.0 },
"history" :
[
{ "t" : t0, "h5" : [ 10.0, "" ], "d7" : null, "sn" : null },
{ "t" : t1, "h5" : [ 25.0, "" ], "d7" : null, "sn" : null },
{ "t" : t2, "h5" : [ 40.0, "" ], "d7" : null, "sn" : null }
]
}
} );
std::fs::write(
store.path().join( "alice@test.com.json" ),
serde_json::to_string_pretty( &meta ).unwrap() + "\n",
).unwrap();
let ( data, _ ) = read_cached_quota( store.path(), "alice@test.com", now_secs )
.expect( "FT-17: cache present → must return Some" );
let h5 = data.five_hour.expect( "FT-17: five_hour must be Some" );
assert!(
( h5.utilization - 55.0 ).abs() < 5.0,
"FT-17: 3 history entries → approximation ~55.0 (not raw 42.0); got {}", h5.utilization,
);
}
#[ test ]
fn test_read_cached_quota_expired_window_returns_zero()
{
let store = tempfile::TempDir::new().unwrap();
let resets_at = "2025-06-01T09:00:00+00:00"; let meta = serde_json::json!(
{
"cache" :
{
"fetched_at" : "2026-06-01T08:30:00Z",
"status" : "ok",
"five_hour" : { "left_pct" : 42.0, "resets_at" : resets_at },
"history" :
[
{ "t" : 1_748_760_000_u64, "h5" : [ 35.0, resets_at ], "d7" : null, "sn" : null },
{ "t" : 1_748_763_600_u64, "h5" : [ 42.0, resets_at ], "d7" : null, "sn" : null }
]
}
} );
std::fs::write(
store.path().join( "alice@test.com.json" ),
serde_json::to_string_pretty( &meta ).unwrap() + "\n",
).unwrap();
let now_secs = 1_748_900_000_u64; let ( data, _ ) = read_cached_quota( store.path(), "alice@test.com", now_secs )
.expect( "FT-18: cache present → must return Some" );
let h5 = data.five_hour.expect( "FT-18: five_hour must be Some" );
assert!(
h5.utilization.abs() < 1e-6,
"FT-18: resets_at elapsed → approximated utilization must be 0.0; got {}", h5.utilization,
);
}
#[ test ]
fn cc08_read_cached_quota_two_history_entries_applies_linear()
{
let store = tempfile::TempDir::new().unwrap();
let t0 = 1_750_000_000_u64;
let t1 = t0 + 3_600;
let now_secs = t1 + 3_600;
let meta = serde_json::json!(
{
"cache" :
{
"fetched_at" : "2026-06-15T00:00:00Z",
"status" : "ok",
"five_hour" : { "left_pct" : 42.0 },
"history" :
[
{ "t" : t0, "h5" : [ 30.0, "" ], "d7" : null, "sn" : null },
{ "t" : t1, "h5" : [ 50.0, "" ], "d7" : null, "sn" : null }
]
}
} );
std::fs::write(
store.path().join( "alice@test.com.json" ),
serde_json::to_string_pretty( &meta ).unwrap() + "\n",
).unwrap();
let ( data, _ ) = read_cached_quota( store.path(), "alice@test.com", now_secs )
.expect( "CC-08: cache present → must return Some" );
let h5 = data.five_hour.expect( "CC-08: five_hour must be Some" );
assert!(
( h5.utilization - 42.0 ).abs() > 1e-6,
"CC-08: 2 history entries must apply linear approx, not return raw 42.0; got {}", h5.utilization,
);
assert!(
( h5.utilization - 70.0 ).abs() < 5.0,
"CC-08: linear extrapolation at +1h after t1 must be ≈70.0; got {}", h5.utilization,
);
}
#[ test ]
fn ft23_g1_non_owned_applies_approximation()
{
let store = tempfile::TempDir::new().unwrap();
let meta = serde_json::json!(
{
"owner" : "other@remote",
"cache" :
{
"fetched_at" : "2026-06-11T10:00:00Z",
"status" : "ok",
"five_hour" : { "left_pct" : 42.0, "resets_at" : "2026-06-11T15:00:00+00:00" },
"history" :
[
{ "t" : 1_749_990_000_u64, "h5" : [ 10.0, "" ], "d7" : null, "sn" : null },
{ "t" : 1_749_993_600_u64, "h5" : [ 25.0, "" ], "d7" : null, "sn" : null },
{ "t" : 1_749_997_200_u64, "h5" : [ 40.0, "" ], "d7" : null, "sn" : null }
]
}
} );
std::fs::write(
store.path().join( "alice@test.com.json" ),
serde_json::to_string_pretty( &meta ).unwrap() + "\n",
).unwrap();
std::fs::write(
store.path().join( "alice@test.com.credentials.json" ),
r#"{"accessToken":"tok","expiresAt":9999999999999}"#,
).unwrap();
let accounts = vec![ crate::account::Account
{
name : "alice@test.com".to_string(),
subscription_type : "pro".to_string(),
rate_limit_tier : String::new(),
expires_at_ms : u64::MAX / 2,
is_active : false,
email : String::new(),
display_name : String::new(),
role : String::new(),
billing : String::new(),
model : String::new(),
tagged_id : String::new(),
uuid : String::new(),
capabilities : Vec::new(),
organization_uuid : String::new(),
organization_name : String::new(),
organization_role : String::new(),
workspace_uuid : String::new(),
workspace_name : String::new(),
profile_host : String::new(),
profile_role : String::new(),
} ];
let absent_live = store.path().join( ".absent_credentials.json" );
let results = fetch_quota_for_list( &accounts, store.path(), &absent_live, false, false, false );
assert_eq!( results.len(), 1 );
let aq = &results[ 0 ];
assert!( !aq.is_owned, "FT-23: G1 gate must set is_owned=false" );
assert!( aq.cached, "FT-23: G1 must return cached=true when cache exists" );
let data = aq.result.as_ref().expect( "FT-23: result must be Ok (cache exists)" );
let h5 = data.five_hour.as_ref().expect( "FT-23: five_hour must be Some" );
assert!(
( h5.utilization - 42.0 ).abs() > 1e-6,
"FT-23 (BUG-304): G1 non-owned path must apply approximation; \
raw cache 42.0 was returned — read_cached_quota() not called",
);
}
#[ test ]
fn ft12_history_non_owned_skips_append()
{
let store = tempfile::TempDir::new().unwrap();
let meta = serde_json::json!(
{
"owner" : "other@remote",
"cache" :
{
"fetched_at" : "2026-06-01T10:00:00Z",
"status" : "ok",
"five_hour" : { "left_pct" : 60.0 }
}
} );
std::fs::write(
store.path().join( "alice@test.com.json" ),
serde_json::to_string_pretty( &meta ).unwrap() + "\n",
).unwrap();
std::fs::write(
store.path().join( "alice@test.com.credentials.json" ),
r#"{"accessToken":"tok","expiresAt":9999999999999}"#,
).unwrap();
let accounts = vec![ crate::account::Account
{
name : "alice@test.com".to_string(),
subscription_type : "pro".to_string(),
rate_limit_tier : String::new(),
expires_at_ms : u64::MAX / 2,
is_active : false,
email : String::new(),
display_name : String::new(),
role : String::new(),
billing : String::new(),
model : String::new(),
tagged_id : String::new(),
uuid : String::new(),
capabilities : Vec::new(),
organization_uuid : String::new(),
organization_name : String::new(),
organization_role : String::new(),
workspace_uuid : String::new(),
workspace_name : String::new(),
profile_host : String::new(),
profile_role : String::new(),
} ];
let absent_live = store.path().join( ".absent_credentials.json" );
let results = fetch_quota_for_list( &accounts, store.path(), &absent_live, false, false, false );
assert_eq!( results.len(), 1 );
assert!( !results[ 0 ].is_owned, "FT-12: G1 gate must set is_owned=false" );
let text = std::fs::read_to_string( store.path().join( "alice@test.com.json" ) ).unwrap();
let json : serde_json::Value = serde_json::from_str( &text ).unwrap();
let has_history = json[ "cache" ].as_object()
.is_some_and( |c| c.contains_key( "history" ) );
assert!(
!has_history,
"FT-12: G1-gated non-owned account must not have 'history' written to account json",
);
}
}