use super::apply_touch;
use crate::usage::types::{ SubprocessModel, SubprocessEffort };
use crate::usage::test_support::mk_aq_with_resets_at;
use tempfile::TempDir;
#[ doc = "bug_reproducer(BUG-211)" ]
#[ test ]
fn test_apply_touch_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-touch-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 aq = mk_aq_with_resets_at( None );
apply_touch( &mut aq, store.path(), Some( &paths ), true, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
!paths.credentials_file().exists(),
"BUG-211: apply_touch 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_touch cycle (no restore)",
);
}
#[ doc = "bug_reproducer(BUG-214)" ]
#[ test ]
fn test_mre_bug214_apply_touch_skips_7d_exhausted_account()
{
use std::io::Read;
let store = tempfile::TempDir::new().unwrap();
let mut aq = mk_aq_with_resets_at( None );
if let Ok( ref mut data ) = aq.result
{
data.seven_day = Some( claude_quota::PeriodUsage
{
utilization : 100.0,
resets_at : None,
} );
}
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch(
&mut aq,
store.path(),
None,
true,
SubprocessModel::Auto,
SubprocessEffort::Auto,
false,
);
let mut captured = String::new();
stderr_buf.read_to_string( &mut captured ).unwrap();
assert!(
captured.contains( "7d-exhausted" ),
"apply_touch must emit '7d-exhausted' trace for idle 7d-exhausted account; got:\n{captured}",
);
}
#[ doc = "bug_reproducer(BUG-215)" ]
#[ test ]
fn test_mre_bug215_apply_touch_fires_when_7d_timer_absent()
{
use std::io::Read;
let store = tempfile::TempDir::new().unwrap();
let mut aq = mk_aq_with_resets_at( Some( "2099-01-01T00:00:00Z" ) );
if let Ok( ref mut data ) = aq.result
{
data.seven_day = Some( claude_quota::PeriodUsage
{
utilization : 0.0,
resets_at : None,
} );
}
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch(
&mut aq,
store.path(),
None,
true,
SubprocessModel::Auto,
SubprocessEffort::Auto,
false,
);
let mut captured = String::new();
stderr_buf.read_to_string( &mut captured ).unwrap();
assert!(
!captured.contains( "skipped" ),
"apply_touch must NOT emit 'skipped' for 5h active but 7d timer absent; got:\n{captured}",
);
}
#[ doc = "bug_reproducer(BUG-288-FixB)" ]
#[ test ]
fn test_mre_bug288_apply_touch_skips_touch_idle_false()
{
use std::io::Read;
let store = tempfile::TempDir::new().unwrap();
claude_profile_core::account::write_cache_string(
store.path(), "test@example.com", "fetched_at",
&claude_profile_core::account::chrono_now_utc(),
);
claude_profile_core::account::write_cache_bool(
store.path(), "test@example.com", "touch_idle", false,
);
let mut aq = mk_aq_with_resets_at( None );
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch(
&mut aq,
store.path(),
None,
true,
SubprocessModel::Auto,
SubprocessEffort::Auto,
false,
);
let mut captured = String::new();
stderr_buf.read_to_string( &mut captured ).unwrap();
assert!(
captured.contains( "touch_idle=false" ),
"apply_touch must emit 'touch_idle=false' skip trace for account with \
touch_idle=false in cache; got:\n{captured}",
);
}
#[ test ]
fn test_apply_touch_touch_idle_true_not_skipped_by_guard()
{
use std::io::Read;
{
let store_a = tempfile::TempDir::new().unwrap();
claude_profile_core::account::write_cache_string(
store_a.path(), "test@example.com", "fetched_at",
&claude_profile_core::account::chrono_now_utc(),
);
claude_profile_core::account::write_cache_bool(
store_a.path(), "test@example.com", "touch_idle", false,
);
let mut aq = mk_aq_with_resets_at( None );
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch(
&mut aq, store_a.path(), None, true, SubprocessModel::Auto, SubprocessEffort::Auto, false
);
let mut captured = String::new();
stderr_buf.read_to_string( &mut captured ).unwrap();
assert!(
captured.contains( "touch_idle=false" ),
"call A: guard must fire for touch_idle=Some(false); got:\n{captured}",
);
}
{
let store_b = tempfile::TempDir::new().unwrap();
claude_profile_core::account::write_cache_string(
store_b.path(), "test@example.com", "fetched_at",
&claude_profile_core::account::chrono_now_utc(),
);
claude_profile_core::account::write_cache_bool(
store_b.path(), "test@example.com", "touch_idle", true,
);
let mut aq = mk_aq_with_resets_at( None );
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch(
&mut aq, store_b.path(), None, true, SubprocessModel::Auto, SubprocessEffort::Auto, false
);
let mut captured = String::new();
stderr_buf.read_to_string( &mut captured ).unwrap();
assert!(
!captured.contains( "touch_idle" ),
"call B: guard must NOT fire for touch_idle=Some(true); got:\n{captured}",
);
assert!(
captured.contains( "read credentials:" ),
"call B: execution must reach subprocess path when touch_idle=Some(true); got:\n{captured}",
);
}
}
#[ test ]
fn test_apply_touch_touch_idle_none_in_cache_not_skipped()
{
use std::io::Read;
{
let store_a = tempfile::TempDir::new().unwrap();
claude_profile_core::account::write_cache_string(
store_a.path(), "test@example.com", "fetched_at",
&claude_profile_core::account::chrono_now_utc(),
);
claude_profile_core::account::write_cache_bool(
store_a.path(), "test@example.com", "touch_idle", false,
);
let mut aq = mk_aq_with_resets_at( None );
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch(
&mut aq, store_a.path(), None, true, SubprocessModel::Auto, SubprocessEffort::Auto, false
);
let mut captured = String::new();
stderr_buf.read_to_string( &mut captured ).unwrap();
assert!(
captured.contains( "touch_idle=false" ),
"call A: guard must fire for touch_idle=Some(false); got:\n{captured}",
);
}
{
let store_b = tempfile::TempDir::new().unwrap();
claude_profile_core::account::write_cache_string(
store_b.path(), "test@example.com", "fetched_at",
&claude_profile_core::account::chrono_now_utc(),
);
let entry = claude_profile_core::account::read_quota_cache( store_b.path(), "test@example.com" );
assert!( entry.is_some(), "precondition: read_quota_cache must return Some when fetched_at is present" );
assert_eq!(
entry.unwrap().touch_idle, None,
"precondition: touch_idle must be None when field was never written",
);
let mut aq = mk_aq_with_resets_at( None );
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch(
&mut aq, store_b.path(), None, true, SubprocessModel::Auto, SubprocessEffort::Auto, false
);
let mut captured = String::new();
stderr_buf.read_to_string( &mut captured ).unwrap();
assert!(
!captured.contains( "touch_idle" ),
"call B: guard must NOT fire for touch_idle=None; got:\n{captured}",
);
assert!(
captured.contains( "read credentials:" ),
"call B: execution must reach subprocess path when touch_idle=None; got:\n{captured}",
);
}
}
#[ test ]
fn test_apply_touch_touch_idle_false_fetched_at_absent_guard_bypassed()
{
use std::io::Read;
{
let store_a = tempfile::TempDir::new().unwrap();
claude_profile_core::account::write_cache_string(
store_a.path(), "test@example.com", "fetched_at",
&claude_profile_core::account::chrono_now_utc(),
);
claude_profile_core::account::write_cache_bool(
store_a.path(), "test@example.com", "touch_idle", false,
);
let mut aq = mk_aq_with_resets_at( None );
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch(
&mut aq, store_a.path(), None, true, SubprocessModel::Auto, SubprocessEffort::Auto, false
);
let mut captured = String::new();
stderr_buf.read_to_string( &mut captured ).unwrap();
assert!(
captured.contains( "touch_idle=false" ),
"call A: guard must fire for touch_idle=Some(false) with valid cache; got:\n{captured}",
);
}
{
let store_b = tempfile::TempDir::new().unwrap();
claude_profile_core::account::write_cache_bool(
store_b.path(), "test@example.com", "touch_idle", false,
);
let entry = claude_profile_core::account::read_quota_cache( store_b.path(), "test@example.com" );
assert!(
entry.is_none(),
"precondition: read_quota_cache must return None when fetched_at is absent",
);
let mut aq = mk_aq_with_resets_at( None );
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch(
&mut aq, store_b.path(), None, true, SubprocessModel::Auto, SubprocessEffort::Auto, false
);
let mut captured = String::new();
stderr_buf.read_to_string( &mut captured ).unwrap();
assert!(
!captured.contains( "touch_idle" ),
"call B: guard must NOT fire when fetched_at absent (read_quota_cache returned None); \
got:\n{captured}",
);
assert!(
captured.contains( "read credentials:" ),
"call B: execution must reach subprocess path when guard is bypassed (fetched_at absent); \
got:\n{captured}",
);
}
}
#[ test ]
fn test_apply_touch_touch_idle_false_silent_when_trace_disabled()
{
use std::io::Read;
let store = tempfile::TempDir::new().unwrap();
claude_profile_core::account::write_cache_string(
store.path(), "test@example.com", "fetched_at",
&claude_profile_core::account::chrono_now_utc(),
);
claude_profile_core::account::write_cache_bool(
store.path(), "test@example.com", "touch_idle", false,
);
let mut aq = mk_aq_with_resets_at( None );
{
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch(
&mut aq,
store.path(),
None,
true,
SubprocessModel::Auto,
SubprocessEffort::Auto, false
);
let mut captured = String::new();
stderr_buf.read_to_string( &mut captured ).unwrap();
assert!(
captured.contains( "touch_idle=false" ),
"call 1 (trace=true): apply_touch must emit 'touch_idle=false' skip; got:\n{captured}",
);
}
{
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch(
&mut aq,
store.path(),
None,
false,
SubprocessModel::Auto,
SubprocessEffort::Auto, false
);
let mut captured = String::new();
stderr_buf.read_to_string( &mut captured ).unwrap();
assert!(
captured.is_empty(),
"call 2 (trace=false): apply_touch must emit nothing to stderr on touch_idle=false skip; \
got:\n{captured}",
);
}
}
#[ test ]
fn test_apply_touch_error_account_skips_before_touch_idle_guard()
{
use std::io::Read;
use crate::usage::test_support::mk_aq_err;
let store = tempfile::TempDir::new().unwrap();
claude_profile_core::account::write_cache_string(
store.path(), "bad@example.com", "fetched_at",
&claude_profile_core::account::chrono_now_utc(),
);
claude_profile_core::account::write_cache_bool(
store.path(), "bad@example.com", "touch_idle", false,
);
let mut aq = mk_aq_err();
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch(
&mut aq,
store.path(),
None,
true,
SubprocessModel::Auto,
SubprocessEffort::Auto,
false,
);
let mut captured = String::new();
stderr_buf.read_to_string( &mut captured ).unwrap();
assert!(
captured.contains( "error account" ),
"apply_touch must emit 'error account' skip for Err result; got:\n{captured}",
);
assert!(
!captured.contains( "touch_idle" ),
"apply_touch must NOT reach touch_idle guard for error accounts; got:\n{captured}",
);
}
#[ test ]
fn test_write_quota_cache_preserves_touch_idle_false()
{
let store = tempfile::TempDir::new().unwrap();
claude_profile_core::account::write_cache_bool(
store.path(), "test@example.com", "touch_idle", false,
);
claude_profile_core::account::write_quota_cache(
store.path(), "test@example.com",
None, None, None, );
let entry = claude_profile_core::account::read_quota_cache( store.path(), "test@example.com" )
.expect( "read_quota_cache must return Some after write_quota_cache (fetched_at is present)" );
assert_eq!(
entry.touch_idle,
Some( false ),
"write_quota_cache must preserve touch_idle=false written before the call; \
Fix A + Fix B integration broken if this fails",
);
}
#[ test ]
fn it_apply_touch_trigger_fires_resets_at_none()
{
let dir = tempfile::TempDir::new().unwrap();
let store = dir.path().join( "store" );
let fake_home = dir.path().join( "home" );
std::fs::create_dir_all( &store ).unwrap();
std::fs::create_dir_all( fake_home.join( ".claude" ) ).unwrap();
std::fs::write(
store.join( "test@example.com.credentials.json" ),
r#"{"accessToken":"tok","expiresAt":9999999999999}"#,
).unwrap();
std::fs::write(
store.join( crate::account::active_marker_filename() ),
"test@example.com",
).unwrap();
let mut aq = mk_aq_with_resets_at( None );
let paths = crate::ClaudePaths::with_home( &fake_home );
apply_touch( &mut aq, &store, Some( &paths ), false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
!paths.credentials_file().exists(),
"BUG-211: apply_touch must not call switch_account; live credentials file must not exist",
);
}
#[ test ]
fn it_apply_touch_trigger_skips_resets_at_some()
{
let dir = tempfile::TempDir::new().unwrap();
let store = dir.path().join( "store" );
let fake_home = dir.path().join( "home" );
std::fs::create_dir_all( &store ).unwrap();
std::fs::create_dir_all( fake_home.join( ".claude" ) ).unwrap();
std::fs::write(
store.join( "test@example.com.credentials.json" ),
r#"{"accessToken":"tok","expiresAt":9999999999999}"#,
).unwrap();
std::fs::write(
store.join( crate::account::active_marker_filename() ),
"test@example.com",
).unwrap();
let mut aq = mk_aq_with_resets_at( Some( "2099-01-01T00:00:00Z" ) );
let paths = crate::ClaudePaths::with_home( &fake_home );
apply_touch( &mut aq, &store, Some( &paths ), false, SubprocessModel::Auto, SubprocessEffort::Auto, false );
assert!(
!fake_home.join( ".claude" ).join( ".credentials.json" ).exists(),
"apply_touch must not enter refresh path when resets_at is Some (already active)",
);
}
#[ doc = "bug_reproducer(BUG-289)" ]
#[ test ]
fn test_mre_bug289_son_running_false_haiku_touch_fires_on_every_call()
{
use std::io::Read;
use crate::usage::test_support::mk_aq_with_son_idle;
{
let store_a = tempfile::TempDir::new().unwrap();
std::fs::write(
store_a.path().join( "test@example.com.credentials.json" ),
r#"{"accessToken":"tok-a","expiresAt":9999999999999}"#,
).unwrap();
let mut aq_a = mk_aq_with_son_idle();
if let Ok( ref mut data ) = aq_a.result
{
data.seven_day = Some( claude_quota::PeriodUsage
{
utilization : 0.0,
resets_at : Some( "2026-06-14T10:00:00Z".to_string() ),
} );
}
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch( &mut aq_a, store_a.path(), None, true, SubprocessModel::Auto, SubprocessEffort::Auto, false );
let mut captured_a = String::new();
stderr_buf.read_to_string( &mut captured_a ).unwrap();
assert!(
captured_a.contains( "run_isolated: invoking" ),
"call A: touch must fire (run_isolated invoked) for son_running=false; got:\n{captured_a}",
);
}
{
let store_b = tempfile::TempDir::new().unwrap();
std::fs::write(
store_b.path().join( "test@example.com.credentials.json" ),
r#"{"accessToken":"tok-b","expiresAt":9999999999999}"#,
).unwrap();
let mut aq_b = mk_aq_with_son_idle();
if let Ok( ref mut data ) = aq_b.result
{
data.seven_day = Some( claude_quota::PeriodUsage
{
utilization : 0.0,
resets_at : Some( "2026-06-14T10:00:00Z".to_string() ),
} );
}
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch( &mut aq_b, store_b.path(), None, true, SubprocessModel::Auto, SubprocessEffort::Auto, false );
let mut captured_b = String::new();
stderr_buf.read_to_string( &mut captured_b ).unwrap();
assert!(
captured_b.contains( "run_isolated: invoking" ),
"call B: touch must fire AGAIN for identical son_running=false state (BUG-289 loop); got:\n{captured_b}",
);
}
}
#[ test ]
fn ft07_touch_skips_non_owned_with_trace()
{
use std::io::Read;
let store = tempfile::TempDir::new().unwrap();
let mut aq = mk_aq_with_resets_at( None );
aq.is_owned = false;
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch( &mut aq, store.path(), None, true, SubprocessModel::Auto, SubprocessEffort::Auto, false );
let mut captured = String::new();
stderr_buf.read_to_string( &mut captured ).unwrap();
assert!(
captured.contains( "not owned" ),
"FT-07: G4 gate must emit 'not owned' trace line; got:\n{captured}",
);
assert!(
captured.contains( "[trace] touch" ),
"FT-07: trace line must start with '[trace] touch'; got:\n{captured}",
);
}
#[ test ]
fn ec8_solo_gate_skips_non_current_with_trace()
{
use std::io::Read;
let store = tempfile::TempDir::new().unwrap();
let mut aq = mk_aq_with_resets_at( None );
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch( &mut aq, store.path(), None, true, SubprocessModel::Auto, SubprocessEffort::Auto, true );
let mut captured = String::new();
stderr_buf.read_to_string( &mut captured ).unwrap();
assert!(
captured.contains( "solo-skip" ),
"EC-8: solo gate must emit 'solo-skip' trace for non-current account; got:\n{captured}",
);
assert!(
captured.contains( "test@example.com" ),
"EC-8: trace must name the skipped account; got:\n{captured}",
);
assert!(
!captured.contains( "read credentials:" ),
"EC-8: solo gate must fire BEFORE credential read; got:\n{captured}",
);
}
#[ doc = "bug_reproducer(BUG-302)" ]
#[ test ]
fn ft_touch_skips_occupied_elsewhere_with_trace()
{
use std::io::Read;
let store = tempfile::TempDir::new().unwrap();
let mut aq = mk_aq_with_resets_at( None );
aq.is_owned = true;
aq.is_occupied_elsewhere = true;
let _stderr_guard = crate::usage::test_support::STDERR_LOCK.lock().unwrap_or_else( std::sync::PoisonError::into_inner );
let mut stderr_buf = gag::BufferRedirect::stderr().expect( "stderr capture failed" );
apply_touch( &mut aq, store.path(), None, true, SubprocessModel::Auto, SubprocessEffort::Auto, false );
let mut captured = String::new();
stderr_buf.read_to_string( &mut captured ).unwrap();
assert!(
captured.contains( "occupied elsewhere" ),
"FT-22: occupancy guard must emit 'occupied elsewhere' trace line; got:\n{captured}",
);
assert!(
captured.contains( "[trace] touch" ),
"FT-22: trace line must start with '[trace] touch'; got:\n{captured}",
);
assert!(
!captured.contains( "run_isolated: invoking" ),
"FT-22: no subprocess must be spawned for occupied-elsewhere account; got:\n{captured}",
);
}