use super::apply_refresh;
use crate::usage::types::{ AccountQuota, SubprocessModel, SubprocessEffort };
use crate::usage::test_support::FAR_FUTURE_MS;
use tempfile::TempDir;
#[ doc = "bug_reproducer(BUG-271)" ]
#[ test ]
fn test_apply_refresh_429_not_retried()
{
let store = TempDir::new().unwrap();
let mut accounts = vec![
AccountQuota
{
name : "test-acct".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Err( "HTTP transport error: HTTP 429".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(),
},
];
apply_refresh( &mut accounts, store.path(), None, false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
matches!( accounts[ 0 ].result, Err( ref e ) if e.contains( "refresh token expired" ) ),
"429+expired: no cred file → refresh_account_token returns None → \
result must be Err(\"refresh token expired\"); result: {:?}", accounts[ 0 ].result,
);
}
#[ test ]
fn test_apply_refresh_ok_result_unchanged()
{
let store = TempDir::new().unwrap();
let quota = claude_quota::OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None };
let mut accounts = vec![
AccountQuota
{
name : "ok-acct".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Ok( quota ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
},
];
apply_refresh( &mut accounts, store.path(), None, false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!( accounts[ 0 ].result.is_ok(), "Ok result must not be changed by apply_refresh" );
}
#[ test ]
fn test_apply_refresh_generic_error_unchanged()
{
let store = TempDir::new().unwrap();
let err_msg = "network timeout after 30s".to_string();
let mut accounts = vec![
AccountQuota
{
name : "net-acct".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Err( err_msg.clone() ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
},
];
apply_refresh( &mut accounts, store.path(), None, false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
matches!( accounts[ 0 ].result, Err( ref e ) if e == &err_msg ),
"generic error must be unchanged; result: {:?}", accounts[ 0 ].result,
);
}
#[ test ]
fn test_apply_refresh_empty_accounts()
{
let store = TempDir::new().unwrap();
let mut accounts : Vec< AccountQuota > = vec![];
apply_refresh( &mut accounts, store.path(), None, false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!( accounts.is_empty(), "empty slice must remain empty" );
}
#[ test ]
fn test_apply_refresh_401_no_cred_file()
{
let store = TempDir::new().unwrap();
let mut accounts = vec![
AccountQuota
{
name : "ghost@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Err( "HTTP transport error: HTTP 401".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(),
},
];
apply_refresh( &mut accounts, store.path(), None, false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
matches!( accounts[ 0 ].result, Err( ref e ) if e.contains( "refresh token expired" ) ),
"401: no cred file → refresh_account_token returns None → \
result must be Err(\"refresh token expired\"); result: {:?}", accounts[ 0 ].result,
);
}
#[ test ]
fn test_apply_refresh_403_no_cred_file()
{
let store = TempDir::new().unwrap();
let mut accounts = vec![
AccountQuota
{
name : "ghost@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Err( "HTTP transport error: HTTP 403".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(),
},
];
apply_refresh( &mut accounts, store.path(), None, false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
matches!( accounts[ 0 ].result, Err( ref e ) if e.contains( "refresh token expired" ) ),
"403: no cred file → refresh_account_token returns None → \
result must be Err(\"refresh token expired\"); result: {:?}", accounts[ 0 ].result,
);
}
#[ test ]
fn test_apply_refresh_mixed_accounts()
{
let store = TempDir::new().unwrap();
let quota = claude_quota::OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None };
let mut accounts = vec![
AccountQuota
{
name : "a@ok.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Ok( quota ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
},
AccountQuota
{
name : "b@ratelimited.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Err( "HTTP transport error: HTTP 429".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(),
},
AccountQuota
{
name : "c@expired.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Err( "HTTP transport error: HTTP 401".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(),
},
AccountQuota
{
name : "d@network.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Err( "connection refused".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(),
},
];
apply_refresh( &mut accounts, store.path(), None, false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!( accounts[ 0 ].result.is_ok(), "Ok account must remain Ok" );
assert!(
matches!( accounts[ 1 ].result, Err( ref e ) if e.contains( "refresh token expired" ) ),
"429+expired: no cred file → result must be Err(\"refresh token expired\"); result: {:?}",
accounts[ 1 ].result,
);
assert!(
matches!( accounts[ 2 ].result, Err( ref e ) if e.contains( "refresh token expired" ) ),
"401: no cred file → result must be Err(\"refresh token expired\"); result: {:?}",
accounts[ 2 ].result,
);
assert!(
matches!( accounts[ 3 ].result, Err( ref e ) if e == "connection refused" ),
"generic error must be unchanged",
);
}
#[ test ]
fn test_apply_refresh_trace_does_not_panic()
{
let store = TempDir::new().unwrap();
let mut accounts = vec![
AccountQuota
{
name : "trace@test.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Err( "HTTP transport error: HTTP 401".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(),
},
];
apply_refresh( &mut accounts, store.path(), None, true, SubprocessModel::Auto, SubprocessEffort::Auto, false );
}
#[ test ]
fn test_apply_refresh_lifecycle_switch_fails_result_unchanged()
{
let store = TempDir::new().unwrap();
let fake_home = TempDir::new().unwrap();
let paths = crate::ClaudePaths::with_home( fake_home.path() );
let mut accounts = vec![
AccountQuota
{
name : "alice@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Err( "HTTP transport error: HTTP 401".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(),
},
];
apply_refresh( &mut accounts, store.path(), Some( &paths ), false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
matches!( accounts[ 0 ].result, Err( ref e ) if e.contains( "refresh token expired" ) ),
"lifecycle: 401 + switch fails → refresh_account_token None → \
result must be Err(\"refresh token expired\"); result: {:?}",
accounts[ 0 ].result,
);
}
#[ doc = "bug_reproducer(BUG-211)" ]
#[ test ]
fn test_apply_refresh_lifecycle_active_marker_unchanged()
{
let store = TempDir::new().unwrap();
let fake_home = TempDir::new().unwrap();
std::fs::write(
store.path().join( "alice@example.com.credentials.json" ),
r#"{"accessToken":"alice-token"}"#,
).unwrap();
std::fs::write( store.path().join( crate::account::active_marker_filename() ), "alice@example.com" ).unwrap();
std::fs::create_dir_all( fake_home.path().join( ".claude" ) ).unwrap();
let paths = crate::ClaudePaths::with_home( fake_home.path() );
let mut accounts = vec![
AccountQuota
{
name : "bob@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Err( "HTTP transport error: HTTP 401".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(),
},
];
apply_refresh( &mut accounts, store.path(), Some( &paths ), false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
!paths.credentials_file().exists(),
"BUG-211: apply_refresh must not call switch_account; live credentials file must not exist",
);
let active = std::fs::read_to_string( store.path().join( crate::account::active_marker_filename() ) ).unwrap();
assert_eq!(
active, "alice@example.com",
"per-machine active marker must be unchanged throughout refresh cycle (BUG-211 fix)",
);
}
#[ test ]
fn test_apply_refresh_lifecycle_429_expired_switch_fails_unchanged()
{
let store = TempDir::new().unwrap();
let fake_home = TempDir::new().unwrap();
let paths = crate::ClaudePaths::with_home( fake_home.path() );
let mut accounts = vec![
AccountQuota
{
name : "alice@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0, result : Err( "HTTP transport error: HTTP 429".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(),
},
];
apply_refresh( &mut accounts, store.path(), Some( &paths ), false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
matches!( accounts[ 0 ].result, Err( ref e ) if e.contains( "refresh token expired" ) ),
"lifecycle: 429+expired + switch fails → refresh_account_token None → \
result must be Err(\"refresh token expired\"); result: {:?}",
accounts[ 0 ].result,
);
}
#[ test ]
fn test_apply_refresh_lifecycle_ft3_403_no_cred_result_unchanged()
{
let store = TempDir::new().unwrap();
let fake_home = TempDir::new().unwrap();
let paths = crate::ClaudePaths::with_home( fake_home.path() );
let mut accounts = vec![
AccountQuota
{
name : "alice@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS, result : Err( "HTTP transport error: HTTP 403".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(),
},
];
apply_refresh( &mut accounts, store.path(), Some( &paths ), false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
matches!( accounts[ 0 ].result, Err( ref e ) if e.contains( "refresh token expired" ) ),
"lifecycle: 403 + switch fails → refresh_account_token None → \
result must be Err(\"refresh token expired\"); result: {:?}",
accounts[ 0 ].result,
);
}
#[ test ]
fn test_apply_refresh_lifecycle_copy_fails_no_dot_claude_dir()
{
let store = TempDir::new().unwrap();
let fake_home = TempDir::new().unwrap();
std::fs::write(
store.path().join( "alice@example.com.credentials.json" ),
r#"{"accessToken":"tok"}"#,
).unwrap();
let paths = crate::ClaudePaths::with_home( fake_home.path() );
let mut accounts = vec![
AccountQuota
{
name : "alice@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Err( "HTTP transport error: HTTP 401".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(),
},
];
apply_refresh( &mut accounts, store.path(), Some( &paths ), false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
matches!( accounts[ 0 ].result, Err( ref e ) if e.contains( "refresh token expired" ) ),
"lifecycle: 401 + fs::copy fails (no .claude/ dir) → refresh_account_token None → \
result must be Err(\"refresh token expired\"); result: {:?}",
accounts[ 0 ].result,
);
}
#[ test ]
fn test_apply_refresh_lifecycle_no_active_file_no_restore()
{
let store = TempDir::new().unwrap();
let fake_home = TempDir::new().unwrap();
let paths = crate::ClaudePaths::with_home( fake_home.path() );
let mut accounts : Vec< AccountQuota > = vec![]; apply_refresh( &mut accounts, store.path(), Some( &paths ), false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
!store.path().join( crate::account::active_marker_filename() ).exists(),
"per-machine active marker must not be created when it was absent before apply_refresh",
);
}
#[ test ]
fn test_apply_refresh_lifecycle_trace_switch_fails_no_panic()
{
let store = TempDir::new().unwrap();
let fake_home = TempDir::new().unwrap();
let paths = crate::ClaudePaths::with_home( fake_home.path() );
let mut accounts = vec![
AccountQuota
{
name : "trace@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Err( "HTTP transport error: HTTP 401".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(),
},
];
apply_refresh( &mut accounts, store.path(), Some( &paths ), true, SubprocessModel::Auto, SubprocessEffort::Auto, false );
}
#[ test ]
fn test_apply_refresh_lifecycle_active_newline_trimmed_restore()
{
let store = TempDir::new().unwrap();
let fake_home = TempDir::new().unwrap();
std::fs::write( store.path().join( crate::account::active_marker_filename() ), "alice@example.com\n" ).unwrap();
let paths = crate::ClaudePaths::with_home( fake_home.path() );
let mut accounts : Vec< AccountQuota > = vec![];
apply_refresh( &mut accounts, store.path(), Some( &paths ), false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
let active = std::fs::read_to_string( store.path().join( crate::account::active_marker_filename() ) ).unwrap();
assert_eq!(
active, "alice@example.com\n",
"active marker must be unchanged after apply_refresh (BUG-211 fix: no restore); got: {active:?}",
);
}
#[ test ]
fn test_apply_refresh_lifecycle_active_whitespace_only_no_restore()
{
let store = TempDir::new().unwrap();
let fake_home = TempDir::new().unwrap();
let ws = " \n ";
std::fs::write( store.path().join( crate::account::active_marker_filename() ), ws ).unwrap();
let paths = crate::ClaudePaths::with_home( fake_home.path() );
let mut accounts : Vec< AccountQuota > = vec![];
apply_refresh( &mut accounts, store.path(), Some( &paths ), false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
let active = std::fs::read_to_string( store.path().join( crate::account::active_marker_filename() ) ).unwrap();
assert_eq!(
active, ws,
"apply_refresh must not modify the active marker file (BUG-211 fix); content must be unchanged",
);
}
#[ test ]
fn test_apply_refresh_none_paths_active_unchanged()
{
let store = TempDir::new().unwrap();
std::fs::write( store.path().join( crate::account::active_marker_filename() ), "alice@example.com" ).unwrap();
let mut accounts : Vec< AccountQuota > = vec![]; apply_refresh( &mut accounts, store.path(), None, false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
let active = std::fs::read_to_string( store.path().join( crate::account::active_marker_filename() ) ).unwrap();
assert_eq!(
active, "alice@example.com",
"per-machine active marker must be unchanged when claude_paths=None (no restore possible)",
);
}
#[ test ]
fn test_apply_refresh_lifecycle_l10_trace_run_isolated_invoked_no_panic()
{
let store = TempDir::new().unwrap();
let fake_home = TempDir::new().unwrap();
std::fs::write(
store.path().join( "alice@example.com.credentials.json" ),
r#"{"accessToken":"fake-tok","expiresAt":9999999999999}"#,
).unwrap();
std::fs::create_dir_all( fake_home.path().join( ".claude" ) ).unwrap();
let paths = crate::ClaudePaths::with_home( fake_home.path() );
let mut accounts = vec![
AccountQuota
{
name : "alice@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Err( "HTTP transport error: HTTP 401".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(),
},
];
apply_refresh( &mut accounts, store.path(), Some( &paths ), true, SubprocessModel::Auto, SubprocessEffort::Auto, false );
}
#[ test ]
fn test_apply_refresh_ft4_429_valid_token_not_retried()
{
let store = TempDir::new().unwrap();
let mut accounts = vec![
AccountQuota
{
name : "alice@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS, result : Err( "HTTP transport error: HTTP 429".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(),
},
];
apply_refresh( &mut accounts, store.path(), None, false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
matches!( accounts[ 0 ].result, Err( ref e ) if e.contains( "429" ) ),
"429 with valid (non-expired) token must NOT be retried; result: {:?}",
accounts[ 0 ].result,
);
}
#[ test ]
fn test_apply_refresh_ft5_429_expired_refresh_path_entered_no_cred()
{
let store = TempDir::new().unwrap();
let mut accounts = vec![
AccountQuota
{
name : "alice@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Err( "HTTP transport error: HTTP 429".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(),
},
];
apply_refresh( &mut accounts, store.path(), None, false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
matches!( accounts[ 0 ].result, Err( ref e ) if e.contains( "refresh token expired" ) ),
"429+expired: no cred file → refresh_account_token None → \
result must be Err(\"refresh token expired\"); result: {:?}",
accounts[ 0 ].result,
);
}
#[ doc = "bug_reproducer(BUG-170)" ]
#[ test ]
fn test_jwt_exp_ms_mre_bug170_opaque_returns_none()
{
let opaque_creds = r#"{"accessToken":"sk-ant-oat01-XXXXXXXXXXXX","expiresAt":9999999999999}"#;
assert!(
crate::output::jwt_exp_ms( opaque_creds ).is_none(),
"jwt_exp_ms must return None for opaque sk-ant-oat01 token (no JWT structure); \
if this fails, jwt_exp_ms was changed to handle opaque tokens — review BUG-170 fix",
);
}
#[ doc = "bug_reproducer(BUG-211)" ]
#[ test ]
fn test_apply_refresh_mre_bug208_restore_trace_emitted()
{
let store = TempDir::new().unwrap();
let fake_home = TempDir::new().unwrap();
std::fs::write(
store.path().join( "alice@example.com.credentials.json" ),
r#"{"accessToken":"alice-restore-tok","expiresAt":9999999999999}"#,
).unwrap();
std::fs::write(
store.path().join( crate::account::active_marker_filename() ),
"alice@example.com",
).unwrap();
std::fs::create_dir_all( fake_home.path().join( ".claude" ) ).unwrap();
let paths = crate::ClaudePaths::with_home( fake_home.path() );
let mut accounts = vec![
AccountQuota
{
name : "bob@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Err( "HTTP transport error: HTTP 401".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(),
},
];
apply_refresh( &mut accounts, store.path(), Some( &paths ), true, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
!paths.credentials_file().exists(),
"BUG-211: apply_refresh must not call switch_account; live credentials file must not exist",
);
let marker = std::fs::read_to_string(
store.path().join( crate::account::active_marker_filename() )
).unwrap();
assert_eq!(
marker, "alice@example.com",
"BUG-211: active marker must be unchanged after apply_refresh cycle (no restore)",
);
}
#[ doc = "bug_reproducer(BUG-256)" ]
#[ test ]
fn mre_bug256_retry_ok_stale_cached_metadata()
{
let src = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/refresh.rs" ) );
let fn_start = src.find( "pub( crate ) fn apply_refresh(" ).expect( "apply_refresh not found" );
let ok_arm_rel = src[ fn_start.. ]
.find( "Ok( retried ) =>" )
.expect( "BUG-256: retry OK arm `Ok( retried ) =>` not found in apply_refresh" );
let ok_arm_start = fn_start + ok_arm_rel;
let err_arm_rel = src[ ok_arm_start.. ]
.find( "Err( e ) =>" )
.expect( "Err arm not found after retry OK arm" );
let ok_arm = &src[ ok_arm_start .. ok_arm_start + err_arm_rel ];
assert!(
ok_arm.contains( "aq.cached = false" ),
"BUG-256: retry OK arm must set `aq.cached = false` to clear ~ prefix from render",
);
assert!(
ok_arm.contains( "aq.cache_age_secs = None" ),
"BUG-256: retry OK arm must set `aq.cache_age_secs = None` to remove (Xh ago) label",
);
assert!(
ok_arm.contains( "write_quota_cache(" ),
"BUG-256: retry OK arm must call write_quota_cache to persist fresh data to {{name}}.json",
);
let cache_write_pos = ok_arm.find( "write_quota_cache(" ).unwrap();
let result_move_pos = ok_arm.find( "aq.result = Ok( retried )" )
.expect( "aq.result = Ok( retried ) not found in retry OK arm" );
assert!(
cache_write_pos < result_move_pos,
"BUG-256: write_quota_cache must appear before `aq.result = Ok( retried )` — \
h5/d7/sn borrow from retried and would be use-after-move otherwise",
);
}
#[ doc = "bug_reproducer(BUG-295)" ]
#[ test ]
fn mre_bug295_apply_refresh_trace_reason_not_owned()
{
let store = TempDir::new().unwrap();
let cached = claude_quota::OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None };
let mut accounts = vec![
AccountQuota
{
name : "alice@remote.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( cached ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : true,
cache_age_secs : Some( 120 ),
is_owned : false,
owner : "other@remote".to_string(),
},
];
use std::io::Read;
let _lock = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut buf = gag::BufferRedirect::stderr().unwrap();
apply_refresh( &mut accounts, store.path(), None, true, SubprocessModel::Auto, SubprocessEffort::Auto, false );
let mut output = String::new();
buf.read_to_string( &mut output ).unwrap();
assert!(
output.contains( "reason: not owned" ),
"BUG-295: trace must emit 'reason: not owned' for non-owned account; got: {output}",
);
assert!(
!output.contains( "reason: ok" ),
"BUG-295: trace must NOT emit 'reason: ok' for non-owned account (misleading); got: {output}",
);
}
#[ doc = "bug_reproducer(BUG-297)" ]
#[ test ]
fn mre_bug297_refresh_none_sets_aq_result_err()
{
let store = TempDir::new().unwrap();
let stale_quota = claude_quota::OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None };
let mut accounts = vec![
AccountQuota
{
name : "rt-expired-acct".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0, result : Ok( stale_quota ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : true, cache_age_secs : Some( 7200 ),
is_owned : true, owner : String::new(),
},
];
apply_refresh( &mut accounts, store.path(), None, false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
matches!( accounts[ 0 ].result, Err( ref e ) if e.contains( "refresh token expired" ) ),
"BUG-297: when refresh_account_token returns None, aq.result must be \
Err(\"refresh token expired\"); got: {:?}",
accounts[ 0 ].result,
);
}
#[ test ]
fn apply_touch_skips_after_refresh_none()
{
use std::io::Read;
let store = TempDir::new().unwrap();
let stale_quota = claude_quota::OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None };
let mut accounts = vec![
AccountQuota
{
name : "rt-expired-touch-pipeline".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0,
result : Ok( stale_quota ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : true,
cache_age_secs : Some( 7200 ),
is_owned : true,
owner : String::new(),
},
];
apply_refresh( &mut accounts, store.path(), None, false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
accounts[ 0 ].result.is_err(),
"pipeline precondition: apply_refresh must set Err result; got: {:?}",
accounts[ 0 ].result,
);
let _lock = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
super::super::touch::apply_touch(
&mut accounts[ 0 ],
store.path(),
None,
true,
SubprocessModel::Auto,
SubprocessEffort::Auto,
false,
);
let mut captured = String::new();
buf.read_to_string( &mut captured ).unwrap();
assert!(
captured.contains( "error account" ),
"BUG-297 pipeline: apply_touch must emit 'error account' skip after refresh None; got:\n{captured}",
);
assert!(
!captured.contains( "run_isolated: invoking" ),
"BUG-297 pipeline: apply_touch must NOT spawn subprocess after refresh None; got:\n{captured}",
);
}
#[ doc = "bug_reproducer(BUG-298)" ]
#[ test ]
fn mre_bug298_apply_refresh_trace_reason_cached_expired()
{
let store = TempDir::new().unwrap();
let stale_quota = claude_quota::OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None };
let mut accounts = vec![
AccountQuota
{
name : "cached-owned@box.pro".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : 0, result : Ok( stale_quota ), account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : true, cache_age_secs : Some( 7200 ),
is_owned : true, owner : String::new(),
},
];
use std::io::Read;
let _lock = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut buf = gag::BufferRedirect::stderr().unwrap();
apply_refresh( &mut accounts, store.path(), None, true, SubprocessModel::Auto, SubprocessEffort::Auto, false );
let mut output = String::new();
buf.read_to_string( &mut output ).unwrap();
assert!(
output.contains( "reason: cached-expired" ),
"BUG-298: trace must emit 'reason: cached-expired' for owned+cached+expired account; got: {output}",
);
assert!(
!output.contains( "reason: ok" ),
"BUG-298: trace must NOT emit 'reason: ok' for owned+cached account (misleading); got: {output}",
);
}
#[ test ]
fn ec7_solo_gate_skips_non_current_with_trace()
{
use std::io::Read;
let store = TempDir::new().unwrap();
let mut accounts = vec![ AccountQuota
{
name : "noncurrent@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None } ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
} ];
let _lock = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut buf = gag::BufferRedirect::stderr().unwrap();
apply_refresh( &mut accounts, store.path(), None, true, SubprocessModel::Auto, SubprocessEffort::Auto, true );
let mut output = String::new();
buf.read_to_string( &mut output ).unwrap();
assert!(
output.contains( "solo-skip" ),
"EC-7: solo gate must emit 'solo-skip' trace for non-current account; got: {output}",
);
assert!(
output.contains( "noncurrent@example.com" ),
"EC-7: trace must name the skipped account; got: {output}",
);
}
#[ test ]
fn mre_bug_gap20_refresh_trace_reason_ok_owned_non_cached_ok()
{
use std::io::Read;
let store = TempDir::new().unwrap();
let ok_quota = claude_quota::OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None };
let mut accounts = vec![ AccountQuota
{
name : "healthy@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS, result : Ok( ok_quota ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false, cache_age_secs : None,
is_owned : true, owner : String::new(),
} ];
let _lock = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut buf = gag::BufferRedirect::stderr().unwrap();
apply_refresh( &mut accounts, store.path(), None, true, SubprocessModel::Auto, SubprocessEffort::Auto, false );
let mut output = String::new();
buf.read_to_string( &mut output ).unwrap();
assert!(
output.contains( "reason: ok" ),
"GAP-20: owned+non-cached+Ok account must emit 'reason: ok' trace; got: {output}",
);
assert!(
!output.contains( "reason: not owned" ) && !output.contains( "reason: cached-expired" ),
"GAP-20: must not emit non-owned or cached-expired reason for this path; got: {output}",
);
}
#[ doc = "bug_reproducer(BUG-306)" ]
#[ test ]
fn mre_bug306_refresh_trace_reason_occupied_elsewhere()
{
let aq = AccountQuota
{
name : "occ@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : true,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None } ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
};
assert_eq!( super::reason_label( &aq ), "occupied elsewhere" );
}
#[ test ]
fn reason_label_not_owned()
{
let aq = AccountQuota
{
name : "x".into(), is_current : false, is_active : false,
is_occupied_elsewhere : false, expires_at_ms : 0,
result : Ok( claude_quota::OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None } ),
account : None, host : String::new(), role : String::new(),
renewal_at : None, cached : false, cache_age_secs : None,
is_owned : false, owner : String::new(),
};
assert_eq!( super::reason_label( &aq ), "not owned" );
}
#[ test ]
fn reason_label_cached_expired()
{
let aq = AccountQuota
{
name : "x".into(), is_current : false, is_active : false,
is_occupied_elsewhere : false, expires_at_ms : 0,
result : Ok( claude_quota::OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None } ),
account : None, host : String::new(), role : String::new(),
renewal_at : None, cached : true, cache_age_secs : Some( 999 ),
is_owned : true, owner : String::new(),
};
assert_eq!( super::reason_label( &aq ), "cached-expired" );
}
#[ test ]
fn reason_label_ok()
{
let aq = AccountQuota
{
name : "x".into(), is_current : false, is_active : false,
is_occupied_elsewhere : false, expires_at_ms : 0,
result : Ok( claude_quota::OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None } ),
account : None, host : String::new(), role : String::new(),
renewal_at : None, cached : false, cache_age_secs : None,
is_owned : true, owner : String::new(),
};
assert_eq!( super::reason_label( &aq ), "ok" );
}
#[ test ]
fn reason_label_err()
{
let aq = AccountQuota
{
name : "x".into(), is_current : false, is_active : false,
is_occupied_elsewhere : false, expires_at_ms : 0,
result : Err( "HTTP 401 Unauthorized".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(),
};
assert_eq!( super::reason_label( &aq ), "HTTP 401 Unauthorized" );
}