mod cli_binary_test_helpers;
use cli_binary_test_helpers::{ run_cli, run_cli_with_env, stderr_str, stdout_str };
#[ cfg( unix ) ]
use cli_binary_test_helpers::{ fake_claude_binary_dir, run_clr_ps, spawn_fake_claude };
#[ test ]
fn it_01_no_sessions_shows_message()
{
let out = run_cli( &[ "ps" ] );
let stdout = stdout_str( &out );
assert!( out.status.success(), "exit code must be 0, got {:?}", out.status.code() );
assert!(
stdout.contains( "No active Claude Code sessions." ),
"stdout must contain the no-sessions message, got: {stdout}"
);
}
#[ cfg( unix ) ]
#[ test ]
fn it_02_sessions_present_plain_style()
{
let ( _dir, path_val ) = fake_claude_binary_dir();
let mut bg = spawn_fake_claude( &path_val );
let out = run_clr_ps( &path_val );
let _ = bg.kill();
let _ = bg.wait();
let stdout = stdout_str( &out );
assert!( out.status.success(), "exit code must be 0, got {:?}", out.status.code() );
assert!(
stdout.contains( "PID" ),
"stdout must contain PID header, got: {stdout}"
);
assert!(
!stdout.contains( '\u{250C}' ), "stdout must use plain style — no ┌ border, got: {stdout}"
);
}
#[ test ]
fn it_03_help_lists_ps()
{
let out = run_cli( &[ "--help" ] );
let stdout = stdout_str( &out );
assert!( out.status.success(), "exit 0 expected, got {:?}", out.status.code() );
assert!(
stdout.contains( "ps" ),
"help output must mention ps, got: {stdout}"
);
}
#[ test ]
fn it_04_typo_clr_p()
{
let out = run_cli( &[ "p" ] );
let stderr = stderr_str( &out );
assert!( !out.status.success(), "expected non-zero exit" );
assert!(
stderr.contains( "Did you mean" ),
"stderr must contain 'Did you mean', got: {stderr}"
);
}
#[ cfg( unix ) ]
#[ test ]
fn it_05_table_headers_present()
{
let ( _dir, path_val ) = fake_claude_binary_dir();
let mut bg = spawn_fake_claude( &path_val );
let out = run_clr_ps( &path_val );
let _ = bg.kill();
let _ = bg.wait();
let stdout = stdout_str( &out );
assert!( out.status.success(), "exit 0 expected, got {:?}", out.status.code() );
assert!( stdout.contains( "PID" ), "missing PID header: {stdout}" );
assert!( stdout.contains( "Elapsed" ), "missing Elapsed header: {stdout}" );
assert!( stdout.contains( "Absolute Path" ), "missing Absolute Path header: {stdout}" );
assert!( stdout.contains( "Task" ), "missing Task header: {stdout}" );
}
#[ test ]
fn it_06_typo_clr_pss()
{
let out = run_cli( &[ "pss" ] );
let stderr = stderr_str( &out );
assert!( !out.status.success(), "expected non-zero exit" );
assert!(
stderr.contains( "Did you mean" ),
"stderr must contain 'Did you mean', got: {stderr}"
);
}
#[ cfg( unix ) ]
#[ test ]
fn it_07_self_exclusion()
{
let ( _dir, path_val ) = fake_claude_binary_dir();
let mut bg = spawn_fake_claude( &path_val );
let out = run_clr_ps( &path_val );
let _ = bg.kill();
let _ = bg.wait();
let stdout = stdout_str( &out );
assert!( out.status.success(), "exit 0 expected, got {:?}", out.status.code() );
let self_pid = std::process::id().to_string();
assert!(
!stdout.contains( &self_pid ),
"test-runner PID {self_pid} must not appear in ps output: {stdout}"
);
}
#[ test ]
fn it_08_unknown_flag()
{
let out = run_cli( &[ "ps", "--unknown" ] );
let stderr = stderr_str( &out );
assert!( !out.status.success(), "expected non-zero exit" );
assert!(
stderr.contains( "unexpected argument" ),
"stderr must mention unexpected argument, got: {stderr}"
);
}
#[ cfg( unix ) ]
#[ test ]
fn it_09_pro_prefix_shortened_in_path_column()
{
let pro_dir = tempfile::TempDir::new().expect( "create tmp PRO dir" );
let sub_dir = pro_dir.path().join( "my" ).join( "project" );
std::fs::create_dir_all( &sub_dir ).expect( "create project subdir" );
let pro_str = pro_dir.path().to_str().expect( "PRO path is UTF-8" );
let ( _bin_dir, path_val ) = fake_claude_binary_dir();
let mut bg = std::process::Command::new( "claude" )
.env( "PATH", &path_val )
.arg( "30" )
.current_dir( &sub_dir )
.stdout( std::process::Stdio::null() )
.stderr( std::process::Stdio::null() )
.spawn()
.expect( "spawn fake claude in sub_dir" );
std::thread::sleep( core::time::Duration::from_millis( 200 ) );
let bin = env!( "CARGO_BIN_EXE_clr" );
let out = std::process::Command::new( bin )
.arg( "ps" )
.env( "PATH", &path_val )
.env( "PRO", pro_str )
.output()
.expect( "run clr ps with PRO set" );
let _ = bg.kill();
let _ = bg.wait();
let stdout = stdout_str( &out );
assert!( out.status.success(), "exit 0 expected, got {:?}", out.status.code() );
assert!(
stdout.contains( "$PRO" ),
"IT-9: path must be shortened to $PRO/… when PRO env var is set. Got:\n{stdout}"
);
assert!(
!stdout.contains( pro_str ),
"IT-9: full PRO prefix must not appear in the table. Got:\n{stdout}"
);
}
#[ test ]
fn it_10_gate_file_present_shows_queued_table()
{
let gate_dir = tempfile::TempDir::new().expect( "create gate temp dir" );
let gate_dir_path = gate_dir.path().to_str().expect( "gate dir UTF-8" );
let gate_file = gate_dir.path().join( "99999.json" );
std::fs::write(
&gate_file,
r#"{"cwd":"/tmp/test-project","since":1720000000,"attempt":3,"message":"waiting for session slot"}"#,
).expect( "write gate file" );
let out = run_cli_with_env( &[ "ps" ], &[ ( "CLR_GATE_DIR", gate_dir_path ) ] );
let stdout = stdout_str( &out );
assert!( out.status.success(), "exit 0 expected, got {:?}", out.status.code() );
assert!( stdout.contains( "PID" ), "missing PID header in queued table: {stdout}" );
assert!( stdout.contains( "CWD" ), "missing CWD header in queued table: {stdout}" );
assert!( stdout.contains( "Waiting" ), "missing Waiting header in queued table: {stdout}" );
}
#[ test ]
fn it_11_no_gate_files_no_queued_table()
{
let gate_dir = tempfile::TempDir::new().expect( "create gate temp dir" );
let gate_dir_path = gate_dir.path().to_str().expect( "gate dir UTF-8" );
let out = run_cli_with_env( &[ "ps" ], &[ ( "CLR_GATE_DIR", gate_dir_path ) ] );
let stdout = stdout_str( &out );
assert!( out.status.success(), "exit 0 expected, got {:?}", out.status.code() );
assert!(
stdout.contains( "No active Claude Code sessions." ),
"must show no-sessions message with empty gate dir: {stdout}"
);
assert!(
!stdout.contains( "Waiting" ),
"must not contain Waiting header when no gate files: {stdout}"
);
assert!(
!stdout.contains( "Attempt" ),
"must not contain Attempt header when no gate files: {stdout}"
);
}
#[ cfg( unix ) ]
#[ test ]
fn it_12_active_table_has_caption()
{
let ( _dir, path_val ) = fake_claude_binary_dir();
let mut bg = spawn_fake_claude( &path_val );
let out = run_clr_ps( &path_val );
let _ = bg.kill();
let _ = bg.wait();
let stdout = stdout_str( &out );
assert!( out.status.success(), "exit 0 expected, got {:?}", out.status.code() );
assert!(
stdout.contains( "Active Sessions" ),
"IT-12: active table caption must contain 'Active Sessions', got: {stdout}"
);
assert!(
stdout.contains( "running" ),
"IT-12: active table caption must contain 'running' count suffix, got: {stdout}"
);
}