mod cli_binary_test_helpers;
use cli_binary_test_helpers::run_cli;
#[ test ]
fn g1cc1_all_claude_native_flags_forwarded_together()
{
let out = run_cli( &[
"--dry-run",
"--print",
"--model", "sonnet",
"--verbose",
"--effort", "high",
"--no-persist",
"--json-schema", r#"{"type":"string"}"#,
"--mcp-config", "/tmp/mcp.json",
"Fix bug",
] );
assert!( out.status.success(), "exit must be 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "--print" ), "output must contain --print: {stdout}" );
assert!( stdout.contains( "--model" ), "output must contain --model: {stdout}" );
assert!( stdout.contains( "sonnet" ), "output must contain model value: {stdout}" );
assert!( stdout.contains( "--verbose" ), "output must contain --verbose: {stdout}" );
assert!( stdout.contains( "--effort" ), "output must contain --effort: {stdout}" );
assert!( stdout.contains( "high" ), "output must contain effort value: {stdout}" );
assert!( stdout.contains( "--no-session-persistence" ), "output must contain --no-session-persistence: {stdout}" );
assert!( stdout.contains( "--json-schema" ), "output must contain --json-schema: {stdout}" );
assert!( stdout.contains( "--mcp-config" ), "output must contain --mcp-config: {stdout}" );
}
#[ test ]
fn g1cc2_model_and_verbose_coexist()
{
let out = run_cli( &[ "--dry-run", "--model", "opus", "--verbose", "Fix bug" ] );
assert!( out.status.success(), "exit must be 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "--model" ), "output must contain --model: {stdout}" );
assert!( stdout.contains( "opus" ), "output must contain model value: {stdout}" );
assert!( stdout.contains( "--verbose" ), "output must contain --verbose: {stdout}" );
}
#[ test ]
fn g1cc3_verbose_and_effort_max_both_present()
{
let out = run_cli( &[ "--dry-run", "--verbose", "--effort", "max", "Fix bug" ] );
assert!( out.status.success(), "exit must be 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "--verbose" ), "output must contain --verbose: {stdout}" );
assert!( stdout.contains( "--effort" ), "output must contain --effort: {stdout}" );
assert!( stdout.contains( "max" ), "output must contain effort value: {stdout}" );
}
#[ test ]
fn g1cc4_no_group_flags_only_defaults_injected()
{
let out = run_cli( &[ "--dry-run", "Fix bug" ] );
assert!( out.status.success(), "exit must be 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "--effort" ), "default --effort must be present: {stdout}" );
assert!( stdout.contains( "max" ), "default effort value must be present: {stdout}" );
assert!( stdout.contains( "--print" ), "default --print must be present: {stdout}" );
assert!( !stdout.contains( "--verbose" ), "no --verbose without explicit flag: {stdout}" );
assert!( !stdout.contains( "--model" ), "no --model without explicit flag: {stdout}" );
}
#[ test ]
fn g1cc5_new_claude_native_flags_forwarded_together()
{
let out = run_cli( &[
"--dry-run",
"--no-persist",
"--json-schema", r#"{"type":"object"}"#,
"--mcp-config", "/tmp/servers.json",
"Fix bug",
] );
assert!( out.status.success(), "exit must be 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--no-session-persistence" ),
"output must contain --no-session-persistence: {stdout}",
);
assert!(
stdout.contains( "--json-schema" ),
"output must contain --json-schema: {stdout}",
);
assert!(
stdout.contains( "/tmp/servers.json" ),
"output must contain mcp-config path: {stdout}",
);
}
#[ test ]
fn g2cc1_dry_run_and_no_ultrathink_preview_suppressed()
{
let out = run_cli( &[ "--dry-run", "--no-ultrathink", "Fix bug" ] );
assert!( out.status.success(), "exit must be 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
!stdout.contains( "ultrathink" ),
"ultrathink must not appear in preview when --no-ultrathink given: {stdout}",
);
}
#[ test ]
fn g2cc2_new_session_and_session_dir_both_accepted()
{
let out = run_cli( &[
"--dry-run", "--new-session", "--session-dir", "/tmp/sessions", "Fix bug",
] );
assert!( out.status.success(), "exit must be 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "CLAUDE_CODE_SESSION_DIR=/tmp/sessions" ),
"output must contain CLAUDE_CODE_SESSION_DIR env var: {stdout}",
);
assert!(
!stdout.contains( " -c" ),
"no -c flag when --new-session given: {stdout}",
);
}
#[ test ]
fn g2cc3_no_skip_permissions_and_no_effort_max_both_suppressed()
{
let out = run_cli( &[ "--dry-run", "--no-skip-permissions", "--no-effort-max", "Fix bug" ] );
assert!( out.status.success(), "exit must be 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
!stdout.contains( "--dangerously-skip-permissions" ),
"no --dangerously-skip-permissions when --no-skip-permissions given: {stdout}",
);
assert!(
!stdout.contains( "--effort" ),
"no --effort when --no-effort-max given: {stdout}",
);
}
#[ test ]
fn g2cc4_all_runner_control_flags_no_conflict()
{
let tmp = tempfile::NamedTempFile::new().expect( "tmp" );
std::io::Write::write_all( &mut tmp.as_file(), b"input" ).expect( "write" );
let file_path = tmp.path().to_str().expect( "path" );
let bin = env!( "CARGO_BIN_EXE_clr" );
let out = std::process::Command::new( bin )
.args( [
"--dry-run",
"--no-skip-permissions",
"--interactive",
"--new-session",
"--dir", "/tmp/test",
"--subdir", "work",
"--max-tokens", "100000",
"--session-dir", "/tmp/sessions",
"--verbosity", "2",
"--trace",
"--no-ultrathink",
"--no-effort-max",
"--no-chrome",
"--no-persist",
"--file", file_path,
"--strip-fences",
"--keep-claudecode",
"--output-file", "/tmp/rc_out.txt",
"--expect", "yes|no",
"--expect-strategy", "fail",
"--expect-retries", "2",
"--max-sessions", "5",
"--retry-on-rate-limit", "3",
"--retry-delay", "30",
"--timeout", "60",
"--retry-on-api-error", "1",
"--api-error-delay", "0",
"--retry-on-unknown-error", "1",
"Fix bug",
] )
.env_remove( "CLAUDECODE" )
.output()
.expect( "failed to invoke clr binary" );
assert!(
out.status.success(),
"all 28 runner control flags must be accepted without conflict: {out:?}",
);
assert!(
out.stderr.is_empty(),
"stderr must be empty (dry-run wins over trace): {:?}",
String::from_utf8_lossy( &out.stderr ),
);
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
!stdout.contains( "--chrome" ),
"--no-chrome must suppress --chrome injection: {stdout}",
);
assert!(
stdout.contains( "/tmp/test/-work" ),
"effective dir must contain /tmp/test/-work: {stdout}",
);
}
#[ test ]
fn g2cc5_file_strip_fences_keep_claudecode_accepted()
{
let tmp = tempfile::NamedTempFile::new().expect( "tmp" );
std::io::Write::write_all( &mut tmp.as_file(), b"task input" ).expect( "write" );
let path = tmp.path().to_str().expect( "path" );
let out = run_cli( &[
"--dry-run",
"--file", path,
"--strip-fences",
"--keep-claudecode",
"task",
] );
assert!( out.status.success(), "exit must be 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( path ), "output must reference file path: {stdout}" );
}
#[ test ]
fn g2cc6_dir_plus_subdir_effective_dir()
{
let out = run_cli( &[ "--dry-run", "--dir", "/tmp", "--subdir", "build", "task" ] );
assert!( out.status.success(), "exit must be 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "/tmp/-build" ),
"effective dir must be /tmp/-build (not /tmp alone, not /tmp/build): {stdout}",
);
}
#[ test ]
fn g3cc1_system_prompt_alone_forwarded()
{
let out = run_cli( &[ "--dry-run", "--system-prompt", "Be concise.", "test" ] );
assert!( out.status.success(), "exit must be 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--system-prompt" ),
"output must contain --system-prompt: {stdout}",
);
assert!(
!stdout.contains( "--append-system-prompt" ),
"no --append-system-prompt when not given: {stdout}",
);
}
#[ test ]
fn g3cc2_append_system_prompt_alone_forwarded()
{
let out = run_cli( &[ "--dry-run", "--append-system-prompt", "Always JSON.", "test" ] );
assert!( out.status.success(), "exit must be 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--append-system-prompt" ),
"output must contain --append-system-prompt: {stdout}",
);
assert!(
!stdout.contains( "--system-prompt " ),
"no bare --system-prompt when not given: {stdout}",
);
}
#[ test ]
fn g3cc3_both_system_prompt_flags_forwarded()
{
let out = run_cli( &[
"--dry-run",
"--system-prompt", "Base.",
"--append-system-prompt", "Extra.",
"test",
] );
assert!( out.status.success(), "exit must be 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--system-prompt" ),
"output must contain --system-prompt: {stdout}",
);
assert!(
stdout.contains( "--append-system-prompt" ),
"output must contain --append-system-prompt: {stdout}",
);
}
#[ test ]
fn g3cc4_neither_system_prompt_no_injection()
{
let out = run_cli( &[ "--dry-run", "test" ] );
assert!( out.status.success(), "exit must be 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
!stdout.contains( "--system-prompt" ),
"no --system-prompt injected by default: {stdout}",
);
assert!(
!stdout.contains( "--append-system-prompt" ),
"no --append-system-prompt injected by default: {stdout}",
);
}
#[ test ]
fn g4cc6_trace_on_credential_ops()
{
use cli_binary_test_helpers::run_cli_with_env;
let mut tmp = tempfile::NamedTempFile::new().expect( "tmp" );
std::io::Write::write_all( &mut tmp, b"{}" ).expect( "write" );
let path = tmp.path().to_str().expect( "path" );
let out = run_cli_with_env(
&[ "isolated", "--creds", path, "--trace" ],
&[ ( "PATH", "/nonexistent" ) ],
);
let stderr = String::from_utf8_lossy( &out.stderr );
assert!( stderr.contains( "# clr isolated" ), "stderr must contain '# clr isolated': {stderr}" );
assert!( stderr.contains( "# creds:" ), "stderr must contain '# creds:': {stderr}" );
assert!( stderr.contains( "# timeout: 30s" ), "stderr must contain '# timeout: 30s': {stderr}" );
}