use crate::cli_runner::{
BIN,
run_cs, run_cs_with_env, run_cs_without_home, run_cs_bytes_for_secs,
stdout, stderr, assert_exit,
write_account, write_account_with_token, write_claude_json, live_active_token, write_account_owner,
write_live_credentials_with_token, write_account_renewal_json, write_account_profile_json,
require_live_api,
FAR_FUTURE_MS, PAST_MS,
};
use tempfile::TempDir;
#[ test ]
fn it001_lim_it_quota_heading_and_columns()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it001: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "myaccount", &token, true );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "Quota" ), "must contain 'Quota' heading, got:\n{text}" );
assert!( text.contains( "Expires" ), "must contain 'Expires' column, got:\n{text}" );
assert!( text.contains( "5h Left" ), "must contain '5h Left' column, got:\n{text}" );
assert!( text.contains( "5h Reset" ), "must contain '5h Reset' column, got:\n{text}" );
assert!( text.contains( "7d Left" ), "must contain '7d Left' column, got:\n{text}" );
assert!( text.contains( "7d(Son)" ), "must contain '7d(Son)' column, got:\n{text}" );
assert!( text.contains( "7d Reset" ), "must contain '7d Reset' column, got:\n{text}" );
assert!(
!text.contains( "Session (5h)" ),
"must NOT contain old 'Session (5h)' column, got:\n{text}",
);
assert!(
!text.contains( "Weekly (7d)" ),
"must NOT contain old 'Weekly (7d)' column, got:\n{text}",
);
assert!(
!text.contains( "Status" ),
"must NOT contain old 'Status' column, got:\n{text}",
);
}
#[ test ]
fn it002_lim_it_active_account_marked()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it002: no live token — skipping" );
return;
};
if !require_live_api( "it002" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_live_credentials_with_token( dir.path(), &token );
write_account_with_token( dir.path(), "acct-a", &token, true );
write_account_with_token( dir.path(), "acct-b", "dummy_inactive_token", false );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let active_marked = text.lines().any( |line| line.contains( '✓' ) && line.contains( "acct-a" ) );
assert!(
active_marked,
"a line must contain both ✓ and active account name 'acct-a', got:\n{text}",
);
let inactive_marked = text.lines().any( |line| line.contains( '✓' ) && line.contains( "acct-b" ) );
assert!(
!inactive_marked,
"no line must contain both ✓ and inactive account name 'acct-b', got:\n{text}",
);
}
#[ test ]
fn it003_failed_token_shows_dash_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "no-token", "max", "default", FAR_FUTURE_MS, true );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( '\u{2014}' ),
"missing accessToken must render as em-dash (\u{2014}), got:\n{text}",
);
assert!(
text.contains( "in " ),
"Expires must show 'in …' (not 'EXPIRED') for FAR_FUTURE_MS token, got:\n{text}",
);
}
#[ test ]
fn it004_lim_it_json_format_valid_array()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it004: no live token — skipping" );
return;
};
if !require_live_api( "it004" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "myaccount", &token, true );
let out = run_cs_with_env( &[ ".usage", "format::json" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let parsed : serde_json::Value = serde_json::from_str( text.trim() )
.unwrap_or_else( |e| panic!( "output must be valid JSON: {e}\ngot:\n{text}" ) );
assert!( parsed.is_array(), "output must be a JSON array, got:\n{text}" );
let arr = parsed.as_array().unwrap();
assert!( !arr.is_empty(), "array must have at least one entry, got:\n{text}" );
assert!( arr[ 0 ][ "account" ].is_string(), "entry must have 'account' string, got:\n{text}" );
assert!( arr[ 0 ][ "is_active" ].is_boolean(), "entry must have 'is_active' boolean, got:\n{text}" );
assert!( arr[ 0 ][ "expires_in_secs" ].is_number(), "entry must have 'expires_in_secs' number, got:\n{text}" );
assert!(
arr[ 0 ][ "session_5h_left_pct" ].is_number() || arr[ 0 ][ "session_5h_left_pct" ].is_null(),
"entry must have 'session_5h_left_pct' number or null, got:\n{text}",
);
let obj = arr[ 0 ].as_object().unwrap();
assert!(
obj.contains_key( "weekly_7d_sonnet_left_pct" ),
"entry must have 'weekly_7d_sonnet_left_pct' field, got:\n{text}",
);
assert!(
!obj.contains_key( "session_5h_pct" ),
"entry must NOT have old 'session_5h_pct' field, got:\n{text}",
);
assert!(
!obj.contains_key( "status" ),
"entry must NOT have old 'status' field, got:\n{text}",
);
}
#[ test ]
fn it005_empty_store_shows_no_accounts()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path()
.join( ".persistent" )
.join( "claude" )
.join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"empty store must show '(no accounts configured)', got:\n{text}",
);
}
#[ cfg( unix ) ]
#[ test ]
fn it006_unreadable_store_exits_2()
{
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path()
.join( ".persistent" )
.join( "claude" )
.join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
std::fs::set_permissions( &store, std::fs::Permissions::from_mode( 0o000 ) ).unwrap();
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
std::fs::set_permissions( &store, std::fs::Permissions::from_mode( 0o755 ) ).unwrap();
assert_exit( &out, 2 );
assert!( !stderr( &out ).is_empty(), "unreadable store must produce error on stderr" );
}
#[ test ]
fn it007_home_unset_exits_2()
{
let out = run_cs_without_home( &[ ".usage" ] );
assert_exit( &out, 2 );
assert!( !stderr( &out ).is_empty(), "unset HOME must produce error on stderr" );
}
#[ test ]
fn it008_lim_it_accounts_in_alpha_order()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it008: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "charlie", &token, false );
write_account_with_token( dir.path(), "alpha", &token, true );
write_account_with_token( dir.path(), "bravo", &token, false );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let pos_alpha = text.find( "alpha" ).expect( "output must contain 'alpha'" );
let pos_bravo = text.find( "bravo" ).expect( "output must contain 'bravo'" );
let pos_charlie = text.find( "charlie" ).expect( "output must contain 'charlie'" );
assert!(
pos_alpha < pos_bravo && pos_bravo < pos_charlie,
"accounts must appear alphabetically (alpha < bravo < charlie), got:\n{text}",
);
}
#[ cfg( unix ) ]
#[ test ]
fn it009_unreadable_credentials_shows_dash()
{
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path()
.join( ".persistent" )
.join( "claude" )
.join( "credential" );
write_account( dir.path(), "locked", "max", "default", FAR_FUTURE_MS, true );
let creds = store.join( "locked.credentials.json" );
std::fs::set_permissions( &creds, std::fs::Permissions::from_mode( 0o000 ) ).unwrap();
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
std::fs::set_permissions( &creds, std::fs::Permissions::from_mode( 0o644 ) ).unwrap();
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( '\u{2014}' ),
"unreadable credentials must render as em-dash (\u{2014}), got:\n{text}",
);
}
#[ test ]
fn it010_expired_token_shows_expired_in_expires_col()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "old-acct", "max", "default", PAST_MS, true );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "EXPIRED" ),
"expired token must show 'EXPIRED' in Expires column, got:\n{text}",
);
}
#[ test ]
fn it011_lim_it_recommendation_marker_shown()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it011: no live token — skipping" );
return;
};
if !require_live_api( "it011" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a", &token, true );
write_account_with_token( dir.path(), "acct-b", &token, false );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let rec_marked = text.lines().any( |line| line.contains( '→' ) && line.contains( "acct-b" ) );
assert!(
rec_marked,
"sort::renew: a line must contain both → and non-active account 'acct-b', got:\n{text}",
);
let active_rec = text.lines().any( |line| line.contains( '→' ) && line.contains( "acct-a" ) );
assert!(
!active_rec,
"active account 'acct-a' must not be marked with →, got:\n{text}",
);
}
#[ test ]
fn it012_lim_it_footer_shows_valid_count()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it012: no live token — skipping" );
return;
};
if !require_live_api( "it012" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a", &token, true );
write_account_with_token( dir.path(), "acct-b", &token, false );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "Valid: 2" ),
"footer must contain 'Valid: 2', got:\n{text}",
);
assert!(
text.contains( "Next:" ),
"footer must contain 'Next:', got:\n{text}",
);
}
#[ test ]
fn it013_active_divergence_shows_star()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "alice@acme.com", "tok-alice", true );
write_account_with_token( dir.path(), "work@acme.com", "tok-work", false );
write_live_credentials_with_token( dir.path(), "tok-work" );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let work_current = text.lines().any( |l| l.contains( '\u{2713}' ) && l.contains( "work@acme.com" ) );
assert!( work_current, "work@acme.com must have ✓ (is_current), got:\n{text}" );
let alice_active = text.lines().any( |l| l.contains( '*' ) && l.contains( "alice@acme.com" ) );
assert!( alice_active, "alice@acme.com must have * (is_active, not current), got:\n{text}" );
}
#[ test ]
fn it014_creds_unreadable_no_checkmark_star_shown()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "alice@acme.com", "tok-alice", true );
write_account_with_token( dir.path(), "work@acme.com", "tok-work", false );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let has_checkmark = text.lines().any( |l| l.contains( '\u{2713}' ) );
assert!( !has_checkmark, "no ✓ when creds file absent, got:\n{text}" );
let alice_star = text.lines().any( |l| l.contains( '*' ) && l.contains( "alice@acme.com" ) );
assert!( alice_star, "alice@acme.com must still have * (is_active), got:\n{text}" );
}
#[ test ]
fn it015_current_equals_active_no_star()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "alice@acme.com", "tok-alice", true );
write_live_credentials_with_token( dir.path(), "tok-alice" );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let alice_current = text.lines().any( |l| l.contains( '\u{2713}' ) && l.contains( "alice@acme.com" ) );
assert!( alice_current, "alice@acme.com must have ✓ when both current and active, got:\n{text}" );
let has_star = text.lines().any( |l| l.contains( '*' ) );
assert!( !has_star, "no * when current == active (no divergence), got:\n{text}" );
}
#[ test ]
fn it016_json_is_current_is_active()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "alice@acme.com", "tok-alice", true );
write_account_with_token( dir.path(), "work@acme.com", "tok-work", false );
write_live_credentials_with_token( dir.path(), "tok-work" );
let out = run_cs_with_env( &[ ".usage", "format::json" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let json = stdout( &out );
assert!( json.contains( "\"is_current\"" ), "JSON must have is_current field, got:\n{json}" );
assert!( json.contains( "\"is_active\"" ), "JSON must have is_active field, got:\n{json}" );
assert!( !json.contains( "\"active\"" ), "JSON must not have old 'active' field, got:\n{json}" );
let work_current = json.contains( "\"work@acme.com\"" ) && json.contains( "\"is_current\":true" );
assert!( work_current, "work@acme.com must have is_current:true, got:\n{json}" );
let alice_active = json.contains( "\"alice@acme.com\"" ) && json.contains( "\"is_active\":true" );
assert!( alice_active, "alice@acme.com must have is_active:true, got:\n{json}" );
}
#[ test ]
fn it018_synthetic_row_when_no_saved_match()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "alice@acme.com", "tok-alice", false );
write_live_credentials_with_token( dir.path(), "tok-unsaved" );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(current session)" ),
"must show synthetic (current session) row, got:\n{text}",
);
let synthetic_current = text.lines().any( |l|
l.contains( '\u{2713}' ) && l.contains( "(current session)" )
);
assert!( synthetic_current, "synthetic row must have ✓ flag, got:\n{text}" );
let alice_current = text.lines().any( |l|
l.contains( '\u{2713}' ) && l.contains( "alice@acme.com" )
);
assert!( !alice_current, "alice must NOT have ✓ when unsaved session is live, got:\n{text}" );
}
#[ test ]
fn it017_format_table_rejected()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "format::table" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ test ]
fn it019_refresh_disabled_param_accepted()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "refresh::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "no accounts" ),
"expected no-accounts message with refresh::0, got:\n{text}",
);
}
#[ test ]
fn it020_refresh_enabled_offline_no_retry_triggered()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "test-acct", "max", "default", FAR_FUTURE_MS, false ); let out = run_cs_with_env( &[ ".usage", "refresh::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "test-acct" ),
"account name must appear in output, got:\n{text}",
);
}
#[ test ]
fn it021_lim_it_live_mode()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it021: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "myaccount", &token, true );
let bytes = run_cs_bytes_for_secs(
&[ ".usage", "live::1", "interval::30", "jitter::0" ],
&[ ( "HOME", home ) ],
10,
);
let text = String::from_utf8_lossy( &bytes );
assert!(
text.contains( "Next update" ),
"live mode must emit countdown footer 'Next update ...', got:\n{text}",
);
}
#[ test ]
fn it022_live_jitter_exceeds_interval()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "live::1", "interval::60", "jitter::70" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
assert!(
!stderr( &out ).is_empty(),
"jitter > interval must produce error on stderr",
);
}
#[ test ]
fn it023_live_interval_below_minimum()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "live::1", "interval::5", "jitter::0" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "30" ),
"interval-too-small error must mention the minimum (30), got:\n{err}",
);
}
#[ test ]
fn it024_live_incompatible_with_json()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "live::1", "format::json" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
assert!(
!stderr( &out ).is_empty(),
"live + json must produce error on stderr",
);
}
#[ test ]
fn it025_synthetic_row_uses_claude_json_email()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "alice@acme.com", "tok-alice", false );
write_live_credentials_with_token( dir.path(), "tok-unsaved" );
write_claude_json( dir.path(), "unsaved@example.com" );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "unsaved@example.com" ),
"synthetic row must use emailAddress from .claude.json, got:\n{text}",
);
assert!(
!text.contains( "(current session)" ),
"must NOT fall back to '(current session)' when .claude.json has emailAddress, got:\n{text}",
);
let synthetic_current = text.lines().any( |l|
l.contains( '\u{2713}' ) && l.contains( "unsaved@example.com" )
);
assert!( synthetic_current, "synthetic row must carry ✓ flag, got:\n{text}" );
}
#[ cfg( unix ) ]
#[ test ]
fn it026_live_jitter_equals_interval_accepted()
{
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
std::fs::set_permissions( &store, std::fs::Permissions::from_mode( 0o000 ) ).unwrap();
let out = run_cs_with_env(
&[ ".usage", "live::1", "interval::30", "jitter::30" ],
&[ ( "HOME", home ) ],
);
std::fs::set_permissions( &store, std::fs::Permissions::from_mode( 0o755 ) ).unwrap();
assert_exit( &out, 2 );
let err = stderr( &out );
assert!(
!err.contains( "jitter" ),
"jitter == interval must not trigger the guard, stderr:\n{err}",
);
}
#[ test ]
fn it027_json_error_field_on_failed_account()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "no-token@acme.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "format::json" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let json = stdout( &out );
assert!(
json.contains( "\"error\":" ),
"failed account must produce JSON with 'error' key, got:\n{json}",
);
assert!(
!json.contains( "session_5h_left_pct" ),
"failed account must NOT have quota fields, got:\n{json}",
);
assert!( json.contains( "\"is_current\"" ), "must have is_current, got:\n{json}" );
assert!( json.contains( "\"is_active\"" ), "must have is_active, got:\n{json}" );
assert!( json.contains( "\"expires_in_secs\"" ), "must have expires_in_secs, got:\n{json}" );
}
#[ test ]
fn it028_interval_jitter_ignored_when_not_live()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env(
&[ ".usage", "interval::5", "jitter::70" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "no accounts" ),
"non-live mode must ignore interval/jitter and show no-accounts message, got:\n{text}",
);
}
#[ cfg( unix ) ]
#[ test ]
fn it030_live_sigint_exits_0()
{
use std::process::Stdio;
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "myaccount", "max", "default", FAR_FUTURE_MS, true );
let child = std::process::Command::new( BIN )
.args( [ ".usage", "live::1", "interval::30", "jitter::0" ] )
.env( "HOME", home )
.env_remove( "PRO" )
.stdout( Stdio::piped() )
.stderr( Stdio::piped() )
.spawn()
.expect( "failed to spawn clp binary" );
std::thread::sleep( core::time::Duration::from_secs( 3 ) );
let _ = std::process::Command::new( "kill" )
.args( [ "-INT", &child.id().to_string() ] )
.status();
let out = child.wait_with_output().expect( "failed to wait on clp binary" );
let text = String::from_utf8_lossy( &out.stdout );
assert_eq!(
out.status.code(),
Some( 0 ),
"SIGINT must cause clean exit 0, got: {:?}\nstdout: {text}\nstderr: {}",
out.status,
String::from_utf8_lossy( &out.stderr ),
);
assert!(
text.contains( "Monitor stopped." ),
"clean SIGINT exit must print 'Monitor stopped.', got:\n{text}",
);
}
#[ cfg( unix ) ]
#[ test ]
fn it029_live_default_interval_accepted()
{
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
std::fs::set_permissions( &store, std::fs::Permissions::from_mode( 0o000 ) ).unwrap();
let out = run_cs_with_env(
&[ ".usage", "live::1" ],
&[ ( "HOME", home ) ],
);
std::fs::set_permissions( &store, std::fs::Permissions::from_mode( 0o755 ) ).unwrap();
assert_exit( &out, 2 );
let err = stderr( &out );
assert!(
!err.contains( "interval" ),
"default interval (30) must not trigger the interval guard, stderr:\n{err}",
);
}
#[ test ]
fn it031_usage_help_shows_live_params()
{
let out = run_cs( &[ ".usage.help" ] );
assert_exit( &out, 0 );
let text = stdout( &out );
for param in &[ "live", "interval", "jitter" ]
{
assert!(
text.contains( param ),
".usage.help must list param `{param}` (AC-32), got:\n{text}",
);
}
}
#[ doc = "bug_reproducer(BUG-279)" ]
#[ test ]
fn it033_mre_refresh_help_excludes_429()
{
let out = run_cs( &[ ".usage.help" ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "401/403" ),
"refresh description must mention 401/403, got:\n{text}",
);
assert!(
!text.contains( "401/403/429" ),
"refresh description must NOT mention 429 (task 150 removed it), got:\n{text}",
);
}
#[ test ]
fn it032_lim_it_refresh_per_account()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it032: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "test-acct", &token, true );
write_live_credentials_with_token( dir.path(), &token );
let out = run_cs_with_env( &[ ".usage", "refresh::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "test-acct" ),
"account must appear in output with refresh::1 (AC-19), got:\n{text}",
);
}
#[ test ]
fn it034_trace_param_writes_to_stderr()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "trace-acct", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "trace::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let err = stderr( &out );
assert!(
err.contains( "[trace]" ),
"trace::1 must write [trace] lines to stderr, got:\n{err}",
);
assert!(
err.contains( "trace-acct" ),
"trace::1 must mention the account name, got:\n{err}",
);
}
#[ test ]
fn it035_empty_store_json_format()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path()
.join( ".persistent" )
.join( "claude" )
.join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "format::json" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert_eq!(
text.trim(),
"[]",
"empty store with format::json must output '[]', got:\n{text}",
);
}
#[ doc = "bug_reproducer(BUG-155)" ]
#[ test ]
fn it037_mre_bug155_refresh_defaults_to_1()
{
let out = run_cs( &[ ".usage.help" ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "1 = enabled, default" ),
"refresh help must indicate 1 is the default (BUG-155), got:\n{text}",
);
assert!(
!text.contains( "0 = disabled, default" ),
"refresh help must NOT say '0 = disabled, default' (BUG-155), got:\n{text}",
);
}
#[ doc = "bug_reproducer(BUG-156)" ]
#[ test ]
fn it038_mre_bug156_refresh_help_mentions_429_expired()
{
let out = run_cs( &[ ".usage.help" ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "429" ),
"refresh help must mention 429 case (BUG-156), got:\n{text}",
);
assert!(
!text.contains( "401/403/429" ),
"refresh help must NOT say '401/403/429' (old incorrect format), got:\n{text}",
);
}
#[ test ]
fn it036_no_footer_when_no_valid_accounts()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "no-quota@test.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
!text.contains( "Valid:" ),
"single failed account must NOT show 'Valid:' footer line, got:\n{text}",
);
}
#[ test ]
fn it039_refresh_2_rejected()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "refresh::2" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
assert!(
!stderr( &out ).is_empty(),
"refresh::2 must produce error on stderr",
);
}
#[ test ]
fn it040_refresh_yes_rejected()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "refresh::yes" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
assert!(
!stderr( &out ).is_empty(),
"refresh::yes must produce error on stderr",
);
}
#[ test ]
fn it041_live_0_single_fetch_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "live::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
!text.contains( "Next update" ),
"live::0 must not emit countdown footer, got:\n{text}",
);
}
#[ test ]
fn it042_live_2_rejected()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "live::2" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
assert!(
!stderr( &out ).is_empty(),
"live::2 must produce error on stderr",
);
}
#[ test ]
fn it043_live_yes_rejected()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "live::yes" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
assert!(
!stderr( &out ).is_empty(),
"live::yes must produce error on stderr",
);
}
#[ test ]
fn it044_interval_abc_rejected()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "interval::abc" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
assert!(
!stderr( &out ).is_empty(),
"interval::abc must produce error on stderr",
);
}
#[ cfg( unix ) ]
#[ test ]
fn it045_interval_60_live_accepted()
{
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
std::fs::set_permissions( &store, std::fs::Permissions::from_mode( 0o000 ) ).unwrap();
let out = run_cs_with_env(
&[ ".usage", "live::1", "interval::60" ],
&[ ( "HOME", home ) ],
);
std::fs::set_permissions( &store, std::fs::Permissions::from_mode( 0o755 ) ).unwrap();
assert_exit( &out, 2 );
let err = stderr( &out );
assert!(
!err.contains( "interval" ),
"interval::60 must not trigger the interval guard, stderr:\n{err}",
);
}
#[ cfg( unix ) ]
#[ test ]
fn it046_jitter_0_explicit_live_accepted()
{
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
std::fs::set_permissions( &store, std::fs::Permissions::from_mode( 0o000 ) ).unwrap();
let out = run_cs_with_env(
&[ ".usage", "live::1", "jitter::0" ],
&[ ( "HOME", home ) ],
);
std::fs::set_permissions( &store, std::fs::Permissions::from_mode( 0o755 ) ).unwrap();
assert_exit( &out, 2 );
let err = stderr( &out );
assert!(
!err.contains( "jitter" ),
"explicit jitter::0 must not trigger the jitter guard, stderr:\n{err}",
);
}
#[ cfg( unix ) ]
#[ test ]
fn it047_jitter_10_live_accepted()
{
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
std::fs::set_permissions( &store, std::fs::Permissions::from_mode( 0o000 ) ).unwrap();
let out = run_cs_with_env(
&[ ".usage", "live::1", "interval::30", "jitter::10" ],
&[ ( "HOME", home ) ],
);
std::fs::set_permissions( &store, std::fs::Permissions::from_mode( 0o755 ) ).unwrap();
assert_exit( &out, 2 );
let err = stderr( &out );
assert!(
!err.contains( "jitter" ),
"jitter::10 with interval::30 must not trigger the jitter guard, stderr:\n{err}",
);
}
#[ test ]
fn it048_jitter_abc_rejected()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "jitter::abc" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
assert!(
!stderr( &out ).is_empty(),
"jitter::abc must produce error on stderr",
);
}
#[ test ]
fn it049_trace_0_no_trace_on_stderr()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "trace-off-acct", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "trace::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let err = stderr( &out );
assert!(
!err.contains( "[trace]" ),
"trace::0 must not emit [trace] lines on stderr, got:\n{err}",
);
}
#[ test ]
fn it050_trace_2_rejected()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "trace::2" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
assert!(
!stderr( &out ).is_empty(),
"trace::2 must produce error on stderr",
);
}
#[ test ]
fn it051_trace_yes_rejected()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "trace::yes" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
assert!(
!stderr( &out ).is_empty(),
"trace::yes must produce error on stderr",
);
}
#[ test ]
fn it052_trace_default_off()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "no-trace-acct", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let err = stderr( &out );
assert!(
!err.contains( "[trace]" ),
"default (no trace:: param) must not emit [trace] lines on stderr, got:\n{err}",
);
}
#[ test ]
fn it053_sort_name_accepted()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "sort::name" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"sort::name must be accepted and show no-accounts message, got:\n{text}",
);
}
#[ test ]
fn it056_sort_renew_accepted()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "sort::renew" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"sort::renew must be accepted and show no-accounts message, got:\n{text}",
);
}
#[ test ]
fn it057_sort_invalid_value_exit_1()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "sort::bogus" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
let err = stderr( &out );
for value in &[ "name", "renew", "renews" ]
{
assert!(
err.contains( value ),
"sort::bogus error must name valid value `{value}` (AC-09), got:\n{err}",
);
}
}
#[ test ]
fn it058_prefer_invalid_value_exit_1()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "prefer::bogus" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
let err = stderr( &out );
for value in &[ "any", "opus", "sonnet" ]
{
assert!(
err.contains( value ),
"prefer::bogus error must name valid value `{value}` (AC-10), got:\n{err}",
);
}
}
#[ test ]
fn it059_usage_help_shows_sort_params()
{
let out = run_cs( &[ ".usage.help" ] );
assert_exit( &out, 0 );
let text = stdout( &out );
for param in &[ "sort", "desc", "prefer" ]
{
assert!(
text.contains( param ),
".usage.help must list param `{param}` (IT-50), got:\n{text}",
);
}
}
#[ test ]
fn it060_desc_0_accepted()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "desc::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"desc::0 must be accepted and show no-accounts message, got:\n{text}",
);
}
#[ test ]
fn it061_desc_1_accepted()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "desc::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"desc::1 must be accepted and show no-accounts message, got:\n{text}",
);
}
#[ test ]
fn it062_desc_2_rejected()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "desc::2" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
assert!( !stderr( &out ).is_empty(), "desc::2 must produce error on stderr" );
}
#[ test ]
fn it063_sort_name_desc_0_identical_to_sort_name()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "a@x.com", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "z@x.com", "max", "default", FAR_FUTURE_MS, false );
let out_default = run_cs_with_env( &[ ".usage", "sort::name" ], &[ ( "HOME", home ) ] );
let out_explicit = run_cs_with_env( &[ ".usage", "sort::name", "desc::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out_default, 0 );
assert_exit( &out_explicit, 0 );
let text_d = stdout( &out_default );
let text_e = stdout( &out_explicit );
let a_d = text_d.find( "a@x.com" ).expect( "a@x.com must appear in sort::name output" );
let z_d = text_d.find( "z@x.com" ).expect( "z@x.com must appear in sort::name output" );
let a_e = text_e.find( "a@x.com" ).expect( "a@x.com must appear in sort::name desc::0 output" );
let z_e = text_e.find( "z@x.com" ).expect( "z@x.com must appear in sort::name desc::0 output" );
assert!(
a_d < z_d,
"sort::name must show a@x.com before z@x.com (ascending), got:\n{text_d}",
);
assert!(
a_e < z_e,
"sort::name desc::0 must show a@x.com before z@x.com (026_desc CC-1 — same as implicit default), got:\n{text_e}",
);
}
#[ test ]
fn it064_sort_name_desc_1_reverses_order()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "a@x.com", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "z@x.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "sort::name", "desc::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let a_pos = text.find( "a@x.com" ).expect( "a@x.com must appear in output" );
let z_pos = text.find( "z@x.com" ).expect( "z@x.com must appear in output" );
assert!(
z_pos < a_pos,
"sort::name desc::1 must show z@x.com before a@x.com (026_desc CC-2 — reversed from ascending default), got:\n{text}",
);
}
#[ test ]
fn it065_prefer_any_accepted()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "prefer::any" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"prefer::any must be accepted and show no-accounts message, got:\n{text}",
);
}
#[ test ]
fn it066_prefer_opus_accepted()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "prefer::opus" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"prefer::opus must be accepted and show no-accounts message, got:\n{text}",
);
}
#[ test ]
fn it067_prefer_sonnet_accepted()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "prefer::sonnet" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"prefer::sonnet must be accepted and show no-accounts message, got:\n{text}",
);
}
#[ test ]
fn it068_sort_json_unaffected_by_sort_strategy()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "b@x.com", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "a@x.com", "max", "default", FAR_FUTURE_MS, false );
let out_name = run_cs_with_env( &[ ".usage", "sort::name", "format::json" ], &[ ( "HOME", home ) ] );
let out_renews = run_cs_with_env( &[ ".usage", "sort::renews", "format::json" ], &[ ( "HOME", home ) ] );
assert_exit( &out_name, 0 );
assert_exit( &out_renews, 0 );
let json_name = stdout( &out_name );
let json_renews = stdout( &out_renews );
let a_n = json_name.find( "a@x.com" ).expect( "a@x.com in sort::name json" );
let b_n = json_name.find( "b@x.com" ).expect( "b@x.com in sort::name json" );
assert!(
a_n < b_n,
"sort::name format::json must place a@x.com before b@x.com (alphabetical), got:\n{json_name}",
);
let a_r = json_renews.find( "a@x.com" ).expect( "a@x.com in sort::renews json" );
let b_r = json_renews.find( "b@x.com" ).expect( "b@x.com in sort::renews json" );
assert!(
a_r < b_r,
"sort::renews format::json must place a@x.com before b@x.com (sort:: does not affect JSON, AC-13), got:\n{json_renews}",
);
}
#[ test ]
fn it069_sort_uppercase_rejected()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "sort::Name" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
assert!( !stderr( &out ).is_empty(), "sort::Name must produce error on stderr (case-sensitive parse)" );
}
#[ test ]
fn it070_prefer_uppercase_rejected()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "prefer::Opus" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
assert!( !stderr( &out ).is_empty(), "prefer::Opus must produce error on stderr (case-sensitive parse)" );
}
#[ test ]
fn it071_sort_renew_desc1_accepted()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "sort::renew", "desc::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"sort::renew desc::1 must be accepted and show no-accounts message, got:\n{text}",
);
}
#[ test ]
fn it073_next_all_rejected_exit_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "next::all" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ test ]
fn it074_next_session_rejected_exit_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "next::session" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ test ]
fn it075_next_endurance_rejected()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "next::endurance" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "sort::" ),
"next::endurance error must mention sort:: redirect, got:\n{err}",
);
}
#[ test ]
fn it076_next_drain_rejected()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "next::drain" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "sort::" ),
"next::drain error must mention sort:: redirect, got:\n{err}",
);
}
#[ test ]
fn it077_next_reset_rejected_exit_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "next::reset" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ test ]
fn it078_next_invalid_value_exit_1()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "next::bogus" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "sort::" ),
"next::bogus error must redirect to sort::, got:\n{err}",
);
for valid in &[ "name", "renew", "renews" ]
{
assert!(
err.contains( valid ),
"next::bogus error must name valid sort:: value `{valid}`, got:\n{err}",
);
}
}
#[ test ]
fn it079_default_sort_renew_no_arrow_without_valid_accounts()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "a@x.com", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "b@x.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let arrow_as_flag = text.lines().any( |l| l.trim_start().starts_with( '\u{2192}' ) );
assert!(
!arrow_as_flag,
"default sort::renew: no eligible account → must not place → flag in any table row, got:\n{text}",
);
}
#[ test ]
fn it080_cols_sub_accepted()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "cols::+sub" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"cols::+sub must be accepted and show no-accounts message, got:\n{text}",
);
}
#[ test ]
fn it081_cols_sub_shows_sub_column()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct@x.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "cols::+sub" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "Sub" ),
"cols::+sub must include the Sub column header in output, got:\n{text}",
);
}
#[ test ]
fn it082_cols_unknown_id_exit_1()
{
let dir = TempDir::new().unwrap();
let out = run_cs_with_env(
&[ ".usage", "cols::+bogus_col" ],
&[ ( "HOME", dir.path().to_str().unwrap() ) ],
);
assert_exit( &out, 1 );
let err = stderr( &out );
let mentions_valid = [ "status", "expires", "sub", "renews", "5h_left" ]
.iter()
.any( |id| err.contains( id ) );
assert!(
mentions_valid,
"cols::+bogus_col error must name at least one valid column ID, got:\n{err}",
);
}
#[ test ]
fn it083_usage_help_shows_next_cols_params()
{
let out = run_cs( &[ ".usage.help" ] );
assert_exit( &out, 0 );
let text = stdout( &out );
for param in &[ "next", "cols" ]
{
assert!(
text.contains( param ),
".usage.help must list param `{param}`, got:\n{text}",
);
}
}
#[ test ]
fn it084_sub_hidden_by_default()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct@x.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
!text.contains( "Sub" ),
"without cols::+sub, the Sub column must not appear in output, got:\n{text}",
);
}
#[ test ]
fn it085_cols_plus_7d_son_reset_shows_header()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct@x.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "cols::+7d_son_reset" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "7d Son Reset" ),
"cols::+7d_son_reset must include 7d Son Reset header, got:\n{text}",
);
}
#[ test ]
fn it086_7d_son_reset_hidden_by_default()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct@x.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
!text.contains( "7d Son Reset" ),
"without cols::+7d_son_reset, the column must not appear in output, got:\n{text}",
);
}
#[ test ]
fn it087_cols_minus_renews_hides_header()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct@x.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "cols::-renews" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
!text.contains( "~Renews" ),
"cols::-renews must hide the ~Renews column header, got:\n{text}",
);
}
#[ test ]
fn it088_cols_composite_add_and_remove()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct@x.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "cols::+sub,-7d_son" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "Sub" ), "cols::+sub must add Sub header, got:\n{text}" );
assert!( !text.contains( "7d(Son)" ), "cols::-7d_son must remove 7d(Son) header, got:\n{text}" );
}
#[ test ]
fn it089_cols_structural_cols_always_present()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "user@x.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "cols::-status,-expires,-renews,-5h_left,-5h_reset,-7d_left,-7d_son,-7d_reset" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "user@x.com" ),
"account name must always appear in output regardless of cols:: removals, got:\n{text}",
);
}
#[ test ]
fn it090_next_footer_absent_when_no_valid_accounts()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "a@x.com", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "b@x.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
!text.contains( "Valid:" ),
"footer must not appear when no accounts have valid quota data, got:\n{text}",
);
}
#[ test ]
fn it091_next_json_output_unchanged_by_next_param()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out_default = run_cs_with_env(
&[ ".usage", "format::json" ],
&[ ( "HOME", home ) ],
);
let out_renews = run_cs_with_env(
&[ ".usage", "format::json", "sort::renews" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out_default, 0 );
assert_exit( &out_renews, 0 );
assert_eq!(
stdout( &out_default ), stdout( &out_renews ),
"format::json output must be identical regardless of sort:: value (AC-13)",
);
}
#[ doc = "bug_reproducer(BUG-171)" ]
#[ test ]
fn mre_bug_171_account_populated_after_refresh()
{
let src = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/refresh.rs" ) );
let fix_present = src.contains( "Fix(BUG-171)" );
let aq_account: Option< bool > = fix_present.then_some( true );
assert!(
aq_account.is_some(),
"BUG-171: aq.account must be populated after apply_refresh() re-fetches quota; \
fix: add `if let Ok(acct) = claude_quota::fetch_oauth_account(&token) {{ aq.account = Some(acct); }}` \
after `aq.result = Ok(retried)` in apply_refresh(); \
without fix, ~Renews and Sub columns show `?` for all refreshed accounts."
);
}
#[ test ]
fn it092_next_all_rejected_exit_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "next::all" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "sort::" ),
"next::all error must redirect to sort::, got:\n{err}",
);
}
#[ test ]
fn it093_footer_not_gated_on_next_all_structural()
{
let src = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/render.rs" ) );
assert!(
!src.contains( "NextStrategy" ),
"Feature 037/038: render.rs must not reference NextStrategy; \
footer is now driven by SortStrategy from active sort:: param",
);
assert!(
!src.contains( "if next ==" ),
"Feature 037/038: footer must not be gated on a next== check",
);
}
#[ test ]
fn it094_next_session_rejected_exit_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "next::session" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "sort::" ),
"next::session error must redirect to sort::, got:\n{err}",
);
}
#[ test ]
fn it095_next_strategy_session_absent_structural()
{
let src = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/types.rs" ) );
assert!(
!src.contains( "NextStrategy::Session" ),
"TSK-184: `NextStrategy::Session` must be completely removed from source; \
check enum declaration, parse() arms, match arms, strategy arrays, and comments",
);
}
#[ test ]
fn it096_json_unaffected_by_sort_strategy()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out_default = run_cs_with_env( &[ ".usage", "format::json" ], &[ ( "HOME", home ) ] );
let out_renews = run_cs_with_env( &[ ".usage", "format::json", "sort::renews" ], &[ ( "HOME", home ) ] );
assert_exit( &out_default, 0 );
assert_exit( &out_renews, 0 );
assert_eq!(
stdout( &out_default ), stdout( &out_renews ),
"format::json output must be identical regardless of sort:: value (AC-13)",
);
}
#[ test ]
fn it097_touch_1_empty_store_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "touch::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "no accounts" ) || text.contains( "No accounts" ) || text.is_empty(),
"touch::1 with empty store must exit 0 (no subprocess spawned — no accounts), got:\n{text}",
);
}
#[ test ]
fn it098_touch_1_errored_account_skipped()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "a@x.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "touch::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "a@x.com" ),
"touch::1 with errored account must still show account row (AC-04), got:\n{text}",
);
}
#[ test ]
fn it099_apply_touch_fn_exists_structural()
{
let src = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/touch.rs" ) );
assert!(
src.contains( "fn apply_touch" ),
"TSK-185: `fn apply_touch` must be present in src/usage/touch.rs; \
add the active-window extension function that calls refresh_account_token() \
for accounts with result.is_ok() AND five_hour.resets_at.is_some()",
);
}
#[ test ]
fn it100_touch_json_format_unaffected()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out_default = run_cs_with_env(
&[ ".usage", "format::json" ],
&[ ( "HOME", home ) ],
);
let out_touch = run_cs_with_env(
&[ ".usage", "format::json", "touch::1" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out_default, 0 );
assert_exit( &out_touch, 0 );
assert_eq!(
stdout( &out_default ), stdout( &out_touch ),
"format::json output must be identical with or without touch::1 (TSK-185 AC-08)",
);
}
#[ test ]
fn it101_usage_help_shows_touch_param()
{
let out = run_cs( &[ ".usage.help" ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "touch" ),
".usage.help must list param `touch` (TSK-185 AC-10), got:\n{text}",
);
}
#[ test ]
fn it102_lim_it_sort_renew_shows_recommendation()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it102: no live token — skipping" );
return;
};
if !require_live_api( "it102" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
write_account_with_token( dir.path(), "acct-b@test.com", &token, false );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "Next (renew):" ),
"sort::renew must show 'Next (renew):' recommendation in footer (IT-51/020), got:\n{text}",
);
}
#[ test ]
fn it103_lim_it_sort_renews_shows_recommendation()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it103: no live token — skipping" );
return;
};
if !require_live_api( "it103" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
write_account_with_token( dir.path(), "acct-b@test.com", &token, false );
let out = run_cs_with_env( &[ ".usage", "sort::renews" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "Next (renews):" ),
"sort::renews must show 'Next (renews):' recommendation in footer (IT-52/020), got:\n{text}",
);
}
#[ test ]
fn it104_lim_it_footer_shows_strategy_recommendation()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it104: no live token — skipping" );
return;
};
if !require_live_api( "it104" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
write_account_with_token( dir.path(), "acct-b@test.com", &token, false );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "renew" ),
"footer must show renew strategy recommendation line (IT-54/020), got:\n{text}",
);
}
#[ test ]
fn it105_lim_it_per_column_emoji_in_5h_left()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it105: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let has_emoji = text.contains( "🟢" ) || text.contains( "🟡" ) || text.contains( "🔴" );
assert!(
has_emoji,
"5h Left / 7d Left columns must contain per-column emoji prefix (IT-58/AC-21); got:\n{text}",
);
}
#[ test ]
fn it106_touch_0_accepted_empty_store_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "touch::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "no accounts" ) || text.contains( "No accounts" ) || text.is_empty(),
"touch::0 with empty store must exit 0 without param error (IT-62/EC-1), got:\n{text}",
);
}
#[ test ]
fn it107_touch_true_accepted_empty_store_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "touch::true" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
}
#[ test ]
fn it108_touch_bogus_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "touch::bogus" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ test ]
fn it109_lim_it_touch_0_no_subprocess_idle_account_unchanged()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it109: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let pre = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &pre, 0 );
let pre_text = stdout( &pre );
if !pre_text.contains( "\u{2014}" )
{
eprintln!( "it109: account is active (resets_at present) — idle condition not met, skipping" );
return;
}
let out = run_cs_with_env( &[ ".usage", "touch::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "\u{2014}" ),
"touch::0 must not activate idle account — 5h Reset must remain as `\u{2014}` (FT-01/EC-7), got:\n{text}",
);
}
#[ test ]
fn it110_lim_it_touch_1_subprocess_spawned_for_idle_account()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it110: no live token — skipping" );
return;
};
if !require_live_api( "it110" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let pre = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &pre, 0 );
if !stdout( &pre ).contains( "\u{2014}" )
{
eprintln!( "it110: account is active (resets_at present) — idle condition not met, skipping" );
return;
}
let out = run_cs_with_env( &[ ".usage", "touch::1", "trace::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let err = stderr( &out );
assert!(
err.contains( "switch_account" ),
"touch::1 with idle account must spawn subprocess — switch_account must appear (FT-02/EC-8), got stderr:\n{err}",
);
}
#[ test ]
fn it111_lim_it_touch_1_5h_reset_changes_from_dash_to_time()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it111: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let pre = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &pre, 0 );
let pre_text = stdout( &pre );
if !pre_text.contains( "\u{2014}" )
{
eprintln!( "it111: account is active (resets_at present) — idle condition not met, skipping" );
return;
}
let out = run_cs_with_env( &[ ".usage", "touch::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "in " ),
"touch::1 must activate idle account; 5h Reset must show countdown after subprocess (FT-03), got:\n{text}",
);
}
#[ test ]
fn it112_structural_refresh_before_touch_ordering_in_source()
{
let src = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/api.rs" ) );
let refresh_pos = src.find( "apply_refresh( &mut accounts, &credential_store" )
.expect( "apply_refresh call site must exist in src/usage/api.rs" );
let touch_pos = src.find( "apply_touch( aq, &credential_store" )
.expect( "apply_touch call site must exist in src/usage/api.rs" );
assert!(
refresh_pos < touch_pos,
"apply_refresh must appear before apply_touch in run_usage() to guarantee refresh-before-touch ordering (FT-05)",
);
}
#[ test ]
fn it113_lim_it_active_account_restored_after_touch()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it113: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "alice@test.com", &token, true );
write_account_with_token( dir.path(), "acct-b@test.com", &token, false );
let pre = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &pre, 0 );
if !stdout( &pre ).contains( "\u{2014}" )
{
eprintln!( "it113: no idle accounts — idle-state condition not met, skipping" );
return;
}
let out = run_cs_with_env( &[ ".usage", "touch::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let active_file = dir.path()
.join( ".persistent" ).join( "claude" ).join( "credential" ).join( claude_profile::account::active_marker_filename() );
let active_content = std::fs::read_to_string( &active_file ).unwrap_or_default();
assert_eq!(
active_content.trim(), "alice@test.com",
"_active must remain alice@test.com after touch (never written during cycling — BUG-211), got: {active_content:?}",
);
}
#[ test ]
fn it114_structural_touch_failure_non_aborting_guard_exists()
{
let src = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/touch.rs" ) );
assert!(
src.contains( "if let Some( ref creds ) = new_creds" ),
"apply_touch must conditionally update expiry when credentials returned (FT-07 + BUG-179)",
);
}
#[ test ]
fn it115_lim_it_trace_1_shows_touch_lifecycle()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it115: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let pre = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &pre, 0 );
if !stdout( &pre ).contains( "\u{2014}" )
{
eprintln!( "it115: account is active (resets_at present) — idle condition not met, skipping" );
return;
}
let out = run_cs_with_env( &[ ".usage", "touch::1", "trace::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let err = stderr( &out );
assert!(
err.contains( "[trace]" ),
"trace::1 must emit [trace] lines for touch subprocess lifecycle (FT-09), got stderr:\n{err}",
);
}
#[ test ]
fn it116_lim_it_account_with_resets_at_absent_is_touched()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it116: no live token — skipping" );
return;
};
if !require_live_api( "it116" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let pre = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &pre, 0 );
let pre_text = stdout( &pre );
if !pre_text.contains( "\u{2014}" )
{
eprintln!( "it116: account is active (resets_at present) — idle condition not met, skipping" );
return;
}
let out = run_cs_with_env( &[ ".usage", "touch::1", "trace::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let err = stderr( &out );
assert!(
err.contains( "switch_account" ),
"idle account must be touched — switch_account must appear in stderr (FT-11), got stderr:\n{err}",
);
}
#[ test ]
fn it117_ft12_cols_plus_reveals_sub_and_7d_son_reset_columns()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct@x.com", "max", "default", FAR_FUTURE_MS, false );
let out_default = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out_default, 0 );
let text_default = stdout( &out_default );
assert!(
!text_default.contains( "Sub" ),
"default output must NOT show Sub column (FT-12/AC-22), got:\n{text_default}",
);
assert!(
!text_default.contains( "7d Son Reset" ),
"default output must NOT show 7d Son Reset column (FT-12/AC-22), got:\n{text_default}",
);
let out_sub = run_cs_with_env( &[ ".usage", "cols::+sub" ], &[ ( "HOME", home ) ] );
assert_exit( &out_sub, 0 );
let text_sub = stdout( &out_sub );
assert!(
text_sub.contains( "Sub" ),
"cols::+sub must show Sub column header (FT-12/AC-22), got:\n{text_sub}",
);
let out_son = run_cs_with_env( &[ ".usage", "cols::+7d_son_reset" ], &[ ( "HOME", home ) ] );
assert_exit( &out_son, 0 );
let text_son = stdout( &out_son );
assert!(
text_son.contains( "7d Son Reset" ),
"cols::+7d_son_reset must show 7d Son Reset column header (FT-12/AC-22), got:\n{text_son}",
);
}
#[ test ]
fn it118_touch_false_accepted_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "touch::false" ], &[ ( "HOME", home ) ] );
assert_exit(
&out, 0,
);
}
#[ test ]
fn it119_touch_2_rejected_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "touch::2" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ test ]
fn it120_lim_it_ft12_touch_trigger_fires_per_idle_account_cycle()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it120: no live token — skipping" );
return;
};
if !require_live_api( "it120" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct@test.com", &token, true );
let pre = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &pre, 0 );
if !stdout( &pre ).contains( "\u{2014}" )
{
eprintln!( "it120: account is active (resets_at present) — idle condition not met, skipping" );
return;
}
let out1 = run_cs_with_env( &[ ".usage", "touch::1", "trace::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out1, 0 );
let err1 = stderr( &out1 );
assert!(
err1.contains( "switch_account" ),
"cycle 1: idle account must trigger touch subprocess; switch_account must appear (FT-12/AC-11), got stderr:\n{err1}",
);
let out2 = run_cs_with_env( &[ ".usage", "touch::1", "trace::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out2, 0 );
let err2 = stderr( &out2 );
let text2 = stdout( &out2 );
if text2.contains( "\u{2014}" )
{
eprintln!( "it120: cycle 1 did not activate account; cycle 2 check inconclusive" );
}
else
{
assert!(
err2.contains( "skipped" ),
"cycle 2: account now active must be skipped by touch (FT-12/AC-11), got stderr:\n{err2}",
);
}
}
#[ test ]
fn it122_imodel_auto_accepted_empty_store_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "imodel::auto" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "no accounts" ) || text.contains( "No accounts" ),
"imodel::auto with empty store must exit 0 (IT-66/EC-1), got:\n{text}",
);
}
#[ test ]
fn it123_imodel_bogus_exits_1()
{
let out = run_cs( &[ ".usage", "imodel::bogus" ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!( err.contains( "auto" ), "stderr must name valid value 'auto', got:\n{err}" );
assert!( err.contains( "sonnet" ), "stderr must name valid value 'sonnet', got:\n{err}" );
assert!( err.contains( "opus" ), "stderr must name valid value 'opus', got:\n{err}" );
assert!( err.contains( "keep" ), "stderr must name valid value 'keep', got:\n{err}" );
assert!( err.contains( "haiku" ), "stderr must name valid value 'haiku', got:\n{err}" );
}
#[ test ]
fn it124_effort_auto_accepted_empty_store_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "effort::auto" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "no accounts" ) || text.contains( "No accounts" ),
"effort::auto with empty store must exit 0 (IT-68/EC-1), got:\n{text}",
);
}
#[ test ]
fn it125_effort_bogus_exits_1()
{
let out = run_cs( &[ ".usage", "effort::bogus" ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!( err.contains( "auto" ), "stderr must name valid value 'auto', got:\n{err}" );
assert!( err.contains( "high" ), "stderr must name valid value 'high', got:\n{err}" );
assert!( err.contains( "max" ), "stderr must name valid value 'max', got:\n{err}" );
assert!( err.contains( "low" ), "stderr must name valid value 'low', got:\n{err}" );
assert!( err.contains( "normal" ), "stderr must name valid value 'normal', got:\n{err}" );
}
#[ test ]
fn it126_usage_help_shows_imodel_effort_params()
{
let out = run_cs( &[ ".usage.help" ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "imodel" ), ".usage.help must list param `imodel` (IT-70), got:\n{text}" );
assert!( text.contains( "effort" ), ".usage.help must list param `effort` (IT-70), got:\n{text}" );
}
#[ test ]
fn it127_imodel_sonnet_accepted_empty_store_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "imodel::sonnet" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
}
#[ test ]
fn it128_imodel_opus_accepted_empty_store_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "imodel::opus" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
}
#[ test ]
fn it129_imodel_keep_accepted_empty_store_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "imodel::keep" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
}
#[ test ]
fn it130_effort_high_accepted_empty_store_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "effort::high" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
}
#[ test ]
fn it131_effort_max_accepted_empty_store_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "effort::max" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
}
#[ test ]
fn it_ft026_13_imodel_effort_both_paths_structural()
{
let touch = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/touch.rs" ) );
let refresh = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/refresh.rs" ) );
assert!(
touch.contains( "resolve_model(" ),
"026 AC-08: touch.rs must call resolve_model() so imodel:: applies to the touch path",
);
assert!(
touch.contains( "effort_pre_args(" ),
"026 AC-08: touch.rs must call effort_pre_args() so effort:: applies to the touch path",
);
assert!(
refresh.contains( "resolve_model(" ),
"026 AC-08: refresh.rs must call resolve_model() so imodel:: applies to the refresh path",
);
assert!(
refresh.contains( "effort_pre_args(" ),
"026 AC-08: refresh.rs must call effort_pre_args() so effort:: applies to the refresh path",
);
}
#[ test ]
fn it_ft026_14_imodel_effort_no_effect_on_json_schema()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "a@x.com", "max", "standard", FAR_FUTURE_MS, false );
let out_default = run_cs_with_env( &[ ".usage", "format::json" ], &[ ( "HOME", home ) ] );
let out_override = run_cs_with_env(
&[ ".usage", "imodel::opus", "effort::max", "format::json" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out_default, 0 );
assert_exit( &out_override, 0 );
assert_eq!(
stdout( &out_default ), stdout( &out_override ),
"026 AC-09: format::json output must be identical regardless of imodel::/effort:: values",
);
}
#[ test ]
fn it132_apply_touch_trigger_is_is_none_structural()
{
let src = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/touch.rs" ) );
assert!(
!src.contains( "let is_active = data.five_hour" ),
"BUG-181: `apply_touch` trigger must use `is_idle` + `is_none()`, not `is_active` + `is_some()`.\n\
Fix the guard: `let is_idle = data.five_hour.as_ref().and_then(|p| p.resets_at.as_deref()).is_none();\n\
if !is_idle {{ return; }}`",
);
}
#[ test ]
fn it133_refresh_account_token_has_label_param_structural()
{
let src = include_str!( concat!(
env!( "CARGO_MANIFEST_DIR" ),
"/../claude_profile_core/src/account.rs"
) );
assert!(
!src.contains( "[trace] refresh {name} switch_account: OK" ),
"TSK-192: `refresh_account_token()` must accept `label: &str` and use `{{label}}` in all\n\
trace `eprintln!` calls instead of the hardcoded string `\"refresh\"`.\n\
Add `label: &str` after `trace: bool` in the signature and replace all\n\
`\"[trace] refresh {{name}} ...\"` patterns with `\"[trace] {{label}} {{name}} ...\"`.",
);
}
#[ test ]
fn it134_apply_touch_passes_touch_label_structural()
{
let src = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/touch.rs" ) );
assert!(
src.contains( r#"credential_store, claude_paths, trace, "touch","# ),
"TSK-192: `apply_touch()` must pass `\"touch\"` as the label argument to `refresh_account_token()`."
);
}
#[ test ]
fn it135_apply_refresh_passes_refresh_label_structural()
{
let src = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/refresh.rs" ) );
assert!(
src.contains( r#"credential_store, claude_paths, trace, "refresh","# ),
"TSK-192: `apply_refresh()` must pass `\"refresh\"` as the label argument to `refresh_account_token()`."
);
}
#[ test ]
fn it136_refresh_account_token_has_instant_timing_structural()
{
let src = include_str!( concat!(
env!( "CARGO_MANIFEST_DIR" ),
"/../claude_profile_core/src/account.rs"
) );
assert!(
src.contains( "Instant::now()" ),
"TSK-192: `refresh_account_token()` must use `std::time::Instant::now()` for per-step timing."
);
}
#[ test ]
fn it137_sort_default_is_renew_structural()
{
let src = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/params.rs" ) );
assert!(
src.contains( "None => SortStrategy::Renew" ),
"TSK-220: sort default must be SortStrategy::Renew, not SortStrategy::Drain.\n\
Change the None arm of the sort argument match to `None => SortStrategy::Renew`."
);
}
#[ test ]
fn it141_trace_skip_lines_emitted_for_non_qualifying_accounts()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "err@x.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "touch::1", "trace::1" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let err = stderr( &out );
assert!(
err.contains( "[trace] touch err@x.com skipped (reason: error account)" ),
"BUG-202: errored account must emit `[trace] touch <name> skipped (reason: error account)` \
when trace=true (AC-09/AC-12 of Feature 024). Got stderr:\n{err}",
);
}
#[ test ]
fn it142_imodel_haiku_accepted_empty_store_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "imodel::haiku" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
}
#[ test ]
fn it143_effort_low_accepted_empty_store_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "effort::low" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
}
#[ test ]
fn it144_effort_normal_accepted_empty_store_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "effort::normal" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
}
#[ doc = "lim_it" ]
#[ test ]
fn it145_lim_it_sort_renew_places_arrow_on_soonest_refill()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it145: no live token — skipping" );
return;
};
if !require_live_api( "it145" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
write_account_with_token( dir.path(), "acct-b@test.com", &token, false );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "renew" ),
"footer must show renew strategy line (020/AC-09), got:\n{text}",
);
}
#[ test ]
fn ut_get_7d_left_extracts_bare_value()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "get::7d_left" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
!text.contains( "Quota" ) && !text.contains( "5h Left" ) && !text.contains( "7d Left" ),
"get::7d_left with empty store must produce no table output, got:\n{text}",
);
}
#[ test ]
fn ut_get_invalid_field_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "get::bogus_field" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "5h_left" ) && err.contains( "7d_left" ) && err.contains( "account" ),
"get::bogus_field must list valid field IDs in stderr, got:\n{err}",
);
}
#[ test ]
fn ut_filter_only_valid_hides_red_rows()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "only_valid::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"only_valid::1 with empty store must show no-accounts message, got:\n{text}",
);
}
#[ test ]
fn it146_next_column_visible_by_default()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alice", "max", "default", FAR_FUTURE_MS, true );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "\u{2192} Next" ),
"default .usage output must contain '→ Next' column header (AC-28), got:\n{text}",
);
}
#[ test ]
fn it147_json_renewal_secs_present()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alice", "max", "default", FAR_FUTURE_MS, true );
let out = run_cs_with_env( &[ ".usage", "format::json" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "\"renewal_secs\"" ),
"format::json must include 'renewal_secs' field (AC-29), got:\n{text}",
);
assert!(
text.contains( "\"renewal_is_estimate\"" ),
"format::json must include 'renewal_is_estimate' field (AC-29), got:\n{text}",
);
assert!(
text.contains( "\"next_event_type\"" ),
"format::json must include 'next_event_type' field (AC-29), got:\n{text}",
);
assert!(
text.contains( "\"next_event_secs\"" ),
"format::json must include 'next_event_secs' field (AC-29), got:\n{text}",
);
assert!(
!text.contains( "\"next_renewal_est\"" ),
"format::json must NOT contain deprecated 'next_renewal_est' field, got:\n{text}",
);
}
#[ test ]
fn it148_status_emoji_column_header_present()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "no-token", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( '●' ),
"table header must contain '●' status emoji column label, got:\n{text}",
);
}
#[ test ]
fn it149_status_emoji_red_on_token_error()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "no-token", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "🔴" ),
"account without accessToken must show 🔴 in table row, got:\n{text}",
);
}
#[ test ]
fn it150_status_emoji_absent_from_json()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "no-token", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "format::json" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
!text.contains( "🔴" ) && !text.contains( "🟡" ) && !text.contains( "🟢" ),
"format::json must NOT contain status emoji, got:\n{text}",
);
}
#[ test ]
fn it151_past_renewal_at_auto_advances_in_usage()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "past@renewal.com", "max", "default", FAR_FUTURE_MS, false );
write_account_renewal_json( dir.path(), "past@renewal.com", "2020-03-15T00:00:00Z" );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let renews_line = text.lines()
.find( |l| l.contains( "past@renewal.com" ) )
.expect( "usage output must have a row for past@renewal.com" );
assert!(
renews_line.contains( "in " ) && !renews_line.contains( "~in " ),
"past _renewal_at must auto-advance and show 'in Xd' (no '~'), got row:\n{renews_line}\nfull output:\n{text}",
);
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
let on_disk = std::fs::read_to_string( store.join( "past@renewal.com.json" ) ).unwrap();
assert!(
on_disk.contains( "2020-03-15T00:00:00Z" ),
"stored _renewal_at must NOT be modified by render-time auto-advance, got: {on_disk}",
);
}
#[ test ]
fn it152_tsv_next_column_present()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alice", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "format::tsv" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let header = text.lines().next().expect( "TSV must have a header row" );
let cols : Vec< &str > = header.split( '\t' ).collect();
assert!(
cols.contains( &"next" ),
"TSV header must contain 'next' column (AC-28), got cols: {cols:?}",
);
}
#[ test ]
fn it153_json_renewal_fields_with_renewal_at()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alice", "max", "default", FAR_FUTURE_MS, false );
write_account_renewal_json( dir.path(), "alice", "2099-01-01T00:00:00Z" );
let out = run_cs_with_env( &[ ".usage", "format::json" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "\"renewal_is_estimate\":false" ),
"explicit _renewal_at must yield renewal_is_estimate:false, got:\n{text}",
);
assert!(
text.contains( "\"renewal_secs\":" ) && !text.contains( "\"renewal_secs\":null" ),
"explicit _renewal_at must yield non-null renewal_secs, got:\n{text}",
);
assert!(
text.contains( "\"next_event_type\":" ) && !text.contains( "\"next_event_type\":null" ),
"with _renewal_at set, next_event_type must not be null, got:\n{text}",
);
assert!(
text.contains( "\"next_event_secs\":" ) && !text.contains( "\"next_event_secs\":null" ),
"with _renewal_at set, next_event_secs must not be null, got:\n{text}",
);
}
#[ test ]
fn it154_only_active_1_shows_active_account_row()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, true ); write_account( dir.path(), "acct-c", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "only_active::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "acct-b" ),
"only_active::1 must show the active account (acct-b), got:\n{text}",
);
assert!(
!text.contains( "acct-a" ),
"only_active::1 must hide non-active account (acct-a), got:\n{text}",
);
assert!(
!text.contains( "acct-c" ),
"only_active::1 must hide non-active account (acct-c), got:\n{text}",
);
}
#[ test ]
fn it155_only_active_0_shows_all_rows()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, true );
write_account( dir.path(), "acct-c", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "only_active::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "acct-a" ), "only_active::0 must show acct-a, got:\n{text}" );
assert!( text.contains( "acct-b" ), "only_active::0 must show acct-b, got:\n{text}" );
assert!( text.contains( "acct-c" ), "only_active::0 must show acct-c, got:\n{text}" );
}
#[ test ]
fn it156_only_active_bad_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "only_active::bad" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( '0' ) && err.contains( '1' ),
"only_active::bad stderr must name valid values (0, 1), got:\n{err}",
);
}
#[ test ]
fn it157_only_active_1_no_active_marker_shows_empty()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-c", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "only_active::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"only_active::1 with no active account must show no-accounts message, got:\n{text}",
);
}
#[ test ]
fn it158_only_active_true_accepted()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, true );
let out = run_cs_with_env( &[ ".usage", "only_active::true" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "acct-b" ),
"only_active::true must show active account (acct-b), got:\n{text}",
);
assert!(
!text.contains( "acct-a" ),
"only_active::true must hide non-active account (acct-a), got:\n{text}",
);
}
#[ test ]
fn it159_only_active_false_shows_all_rows()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, true );
let out = run_cs_with_env( &[ ".usage", "only_active::false" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "acct-a" ), "only_active::false must show acct-a, got:\n{text}" );
assert!( text.contains( "acct-b" ), "only_active::false must show acct-b, got:\n{text}" );
}
#[ test ]
fn it160_only_next_1_no_valid_accounts_shows_empty()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "only_next::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"only_next::1 with all-error accounts must show no-accounts message, got:\n{text}",
);
}
#[ test ]
fn it161_only_next_bad_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "only_next::bad" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( '0' ) && err.contains( '1' ),
"only_next::bad stderr must name valid values, got:\n{err}",
);
}
#[ test ]
fn it162_only_next_0_shows_all_rows()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "only_next::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "acct-a" ), "only_next::0 must show acct-a, got:\n{text}" );
assert!( text.contains( "acct-b" ), "only_next::0 must show acct-b, got:\n{text}" );
}
#[ test ]
fn it163_min_5h_0_shows_all_rows()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "min_5h::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "acct-a" ), "min_5h::0 must show acct-a, got:\n{text}" );
}
#[ test ]
fn it164_min_5h_abc_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "min_5h::abc" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ test ]
fn it165_min_5h_101_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "min_5h::101" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ test ]
fn it166_min_7d_0_shows_all_rows()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "min_7d::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "acct-a" ), "min_7d::0 must show acct-a, got:\n{text}" );
}
#[ test ]
fn it167_min_7d_abc_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "min_7d::abc" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ test ]
fn it168_min_7d_101_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "min_7d::101" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ test ]
fn it169_only_valid_0_shows_all_rows()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-c", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "only_valid::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "acct-a" ), "only_valid::0 must show acct-a, got:\n{text}" );
assert!( text.contains( "acct-b" ), "only_valid::0 must show acct-b, got:\n{text}" );
assert!( text.contains( "acct-c" ), "only_valid::0 must show acct-c, got:\n{text}" );
}
#[ test ]
fn it170_only_valid_bad_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "only_valid::bad" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( '0' ) && err.contains( '1' ),
"only_valid::bad stderr must name valid values, got:\n{err}",
);
}
#[ test ]
fn it171_only_valid_1_all_red_shows_empty()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "only_valid::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"only_valid::1 with all-error accounts must show no-accounts message, got:\n{text}",
);
}
#[ test ]
fn it172_only_valid_true_accepted()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "only_valid::true" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"only_valid::true must be accepted and filter error accounts, got:\n{text}",
);
}
#[ test ]
fn it173_only_valid_false_shows_all_rows()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-c", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "only_valid::false" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "acct-a" ), "only_valid::false must show acct-a, got:\n{text}" );
assert!( text.contains( "acct-b" ), "only_valid::false must show acct-b, got:\n{text}" );
assert!( text.contains( "acct-c" ), "only_valid::false must show acct-c, got:\n{text}" );
}
#[ test ]
fn it174_exclude_exhausted_0_shows_all_rows()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-c", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "exclude_exhausted::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "acct-a" ), "exclude_exhausted::0 must show acct-a, got:\n{text}" );
assert!( text.contains( "acct-b" ), "exclude_exhausted::0 must show acct-b, got:\n{text}" );
assert!( text.contains( "acct-c" ), "exclude_exhausted::0 must show acct-c, got:\n{text}" );
}
#[ test ]
fn it175_exclude_exhausted_bad_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "exclude_exhausted::bad" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( '0' ) && err.contains( '1' ),
"exclude_exhausted::bad stderr must name valid values, got:\n{err}",
);
}
#[ test ]
fn it176_exclude_exhausted_1_all_red_shows_empty()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "exclude_exhausted::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"exclude_exhausted::1 with all-error accounts must show no-accounts message, got:\n{text}",
);
}
#[ test ]
fn it177_exclude_exhausted_true_accepted()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "exclude_exhausted::true" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"exclude_exhausted::true must be accepted and filter error accounts, got:\n{text}",
);
}
#[ test ]
fn it178_count_3_shows_first_3_rows()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-c", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-d", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-e", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "count::3", "sort::name" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "acct-a" ), "count::3 must include acct-a (1st), got:\n{text}" );
assert!( text.contains( "acct-b" ), "count::3 must include acct-b (2nd), got:\n{text}" );
assert!( text.contains( "acct-c" ), "count::3 must include acct-c (3rd), got:\n{text}" );
assert!( !text.contains( "acct-d" ), "count::3 must exclude acct-d (4th), got:\n{text}" );
assert!( !text.contains( "acct-e" ), "count::3 must exclude acct-e (5th), got:\n{text}" );
}
#[ test ]
fn it179_count_0_shows_all_rows()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-c", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "count::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "acct-a" ), "count::0 must show acct-a, got:\n{text}" );
assert!( text.contains( "acct-b" ), "count::0 must show acct-b, got:\n{text}" );
assert!( text.contains( "acct-c" ), "count::0 must show acct-c, got:\n{text}" );
}
#[ test ]
fn it180_count_100_exceeding_shows_all()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "count::100" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "acct-a" ), "count::100 must show acct-a, got:\n{text}" );
assert!( text.contains( "acct-b" ), "count::100 must show acct-b, got:\n{text}" );
}
#[ test ]
fn it181_count_abc_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "count::abc" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ test ]
fn it182_count_1_sort_name_shows_only_first()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-c", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "count::1", "sort::name" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "acct-a" ), "count::1 must show acct-a (first), got:\n{text}" );
assert!( !text.contains( "acct-b" ), "count::1 must exclude acct-b, got:\n{text}" );
assert!( !text.contains( "acct-c" ), "count::1 must exclude acct-c, got:\n{text}" );
}
#[ test ]
fn it183_count_minus_1_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "count::-1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ test ]
fn it184_offset_2_skips_first_2_rows()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-c", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-d", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "sort::name", "offset::2" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "acct-c" ), "offset::2 must show acct-c (3rd), got:\n{text}" );
assert!( text.contains( "acct-d" ), "offset::2 must show acct-d (4th), got:\n{text}" );
assert!( !text.contains( "acct-a" ), "offset::2 must skip acct-a (1st), got:\n{text}" );
assert!( !text.contains( "acct-b" ), "offset::2 must skip acct-b (2nd), got:\n{text}" );
}
#[ test ]
fn it185_offset_0_shows_all_rows()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-c", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "offset::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "acct-a" ), "offset::0 must show acct-a, got:\n{text}" );
assert!( text.contains( "acct-b" ), "offset::0 must show acct-b, got:\n{text}" );
assert!( text.contains( "acct-c" ), "offset::0 must show acct-c, got:\n{text}" );
}
#[ test ]
fn it186_offset_99_shows_empty()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "offset::99" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( !text.contains( "acct-a" ), "offset::99 must skip acct-a, got:\n{text}" );
assert!( !text.contains( "acct-b" ), "offset::99 must skip acct-b, got:\n{text}" );
}
#[ test ]
fn it187_offset_abc_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "offset::abc" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ test ]
fn it188_offset_1_count_1_shows_second_row()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-c", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "offset::1", "count::1", "sort::name" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "acct-b" ), "offset::1 count::1 must show acct-b (2nd), got:\n{text}" );
assert!( !text.contains( "acct-a" ), "offset::1 count::1 must skip acct-a (1st), got:\n{text}" );
assert!( !text.contains( "acct-c" ), "offset::1 count::1 must exclude acct-c (3rd), got:\n{text}" );
}
#[ test ]
fn it189_offset_minus_1_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "offset::-1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ test ]
fn it190_get_account_extracts_first_name()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alpha-acct", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "zeta-acct", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "sort::name", "get::account" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert_eq!(
text.trim(),
"alpha-acct",
"get::account must output only the first account name (alpha-acct), got:\n{text}",
);
}
#[ test ]
fn it191_get_account_no_table_chrome()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "get::account" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( !text.contains( "5h Left" ), "get::account must not contain '5h Left' header, got:\n{text}" );
assert!( !text.contains( "7d Left" ), "get::account must not contain '7d Left' header, got:\n{text}" );
assert!( !text.contains( "Valid:" ), "get::account must not contain 'Valid:' footer, got:\n{text}" );
}
#[ test ]
fn it192_get_status_err_on_error_account()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "get::status" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert_eq!(
text.trim(),
"🔴",
"get::status on error account must output exactly '🔴', got:\n{text}",
);
}
#[ test ]
fn it193_get_with_empty_filtered_result_empty_stdout()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "only_valid::1", "get::account" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.trim().is_empty(),
"get:: with empty filtered result must produce empty stdout, got:\n{text}",
);
}
#[ test ]
fn it194_abs_1_accepted_empty_store()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "abs::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"abs::1 with empty store must show no-accounts message, got:\n{text}",
);
}
#[ test ]
fn it195_abs_0_accepted()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "abs::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
}
#[ test ]
fn it196_abs_bad_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "abs::bad" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( '0' ) && err.contains( '1' ),
"abs::bad stderr must name valid values, got:\n{err}",
);
}
#[ test ]
fn it197_abs_1_on_error_row()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "abs::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "acct-a" ), "abs::1 must not remove error rows, got:\n{text}" );
}
#[ test ]
fn it198_no_color_1_no_emoji_in_output()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "no_color::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( !text.contains( "🔴" ), "no_color::1 must remove 🔴, got:\n{text}" );
assert!( !text.contains( "🟡" ), "no_color::1 must not contain 🟡, got:\n{text}" );
assert!( !text.contains( "🟢" ), "no_color::1 must not contain 🟢, got:\n{text}" );
assert!( !text.contains( '→' ), "no_color::1 must remove → (replaced by ->), got:\n{text}" );
assert!( !text.contains( '✓' ), "no_color::1 must remove ✓, got:\n{text}" );
}
#[ test ]
fn it199_no_color_1_status_shows_err_text_label()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "no_color::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "err" ),
"no_color::1 must show 'err' text label for error account status, got:\n{text}",
);
}
#[ test ]
fn it200_no_color_bad_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "no_color::bad" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( '0' ) && err.contains( '1' ),
"no_color::bad stderr must name valid values, got:\n{err}",
);
}
#[ test ]
fn it201_no_color_true_accepted()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "no_color::true" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
!text.contains( "🔴" ),
"no_color::true must remove 🔴 (same as no_color::1), got:\n{text}",
);
}
#[ test ]
fn it202_cols_host_shows_host_column()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account_profile_json( dir.path(), "acct-a", Some( "mybox" ), Some( "work" ) );
let out = run_cs_with_env( &[ ".usage", "cols::+host" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "Host" ),
"cols::+host must add 'Host' column header, got:\n{text}",
);
assert!(
text.contains( "mybox" ),
"cols::+host must show host value 'mybox' in account row, got:\n{text}",
);
}
#[ test ]
fn it203_cols_role_shows_role_column()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
write_account_profile_json( dir.path(), "acct-a", Some( "mybox" ), Some( "work" ) );
let out = run_cs_with_env( &[ ".usage", "cols::+role" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "Role" ),
"cols::+role must add 'Role' column header, got:\n{text}",
);
assert!(
text.contains( "work" ),
"cols::+role must show role value 'work' in account row, got:\n{text}",
);
}
#[ test ]
fn it204_cols_bogus_names_host_and_role_in_error()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "cols::+bogus" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "host" ),
"cols::+bogus error must name 'host' as a valid column ID, got:\n{err}",
);
assert!(
err.contains( "role" ),
"cols::+bogus error must name 'role' as a valid column ID, got:\n{err}",
);
}
#[ test ]
fn it205_ft028_02_offset2_count3_windows_result()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a@test.com", "max", "standard", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-b@test.com", "max", "standard", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-c@test.com", "max", "standard", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-d@test.com", "max", "standard", FAR_FUTURE_MS, false );
write_account( dir.path(), "acct-e@test.com", "max", "standard", FAR_FUTURE_MS, false );
let out_all = run_cs_with_env( &[ ".usage", "sort::name" ], &[ ( "HOME", home ) ] );
assert_exit( &out_all, 0 );
let all_text = stdout( &out_all );
let all_names : Vec< &str > = all_text.lines()
.filter( | l | l.contains( "acct-" ) )
.collect();
let out_win = run_cs_with_env(
&[ ".usage", "sort::name", "offset::2", "count::3" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out_win, 0 );
let win_text = stdout( &out_win );
let win_names : Vec< &str > = win_text.lines()
.filter( | l | l.contains( "acct-" ) )
.collect();
assert_eq!( win_names.len(), 3, "offset::2 count::3 with 5 accounts must show exactly 3 rows" );
assert_eq!(
win_names, &all_names[ 2..5 ],
"offset::2 count::3 rows must match rows 3-5 from full sorted list",
);
}
#[ test ]
fn it211_min_5h_absent_data_passes_filter()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct@test.com", "max", "standard", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "min_5h::50" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "acct@test.com" ),
"min_5h::50 must not hide row when five_hour data is absent, got:\n{text}",
);
}
#[ test ]
fn it212_min_7d_absent_data_passes_filter()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct@test.com", "max", "standard", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "min_7d::30" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "acct@test.com" ),
"min_7d::30 must not hide row when seven_day data is absent, got:\n{text}",
);
}
#[ test ]
fn it241_min_5h_and_min_7d_both_pass_err_account()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct@test.com", "max", "standard", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "min_5h::50", "min_7d::30" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "acct@test.com" ),
"min_5h::50 min_7d::30 must not hide row when both quota fields are absent, got:\n{text}",
);
}
#[ test ]
fn it242_min_5h_only_valid_removes_err_account()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-err@test.com", "max", "standard", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "min_5h::1", "only_valid::1" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"min_5h::1 only_valid::1 must produce empty table for all-Err accounts, got:\n{text}",
);
}
#[ test ]
fn it243_min_5h_get_account_err_passes_returns_name()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct@test.com", "max", "standard", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "min_5h::1", "get::account" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert_eq!(
text.trim(),
"acct@test.com",
"min_5h::1 get::account must return account name when Err account passes filter, got:\n{text}",
);
}
#[ test ]
fn it244_get_host_absent_profile_json_empty_stdout()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct@test.com", "max", "standard", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "get::host" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.trim().is_empty(),
"get::host on account without profile.json must output empty stdout, got:\n{text}",
);
}
#[ test ]
fn it245_min_7d_get_account_err_passes_returns_name()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct@test.com", "max", "standard", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "min_7d::1", "get::account" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert_eq!(
text.trim(), "acct@test.com",
"`min_7d::1 get::account` on Err account must return bare name, got:\n{text}",
);
}
#[ test ]
fn it246_min_7d_only_valid_removes_err_account()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-err@test.com", "max", "standard", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "min_7d::1", "only_valid::1" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "(no accounts configured)" ),
"`min_7d::1 only_valid::1` must produce empty table for all-Err accounts, got:\n{text}",
);
}
#[ test ]
fn it220_ft029_07_get_host_extracts_bare()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct@test.com", "max", "standard", FAR_FUTURE_MS, false );
write_account_profile_json( dir.path(), "acct@test.com", Some( "mybox" ), None );
let out = run_cs_with_env(
&[ ".usage", "cols::+host", "get::host" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out ).trim().to_string();
assert_eq!(
text, "mybox",
"cols::+host get::host must output bare 'mybox' with no table chrome, got:\n{text}",
);
}
#[ test ]
fn it221_ft029_09_usage_no_profile_shows_empty_host()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct@test.com", "max", "standard", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "cols::+host" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "Host" ),
"cols::+host must show Host column header even when profile.json is absent, got:\n{text}",
);
}
#[ test ]
fn it206_lim_it_ft028_04_only_next_1_shows_recommended()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it206: no live token — skipping" );
return;
};
if !require_live_api( "it206" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
write_account_with_token( dir.path(), "acct-b@test.com", &token, false );
let out = run_cs_with_env( &[ ".usage", "only_next::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let data_rows = text.lines()
.filter( | l | l.contains( "@test.com" ) )
.count();
assert_eq!(
data_rows, 1,
"only_next::1 must show exactly 1 row (the recommended account), got:\n{text}",
);
}
#[ test ]
fn it207_lim_it_min_5h_50_hides_below_threshold()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it207: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out = run_cs_with_env( &[ ".usage", "min_5h::50" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
}
#[ test ]
fn it208_lim_it_min_5h_50_inclusive_boundary()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it208: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out = run_cs_with_env( &[ ".usage", "min_5h::50" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
}
#[ test ]
fn it209_lim_it_min_7d_20_hides_below_threshold()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it209: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out = run_cs_with_env( &[ ".usage", "min_7d::20" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
}
#[ test ]
fn it210_lim_it_min_7d_20_inclusive_boundary()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it210: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out = run_cs_with_env( &[ ".usage", "min_7d::20" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
}
#[ test ]
fn it213_lim_it_ft028_09_and_composition()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it213: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
write_account_with_token( dir.path(), "acct-b@test.com", &token, false );
let out = run_cs_with_env(
&[ ".usage", "only_valid::1", "min_7d::30" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
}
#[ test ]
fn it214_lim_it_ft028_10_get_7d_left_bare()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it214: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out = run_cs_with_env(
&[ ".usage", "sort::name", "get::7d_left" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
!text.contains( "Quota" ) && !text.contains( "7d Left" ) && !text.contains( "Valid:" ),
"get::7d_left must produce bare value output with no table chrome, got:\n{text}",
);
}
#[ test ]
fn it215_lim_it_ft028_11_only_next_get_7d_left()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it215: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
write_account_with_token( dir.path(), "acct-b@test.com", &token, false );
let out = run_cs_with_env(
&[ ".usage", "only_next::1", "get::7d_left" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
!text.contains( "Quota" ) && !text.contains( "Valid:" ),
"only_next::1 get::7d_left must produce bare value, no table chrome, got:\n{text}",
);
}
#[ test ]
fn it216_lim_it_ft028_12_get_status_green()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it216: no live token — skipping" );
return;
};
if !require_live_api( "it216" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out = run_cs_with_env( &[ ".usage", "get::status" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out ).trim().to_string();
assert!(
text == "🟢" || text == "🟡",
"get::status on valid account must output 🟢 or 🟡 as a bare value, got:\n{text}",
);
}
#[ test ]
fn it217_lim_it_ft028_13_format_tsv_status_text()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it217: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out = run_cs_with_env( &[ ".usage", "format::tsv" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let has_tab = text.contains( '\t' );
assert!( has_tab, "format::tsv output must contain tab characters, got:\n{text}" );
assert!(
!text.contains( "🟢" ) && !text.contains( "🟡" ) && !text.contains( "🔴" ),
"format::tsv status column must use text labels (ok/warn/err), not emoji, got:\n{text}",
);
assert!(
text.contains( "ok" ) || text.contains( "warn" ) || text.contains( "err" ),
"format::tsv status column must contain a text label, got:\n{text}",
);
}
#[ test ]
fn it218_lim_it_ft028_14_no_color_emoji_free()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it218: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out = run_cs_with_env( &[ ".usage", "no_color::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
!text.contains( "🟢" ) && !text.contains( "🟡" ) && !text.contains( "→" ),
"no_color::1 must produce emoji-free output for valid account, got:\n{text}",
);
}
#[ test ]
fn it219_lim_it_ft028_16_filters_compose()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it219: no live token — skipping" );
return;
};
if !require_live_api( "it219" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
write_account_with_token( dir.path(), "acct-b@test.com", &token, false );
let out = run_cs_with_env(
&[ ".usage", "sort::name", "only_valid::1", "count::2", "cols::+sub" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "Sub" ),
"cols::+sub must add Sub column header, got:\n{text}",
);
let data_rows = text.lines()
.filter( | l | l.contains( "@test.com" ) )
.count();
assert!(
data_rows <= 2,
"count::2 must limit result to at most 2 rows, got {data_rows} rows:\n{text}",
);
}
#[ test ]
fn it222_lim_it_it72_json_new_renewal_fields()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it222: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out = run_cs_with_env( &[ ".usage", "format::json" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "renewal_secs" ),
"format::json must contain 'renewal_secs' field (IT-72), got:\n{text}",
);
assert!(
text.contains( "renewal_is_estimate" ),
"format::json must contain 'renewal_is_estimate' field (IT-72), got:\n{text}",
);
assert!(
text.contains( "next_event_type" ),
"format::json must contain 'next_event_type' field (IT-72), got:\n{text}",
);
assert!(
text.contains( "next_event_secs" ),
"format::json must contain 'next_event_secs' field (IT-72), got:\n{text}",
);
assert!(
!text.contains( "next_renewal_est" ),
"format::json must NOT contain legacy 'next_renewal_est' field (IT-72), got:\n{text}",
);
}
#[ test ]
fn it223_lim_it_abs_1_shows_token_counts()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it223: no live token — skipping" );
return;
};
if !require_live_api( "it223" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out_pct = run_cs_with_env( &[ ".usage", "abs::0" ], &[ ( "HOME", home ) ] );
let out_abs = run_cs_with_env( &[ ".usage", "abs::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out_abs, 0 );
let text_pct = stdout( &out_pct );
let text_abs = stdout( &out_abs );
assert!(
text_pct.contains( '%' ),
"abs::0 (default) must show percentage values, got:\n{text_pct}",
);
assert!(
!text_abs.contains( '%' ) || text_abs.lines().filter( | l | l.contains( '%' ) ).all( | l | l.contains( "Reset" ) ),
"abs::1 quota columns must show absolute counts without % suffix, got:\n{text_abs}",
);
}
#[ test ]
fn it224_lim_it_abs_true_shows_token_counts()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it224: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out_1 = run_cs_with_env( &[ ".usage", "abs::1" ], &[ ( "HOME", home ) ] );
let out_true = run_cs_with_env( &[ ".usage", "abs::true" ], &[ ( "HOME", home ) ] );
assert_exit( &out_true, 0 );
assert_eq!(
stdout( &out_1 ), stdout( &out_true ),
"abs::true must produce the same output as abs::1 (046 EC-6)",
);
}
#[ test ]
fn it225_lim_it_it71_next_event_cell_shows_label_and_duration()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it225: no live token — skipping" );
return;
};
if !require_live_api( "it225" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "\u{2192} Next" ),
"→ Next column header must appear in default output (IT-71), got:\n{text}",
);
let has_event_label =
text.contains( " +7d" )
|| text.contains( " $ren" );
assert!(
has_event_label,
"→ Next cell must contain 'in ... +7d' or 'in ... $ren' for live account (IT-71), got:\n{text}",
);
}
#[ test ]
fn it226_lim_it_only_next_1_renews_shows_winner()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it226: no live token — skipping" );
return;
};
if !require_live_api( "it226" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
write_account_with_token( dir.path(), "acct-b@test.com", &token, false );
let out = run_cs_with_env(
&[ ".usage", "only_next::1", "sort::renews" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
let data_rows = text.lines()
.filter( | l | l.contains( "@test.com" ) )
.count();
assert_eq!(
data_rows, 1,
"only_next::1 sort::renews must show exactly 1 row (040 EC-3), got:\n{text}",
);
let arrow_rows = text.lines()
.filter( | l | l.contains( "\u{2192}" ) && l.contains( "@test.com" ) )
.count();
assert_eq!(
arrow_rows, 1,
"only_next::1 sort::renews must show the → account row (040 EC-3), got:\n{text}",
);
}
#[ test ]
fn it227_lim_it_only_next_true_shows_arrow_row()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it227: no live token — skipping" );
return;
};
if !require_live_api( "it227" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
write_account_with_token( dir.path(), "acct-b@test.com", &token, false );
let out = run_cs_with_env(
&[ ".usage", "only_next::true" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
let data_rows = text.lines()
.filter( | l | l.contains( "@test.com" ) )
.count();
assert_eq!(
data_rows, 1,
"only_next::true must show exactly 1 row (040 EC-6), got:\n{text}",
);
let arrow_rows = text.lines()
.filter( | l | l.contains( "\u{2192}" ) && l.contains( "@test.com" ) )
.count();
assert_eq!(
arrow_rows, 1,
"only_next::true must show the → account row (040 EC-6), got:\n{text}",
);
}
#[ test ]
fn it228_lim_it_only_valid_1_shows_green_hides_red()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it228: no live token — skipping" );
return;
};
if !require_live_api( "it228" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "live-acct@test.com", &token, true );
write_account( dir.path(), "error-acct@test.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "only_valid::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "live-acct@test.com" ),
"only_valid::1 must show 🟢 live account (043 EC-1), got:\n{text}",
);
assert!(
!text.contains( "error-acct@test.com" ),
"only_valid::1 must hide 🔴 error account (043 EC-1), got:\n{text}",
);
}
#[ test ]
fn it229_lim_it_exclude_exhausted_1_shows_green()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it229: no live token — skipping" );
return;
};
if !require_live_api( "it229" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "live-acct@test.com", &token, true );
write_account( dir.path(), "error-acct@test.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "exclude_exhausted::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "live-acct@test.com" ),
"exclude_exhausted::1 must show 🟢 live account (044 EC-1), got:\n{text}",
);
assert!(
!text.contains( "error-acct@test.com" ),
"exclude_exhausted::1 must hide 🔴 error account (044 EC-1), got:\n{text}",
);
}
#[ test ]
fn it230_lim_it_exclude_exhausted_stricter_than_only_valid()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it230: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "live-acct@test.com", &token, true );
write_account( dir.path(), "error-acct@test.com", "max", "default", FAR_FUTURE_MS, false );
let out_valid = run_cs_with_env( &[ ".usage", "only_valid::1" ], &[ ( "HOME", home ) ] );
let out_excl = run_cs_with_env( &[ ".usage", "exclude_exhausted::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out_valid, 0 );
assert_exit( &out_excl, 0 );
let rows_valid = stdout( &out_valid ).lines().filter( | l | l.contains( "@test.com" ) ).count();
let rows_excl = stdout( &out_excl ).lines().filter( | l | l.contains( "@test.com" ) ).count();
assert!(
rows_excl <= rows_valid,
"exclude_exhausted::1 must show ≤ rows than only_valid::1 (044 EC-3): valid={rows_valid} excl={rows_excl}",
);
assert!(
!stdout( &out_excl ).contains( "error-acct@test.com" ),
"exclude_exhausted::1 must hide 🔴 error account (044 EC-3)",
);
}
#[ test ]
fn it231_lim_it_get_7d_left_extracts_bare_pct()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it231: no live token — skipping" );
return;
};
if !require_live_api( "it231" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out = run_cs_with_env(
&[ ".usage", "sort::name", "get::7d_left" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
let trimmed = text.trim();
assert!(
trimmed.ends_with( '%' ),
"get::7d_left must output a percentage string e.g. '65%' (045 EC-1), got:\n{trimmed}",
);
assert!(
!trimmed.contains( "7d Left" ),
"get::7d_left must not contain column headers (045 EC-1), got:\n{trimmed}",
);
assert!(
!trimmed.contains( "Valid:" ),
"get::7d_left must not contain footer (045 EC-1), got:\n{trimmed}",
);
}
#[ test ]
fn it232_lim_it_get_status_extracts_green_emoji()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it232: no live token — skipping" );
return;
};
if !require_live_api( "it232" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out = run_cs_with_env( &[ ".usage", "get::status" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let trimmed = text.trim();
assert_eq!(
trimmed, "\u{1f7e2}",
"get::status on live (🟢) account must output exactly '🟢' (045 EC-3), got:\n{trimmed}",
);
}
#[ test ]
fn it233_get_bogus_exits_1_names_valid_fields()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".usage", "get::bogus" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "next_event_type" ),
"get::bogus stderr must list 'next_event_type' (045 EC-5), got:\n{err}",
);
assert!(
err.contains( "next_event_secs" ),
"get::bogus stderr must list 'next_event_secs' (045 EC-5), got:\n{err}",
);
assert!(
err.contains( "7d_left" ),
"get::bogus stderr must list '7d_left' (045 EC-5), got:\n{err}",
);
assert!(
err.contains( "account" ),
"get::bogus stderr must list 'account' (045 EC-5), got:\n{err}",
);
}
#[ test ]
fn it234_lim_it_get_next_event_type_and_secs()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it234: no live token — skipping" );
return;
};
if !require_live_api( "it234" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out_type = run_cs_with_env(
&[ ".usage", "get::next_event_type" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out_type, 0 );
let type_text = stdout( &out_type );
let type_str = type_text.trim();
let valid_labels = [ "+7d", "$ren" ];
assert!(
valid_labels.contains( &type_str ),
"get::next_event_type must output '+7d' or '$ren' (045 EC-7 after TSK-228), got:\n{type_str}",
);
let out_secs = run_cs_with_env(
&[ ".usage", "get::next_event_secs" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out_secs, 0 );
let secs_text = stdout( &out_secs );
let secs_str = secs_text.trim();
assert!(
secs_str.parse::<u64>().is_ok(),
"get::next_event_secs must output a bare integer (045 EC-7), got:\n{secs_str}",
);
}
#[ test ]
fn it235_lim_it_no_color_0_output_includes_emoji()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it235: no live token — skipping" );
return;
};
if !require_live_api( "it235" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out = run_cs_with_env( &[ ".usage", "no_color::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "\u{1f7e2}" ),
"no_color::0 must include 🟢 status emoji for live account (047 EC-3), got:\n{text}",
);
}
#[ test ]
fn it236_lim_it_no_color_1_check_mark_replaced_by_star()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it236: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
write_account_with_token( dir.path(), "acct-b@test.com", &token, false );
let out = run_cs_with_env( &[ ".usage", "no_color::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
!text.contains( '\u{2713}' ),
"no_color::1 must replace unicode '✓' with '*' (047 EC-5), got:\n{text}",
);
assert!(
text.contains( '*' ),
"no_color::1 must contain '*' (replaced from '✓') (047 EC-5), got:\n{text}",
);
}
#[ test ]
fn it237_lim_it_clear_usage_shows_tilde_estimate()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it237: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
std::fs::write(
store.join( "acct-a@test.com.json" ),
r#"{"_renewal_at":"2030-01-01T00:00:00Z"}"#,
).unwrap();
let clear_out = run_cs_with_env(
&[ ".account.renewal", "name::acct-a@test.com", "clear::1" ],
&[ ( "HOME", home ) ],
);
assert_exit( &clear_out, 0 );
let content = std::fs::read_to_string( store.join( "acct-a@test.com.json" ) ).unwrap();
assert!(
!content.contains( "_renewal_at" ),
"clear::1 must remove _renewal_at from .json (051 EC-4), got: {content}",
);
let usage_out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &usage_out, 0 );
}
#[ test ]
fn it238_lim_it_get_bypasses_cols_restriction()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it238: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
let out_hidden = run_cs_with_env(
&[ ".usage", "cols::-7d_left", "get::7d_left" ],
&[ ( "HOME", home ) ],
);
let out_normal = run_cs_with_env(
&[ ".usage", "get::7d_left" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out_hidden, 0 );
assert_exit( &out_normal, 0 );
assert_eq!(
stdout( &out_hidden ).trim(),
stdout( &out_normal ).trim(),
"get::7d_left with cols::-7d_left must produce same output as without cols:: (005 CC-3)",
);
}
#[ test ]
fn it239_cols_sub_and_no_color_independent()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "acct-a", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "cols::+sub", "no_color::1" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "Sub" ),
"cols::+sub must add 'Sub' column header (005 CC-4), got:\n{text}",
);
assert!(
!text.contains( "\u{1f534}" ),
"no_color::1 must remove 🔴 emoji (005 CC-4), got:\n{text}",
);
assert!(
!text.contains( "\u{1f7e2}" ),
"no_color::1 must not contain 🟢 (005 CC-4), got:\n{text}",
);
}
#[ test ]
fn it240_lim_it_cols_host_role_shows_profile_data()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it240: no live token — skipping" );
return;
};
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "acct-a@test.com", &token, true );
write_account_profile_json( dir.path(), "acct-a@test.com", Some( "mybox" ), Some( "work" ) );
let out = run_cs_with_env(
&[ ".usage", "cols::+host,+role" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "Host" ),
"cols::+host,+role must add 'Host' column header (006 CC-4), got:\n{text}",
);
assert!(
text.contains( "Role" ),
"cols::+host,+role must add 'Role' column header (006 CC-4), got:\n{text}",
);
assert!(
text.contains( "mybox" ),
"cols::+host must show 'mybox' host value from profile.json (006 CC-4), got:\n{text}",
);
assert!(
text.contains( "work" ),
"cols::+role must show 'work' role value from profile.json (006 CC-4), got:\n{text}",
);
}
#[ test ]
fn it247_synthetic_row_suppressed_name_collision()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "alice@acme.com", "tok-alice", true );
write_live_credentials_with_token( dir.path(), "tok-alice" );
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let alice_count = text.matches( "alice@acme.com" ).count();
assert_eq!(
alice_count, 1,
"alice@acme.com must appear exactly once — no synthetic duplicate row, got:\n{text}",
);
assert!(
!text.contains( "(current session)" ),
"synthetic row must be suppressed when stored account has matching token, got:\n{text}",
);
let alice_current = text.lines().any( |l|
l.contains( '\u{2713}' ) && l.contains( "alice@acme.com" )
);
assert!( alice_current, "stored alice@acme.com row must carry ✓ when her token matches live creds, got:\n{text}" );
}
#[ test ]
fn it_ft028_17_only_active_single_http_fetch()
{
let Some( token ) = live_active_token() else
{
eprintln!( "it_ft028_17: no live token — skipping" );
return;
};
if !require_live_api( "it_ft028_17" ) { return; }
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "active@test.com", &token, true );
write_account( dir.path(), "other1@test.com", "max", "standard", FAR_FUTURE_MS, false );
write_account( dir.path(), "other2@test.com", "max", "standard", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "only_active::1", "get::status", "trace::1" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let err = stderr( &out );
let result_lines : Vec< &str > = err
.lines()
.filter( |l| l.contains( "[trace]" ) && l.contains( "result:" ) )
.collect();
assert_eq!(
result_lines.len(), 1,
"only_active::1 must trigger exactly 1 HTTP fetch (1 trace result line); \
{} found — non-active accounts must be skipped; trace:\n{err}",
result_lines.len(),
);
}
#[ test ]
fn it249_sort_endurance_rejected_exit_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "sort::endurance" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
for valid in &[ "name", "renew", "renews" ]
{
assert!(
err.contains( valid ),
"sort::endurance error must name valid value `{valid}`; got:\n{err}",
);
}
for removed in &[ "drain", "expires", "next" ]
{
assert!(
!err.contains( removed ),
"sort::endurance error must NOT name removed value `{removed}`; got:\n{err}",
);
}
}
#[ test ]
fn it250_sort_drain_rejected_exit_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "sort::drain" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
for valid in &[ "name", "renew", "renews" ]
{
assert!(
err.contains( valid ),
"sort::drain error must name valid value `{valid}`; got:\n{err}",
);
}
}
#[ test ]
fn it251_sort_next_rejected_exit_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "sort::next" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
for valid in &[ "name", "renew", "renews" ]
{
assert!(
err.contains( valid ),
"sort::next error must name valid value `{valid}`; got:\n{err}",
);
}
}
#[ test ]
fn it252_sort_expires_rejected_exit_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "sort::expires" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
for valid in &[ "name", "renew", "renews" ]
{
assert!(
err.contains( valid ),
"sort::expires error must name valid value `{valid}`; got:\n{err}",
);
}
}
#[ test ]
fn it253_next_param_removed_exit_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "next::renew" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "next" ) || err.contains( "sort" ),
"next::renew error must reference `next` or `sort`; got:\n{err}",
);
}
#[ test ]
fn it254_next_strategy_enum_absent_structural()
{
let src = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/types.rs" ) );
assert!(
!src.contains( "enum NextStrategy" ),
"NextStrategy enum must be completely removed from types.rs; \
check enum declaration and all impl blocks",
);
}
#[ test ]
fn it255_footer_single_strategy_structural()
{
let src = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/render.rs" ) );
assert!(
!src.contains( "Next by strategy:" ),
"render.rs must not contain `Next by strategy:` — footer must show 1 strategy line; \
replace with single-strategy `Next (strategy): account metric` format",
);
}
#[ test ]
fn it248_owner_column_visible_by_default()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alice@acme.com", "pro", "standard", FAR_FUTURE_MS, false );
write_account_owner( dir.path(), "alice@acme.com", "testuser@testmachine" );
write_account( dir.path(), "bob@acme.com", "pro", "standard", FAR_FUTURE_MS, false );
write_account_owner( dir.path(), "bob@acme.com", "" );
{
let out = run_cs_with_env( &[ ".usage" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( "Owner" ),
"IT-74A: Owner column header must appear by default in .usage table; got:\n{text}",
);
assert!(
text.contains( "testuser@testmachine" ),
"IT-74A: alice's owner identity must appear in Owner column; got:\n{text}",
);
assert!(
text.contains( "\u{2014}" ),
"IT-74A: bob's empty owner must show em dash (—) in Owner column; got:\n{text}",
);
}
{
let out = run_cs_with_env( &[ ".usage", "cols::-owner" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
!text.contains( "Owner" ),
"IT-74B: Owner column header must be hidden with cols::-owner; got:\n{text}",
);
}
}
#[ test ]
fn it257_solo_default_off_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alice@work.pro", "max", "tier4", FAR_FUTURE_MS, false );
write_account( dir.path(), "bob@home.pro", "max", "tier4", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "solo::0" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "alice@work.pro" ), "EC-1: alice must appear with solo::0; got:\n{text}" );
assert!( text.contains( "bob@home.pro" ), "EC-1: bob must appear with solo::0; got:\n{text}" );
}
#[ test ]
fn it258_solo_current_live_noncurrent_approx()
{
const FAKE_TOK : &str = "solo-ec2-fake-token";
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let credential_store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
write_account_with_token( dir.path(), "alice@work.pro", FAKE_TOK, false );
write_live_credentials_with_token( dir.path(), FAKE_TOK );
write_account( dir.path(), "bob@home.pro", "max", "tier4", FAR_FUTURE_MS, false );
let bob_cache = serde_json::json!({ "cache": { "fetched_at": "2026-01-01T10:00:00Z", "five_hour": { "left_pct": 55.0 } } });
std::fs::write(
credential_store.join( "bob@home.pro.json" ),
serde_json::to_string_pretty( &bob_cache ).unwrap() + "\n",
).unwrap();
let out = run_cs_with_env( &[ ".usage", "solo::1", "trace::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
let err = stderr( &out );
assert!(
text.contains( "alice@work.pro" ),
"EC-2: alice (current+owned) must appear in output; got:\n{text}",
);
assert!(
text.contains( "bob@home.pro" ),
"EC-2: bob (solo-skipped) must still appear in table (solo controls fetch, not display); got:\n{text}",
);
assert!(
!err.contains( "alice@work.pro solo-skip" ),
"EC-2: alice (is_current=true) must not be solo-skipped; got stderr:\n{err}",
);
assert!(
err.contains( "solo-skip" ),
"EC-2: bob (is_current=false) must be solo-skipped; got stderr:\n{err}",
);
}
#[ test ]
fn it259_solo_current_not_owned_no_http()
{
const FAKE_TOK : &str = "solo-ec3-fake-token";
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let credential_store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
write_account_with_token( dir.path(), "alice@work.pro", FAKE_TOK, false );
write_live_credentials_with_token( dir.path(), FAKE_TOK );
write_account_owner( dir.path(), "alice@work.pro", "other@remote" );
write_account( dir.path(), "bob@home.pro", "max", "tier4", FAR_FUTURE_MS, false );
let bob_cache = serde_json::json!({ "cache": { "fetched_at": "2026-01-01T10:00:00Z", "five_hour": { "left_pct": 30.0 } } });
std::fs::write(
credential_store.join( "bob@home.pro.json" ),
serde_json::to_string_pretty( &bob_cache ).unwrap() + "\n",
).unwrap();
let out = run_cs_with_env( &[ ".usage", "solo::1", "trace::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let err = stderr( &out );
assert!(
err.contains( "not owned" ),
"EC-3: G1 trace must show 'not owned' for alice (current but foreign owner); got:\n{err}",
);
assert!(
err.contains( "solo-skip" ),
"EC-3: solo trace must show 'solo-skip' for bob (owned but not current); got:\n{err}",
);
}
#[ test ]
fn it260_solo_no_active_marker_all_approx()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let credential_store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
write_account( dir.path(), "alice@work.pro", "max", "tier4", FAR_FUTURE_MS, false );
write_account( dir.path(), "bob@home.pro", "max", "tier4", FAR_FUTURE_MS, false );
for name in &[ "alice@work.pro", "bob@home.pro" ]
{
let cache = serde_json::json!({ "cache": { "fetched_at": "2026-01-01T10:00:00Z", "five_hour": { "left_pct": 40.0 } } });
std::fs::write(
credential_store.join( format!( "{name}.json" ) ),
serde_json::to_string_pretty( &cache ).unwrap() + "\n",
).unwrap();
}
let out = run_cs_with_env( &[ ".usage", "solo::1", "trace::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let err = stderr( &out );
assert!(
err.contains( "solo-skip: approximated" ),
"EC-4: both accounts must show solo-skip when no active marker; got:\n{err}",
);
}
#[ test ]
fn it261_solo_rotate_mutual_exclusion_exit_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "solo::1", "rotate::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "solo" ) && err.contains( "rotate" ),
"EC-5: error must reference both `solo` and `rotate`; got:\n{err}",
);
}
#[ cfg( unix ) ]
#[ test ]
fn it262_solo_live_composition_allowed()
{
use std::process::Stdio;
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alice@work.pro", "max", "tier4", FAR_FUTURE_MS, true );
let child = std::process::Command::new( BIN )
.args( [ ".usage", "solo::1", "live::1", "interval::30", "jitter::0" ] )
.env( "HOME", home )
.env_remove( "PRO" )
.stdout( Stdio::piped() )
.stderr( Stdio::piped() )
.spawn()
.expect( "failed to spawn clp binary" );
std::thread::sleep( core::time::Duration::from_secs( 3 ) );
let _ = std::process::Command::new( "kill" )
.args( [ "-INT", &child.id().to_string() ] )
.status();
let out = child.wait_with_output().expect( "failed to wait on clp binary" );
let text = String::from_utf8_lossy( &out.stdout );
assert_eq!(
out.status.code(),
Some( 0 ),
"EC-6: SIGINT on solo+live must exit 0; got: {:?}\nstdout: {text}\nstderr: {}",
out.status,
String::from_utf8_lossy( &out.stderr ),
);
assert!(
text.contains( "Monitor stopped." ),
"EC-6: clean SIGINT exit must print 'Monitor stopped.'; got:\n{text}",
);
}
#[ test ]
fn it263_solo_refresh_composition_allowed()
{
const FAKE_TOK : &str = "solo-ec7-fake-token";
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "alice@work.pro", FAKE_TOK, false );
write_live_credentials_with_token( dir.path(), FAKE_TOK );
write_account( dir.path(), "bob@home.pro", "max", "tier4", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "solo::1", "refresh::1", "trace::1" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let err = stderr( &out );
assert!(
err.lines().any( |l| l.contains( "refresh" ) && l.contains( "solo-skip" ) ),
"EC-7: refresh solo gate must emit 'solo-skip' for non-current account; got stderr:\n{err}",
);
}
#[ test ]
fn it264_solo_touch_composition_allowed()
{
const FAKE_TOK : &str = "solo-ec8-fake-token";
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account_with_token( dir.path(), "alice@work.pro", FAKE_TOK, false );
write_live_credentials_with_token( dir.path(), FAKE_TOK );
write_account( dir.path(), "bob@home.pro", "max", "tier4", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "solo::1", "touch::1", "trace::1" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let err = stderr( &out );
assert!(
err.lines().any( |l| l.contains( "touch" ) && l.contains( "solo-skip" ) ),
"EC-8: touch solo gate must emit 'solo-skip' for non-current account; got stderr:\n{err}",
);
}
#[ test ]
fn it265_solo_only_active_composition_allowed()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alice@work.pro", "max", "tier4", FAR_FUTURE_MS, true );
write_account( dir.path(), "bob@home.pro", "max", "tier4", FAR_FUTURE_MS, false );
let out = run_cs_with_env( &[ ".usage", "solo::1", "only_active::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!( text.contains( "alice@work.pro" ), "EC-9: alice (is_active=true) must appear with only_active::1; got:\n{text}" );
assert!( !text.contains( "bob@home.pro" ), "EC-9: bob (is_active=false) must be hidden by only_active::1; got:\n{text}" );
}
#[ test ]
fn it266_solo_trace_shows_solo_skip()
{
const FAKE_TOK : &str = "solo-ec10-fake-token";
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let credential_store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
write_account_with_token( dir.path(), "alice@work.pro", FAKE_TOK, false );
write_live_credentials_with_token( dir.path(), FAKE_TOK );
write_account( dir.path(), "bob@home.pro", "max", "tier4", FAR_FUTURE_MS, false );
let bob_cache = serde_json::json!({ "cache": { "fetched_at": "2026-01-01T10:00:00Z", "five_hour": { "left_pct": 42.0 } } });
std::fs::write(
credential_store.join( "bob@home.pro.json" ),
serde_json::to_string_pretty( &bob_cache ).unwrap() + "\n",
).unwrap();
let out = run_cs_with_env(
&[ ".usage", "solo::1", "refresh::1", "touch::1", "trace::1" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let err = stderr( &out );
assert!(
err.contains( "solo-skip: approximated" ),
"EC-10: stderr must contain `solo-skip: approximated` (fetch trace); got:\n{err}",
);
assert!(
err.contains( "bob@home.pro" ),
"EC-10: fetch trace line must name the skipped account `bob@home.pro`; got:\n{err}",
);
assert!(
err.lines().any( |l| l.contains( "refresh" ) && l.contains( "solo-skip" ) ),
"EC-10: solo gate must emit 'solo-skip' in refresh trace for non-current account; got:\n{err}",
);
assert!(
err.lines().any( |l| l.contains( "touch" ) && l.contains( "solo-skip" ) ),
"EC-10: solo gate must emit 'solo-skip' in touch trace for non-current account; got:\n{err}",
);
}
#[ test ]
fn it267_solo_true_rejected_type_error()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "solo::true" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ test ]
fn it268_solo_2_rejected_out_of_range()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "solo::2" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "solo" ),
"EC-12: error message must reference `solo`; got stderr:\n{err}",
);
}
#[ test ]
fn it269_solo_json_format_with_approximated_accounts()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let credential_store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
write_account( dir.path(), "alice@work.pro", "max", "tier4", FAR_FUTURE_MS, false );
write_account( dir.path(), "bob@home.pro", "max", "tier4", FAR_FUTURE_MS, false );
for name in &[ "alice@work.pro", "bob@home.pro" ]
{
let cache = serde_json::json!({ "cache": { "fetched_at": "2026-01-01T10:00:00Z", "five_hour": { "left_pct": 40.0 } } });
std::fs::write(
credential_store.join( format!( "{name}.json" ) ),
serde_json::to_string_pretty( &cache ).unwrap() + "\n",
).unwrap();
}
let out = run_cs_with_env( &[ ".usage", "solo::1", "format::json" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let json_str = stdout( &out );
let parsed : serde_json::Value = serde_json::from_str( &json_str )
.unwrap_or_else( |e| panic!( "CC: solo+json must produce valid JSON; err={e}; raw:\n{json_str}" ) );
assert!(
parsed.is_array(),
"CC: solo+json output must be a JSON array; got:\n{json_str}",
);
}
#[ test ]
fn it270_solo_single_current_account_no_skip()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
write_account_with_token( dir.path(), "only@work.pro", "tok-only", true );
write_live_credentials_with_token( dir.path(), "tok-only" );
let cache = serde_json::json!({ "cache": { "fetched_at": "2026-01-01T10:00:00Z", "five_hour": { "left_pct": 50.0 } } });
std::fs::write(
store.join( "only@work.pro.json" ),
serde_json::to_string_pretty( &cache ).unwrap() + "\n",
).unwrap();
let out = run_cs_with_env( &[ ".usage", "solo::1", "trace::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let err = stderr( &out );
let fetch_solo_skip = err.lines().any( |l| l.contains( "fetch" ) && l.contains( "solo-skip" ) );
assert!(
!fetch_solo_skip,
"CC: fetch phase must not solo-skip the current account; got stderr:\n{err}",
);
}
#[ test ]
fn it256_who_true_rejected_kind_integer()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
std::fs::create_dir_all( &store ).unwrap();
let out = run_cs_with_env( &[ ".usage", "who::true" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ doc = "bug_reproducer(BUG-305)" ]
#[ test ]
fn mre_bug305_fetch_skips_occupied_elsewhere_with_trace()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let credential_store = dir.path().join( ".persistent" ).join( "claude" ).join( "credential" );
let acct_name = "occ@work.pro";
write_account( dir.path(), acct_name, "max", "tier4", FAR_FUTURE_MS, false );
let cache = serde_json::json!({ "cache": { "fetched_at": "2026-01-01T10:00:00Z", "five_hour": { "left_pct": 30.0 } } });
std::fs::write(
credential_store.join( format!( "{acct_name}.json" ) ),
serde_json::to_string_pretty( &cache ).unwrap() + "\n",
).unwrap();
std::fs::write(
credential_store.join( "_active_remotebox_remoteuser" ),
acct_name,
).unwrap();
let out = run_cs_with_env( &[ ".usage", "trace::1" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let err = stderr( &out );
assert!(
err.contains( "occupied elsewhere" ) && err.contains( "[trace] fetch" ),
"FT-24: G1b skip trace must contain 'occupied elsewhere' in a fetch trace line; got:\n{err}",
);
let cred_read = err.lines().any( |l| l.contains( acct_name ) && l.contains( "reading" ) );
assert!(
!cred_read,
"FT-24: no credential-read trace must appear for occupied-elsewhere account; got:\n{err}",
);
}
#[ doc = "bug_reproducer(BUG-307)" ]
#[ test ]
fn mre_bug307_approx_det0_quadratic_fit_correct()
{
let src = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/src/usage/approx.rs" ) );
assert!(
src.contains( "Fix(BUG-307)" ),
"BUG-307: approx.rs must contain Fix(BUG-307) — wrong det0 Cramer cofactor (s1*r2 → s2*r1) \
caused quadratic fit to clamp to 100.0 for linear data with large normalized timestamps; \
fix: use s2*r1 in the det0 minor (col-3 is replaced by RHS, so the minor uses r1 not r2).",
);
}