#![ cfg( unix ) ]
use std::process::Command;
mod cli_binary_test_helpers;
use cli_binary_test_helpers::{ fake_claude, fake_claude_dir, make_session_dir, run_with_path };
#[ test ]
fn e01_interactive_binary_not_found()
{
let out = run_with_path( &[ "test message" ], "/nonexistent" );
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_with_path( &[ "-p", "test message" ], "/nonexistent" );
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_stderr_forwarded()
{
let ( _tmp, path ) = fake_claude( "#!/bin/sh\necho 'claude error detail' >&2\nexit 3\n" );
let out = run_with_path( &[ "-p", "--retry-override", "0", "--max-sessions", "0", "test" ], &path );
assert!( !out.status.success(), "must exit non-zero" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "claude error detail" ),
"subprocess stderr must be forwarded on failure. 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" )
.env_remove( "CLR_TRACE" ) .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 still emit fatal spawn errors (BUG-240 fix). Got empty 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" )
.env_remove( "CLR_TRACE" ) .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 still emit fatal spawn errors (BUG-240 fix). Got empty 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 args_file = tempfile::NamedTempFile::new().expect( "create args file" );
let args_path = args_file.path().display().to_string();
let script = format!( "echo \"$@\" > \"{args_path}\"\n" );
let ( _tmp, path ) = fake_claude_dir( &script );
let ( _session, session_path ) = make_session_dir();
let bin = env!( "CARGO_BIN_EXE_clr" );
let out = Command::new( bin )
.args( [ "--session-dir", &session_path, "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.path() )
.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 args_file = tempfile::NamedTempFile::new().expect( "create args file" );
let args_path = args_file.path().display().to_string();
let script = format!( "echo \"$@\" > \"{args_path}\"\n" );
let ( _tmp, path ) = fake_claude_dir( &script );
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.path() )
.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 args_file = tempfile::NamedTempFile::new().expect( "create args file" );
let args_path = args_file.path().display().to_string();
let script = format!( "echo \"$@\" > \"{args_path}\"\n" );
let ( _tmp, path ) = fake_claude_dir( &script );
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.path() )
.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 args_file = tempfile::NamedTempFile::new().expect( "create args file" );
let args_path = args_file.path().display().to_string();
let script = format!( "echo \"$@\" > \"{args_path}\"\n" );
let ( _tmp, path ) = fake_claude_dir( &script );
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.path() )
.expect( "read args file" );
assert!(
!received.contains( "--print" ),
"--interactive must suppress --print (interactive mode). Received: {received}"
);
}
#[ test ]
fn e14_print_silent_failure_rate_limit_diagnostic()
{
let ( _tmp, path ) = fake_claude( "#!/bin/sh\nexit 1\n" );
let out = run_with_path( &[ "-p", "--retry-override", "0", "--max-sessions", "0", "test" ], &path );
assert!( !out.status.success(), "must exit non-zero on silent failure" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "Error: [Unknown] unknown error (exit 1)" ),
"must emit classified diagnostic on silent failure (empty stderr + non-zero exit). Got:\n{stderr}"
);
assert!(
!stderr.contains( "possible rate limit or quota exhaustion" ),
"generic phrase must be absent after BUG-037 fix. Got:\n{stderr}"
);
}
#[ test ]
fn s76_strip_fences_applied_to_captured_output()
{
let script = "#!/bin/sh\nprintf '```rust\\nfn main(){}\\n```\\n'\n";
let ( _tmp, path ) = fake_claude( script );
let out = run_with_path(
&[ "--strip-fences", "--no-ultrathink", "t" ],
&path,
);
assert!( out.status.success(), "must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
!stdout.contains( "```" ),
"fences must be stripped. Got:\n{stdout}",
);
assert!(
stdout.contains( "fn main(){}" ),
"content must remain. Got:\n{stdout}",
);
}
#[ test ]
fn s77_keep_claudecode_preserves_env_in_subprocess()
{
let script = "#!/bin/sh\necho \"CLAUDECODE=$CLAUDECODE\"\n";
let ( _tmp, path ) = fake_claude( script );
let bin = env!( "CARGO_BIN_EXE_clr" );
let out = Command::new( bin )
.args( [ "--keep-claudecode", "--no-ultrathink", "t" ] )
.env( "PATH", &path )
.env( "CLAUDECODE", "test_val" )
.output()
.expect( "failed to invoke clr" );
assert!( out.status.success(), "must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "CLAUDECODE=test_val" ),
"--keep-claudecode must preserve env var in subprocess. Got:\n{stdout}",
);
}
#[ test ]
fn s78_file_content_piped_to_subprocess_stdin()
{
let script = "#!/bin/sh\ncat\n";
let ( _tmp, path ) = fake_claude( script );
let input_file = tempfile::NamedTempFile::new().expect( "create temp" );
std::fs::write( input_file.path(), "piped_content_s78" ).expect( "write" );
let out = run_with_path(
&[ "--no-ultrathink", "--file", input_file.path().to_str().unwrap(), "t" ],
&path,
);
assert!( out.status.success(), "must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "piped_content_s78" ),
"--file must pipe content to subprocess stdin. Got:\n{stdout}",
);
}
#[ test ]
fn s80_file_nonexistent_path_errors()
{
let ( _tmp, path ) = fake_claude( "#!/bin/sh\necho ok\n" );
let out = run_with_path(
&[ "--no-ultrathink", "--file", "/tmp/nonexistent_99999.txt", "t" ],
&path,
);
assert!( !out.status.success(), "--file with nonexistent path must fail" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "/tmp/nonexistent_99999.txt" ),
"stderr must contain the file path. Got: {stderr}",
);
}