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" );
let content = std::fs::read_to_string( paths.settings_file() ).unwrap_or_default();
assert!(
content.contains( "\"sonnet\"" ),
"BUG-300/BUG-311: absent Sonnet tier must write sonnet (conservative default), got: {content}",
);
assert!(
!content.contains( "\"opus\"" ),
"BUG-300: absent Sonnet tier must NOT write opus (absent tier ≠ exhausted), got: {content}",
);
}
#[ 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_writes_sonnet_when_quota_sufficient()
{
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" );
let content = std::fs::read_to_string( paths.settings_file() ).unwrap_or_default();
assert!(
content.contains( "\"sonnet\"" ),
"Fix(BUG-311): 7d(Son) utilization=70% (30% left) must write sonnet as recommended model, got: {content}",
);
assert!(
!content.contains( "\"opus\"" ),
"7d(Son) 30% left: opus override must NOT fire, got: {content}",
);
}
#[ 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_writes_sonnet_at_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" );
let content = std::fs::read_to_string( paths.settings_file() ).unwrap_or_default();
assert!(
content.contains( "\"sonnet\"" ),
"Fix(BUG-311): exact 15% boundary (utilization=85.0) must write sonnet, got: {content}",
);
assert!(
!content.contains( "\"opus\"" ),
"exact 15% boundary: opus override must NOT fire (strict <), got: {content}",
);
}
#[ 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-311)" ]
#[ test ]
fn mre_bug311_model_restored_to_sonnet_when_opus_and_quota_sufficient()
{
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 : 4.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( "\"sonnet\"" ) && !content.contains( "\"opus\"" ),
"BUG-311: must restore sonnet when prior model=opus and 7d(Son) 96% left, got: {content}",
);
}
#[ test ]
fn t09_model_override_trace_opus_to_sonnet()
{
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();
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 : 4.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, "account.use", "test-account" );
let mut output = String::new();
buf.read_to_string( &mut output ).unwrap();
assert!(
output.contains( "model override: opus→sonnet" ),
"BUG-311: trace must contain 'opus→sonnet' when restoring from stale opus, got: {output}",
);
}
#[ doc = "bug_reproducer(BUG-312)" ]
#[ test ]
fn mre_bug312_effort_initialized_to_low_when_absent()
{
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 : 4.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( "\"effortLevel\"" ) && content.contains( "\"low\"" ),
"BUG-312: effortLevel must be initialized to 'low' when absent, got: {content}",
);
}
#[ test ]
fn t10_effort_preserved_when_already_configured()
{
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":"sonnet","effortLevel":"high"}"# ).unwrap();
let quota = OauthUsageData
{
five_hour : None,
seven_day : None,
seven_day_sonnet : Some( PeriodUsage { utilization : 4.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( "\"high\"" ),
"BUG-312 guard: user-set effortLevel='high' must not be overwritten, got: {content}",
);
assert!(
!content.contains( "\"low\"" ),
"BUG-312 guard: 'low' must not replace user-set 'high', got: {content}",
);
}
#[ 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(
OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None },
);
apply_post_switch_touch( name, ctx, "auto", "auto", false, &paths, paths.base() );
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(
OauthUsageData { five_hour : None, seven_day : None, seven_day_sonnet : None },
);
apply_post_switch_touch( name, ctx, "auto", "auto", false, &paths, paths.base() );
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: carry-forward set_session_effort must be guarded by \
`if let Some( se ) = session_effort` (Feature 062; init path handled by apply_model_override BUG-312)\n\
block:\n{block}",
);
}
#[ doc = "bug_reproducer(BUG-310)" ]
#[ test ]
fn mre_bug310_rotation_touch_resyncs_live_credentials()
{
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 ];
let touch_pos = block
.find( "apply_touch(" )
.expect( "BUG-310: apply_touch call not found in rotation dispatch block" );
let after_touch = &block[ touch_pos.. ];
assert!(
after_touch.contains( "fs::copy" ),
"BUG-310 AC-11: rotation dispatch block must re-sync live credentials from store \
after apply_touch via fs::copy. Without this, live retains stale pre-refresh token.\n\
block after apply_touch:\n{after_touch}",
);
}
#[ test ]
fn mre_bug310_rotation_no_refresh_no_divergence()
{
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( "switch_account(" ),
"BUG-310 control: switch_account must be called in the rotation dispatch block \
to copy store credentials to live at step 4d.\n\
block:\n{block}",
);
assert!(
block.contains( "apply_touch(" ),
"BUG-310 control: apply_touch must be called in the rotation dispatch block \
at step 4e. Without it, no token refresh can create store/live divergence.\n\
block:\n{block}",
);
}
#[ test ]
fn reach_rotation_switch_account_precedes_apply_touch()
{
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 ];
let switch_pos = block.find( "switch_account(" )
.expect( "D4: switch_account not found in rotation block" );
let touch_pos = block.find( "apply_touch(" )
.expect( "D4: apply_touch not found in rotation block" );
assert!(
switch_pos < touch_pos,
"D4: switch_account (step 4d) must precede apply_touch (step 4e) in rotation block. \
When touch::0 skips apply_touch, the store→live copy from switch_account is already consistent.",
);
}
#[ test ]
fn reach_structural_guard_fs_copy_follows_apply_touch_in_rotation()
{
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 ];
let touch_pos = block.find( "apply_touch(" )
.expect( "D5: apply_touch not found in rotation block" );
let after_touch = &block[ touch_pos.. ];
assert!(
after_touch.contains( "fs::copy" ),
"D5 AC-11: fs::copy must follow apply_touch in rotation block to re-sync \
live credentials after potential token refresh.\n\
block after apply_touch:\n{after_touch}",
);
assert!(
after_touch.contains( "credentials_file()" ),
"D5 AC-11: fs::copy target must be credentials_file() (the live session file).\n\
block after apply_touch:\n{after_touch}",
);
}
#[ test ]
fn ft_apply_post_switch_touch_routes_through_refresh_account_token()
{
let src = include_str!( "api.rs" );
let fn_start = src
.find( "pub( crate ) fn apply_post_switch_touch(" )
.expect( "apply_post_switch_touch must exist in api.rs (AC-34 routing entry point)" );
let fn_end = src[ fn_start.. ]
.find( "\n// ── Main routine" )
.map( |rel| fn_start + rel )
.expect( "'// ── Main routine' anchor must follow apply_post_switch_touch in api.rs" );
let fn_body = &src[ fn_start..fn_end ];
assert!(
fn_body.contains( "refresh_account_token(" ),
"AC-34: apply_post_switch_touch must call refresh_account_token() for token refresh \
(not run_isolated directly) — see invariant 008 and Feature 017 AC-34\n\
function body:\n{fn_body}",
);
let direct_call_pattern = format!( "{}(", "run_isolated" );
let violations : Vec< &str > = fn_body.lines()
.filter( | line | !line.trim_start().starts_with( "//" ) )
.filter( | line | line.contains( &direct_call_pattern ) )
.collect();
assert!(
violations.is_empty(),
"AC-34: apply_post_switch_touch must not invoke run_isolated directly; \
all token refresh must route through refresh_account_token:\n{}",
violations.join( "\n" ),
);
}