use super::{ pre_switch_touch_ctx, apply_model_override, apply_post_switch_touch, PreSwitchOutcome, TouchCtx };
use tempfile::TempDir;
#[ doc = "bug_reproducer(BUG-210)" ]
#[ test ]
fn test_pre_switch_touch_ctx_model_effort_absent_on_fetch_failure()
{
let store = TempDir::new().unwrap();
std::fs::write(
store.path().join( "noaccess@example.com.credentials.json" ),
r#"{"subscriptionType":"pro"}"#,
).unwrap();
let result = pre_switch_touch_ctx(
"noaccess@example.com",
store.path(),
true,
"auto",
"auto",
);
assert!(
matches!( result, PreSwitchOutcome::Unavailable ),
"fetch-failed path must return Unavailable, got {result:?}",
);
}
#[ doc = "bug_reproducer(BUG-238)" ]
#[ test ]
fn mre_bug238_model_override_fires_for_active_account()
{
use claude_quota::{ OauthUsageData, PeriodUsage };
let dir = TempDir::new().unwrap();
let paths = crate::ClaudePaths::with_home( dir.path() );
std::fs::create_dir_all( paths.base() ).unwrap();
let quota = OauthUsageData
{
five_hour : None,
seven_day : None,
seven_day_sonnet : Some( PeriodUsage { utilization : 90.0, resets_at : None } ),
};
apply_model_override( "a, &paths, false, "account.use", "test-account" );
let content = std::fs::read_to_string( paths.settings_file() ).unwrap();
assert!(
content.contains( "\"opus\"" ) && !content.contains( "claude-opus-4-6" ),
"apply_model_override must write opus shorthand to settings.json when 7d(Son) is 90% consumed (10% left), got: {content}",
);
}
#[ doc = "bug_reproducer(BUG-286)" ]
#[ test ]
fn mre_bug286_full_opus_id_normalized_to_shorthand()
{
use claude_quota::{ OauthUsageData, PeriodUsage };
let dir = TempDir::new().unwrap();
let paths = crate::ClaudePaths::with_home( dir.path() );
std::fs::create_dir_all( paths.base() ).unwrap();
std::fs::write( paths.settings_file(), r#"{"model":"claude-opus-4-6"}"# ).unwrap();
let quota = OauthUsageData
{
five_hour : None,
seven_day : None,
seven_day_sonnet : Some( PeriodUsage { utilization : 90.0, resets_at : None } ),
};
apply_model_override( "a, &paths, false, "account.use", "test-account" );
let content = std::fs::read_to_string( paths.settings_file() ).unwrap();
assert!(
content.contains( "\"opus\"" ) && !content.contains( "claude-opus-4-6" ),
"BUG-286: full-ID opus must be normalized to shorthand 'opus', got: {content}",
);
}
#[ doc = "bug_reproducer(BUG-300)" ]
#[ test ]
fn mre_bug300_model_override_absent_sonnet_no_override()
{
use claude_quota::OauthUsageData;
let dir = TempDir::new().unwrap();
let paths = crate::ClaudePaths::with_home( dir.path() );
std::fs::create_dir_all( paths.base() ).unwrap();
let quota = OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None };
apply_model_override( "a, &paths, false, "usage", "test-account" );
assert!(
!paths.settings_file().exists(),
"BUG-300: settings.json must NOT be written when seven_day_sonnet is None \
(absent tier is unknown, not exhausted)",
);
}
#[ test ]
fn t01_model_override_fires_when_sonnet_below_threshold()
{
use claude_quota::{ OauthUsageData, PeriodUsage };
let dir = TempDir::new().unwrap();
let paths = crate::ClaudePaths::with_home( dir.path() );
std::fs::create_dir_all( paths.base() ).unwrap();
let quota = OauthUsageData
{
five_hour : None,
seven_day : None,
seven_day_sonnet : Some( PeriodUsage { utilization : 90.0, resets_at : None } ),
};
apply_model_override( "a, &paths, false, "usage", "test-account" );
let content = std::fs::read_to_string( paths.settings_file() ).unwrap();
assert!(
content.contains( "\"opus\"" ) && !content.contains( "claude-opus-4-6" ),
"must write opus shorthand when 7d(Son) utilization=90% (10% left), got: {content}",
);
}
#[ test ]
fn t02_model_override_skips_when_sonnet_above_threshold()
{
use claude_quota::{ OauthUsageData, PeriodUsage };
let dir = TempDir::new().unwrap();
let paths = crate::ClaudePaths::with_home( dir.path() );
std::fs::create_dir_all( paths.base() ).unwrap();
let quota = OauthUsageData
{
five_hour : None,
seven_day : None,
seven_day_sonnet : Some( PeriodUsage { utilization : 70.0, resets_at : None } ),
};
apply_model_override( "a, &paths, false, "usage", "test-account" );
assert!(
!paths.settings_file().exists(),
"must NOT write settings.json when 7d(Son) utilization=70% (30% left)",
);
}
#[ test ]
fn t03_model_override_skips_when_already_opus()
{
use claude_quota::{ OauthUsageData, PeriodUsage };
let dir = TempDir::new().unwrap();
let paths = crate::ClaudePaths::with_home( dir.path() );
std::fs::create_dir_all( paths.base() ).unwrap();
std::fs::write( paths.settings_file(), r#"{"model":"opus"}"# ).unwrap();
let quota = OauthUsageData
{
five_hour : None,
seven_day : None,
seven_day_sonnet : Some( PeriodUsage { utilization : 90.0, resets_at : None } ),
};
apply_model_override( "a, &paths, false, "usage", "test-account" );
let content = std::fs::read_to_string( paths.settings_file() ).unwrap();
assert!(
content.contains( "\"opus\"" ),
"settings.json must still contain opus after call when already opus, got: {content}",
);
assert!(
!content.contains( "sonnet" ),
"must not downgrade to sonnet, got: {content}",
);
}
#[ test ]
fn t04_model_override_skips_on_error_result()
{
let src = include_str!( "api.rs" );
let fn_start = src.find( "pub fn usage_routine" ).expect( "usage_routine not found" );
let call_rel = src[ fn_start.. ]
.find( "apply_model_override(" )
.expect( "BUG-244: apply_model_override must be called in usage_routine" );
let before_call = &src[ fn_start .. fn_start + call_rel ];
assert!(
before_call.contains( "is_ok()" ) || before_call.contains( "Ok( ref " ),
"apply_model_override call in usage_routine must be guarded by result.is_ok()",
);
}
#[ test ]
fn t05_apply_model_override_absent_from_usage_routine_before_fix()
{
let src = include_str!( "api.rs" );
let fn_start = src.find( "pub fn usage_routine" ).expect( "usage_routine not found" );
let body_end = {
let after = &src[ fn_start + 1.. ];
let a = after.find( "\npub fn " ).unwrap_or( after.len() );
let b = after.find( "\n#[ cfg( t" ).unwrap_or( after.len() );
let c = after.find( "\n#[cfg(t" ).unwrap_or( after.len() );
fn_start + 1 + a.min( b ).min( c )
};
let body = &src[ fn_start..body_end ];
let call_count = body.matches( "apply_model_override(" ).count();
assert_eq!(
call_count, 2,
"apply_model_override must be called exactly twice in usage_routine body: \
once for current-account (BUG-244) and once for rotation-winner (Feature 062 AC-05)",
);
}
#[ test ]
fn t06_model_override_skips_for_non_current_account()
{
let src = include_str!( "api.rs" );
let fn_start = src.find( "pub fn usage_routine" ).expect( "usage_routine not found" );
let call_rel = src[ fn_start.. ]
.find( "apply_model_override(" )
.expect( "BUG-244: apply_model_override must be called in usage_routine" );
let before_call = &src[ fn_start .. fn_start + call_rel ];
assert!(
before_call.contains( "is_current" ),
"apply_model_override call in usage_routine must be guarded by is_current check",
);
}
#[ test ]
fn t07_model_override_skips_at_and_above_15pct_boundary()
{
use claude_quota::{ OauthUsageData, PeriodUsage };
let dir = TempDir::new().unwrap();
let paths = crate::ClaudePaths::with_home( dir.path() );
std::fs::create_dir_all( paths.base() ).unwrap();
let quota = OauthUsageData
{
five_hour : None,
seven_day : None,
seven_day_sonnet : Some( PeriodUsage { utilization : 85.0, resets_at : None } ),
};
apply_model_override( "a, &paths, false, "usage", "test-account" );
assert!(
!paths.settings_file().exists(),
"must NOT write at exact 15% boundary (utilization=85.0)",
);
}
#[ test ]
fn t08_model_override_trace_label_is_usage()
{
use claude_quota::{ OauthUsageData, PeriodUsage };
use std::io::Read;
let dir = TempDir::new().unwrap();
let paths = crate::ClaudePaths::with_home( dir.path() );
std::fs::create_dir_all( paths.base() ).unwrap();
let quota = OauthUsageData
{
five_hour : None,
seven_day : None,
seven_day_sonnet : Some( PeriodUsage { utilization : 90.0, resets_at : None } ),
};
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut buf = gag::BufferRedirect::stderr().unwrap();
apply_model_override( "a, &paths, true, "usage", "test-account" );
let mut output = String::new();
buf.read_to_string( &mut output ).unwrap();
assert!(
output.contains( "[trace] usage" ),
"trace output must contain '[trace] usage' when label='usage', got: {output}",
);
assert!(
!output.contains( "[trace] account.use" ),
"trace output must NOT contain '[trace] account.use' when label='usage', got: {output}",
);
}
#[ doc = "bug_reproducer(BUG-245)" ]
#[ doc = "bug_reproducer(BUG-246)" ]
#[ test ]
fn mre_bug245_only_active_retain_fires_before_http_fetch()
{
let src = include_str!( "api.rs" );
let fn_start = src.find( "pub fn usage_routine" ).expect( "usage_routine not found" );
let body_end = {
let after = &src[ fn_start + 1.. ];
let a = after.find( "\npub fn " ).unwrap_or( after.len() );
let b = after.find( "\n#[ cfg( t" ).unwrap_or( after.len() );
let c = after.find( "\n#[cfg(t" ).unwrap_or( after.len() );
fn_start + 1 + a.min( b ).min( c )
};
let body = &src[ fn_start..body_end ];
let retain_pos = body.find( "retain( |aq| aq.is_active )" )
.expect( "BUG-245: retain( |aq| aq.is_active ) must exist in usage_routine body" );
let fetch_pos = body.find( "fetch_quota_for_list(" )
.expect( "BUG-245: fetch_quota_for_list call must exist in usage_routine body" );
assert!(
retain_pos < fetch_pos,
"BUG-245/246: retain must fire BEFORE fetch_quota_for_list — \
retain at ~{retain_pos}, fetch at ~{fetch_pos}",
);
}
#[ doc = "bug_reproducer(BUG-244)" ]
#[ test ]
fn mre_bug244_usage_routine_never_calls_apply_model_override()
{
let src = include_str!( "api.rs" );
let fn_start = src.find( "pub fn usage_routine" ).expect( "usage_routine not found" );
let body_end = {
let after = &src[ fn_start + 1.. ];
let a = after.find( "\npub fn " ).unwrap_or( after.len() );
let b = after.find( "\n#[ cfg( t" ).unwrap_or( after.len() );
let c = after.find( "\n#[cfg(t" ).unwrap_or( after.len() );
fn_start + 1 + a.min( b ).min( c )
};
let body = &src[ fn_start..body_end ];
assert!(
body.contains( "apply_model_override(" ),
"BUG-244: apply_model_override must be called from usage_routine — was absent before fix",
);
}
#[ doc = "bug_reproducer(BUG-285)" ]
#[ test ]
fn mre_bug285_idle_check_uses_resets_at_as_wrong_oracle()
{
let src = include_str!( "api.rs" );
let fn_start = src
.find( "pub( crate ) fn pre_switch_touch_ctx(" )
.expect( "pre_switch_touch_ctx not found in api.rs" );
let fn_end = src[ fn_start + 1.. ]
.find( "\npub( crate ) fn " )
.map_or( src.len(), |rel| fn_start + 1 + rel );
let fn_body = &src[ fn_start..fn_end ];
assert!(
!fn_body.contains( "let is_idle" ),
"BUG-285 regression: `let is_idle` variable must not exist in pre_switch_touch_ctx body\n\
resets_at is server-side state and cannot proxy local subprocess identity",
);
assert!(
!fn_body.contains( "AlreadyActive" ),
"BUG-285 regression: AlreadyActive must not be returned from pre_switch_touch_ctx",
);
}
#[ doc = "bug_reproducer(BUG-288)" ]
#[ test ]
fn mre_bug288_post_switch_touch_refetch_updates_quota()
{
use claude_quota::OauthUsageData;
{
let src = include_str!( "api.rs" );
let fn_start = src
.find( "pub( crate ) fn apply_post_switch_touch(" )
.expect( "apply_post_switch_touch not found in api.rs" );
let fn_end = src[ fn_start + 1.. ]
.find( "\n// ── Main routine" )
.map( |rel| fn_start + 1 + rel )
.expect( "anchor '// ── Main routine' must follow apply_post_switch_touch — update structural test if section was renamed" );
let fn_body = &src[ fn_start..fn_end ];
assert!(
fn_body.contains( "fetch_oauth_usage" ),
"BUG-288: apply_post_switch_touch must call fetch_oauth_usage for AC-21 re-fetch",
);
assert!(
fn_body.contains( "write_quota_cache" ),
"BUG-288: apply_post_switch_touch must call write_quota_cache on successful re-fetch \
so subsequent .usage sees the updated resets_at (no double-subprocess race)",
);
}
let dir = TempDir::new().unwrap();
let paths = crate::ClaudePaths::with_home( dir.path() );
std::fs::create_dir_all( paths.base() ).unwrap();
let name = "test@example.com";
std::fs::write(
paths.base().join( format!( "{name}.credentials.json" ) ),
r#"{"subscriptionType":"pro","expiresAt":9999999999999}"#,
).unwrap();
let ctx = TouchCtx::for_test(
r#"{"subscriptionType":"pro"}"#.to_string(),
OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None },
);
apply_post_switch_touch( name, ctx, "auto", "auto", false, &paths );
let cache_path = paths.base().join( format!( "{name}.json" ) );
let cache = std::fs::read_to_string( &cache_path )
.expect( "BUG-288: cache file must exist after apply_post_switch_touch" );
assert!(
cache.contains( "last_touch_at" ),
"BUG-288: last_touch_at must be written to cache even when re-fetch is skipped, got: {cache}",
);
assert!(
cache.contains( "touch_idle" ),
"BUG-288: touch_idle must be written to cache even when re-fetch is skipped, got: {cache}",
);
assert!(
!cache.contains( "resets_at" ),
"BUG-288: resets_at must not be written when accessToken is absent (re-fetch skipped), got: {cache}",
);
}
#[ test ]
fn it_apply_post_switch_touch_cred_file_absent_skips_refetch()
{
use claude_quota::OauthUsageData;
let dir = TempDir::new().unwrap();
let paths = crate::ClaudePaths::with_home( dir.path() );
std::fs::create_dir_all( paths.base() ).unwrap();
let name = "absent@example.com";
let ctx = TouchCtx::for_test(
r#"{"subscriptionType":"pro"}"#.to_string(),
OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None },
);
apply_post_switch_touch( name, ctx, "auto", "auto", false, &paths );
let cache_path = paths.base().join( format!( "{name}.json" ) );
let cache = std::fs::read_to_string( &cache_path )
.expect( "cache file must exist even when credential file is absent" );
assert!(
cache.contains( "last_touch_at" ),
"last_touch_at must be written even when credential file is absent; got: {cache}",
);
assert!(
cache.contains( "touch_idle" ),
"touch_idle must be written even when credential file is absent; got: {cache}",
);
assert!(
!cache.contains( "resets_at" ),
"resets_at must not appear when credential file is absent (outer guard bypassed); got: {cache}",
);
}
#[ test ]
fn ft11_rotation_dispatcher_calls_apply_model_override_for_winner()
{
let src = include_str!( "api.rs" );
let block_start = src
.find( "Rotation dispatch (Feature 038" )
.expect( "rotation dispatch block not found in api.rs" );
let block_end = src[ block_start.. ]
.find( "\n Ok( OutputData::new( content" )
.map_or( src.len(), |rel| block_start + rel );
let block = &src[ block_start..block_end ];
assert!(
block.contains( "apply_model_override(" ),
"AC-05: apply_model_override must be called in the rotation dispatch block (Feature 062)\n\
block:\n{block}",
);
}
#[ test ]
fn ft12_rotation_dispatcher_calls_set_session_effort_for_carry_forward()
{
let src = include_str!( "api.rs" );
let block_start = src
.find( "Rotation dispatch (Feature 038" )
.expect( "rotation dispatch block not found in api.rs" );
let block_end = src[ block_start.. ]
.find( "\n Ok( OutputData::new( content" )
.map_or( src.len(), |rel| block_start + rel );
let block = &src[ block_start..block_end ];
assert!(
block.contains( "set_session_effort(" ),
"AC-06: set_session_effort must be called in the rotation dispatch block (Feature 062)\n\
block:\n{block}",
);
}
#[ test ]
fn ft13_rotation_set_session_effort_guarded_by_some_not_injected()
{
let src = include_str!( "api.rs" );
let block_start = src
.find( "Rotation dispatch (Feature 038" )
.expect( "rotation dispatch block not found in api.rs" );
let block_end = src[ block_start.. ]
.find( "\n Ok( OutputData::new( content" )
.map_or( src.len(), |rel| block_start + rel );
let block = &src[ block_start..block_end ];
assert!(
block.contains( "if let Some( se ) = session_effort" ),
"AC-07: set_session_effort must be guarded by `if let Some( se ) = session_effort` \
to prevent effort injection when none is configured (Feature 062)\n\
block:\n{block}",
);
}