use super::{ render_text, render_tsv, render_json };
use crate::usage::types::{ AccountQuota, SortStrategy, PreferStrategy, ColsVisibility };
use crate::usage::test_support::{ FAR_FUTURE_MS, mk_aq_sort, mk_aq_sort_weekly };
#[ doc = "bug_reproducer(BUG-220)" ]
#[ test ]
fn mre_bug_220_renews_preserved_for_429_accounts()
{
let aq = AccountQuota
{
name : "i11@test.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Err( "rate limited (429)".to_string() ),
account : Some( claude_quota::OauthAccountData
{
tagged_id : String::new(),
uuid : String::new(),
email_address : String::new(),
full_name : String::new(),
display_name : String::new(),
billing_type : "stripe_subscription".to_string(),
has_max : true,
capabilities : vec![],
rate_limit_tier : String::new(),
org_created_at : "1970-01-15T00:00:00Z".to_string(),
memberships : vec![],
}),
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
};
let accounts = vec![ aq ];
let cols = ColsVisibility::default_set();
let tsv = render_tsv( &accounts, SortStrategy::Name, None, PreferStrategy::Any, &cols );
let mut lines = tsv.lines();
let header = lines.next().expect( "TSV must have a header row" );
let data_row = lines.next().expect( "TSV must have a data row" );
let headers : Vec< &str > = header.split( '\t' ).collect();
let fields : Vec< &str > = data_row.split( '\t' ).collect();
let renews_idx = headers.iter().position( |h| *h == "renews" )
.expect( "renews column must be present in TSV header" );
let renews_val = fields.get( renews_idx ).copied().unwrap_or( "" );
assert_ne!(
renews_val,
"(rate limited (429))",
"BUG-220: TSV ~Renews must not contain error_str for 429 accounts with valid \
OauthAccountData; got {renews_val:?}",
);
assert_ne!(
renews_val,
"?",
"BUG-220: TSV ~Renews must show billing date when OauthAccountData is available; \
got {renews_val:?}",
);
let text = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any, &cols, None, None, None, None,
);
assert!(
text.contains( "(rate limited (429))" ),
"BUG-220: error reason must appear in render_text output (in a quota column)",
);
}
#[ test ]
fn test_ft21_009_occupied_elsewhere_at_flag()
{
let mk_aq = | name : &str, is_current : bool, is_active : bool, is_occupied_elsewhere : bool |
{
AccountQuota
{
name : name.to_string(),
is_current,
is_active,
is_occupied_elsewhere,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : Some( claude_quota::PeriodUsage { utilization : 50.0, resets_at : None } ),
seven_day : None,
seven_day_sonnet : None,
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
}
};
let accounts = vec!
[
mk_aq( "alice@test.com", true, true, false ),
mk_aq( "bob@test.com", false, false, true ),
];
let cols = ColsVisibility::default_set();
let text = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any, &cols, None, None, None, None,
);
let alice_text = text.lines().find( | l | l.contains( "alice@test.com" ) )
.expect( "FT-21: alice line missing from render_text" );
let bob_text = text.lines().find( | l | l.contains( "bob@test.com" ) )
.expect( "FT-21: bob line missing from render_text" );
assert!(
alice_text.contains( '\u{2713}' ),
"FT-21: alice (is_current=true) must show ✓ in text render; got: {alice_text:?}",
);
assert_eq!(
bob_text.split_whitespace().next().unwrap_or( "" ),
"@",
"FT-21: bob (is_occupied_elsewhere=true) first token must be @ in text render; got: {bob_text:?}",
);
let tsv = render_tsv( &accounts, SortStrategy::Name, None, PreferStrategy::Any, &cols );
let mut tsv_lines = tsv.lines();
let _header = tsv_lines.next().expect( "FT-21: TSV must have a header row" );
let alice_tsv = tsv_lines.next().expect( "FT-21: alice TSV row missing" );
let bob_tsv = tsv_lines.next().expect( "FT-21: bob TSV row missing" );
let alice_flag_tsv = alice_tsv.split( '\t' ).next().unwrap_or( "" );
let bob_flag_tsv = bob_tsv.split( '\t' ).next().unwrap_or( "" );
assert_eq!(
alice_flag_tsv, "\u{2713}",
"FT-21: alice TSV flag cell must be ✓; got: {alice_flag_tsv:?}",
);
assert_eq!(
bob_flag_tsv, "@",
"FT-21: bob TSV flag cell must be @; got: {bob_flag_tsv:?}",
);
}
#[ test ]
fn ft03_033_render_text_cached_shows_tilde_prefix()
{
let aq = AccountQuota
{
name : "cached@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : Some( claude_quota::PeriodUsage { utilization : 14.0, resets_at : None } ),
seven_day : None,
seven_day_sonnet : None,
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : true,
cache_age_secs : Some( 300 ),
is_owned : true,
owner : String::new(),
};
let accounts = vec![ aq ];
let cols = ColsVisibility::default_set();
let text = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any, &cols, None, None, None, None,
);
assert!(
text.contains( '~' ),
"FT-03/033: cached row must show ~ prefix on non-dash quota cells; got:\n{text}",
);
assert!(
text.contains( "~🟢 86%" ),
"FT-03/033: 5h Left cell (14% util → 86% left, green) must be '~🟢 86%'; got:\n{text}",
);
}
#[ test ]
fn ft09_033_render_json_cached_includes_fields()
{
let aq = AccountQuota
{
name : "cached@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : Some( claude_quota::PeriodUsage { utilization : 14.0, resets_at : None } ),
seven_day : None,
seven_day_sonnet : None,
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : true,
cache_age_secs : Some( 720 ),
is_owned : true,
owner : String::new(),
};
let accounts = vec![ aq ];
let json = render_json( &accounts );
assert!(
json.contains( "\"cached\":true" ),
"FT-09/033: render_json must include '\"cached\":true' for cached rows; got:\n{json}",
);
assert!(
json.contains( "\"cache_age_secs\":720" ),
"FT-09/033: render_json must include '\"cache_age_secs\":720'; got:\n{json}",
);
}
#[ test ]
fn ft03_033_cached_sonnet_reset_shows_tilde()
{
let aq = AccountQuota
{
name : "cached@example.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : None,
seven_day : None,
seven_day_sonnet : Some( claude_quota::PeriodUsage
{
utilization : 80.0,
resets_at : Some( "2099-01-01T00:00:00Z".to_string() ),
} ),
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : true,
cache_age_secs : Some( 600 ),
is_owned : true,
owner : String::new(),
};
let accounts = vec![ aq ];
let mut cols = ColsVisibility::default_set();
cols.d7_son_reset = true;
let text = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any, &cols, None, None, None, None,
);
assert!(
text.contains( "~in " ),
"cached sonnet reset must show ~in prefix; got:\n{text}",
);
}
#[ doc = "bug_reproducer(BUG-232)" ]
#[ test ]
fn test_ft23_009_renews_dash_for_cancelled_subscription()
{
let aq = AccountQuota
{
name : "cancelled@test.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Err( "no subscription".to_string() ),
account : Some( claude_quota::OauthAccountData
{
tagged_id : String::new(),
uuid : String::new(),
email_address : String::new(),
full_name : String::new(),
display_name : String::new(),
billing_type : "none".to_string(),
has_max : false,
capabilities : vec![],
rate_limit_tier : String::new(),
org_created_at : "2024-01-15T00:00:00Z".to_string(),
memberships : vec![],
} ),
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
};
let accounts = vec![ aq ];
let cols = ColsVisibility::default_set();
let text = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any, &cols, None, None, None, None,
);
assert!(
text.contains( "\u{2014}" ),
"FT-23: render_text must contain em-dash for cancelled subscription ~Renews; got:\n{text}",
);
assert!(
!text.contains( "~in " ),
"FT-23: render_text must NOT contain '~in ' for cancelled subscription; got:\n{text}",
);
let tsv = render_tsv( &accounts, SortStrategy::Name, None, PreferStrategy::Any, &cols );
let mut lines = tsv.lines();
let header = lines.next().expect( "FT-23: TSV must have header row" );
let data_row = lines.next().expect( "FT-23: TSV must have data row" );
let headers : Vec< &str > = header.split( '\t' ).collect();
let fields : Vec< &str > = data_row.split( '\t' ).collect();
let renews_idx = headers.iter().position( |h| *h == "renews" )
.expect( "FT-23: renews column must be present in TSV header" );
let renews_val = fields.get( renews_idx ).copied().unwrap_or( "" );
assert_eq!(
renews_val, "\u{2014}",
"FT-23: TSV ~Renews must be em-dash for billing_type='none'; got: {renews_val:?}",
);
}
#[ test ]
fn ft05_non_owned_display_tilde_or_dashes()
{
let cols = ColsVisibility::default_set();
let aq_cached = AccountQuota
{
name : "alice@test.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : Some( claude_quota::PeriodUsage { utilization : 30.0, resets_at : None } ),
seven_day : None,
seven_day_sonnet : None,
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : true,
cache_age_secs : Some( 600 ),
is_owned : false,
owner : String::new(),
};
let text_a = render_text(
&[ aq_cached ],
SortStrategy::Name, None, PreferStrategy::Any,
&cols, None, None, None, None,
);
assert!(
text_a.contains( '~' ),
"FT-05 case A: non-owned cached row must show ~ prefix; got:\n{text_a}",
);
let aq_no_cache = AccountQuota
{
name : "bob@test.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Err( "not owned".to_string() ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : false,
owner : String::new(),
};
let text_b = render_text(
&[ aq_no_cache ],
SortStrategy::Name, None, PreferStrategy::Any,
&cols, None, None, None, None,
);
assert!(
!text_b.contains( "~🟢" ) && !text_b.contains( "~🟡" ) && !text_b.contains( "~🔴" ),
"FT-05 case B: non-owned non-cached must not show ~ tilde on status emoji; got:\n{text_b}",
);
assert!(
text_b.contains( "🔴" ),
"FT-05 case B: non-owned non-cached Err must show 🔴 status; got:\n{text_b}",
);
}
#[ test ]
fn ft12_json_output_includes_is_owned()
{
let owned = AccountQuota
{
name : "alice@test.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : Some( claude_quota::PeriodUsage { utilization : 20.0, resets_at : None } ),
seven_day : None,
seven_day_sonnet : None,
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
};
let not_owned = AccountQuota
{
name : "bob@test.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Err( "not owned".to_string() ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : false,
owner : String::new(),
};
let json = render_json( &[ owned, not_owned ] );
assert!(
json.contains( "\"is_owned\":true" ),
"FT-12: JSON must include '\"is_owned\":true' for owned account; got:\n{json}",
);
assert!(
json.contains( "\"is_owned\":false" ),
"FT-12: JSON must include '\"is_owned\":false' for non-owned account; got:\n{json}",
);
}
#[ test ]
fn test_render_footer_model_label_at_15pct_no_override()
{
let aq_a = AccountQuota
{
name : "a@x.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day_sonnet : Some( claude_quota::PeriodUsage { utilization : 85.0, resets_at : None } ),
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
};
let aq_b = AccountQuota
{
name : "b@x.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day_sonnet : Some( claude_quota::PeriodUsage { utilization : 50.0, resets_at : None } ),
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
};
let aq_cur = AccountQuota
{
name : "cur@x.com".to_string(),
is_current : true,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day_sonnet : Some( claude_quota::PeriodUsage { utilization : 50.0, resets_at : None } ),
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
};
let output = render_text(
&[ aq_cur, aq_a, aq_b ], SortStrategy::Name, None, PreferStrategy::Any,
&ColsVisibility::default_set(), None, None, None, None,
);
assert!(
output.contains( "· sonnet" ),
"FT-28 boundary: footer line 2 must show '· sonnet' when sonnet_left = 15.0% (not < 15%); got:\n{output}",
);
}
#[ test ]
fn test_render_footer_model_label_below_15pct_opus()
{
let aq_a = AccountQuota
{
name : "a@x.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day_sonnet : Some( claude_quota::PeriodUsage { utilization : 85.1, resets_at : None } ),
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
};
let aq_b = AccountQuota
{
name : "b@x.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day_sonnet : Some( claude_quota::PeriodUsage { utilization : 50.0, resets_at : None } ),
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
};
let aq_cur = AccountQuota
{
name : "cur@x.com".to_string(),
is_current : true,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day_sonnet : Some( claude_quota::PeriodUsage { utilization : 50.0, resets_at : None } ),
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
};
let output = render_text(
&[ aq_cur, aq_a, aq_b ], SortStrategy::Name, None, PreferStrategy::Any,
&ColsVisibility::default_set(), None, None, None, None,
);
assert!(
output.contains( "· opus" ),
"FT-28 boundary: footer line 2 must show '· opus' when sonnet_left ≈ 14.9% (< 15%); got:\n{output}",
);
}
#[ test ]
#[ allow( clippy::too_many_lines ) ]
fn test_ft29_009_footer_session_effort_display()
{
fn make_accounts() -> Vec< AccountQuota >
{
vec![
AccountQuota
{
name : "cur@x.com".to_string(),
is_current : true,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day_sonnet : Some( claude_quota::PeriodUsage { utilization : 50.0, resets_at : None } ),
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
},
AccountQuota
{
name : "a@x.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day_sonnet : Some( claude_quota::PeriodUsage { utilization : 50.0, resets_at : None } ),
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
},
AccountQuota
{
name : "b@x.com".to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day_sonnet : Some( claude_quota::PeriodUsage { utilization : 50.0, resets_at : None } ),
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
},
]
}
{
let accounts = make_accounts();
let output = render_text(
&accounts, SortStrategy::Renew, None, PreferStrategy::Any,
&ColsVisibility::default_set(), Some( "claude-sonnet-4-6" ), Some( "low" ), None, None,
);
assert!(
output.contains( "claude-sonnet-4-6/low" ),
"FT-29 s1: footer Current line col3 must be 'claude-sonnet-4-6/low'; got:\n{output}",
);
assert!(
output.contains( "Current" ),
"FT-29 s1: footer must have Current line; got:\n{output}",
);
}
{
let accounts = make_accounts();
let output = render_text(
&accounts, SortStrategy::Renew, None, PreferStrategy::Any,
&ColsVisibility::default_set(), Some( "claude-sonnet-4-6" ), None, None, None,
);
assert!(
output.contains( "claude-sonnet-4-6" ),
"FT-29 s2: footer Current line must contain model name; got:\n{output}",
);
assert!(
!output.contains( "effort:" ),
"FT-29 s2: footer must not contain 'effort:' label when effort is None; got:\n{output}",
);
assert!(
!output.contains( "/low" ),
"FT-29 s2: footer must not contain '/low' when effort is None; got:\n{output}",
);
}
{
let accounts = make_accounts();
let output = render_text(
&accounts, SortStrategy::Renew, None, PreferStrategy::Any,
&ColsVisibility::default_set(), None, None, None, None,
);
assert!(
output.contains( "Current" ),
"FT-29 s3: footer must have Current line even with no model/effort; got:\n{output}",
);
assert!(
!output.contains( "session:" ),
"FT-29 s3: footer must not contain 'session:' label when model is None; got:\n{output}",
);
assert!(
!output.contains( "effort:" ),
"FT-29 s3: footer must not contain 'effort:' label when effort is None; got:\n{output}",
);
}
}
#[ test ]
fn ft30_009_sessions_table_shown_auto_multiple_markers()
{
use tempfile::TempDir;
use crate::usage::test_support::mk_aq_ok;
let store = TempDir::new().unwrap();
let spath = store.path();
let own_fname = claude_profile_core::account::active_marker_filename();
std::fs::write( spath.join( &own_fname ), "own@example.com" ).unwrap();
std::fs::write( spath.join( "_active_serverA_bob" ), "bob@example.com" ).unwrap();
std::fs::write( spath.join( "_active_serverB_carol" ), "carol@example.com" ).unwrap();
let accounts = vec![ mk_aq_ok( 10.0 ) ];
let cols = ColsVisibility::default_set();
let output = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any,
&cols, None, None, Some( spath ), None,
);
assert!(
output.contains( "Sessions" ),
"FT-30: sessions table header must appear with 3 markers (who=None); got:\n{output}",
);
assert!(
output.contains( "own@example.com \u{2713}" ),
"FT-30: own session must show '\u{2713}' on account cell; got:\n{output}",
);
assert!(
output.contains( "bob@example.com" ),
"FT-30: bob row must appear in sessions table; got:\n{output}",
);
assert!(
output.contains( "carol@example.com" ),
"FT-30: carol row must appear in sessions table; got:\n{output}",
);
}
#[ test ]
fn ft31_009_sessions_table_hidden_auto_single_marker()
{
use tempfile::TempDir;
use crate::usage::test_support::mk_aq_ok;
let store = TempDir::new().unwrap();
let spath = store.path();
let own_fname = claude_profile_core::account::active_marker_filename();
std::fs::write( spath.join( &own_fname ), "own@example.com" ).unwrap();
let accounts = vec![ mk_aq_ok( 10.0 ) ];
let cols = ColsVisibility::default_set();
let output = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any,
&cols, None, None, Some( spath ), None,
);
assert!(
!output.contains( "Sessions" ),
"FT-31: sessions table must be hidden with only 1 marker (who=None); got:\n{output}",
);
}
#[ test ]
fn ft32_009_sessions_table_who_override()
{
use tempfile::TempDir;
use crate::usage::test_support::mk_aq_ok;
{
let store = TempDir::new().unwrap();
let spath = store.path();
let own_fname = claude_profile_core::account::active_marker_filename();
std::fs::write( spath.join( &own_fname ), "own@example.com" ).unwrap();
let accounts = vec![ mk_aq_ok( 10.0 ) ];
let output = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any,
&ColsVisibility::default_set(), None, None, Some( spath ), Some( true ),
);
assert!(
output.contains( "Sessions" ),
"FT-32 who=1: sessions table must appear with who=Some(true) even with 1 marker; got:\n{output}",
);
}
{
let store = TempDir::new().unwrap();
let spath = store.path();
let own_fname = claude_profile_core::account::active_marker_filename();
std::fs::write( spath.join( &own_fname ), "own@example.com" ).unwrap();
std::fs::write( spath.join( "_active_serverA_bob" ), "bob@example.com" ).unwrap();
std::fs::write( spath.join( "_active_serverB_carol" ), "carol@example.com" ).unwrap();
let accounts = vec![ mk_aq_ok( 10.0 ) ];
let output = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any,
&ColsVisibility::default_set(), None, None, Some( spath ), Some( false ),
);
assert!(
!output.contains( "Sessions" ),
"FT-32 who=0: sessions table must be suppressed with who=Some(false) even with 3 markers; got:\n{output}",
);
}
}
#[ doc = "bug_reproducer(BUG-308)" ]
#[ test ]
fn ft13_025_sessions_table_parses_marker_identity_from_filename()
{
use tempfile::TempDir;
use crate::usage::test_support::mk_aq_ok;
let store = TempDir::new().unwrap();
let spath = store.path();
let own_fname = claude_profile_core::account::active_marker_filename();
let other1_fname = "_active_testhost1_tst1";
let other2_fname = "_active_testhost2_tst2";
assert_ne!(
own_fname.as_str(), other1_fname,
"BUG-308 guard: own marker '{own_fname}' must not equal other1 '{other1_fname}' — pick different synthetic names",
);
assert_ne!(
own_fname.as_str(), other2_fname,
"BUG-308 guard: own marker '{own_fname}' must not equal other2 '{other2_fname}' — pick different synthetic names",
);
std::fs::write( spath.join( other1_fname ), "alice@test.com" ).unwrap();
std::fs::write( spath.join( other2_fname ), "bob@test.com" ).unwrap();
std::fs::write( spath.join( &own_fname ), "own@test.com" ).unwrap();
let accounts = vec![ mk_aq_ok( 10.0 ) ];
let cols = ColsVisibility::default_set();
let output = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any,
&cols, None, None, Some( spath ), None,
);
assert!(
output.contains( "Sessions" ),
"FT-13: sessions table must appear with 3 markers; got:\n{output}",
);
assert!(
output.contains( "tst1@testhost1" ),
"FT-13: `_active_testhost1_tst1` must render session 'tst1@testhost1'; got:\n{output}",
);
assert!(
output.contains( "tst2@testhost2" ),
"FT-13: `_active_testhost2_tst2` must render session 'tst2@testhost2'; got:\n{output}",
);
assert!(
output.contains( "alice@test.com" ),
"FT-13: alice account from file content must appear; got:\n{output}",
);
assert!(
output.contains( "bob@test.com" ),
"FT-13: bob account from file content must appear; got:\n{output}",
);
assert!(
output.contains( "own@test.com \u{2713}" ),
"FT-13: own session row must show '\u{2713}' on account cell; got:\n{output}",
);
}
#[ test ]
fn ec5_062_who_force_on_zero_markers_omits_gracefully()
{
use tempfile::TempDir;
use crate::usage::test_support::mk_aq_ok;
let store = TempDir::new().unwrap();
let spath = store.path();
let accounts = vec![ mk_aq_ok( 10.0 ) ];
let output = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any,
&ColsVisibility::default_set(), None, None, Some( spath ), Some( true ),
);
assert!(
!output.contains( "Sessions" ),
"EC-5: sessions table must be gracefully omitted when store has 0 markers \
and who=Some(true); got:\n{output}",
);
}
#[ test ]
fn cc_single_valid_account_no_footer()
{
use crate::usage::test_support::mk_aq_ok;
let mut aq = mk_aq_ok( 20.0 );
aq.is_current = true;
let accounts = vec![ aq ];
let output = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any,
&ColsVisibility::default_set(), None, None, None, None,
);
assert!(
!output.contains( "Current \u{00b7}" ),
"CC-06: single valid account must not show 'Current ·' footer; got:\n{output}",
);
assert!(
!output.contains( "Next (" ),
"CC-06: single valid account must not show 'Next (...)' footer; got:\n{output}",
);
assert!(
!output.contains( "Valid:" ),
"CC-06: single valid account must not show 'Valid:' footer; got:\n{output}",
);
}
#[ test ]
fn cc_no_current_account_uses_legacy_footer()
{
let mk = | name : &str |
{
AccountQuota
{
name : name.to_string(),
is_current : false,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day : None,
seven_day_sonnet : None,
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
}
};
let accounts = vec![ mk( "a@x.com" ), mk( "b@x.com" ) ];
let output = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any,
&ColsVisibility::default_set(), None, None, None, None,
);
assert!(
output.contains( "Valid:" ),
"CC-07: no is_current among ≥2 valid must use legacy 'Valid:' footer; got:\n{output}",
);
assert!(
!output.contains( "Current" ),
"CC-07: legacy footer must not contain 'Current' line; got:\n{output}",
);
}
#[ test ]
fn cc_effort_only_footer_shows_effort_without_model()
{
let mk = | name : &str, cur : bool |
{
AccountQuota
{
name : name.to_string(),
is_current : cur,
is_active : false,
is_occupied_elsewhere : false,
expires_at_ms : FAR_FUTURE_MS,
result : Ok( claude_quota::OauthUsageData
{
five_hour : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day : Some( claude_quota::PeriodUsage { utilization : 10.0, resets_at : None } ),
seven_day_sonnet : Some( claude_quota::PeriodUsage { utilization : 50.0, resets_at : None } ),
} ),
account : None,
host : String::new(),
role : String::new(),
renewal_at : None,
cached : false,
cache_age_secs : None,
is_owned : true,
owner : String::new(),
}
};
let accounts = vec![ mk( "cur@x.com", true ), mk( "a@x.com", false ), mk( "b@x.com", false ) ];
let output = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any,
&ColsVisibility::default_set(), None, Some( "high" ), None, None,
);
let current_line = output.lines().find( |l| l.trim_start().starts_with( "Current" ) )
.unwrap_or( "" );
assert!(
current_line.contains( "high" ),
"CC-08: effort-only footer Current line must contain effort level 'high'; got:\n{output}",
);
assert!(
!current_line.contains( "/high" ),
"CC-08: effort-only footer Current line must not have model prefix '/high'; got:\n{output}",
);
}
#[ test ]
fn ft05_footer_next_shows_model_and_effort_when_set()
{
let mut cur = mk_aq_sort( "cur@x.com", 50.0, FAR_FUTURE_MS );
cur.is_current = true;
let rec = mk_aq_sort_weekly( "aaa@x.com", 50.0, 50.0, 80.0 ); let accounts = vec![ cur, rec ];
let output = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any,
&ColsVisibility::default_set(), None, Some( "high" ), None, None,
);
assert!(
output.contains( "sonnet/high" ),
"FT-05: footer Next must contain 'sonnet/high' when session_effort=Some(\"high\") and Sonnet available; got:\n{output}",
);
}
#[ test ]
fn ft06_footer_next_shows_only_model_when_effort_absent()
{
let mut cur = mk_aq_sort( "cur@x.com", 50.0, FAR_FUTURE_MS );
cur.is_current = true;
let rec = mk_aq_sort_weekly( "aaa@x.com", 50.0, 50.0, 80.0 ); let accounts = vec![ cur, rec ];
let output = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any,
&ColsVisibility::default_set(), None, None, None, None,
);
assert!(
output.contains( "sonnet" ) && !output.contains( "sonnet/" ),
"FT-06: footer Next must contain 'sonnet' with no slash when session_effort=None; got:\n{output}",
);
}
#[ test ]
fn ft07_footer_next_shows_opus_and_effort_when_sonnet_exhausted()
{
let mut cur = mk_aq_sort( "cur@x.com", 50.0, FAR_FUTURE_MS );
cur.is_current = true;
let rec = mk_aq_sort_weekly( "aaa@x.com", 50.0, 50.0, 90.0 ); let accounts = vec![ cur, rec ];
let output = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any,
&ColsVisibility::default_set(), None, Some( "max" ), None, None,
);
assert!(
output.contains( "opus/max" ),
"FT-07: footer Next must contain 'opus/max' when Sonnet exhausted and session_effort=Some(\"max\"); got:\n{output}",
);
}
#[ test ]
fn ft08_footer_column_alignment_third_dot()
{
let mut cur = mk_aq_sort( "cur@x.com", 50.0, FAR_FUTURE_MS );
cur.is_current = true;
let rec = mk_aq_sort_weekly( "aaa@x.com", 50.0, 50.0, 80.0 );
let accounts = vec![ cur, rec ];
let output = render_text(
&accounts, SortStrategy::Name, None, PreferStrategy::Any,
&ColsVisibility::default_set(), Some( "s" ), None, None, None,
);
let footer_lines : Vec< &str > = output.lines()
.filter( |l| l.contains( '·' ) )
.collect();
assert!(
footer_lines.len() >= 2,
"FT-08: must have ≥2 footer lines with ·; got:\n{output}",
);
let cur_line = footer_lines[ footer_lines.len() - 2 ];
let next_line = footer_lines[ footer_lines.len() - 1 ];
let third_dot_char_pos = |line : &str| -> Option< usize >
{
let mut count = 0usize;
for ( i, ch ) in line.chars().enumerate()
{
if ch == '·' { count += 1; if count == 3 { return Some( i ); } }
}
None
};
let cur_pos = third_dot_char_pos( cur_line );
let next_pos = third_dot_char_pos( next_line );
assert_eq!(
cur_pos, next_pos,
"FT-08: third · must be at same char position in Current and Next lines;\n cur: '{cur_line}'\n next: '{next_line}'",
);
}