mod common;
use tempfile::TempDir;
fn stdout( out : &std::process::Output ) -> String
{
String::from_utf8_lossy( &out.stdout ).into_owned()
}
fn stderr( out : &std::process::Output ) -> String
{
String::from_utf8_lossy( &out.stderr ).into_owned()
}
fn assert_exit( out : &std::process::Output, code : i32 )
{
assert_eq!(
out.status.code().unwrap_or( -1 ),
code,
"expected exit {code}, got {:?}; stderr: {}",
out.status.code(),
stderr( out )
);
}
#[test]
fn it_17_v1_groups_sessions_by_project_path()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project_a = root.path().join( "proj_alpha" );
let project_b = root.path().join( "proj_beta" );
common::write_path_project_session( &storage_root, &project_a, "session-alpha-001", 2 );
common::write_path_project_session( &storage_root, &project_b, "session-beta-001", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.lines().any( | l | l.contains( ':' ) && ( l.contains( '/' ) || l.contains( '~' ) ) ),
"v1 must show project path headers ending with ':'; got:\n{s}"
);
assert!( s.contains( "session-alpha-001" ), "must contain session-alpha-001; got:\n{s}" );
assert!( s.contains( "session-beta-001" ), "must contain session-beta-001; got:\n{s}" );
}
#[test]
fn it_18_path_header_present_at_v1_single_project()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "my_proj" );
common::write_path_project_session( &storage_root, &project, "session-path-test", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::local" )
.arg( format!( "path::{}", project.display() ) )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.lines().any( | l | l.contains( ':' ) && ( l.contains( '/' ) || l.contains( '~' ) ) ),
"path header must be shown at v1 even for single-project local scope; got:\n{s}"
);
}
#[test]
fn it_19_agent_sessions_collapsed_at_v1_no_filter()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "mixed_project" );
common::write_path_project_session( &storage_root, &project, "session-main-a", 2 );
common::write_path_project_session( &storage_root, &project, "session-main-b", 2 );
common::write_path_project_session( &storage_root, &project, "agent-task-001", 2 );
common::write_path_project_session( &storage_root, &project, "agent-task-002", 2 );
common::write_path_project_session( &storage_root, &project, "agent-task-003", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!( !s.contains( "agent-task-001" ), "agent-task-001 must NOT appear individually at v1; got:\n{s}" );
assert!( !s.contains( "agent-task-002" ), "agent-task-002 must NOT appear individually at v1; got:\n{s}" );
assert!( !s.contains( "agent-task-003" ), "agent-task-003 must NOT appear individually at v1; got:\n{s}" );
assert!(
s.contains( "agent" ),
"must show agent info in family display at v1; got:\n{s}"
);
assert!(
!s.contains( "+ " ),
"must NOT show old '+ N agent' collapse line; got:\n{s}"
);
assert!( s.contains( "session-main-a" ), "session-main-a must appear; got:\n{s}" );
assert!( s.contains( "session-main-b" ), "session-main-b must appear; got:\n{s}" );
}
#[test]
fn it_20_agent_sessions_shown_individually_at_v2()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "mixed_project_v2" );
common::write_path_project_session( &storage_root, &project, "session-main-a", 2 );
common::write_path_project_session( &storage_root, &project, "agent-task-001", 2 );
common::write_path_project_session( &storage_root, &project, "agent-task-002", 2 );
common::write_path_project_session( &storage_root, &project, "agent-task-003", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.arg( "verbosity::2" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!( s.contains( "agent-task-001" ), "agent-task-001 must appear individually at v2; got:\n{s}" );
assert!( s.contains( "agent-task-002" ), "agent-task-002 must appear individually at v2; got:\n{s}" );
assert!( s.contains( "agent-task-003" ), "agent-task-003 must appear individually at v2; got:\n{s}" );
assert!(
!s.contains( "+ " ),
"must NOT show old '+ N agent' collapse at v2; got:\n{s}"
);
assert!(
s.lines().any( | l | l.contains( ':' ) && ( l.contains( '/' ) || l.contains( '~' ) ) ),
"v2 must show project path header ending with ':'; got:\n{s}"
);
}
#[test]
fn it_21_entry_count_shown_at_v2()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "count_proj" );
common::write_path_project_session( &storage_root, &project, "session-count-test", 4 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.arg( "verbosity::2" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.contains( "(4 entries)" ),
"v2 must show '(4 entries)' for a 4-entry session; got:\n{s}"
);
}
#[test]
fn it_22_agent_filter_disables_collapse_at_v1()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "agent_filter_proj" );
common::write_path_project_session( &storage_root, &project, "session-main-z", 2 );
common::write_path_project_session( &storage_root, &project, "agent-task-001", 2 );
common::write_path_project_session( &storage_root, &project, "agent-task-002", 2 );
common::write_path_project_session( &storage_root, &project, "agent-task-003", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.arg( "verbosity::1" )
.arg( "agent::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!( s.contains( "agent-task-001" ), "agent-task-001 must appear individually with agent::1; got:\n{s}" );
assert!( s.contains( "agent-task-002" ), "agent-task-002 must appear individually with agent::1; got:\n{s}" );
assert!( s.contains( "agent-task-003" ), "agent-task-003 must appear individually with agent::1; got:\n{s}" );
assert!(
!s.contains( "3 agent" ),
"must NOT show collapse line when agent::1 is set; got:\n{s}"
);
assert!(
s.lines().any( | l | l.contains( ':' ) && ( l.contains( '/' ) || l.contains( '~' ) ) ),
"agent::1 v1 must show project path header ending with ':'; got:\n{s}"
);
}
#[test]
fn it_27_entry_count_shown_at_v1()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "count_v1_proj" );
common::write_path_project_session( &storage_root, &project, "session-v1-count", 4 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.contains( "(4 entries)" ),
"v1 must show '(4 entries)' for a 4-entry session; got:\n{s}"
);
}
#[test]
fn it_28_limit_truncates_display()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "limit_proj" );
common::write_path_project_session( &storage_root, &project, "session-limit-a", 2 );
common::write_path_project_session( &storage_root, &project, "session-limit-b", 2 );
common::write_path_project_session( &storage_root, &project, "session-limit-c", 2 );
common::write_path_project_session( &storage_root, &project, "session-limit-d", 2 );
common::write_path_project_session( &storage_root, &project, "session-limit-e", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.arg( "verbosity::1" )
.arg( "limit::2" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.contains( "and 3 more" ),
"limit::2 with 5 sessions must show '... and 3 more'; got:\n{s}"
);
}
#[test]
fn it_29_zero_byte_sessions_excluded_at_v1()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "zero_byte_proj" );
common::write_path_project_session( &storage_root, &project, "session-real", 2 );
{
let encoded = claude_storage_core::encode_path( &project ).unwrap();
let dir = storage_root.join( "projects" ).join( &encoded );
std::fs::create_dir_all( &dir ).unwrap();
let _ = std::fs::File::create( dir.join( "session-placeholder.jsonl" ) ).unwrap();
}
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
!s.contains( "session-placeholder" ),
"zero-byte placeholder must NOT appear at v1; got:\n{s}"
);
assert!(
s.contains( "session-real" ),
"real session must still appear when zero-byte is excluded; got:\n{s}"
);
}
#[test]
fn it_summary_mode_shows_active_project_header()
{
let root = TempDir::new().unwrap();
let project_path = root.path().join( "summary_proj" );
std::fs::create_dir_all( &project_path ).unwrap();
common::write_path_project_session( root.path(), &project_path, "session-sp-001", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", root.path().to_str().unwrap() )
.current_dir( &project_path )
.arg( ".projects" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.contains( "Active project" ),
"summary must say 'Active project'; got:\n{s}"
);
assert!(
!s.contains( "Active session" ),
"summary must NOT say 'Active session'; got:\n{s}"
);
}
#[test]
fn it_summary_mode_shows_session_count()
{
let root = TempDir::new().unwrap();
let project_path = root.path().join( "session_count_proj" );
std::fs::create_dir_all( &project_path ).unwrap();
common::write_path_project_session( root.path(), &project_path, "session-sc-001", 2 );
common::write_path_project_session( root.path(), &project_path, "session-sc-002", 2 );
common::write_path_project_session( root.path(), &project_path, "session-sc-003", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", root.path().to_str().unwrap() )
.current_dir( &project_path )
.arg( ".projects" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.contains( "sessions," ),
"summary must contain 'sessions,' aggregate count; got:\n{s}"
);
}
#[test]
fn it_list_mode_shows_projects_sorted_by_recency()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project_alpha = root.path().join( "proj_alpha" );
let project_beta = root.path().join( "proj_beta" );
std::fs::create_dir_all( &project_alpha ).unwrap();
std::fs::create_dir_all( &project_beta ).unwrap();
let enc_alpha = common::write_path_project_session(
&storage_root, &project_alpha, "session-alpha", 2
);
let enc_beta = common::write_path_project_session(
&storage_root, &project_beta, "session-beta", 2
);
let old_t = std::time::SystemTime::UNIX_EPOCH + core::time::Duration::from_secs( 1_000 );
let new_t = std::time::SystemTime::UNIX_EPOCH + core::time::Duration::from_secs( 2_000 );
{
let p = storage_root.join( "projects" ).join( &enc_alpha ).join( "session-alpha.jsonl" );
let f = std::fs::OpenOptions::new().write( true ).open( &p ).unwrap();
f.set_times( std::fs::FileTimes::new().set_modified( old_t ) ).unwrap();
}
{
let p = storage_root.join( "projects" ).join( &enc_beta ).join( "session-beta.jsonl" );
let f = std::fs::OpenOptions::new().write( true ).open( &p ).unwrap();
f.set_times( std::fs::FileTimes::new().set_modified( new_t ) ).unwrap();
}
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
let pos_beta = s.find( "proj_beta" ).expect( "proj_beta must appear in output" );
let pos_alpha = s.find( "proj_alpha" ).expect( "proj_alpha must appear in output" );
assert!(
pos_beta < pos_alpha,
"proj_beta (newer, t=2000) must appear before proj_alpha (older, t=1000); got:\n{s}"
);
}
#[test]
fn it_verbosity_0_shows_paths_only()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project_path = root.path().join( "proj_v0" );
std::fs::create_dir_all( &project_path ).unwrap();
common::write_path_project_session( &storage_root, &project_path, "session-v0-001", 2 );
common::write_path_project_session( &storage_root, &project_path, "session-v0-002", 2 );
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.env( "CLAUDE_STORAGE_ROOT", storage_root.to_str().unwrap() )
.arg( ".projects" )
.arg( "scope::global" )
.arg( "verbosity::0" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.contains( "proj_v0" ),
"v0 must output project path containing 'proj_v0'; got:\n{s}"
);
assert!(
!s.contains( "session-v0" ),
"v0 must NOT output session IDs; got:\n{s}"
);
}