#![ cfg( unix ) ]
use std::os::unix::fs::PermissionsExt;
use std::process::Command;
fn fake_claude( script : &str ) -> ( tempfile::TempDir, String )
{
let tmp = tempfile::tempdir().expect( "Failed to create temp dir" );
let fake = tmp.path().join( "claude" );
std::fs::write( &fake, script ).expect( "Failed to write fake claude" );
std::fs::set_permissions( &fake, std::fs::Permissions::from_mode( 0o755 ) )
.expect( "Failed to chmod fake claude" );
let old_path = std::env::var( "PATH" ).unwrap_or_default();
let new_path = format!( "{}:{old_path}", tmp.path().display() );
( tmp, new_path )
}
fn run_with_path( args : &[ &str ], path : &str ) -> std::process::Output
{
let bin = env!( "CARGO_BIN_EXE_clr" );
Command::new( bin )
.args( args )
.env( "PATH", path )
.output()
.expect( "Failed to invoke clr binary" )
}
fn run_no_claude( args : &[ &str ] ) -> std::process::Output
{
let bin = env!( "CARGO_BIN_EXE_clr" );
Command::new( bin )
.args( args )
.env( "PATH", "/nonexistent" )
.output()
.expect( "Failed to invoke clr binary" )
}
#[ test ]
fn e01_interactive_binary_not_found()
{
let out = run_no_claude( &[ "test message" ] );
assert!( !out.status.success(), "must exit non-zero when claude not found" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "Error:" ),
"must report error to stderr. Got:\n{stderr}"
);
}
#[ test ]
fn e02_print_binary_not_found()
{
let out = run_no_claude( &[ "-p", "test message" ] );
assert!( !out.status.success(), "must exit non-zero when claude not found" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "Error:" ),
"must report error to stderr. Got:\n{stderr}"
);
}
#[ test ]
fn e03_interactive_exit_code_propagated()
{
let ( _tmp, path ) = fake_claude( "#!/bin/sh\nexit 42\n" );
let out = run_with_path( &[ "--interactive", "test" ], &path );
assert_eq!(
out.status.code(), Some( 42 ),
"interactive mode must propagate subprocess exit code. Got: {:?}",
out.status.code()
);
}
#[ test ]
fn e04_print_exit_nonzero_error()
{
let ( _tmp, path ) = fake_claude( "#!/bin/sh\nexit 3\n" );
let out = run_with_path( &[ "-p", "test" ], &path );
assert!( !out.status.success(), "must exit non-zero" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "Claude exited with code 3" ),
"must report exit code in error. Got:\n{stderr}"
);
}
#[ test ]
fn e05_print_stderr_forwarded()
{
let ( _tmp, path ) = fake_claude( "#!/bin/sh\necho STDERR_MARKER >&2\necho STDOUT_OK\n" );
let out = run_with_path( &[ "-p", "test" ], &path );
assert!( out.status.success(), "must exit 0" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "STDERR_MARKER" ),
"subprocess stderr must be forwarded. Got:\n{stderr}"
);
}
#[ test ]
fn e06_print_stdout_captured()
{
let ( _tmp, path ) = fake_claude( "#!/bin/sh\necho CAPTURED_OUTPUT\n" );
let out = run_with_path( &[ "-p", "test" ], &path );
assert!( out.status.success(), "must exit 0" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "CAPTURED_OUTPUT" ),
"subprocess stdout must appear in runner stdout. Got:\n{stdout}"
);
}
#[ test ]
fn e07_interactive_not_found_verbosity_zero()
{
let bin = env!( "CARGO_BIN_EXE_clr" );
let out = Command::new( bin )
.args( [ "--verbosity", "0", "test" ] )
.env( "PATH", "/nonexistent" )
.output()
.expect( "Failed to invoke" );
assert!( !out.status.success(), "must exit non-zero" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.is_empty(),
"--verbosity 0 must suppress error output. Got:\n{stderr}"
);
}
#[ test ]
fn e08_print_not_found_verbosity_zero()
{
let bin = env!( "CARGO_BIN_EXE_clr" );
let out = Command::new( bin )
.args( [ "--verbosity", "0", "-p", "test" ] )
.env( "PATH", "/nonexistent" )
.output()
.expect( "Failed to invoke" );
assert!( !out.status.success(), "must exit non-zero" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.is_empty(),
"--verbosity 0 must suppress error output. Got:\n{stderr}"
);
}
#[ test ]
fn e09_verbosity_four_stderr_preview()
{
let ( _tmp, path ) = fake_claude( "#!/bin/sh\necho OK\n" );
let bin = env!( "CARGO_BIN_EXE_clr" );
let out = Command::new( bin )
.args( [ "--verbosity", "4", "-p", "test" ] )
.env( "PATH", &path )
.output()
.expect( "Failed to invoke" );
assert!( out.status.success(), "must exit 0" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "CLAUDE_CODE_MAX_OUTPUT_TOKENS=" ),
"--verbosity 4 must print env preview to stderr. Got:\n{stderr}"
);
assert!(
stderr.contains( "claude" ),
"--verbosity 4 must print command preview to stderr. Got:\n{stderr}"
);
}
#[ test ]
fn e10_interactive_message_forwarded()
{
let tmp = tempfile::tempdir().expect( "create temp dir" );
let args_file = tmp.path().join( "received_args" );
let fake = tmp.path().join( "claude" );
let script = format!(
"#!/bin/sh\necho \"$@\" > \"{}\"\n",
args_file.display()
);
std::fs::write( &fake, script ).expect( "write fake" );
std::fs::set_permissions( &fake, std::fs::Permissions::from_mode( 0o755 ) )
.expect( "chmod" );
let old_path = std::env::var( "PATH" ).unwrap_or_default();
let path = format!( "{}:{old_path}", tmp.path().display() );
let bin = env!( "CARGO_BIN_EXE_clr" );
let out = Command::new( bin )
.args( [ "hello world" ] )
.env( "PATH", &path )
.output()
.expect( "invoke" );
assert!( out.status.success(), "must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let received = std::fs::read_to_string( &args_file )
.expect( "read args file" );
assert!(
received.contains( "hello world" ),
"message must be forwarded to subprocess. Received args: {received}"
);
assert!(
received.contains( "-c" ),
"automatic -c must be forwarded to subprocess. Received args: {received}"
);
}
#[ test ]
fn e11_new_session_does_not_pass_continue()
{
let tmp = tempfile::tempdir().expect( "create temp dir" );
let args_file = tmp.path().join( "received_args" );
let fake = tmp.path().join( "claude" );
let script = format!(
"#!/bin/sh\necho \"$@\" > \"{}\"\n",
args_file.display()
);
std::fs::write( &fake, script ).expect( "write fake" );
std::fs::set_permissions( &fake, std::fs::Permissions::from_mode( 0o755 ) )
.expect( "chmod" );
let old_path = std::env::var( "PATH" ).unwrap_or_default();
let path = format!( "{}:{old_path}", tmp.path().display() );
let bin = env!( "CARGO_BIN_EXE_clr" );
let out = Command::new( bin )
.args( [ "--new-session", "hello world" ] )
.env( "PATH", &path )
.output()
.expect( "invoke" );
assert!( out.status.success(), "must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let received = std::fs::read_to_string( &args_file )
.expect( "read args file" );
assert!(
!received.contains( " -c" ),
"--new-session must suppress -c. Received args: {received}"
);
}
#[ test ]
fn e12_message_without_print_flag_uses_print_mode()
{
let tmp = tempfile::tempdir().expect( "create temp dir" );
let args_file = tmp.path().join( "received_args" );
let fake = tmp.path().join( "claude" );
let script = format!(
"#!/bin/sh\necho \"$@\" > \"{}\"\n",
args_file.display()
);
std::fs::write( &fake, &script ).expect( "write fake" );
std::fs::set_permissions( &fake, std::fs::Permissions::from_mode( 0o755 ) )
.expect( "chmod" );
let old_path = std::env::var( "PATH" ).unwrap_or_default();
let path = format!( "{}:{old_path}", tmp.path().display() );
let bin = env!( "CARGO_BIN_EXE_clr" );
let out = Command::new( bin )
.args( [ "Fix the bug" ] )
.env( "PATH", &path )
.output()
.expect( "invoke" );
assert!( out.status.success(), "must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let received = std::fs::read_to_string( &args_file )
.expect( "read args file" );
assert!(
received.contains( "--print" ),
"message without -p must route to print mode (--print in subprocess args). Received: {received}"
);
}
#[ test ]
fn e13_interactive_flag_with_message_uses_interactive_mode()
{
let tmp = tempfile::tempdir().expect( "create temp dir" );
let args_file = tmp.path().join( "received_args" );
let fake = tmp.path().join( "claude" );
let script = format!(
"#!/bin/sh\necho \"$@\" > \"{}\"\n",
args_file.display()
);
std::fs::write( &fake, &script ).expect( "write fake" );
std::fs::set_permissions( &fake, std::fs::Permissions::from_mode( 0o755 ) )
.expect( "chmod" );
let old_path = std::env::var( "PATH" ).unwrap_or_default();
let path = format!( "{}:{old_path}", tmp.path().display() );
let bin = env!( "CARGO_BIN_EXE_clr" );
let out = Command::new( bin )
.args( [ "--interactive", "Fix the bug" ] )
.env( "PATH", &path )
.output()
.expect( "invoke" );
assert!( out.status.success(), "must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let received = std::fs::read_to_string( &args_file )
.expect( "read args file" );
assert!(
!received.contains( "--print" ),
"--interactive must suppress --print (interactive mode). Received: {received}"
);
}