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 verbosity_zero_no_header()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "proj" );
common::write_path_project_session( &storage_root, &project, "session-v0-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::global" )
.arg( "verbosity::0" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!( !s.contains( "Found" ), "verbosity::0 must not emit 'Found N projects' header; got:\n{s}" );
assert!( s.contains( "proj" ), "verbosity::0 must output project path; got:\n{s}" );
}
#[ test ]
fn session_filter_narrows_results()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "proj" );
common::write_path_project_session( &storage_root, &project, "session-keep-001", 2 );
common::write_path_project_session( &storage_root, &project, "session-drop-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( "session::keep" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!( s.contains( "session-keep-001" ), "must contain matching session; got:\n{s}" );
assert!( !s.contains( "session-drop-002" ), "must exclude non-matching session; got:\n{s}" );
}
#[ test ]
fn invalid_verbosity_rejected()
{
let root = TempDir::new().unwrap();
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.arg( ".projects" ).arg( "verbosity::99" )
.output().unwrap();
assert_exit( &out, 1 );
assert!(
stderr( &out ).contains( "verbosity" ),
"error must mention verbosity; got: {}",
stderr( &out )
);
}
#[ test ]
fn invalid_min_entries_rejected()
{
let root = TempDir::new().unwrap();
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.arg( ".projects" ).arg( "min_entries::-1" )
.output().unwrap();
assert_exit( &out, 1 );
assert!(
stderr( &out ).contains( "min_entries" ),
"error must mention min_entries; got: {}",
stderr( &out )
);
}
#[ test ]
fn agent_filter_includes_only_agent_sessions()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "proj" );
common::write_path_project_session( &storage_root, &project, "session-main", 2 );
common::write_path_project_session( &storage_root, &project, "agent-task-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( "agent::1" )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!( s.contains( "agent-task-001" ), "agent::1 must include agent session; got:\n{s}" );
assert!( !s.contains( "session-main" ), "agent::1 must exclude main session; got:\n{s}" );
}
#[ test ]
fn agent_filter_excludes_agent_sessions()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "proj" );
common::write_path_project_session( &storage_root, &project, "session-main", 2 );
common::write_path_project_session( &storage_root, &project, "agent-task-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( "agent::0" )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!( s.contains( "session-main" ), "agent::0 must include main session; got:\n{s}" );
assert!( !s.contains( "agent-task-002" ), "agent::0 must exclude agent session; got:\n{s}" );
}
#[ test ]
fn min_entries_filters_by_entry_count()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "proj" );
common::write_path_project_session( &storage_root, &project, "session-short", 2 );
common::write_path_project_session( &storage_root, &project, "session-long", 6 );
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( "min_entries::3" )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!( s.contains( "session-long" ), "min_entries::3 must include 6-entry session; got:\n{s}" );
assert!( !s.contains( "session-short" ), "min_entries::3 must exclude 2-entry session; got:\n{s}" );
}
#[ test ]
fn verbosity_two_includes_project_label()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "proj" );
common::write_path_project_session( &storage_root, &project, "session-v2-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::global" )
.arg( "verbosity::2" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!( s.contains( "Found" ), "verbosity::2 must emit 'Found N sessions' header; got:\n{s}" );
assert!(
s.lines().any( | l | l.contains( ':' ) && ( l.contains( '/' ) || l.contains( '~' ) ) ),
"verbosity::2 must show project path header; got:\n{s}"
);
assert!( s.contains( "session-v2-test" ), "must list session ID; got:\n{s}" );
}
#[ test ]
fn output_uses_singular_noun_when_exactly_one_session_found()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "proj" );
common::write_path_project_session( &storage_root, &project, "session-singular-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::global" )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
s.contains( "Found 1 project:" ),
"with 1 result, header must use singular 'project' (not 'projects'); got:\n{s}"
);
assert!(
!s.contains( "Found 1 projects:" ),
"with 1 result, header must NOT use plural 'projects'; got:\n{s}"
);
assert!( s.contains( "session-singular-test" ), "must list the session ID; got:\n{s}" );
}
#[ test ]
fn output_uses_plural_noun_when_multiple_sessions_found()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project_a = root.path().join( "proj-a" );
let project_b = root.path().join( "proj-b" );
common::write_path_project_session( &storage_root, &project_a, "session-plural-a", 2 );
common::write_path_project_session( &storage_root, &project_b, "session-plural-b", 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( "Found 2 projects:" ),
"with 2 distinct projects, header must use plural 'projects'; got:\n{s}"
);
}
#[ test ]
fn output_uses_plural_noun_when_zero_sessions_found()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
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( "Found 0 projects:" ),
"with 0 results, header must use plural 'projects' (zero takes plural in English); got:\n{s}"
);
}
#[ test ]
fn verbosity_negative_one_rejected()
{
let root = TempDir::new().unwrap();
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.arg( ".projects" ).arg( "verbosity::-1" )
.output().unwrap();
assert_exit( &out, 1 );
assert!(
stderr( &out ).contains( "verbosity" ),
"error must mention verbosity; got: {}",
stderr( &out )
);
}
#[ test ]
fn agent_value_out_of_range_rejected()
{
let root = TempDir::new().unwrap();
let out = common::clg_cmd()
.env( "HOME", root.path().to_str().unwrap() )
.arg( ".projects" ).arg( "agent::2" )
.output().unwrap();
assert_exit( &out, 1 );
}
#[ test ]
fn min_entries_zero_includes_all_sessions()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "proj" );
common::write_path_project_session( &storage_root, &project, "session-one-entry", 1 );
common::write_path_project_session( &storage_root, &project, "session-four-entry", 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( "min_entries::0" )
.arg( "verbosity::1" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!( s.contains( "session-one-entry" ), "min_entries::0 must include 1-entry session; got:\n{s}" );
assert!( s.contains( "session-four-entry" ), "min_entries::0 must include 4-entry session; got:\n{s}" );
}
#[ test ]
fn it_header_uses_conversations_not_sessions()
{
let root = TempDir::new().unwrap();
let storage_root = root.path().join( ".claude" );
let project = root.path().join( "proj" );
common::write_path_project_session( &storage_root, &project, "session-a", 2 );
common::write_path_project_session( &storage_root, &project, "session-b", 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" )
.output()
.unwrap();
assert_exit( &out, 0 );
let s = stdout( &out );
assert!(
!s.contains( "sessions)" ),
"Project header must not contain 'sessions)' — must say 'conversations)'\nOutput: {s}",
);
assert!(
s.contains( "conversations)" ),
"Project header must contain 'conversations)'\nOutput: {s}",
);
}