mod cli_binary_test_helpers;
use cli_binary_test_helpers::run_cli;
#[ test ]
fn t36_flags_after_positional()
{
let out = run_cli( &[ "--dry-run", "msg", "--verbose" ] );
assert!( out.status.success(), "flags after positional must work" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--verbose" ),
"--verbose after positional must be parsed as flag. Got:\n{stdout}"
);
}
#[ test ]
fn t37_multiple_positional_words_joined()
{
let out = run_cli( &[ "--dry-run", "Fix", "the", "bug", "now" ] );
assert!( out.status.success(), "multiple positional words must be accepted" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "\"Fix the bug now\n\nultrathink\"" ),
"all positional words must join with space and be ultrathink-suffixed. Got:\n{stdout}"
);
}
#[ test ]
fn t38_double_dash_only_no_message()
{
let empty_dir = tempfile::TempDir::new().expect( "create empty session dir" );
let session_path = empty_dir.path().to_str().expect( "session dir path valid utf-8" );
let out = run_cli( &[ "--dry-run", "--session-dir", session_path, "--" ] );
assert!( out.status.success(), "-- as only arg must not error" );
let stdout = String::from_utf8_lossy( &out.stdout );
let last_line = stdout.trim_end().lines().last().unwrap_or_default();
assert_eq!(
last_line,
"env -u CLAUDECODE claude --dangerously-skip-permissions --chrome --effort max",
"-- with nothing after must produce bare command (no -c in empty session dir). Got:\n{stdout}"
);
}
#[ test ]
fn t39_max_tokens_empty_string_rejected()
{
let out = run_cli( &[ "--dry-run", "--max-tokens", "", "test" ] );
assert!( !out.status.success(), "--max-tokens '' must be rejected" );
}
#[ test ]
fn t40_all_value_flags_require_value()
{
for flag in &[
"--max-tokens", "--verbosity", "--session-dir", "--dir",
"--system-prompt", "--append-system-prompt",
]
{
let out = run_cli( &[ "--dry-run", flag ] );
assert!(
!out.status.success(),
"{flag} as last arg must exit non-zero"
);
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "requires a value" ),
"{flag} must mention 'requires a value'. Got:\n{stderr}"
);
}
}
#[ test ]
fn t41_new_session_suppresses_continue_flag()
{
let out = run_cli( &[ "--dry-run", "--new-session", "test" ] );
assert!( out.status.success(), "--new-session --dry-run must exit 0" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
!stdout.contains( " -c" ),
"--new-session must suppress -c in dry-run output. Got:\n{stdout}"
);
}
#[ test ]
fn t42_message_defaults_to_print_mode()
{
let out = run_cli( &[ "--dry-run", "Fix the bug" ] );
assert!( out.status.success(), "message without -p must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--print" ),
"message without -p must default to print mode (--print in dry-run). Got:\n{stdout}"
);
}
#[ test ]
fn t43_interactive_flag_suppresses_print()
{
let out = run_cli( &[ "--dry-run", "--interactive", "Fix the bug" ] );
assert!( out.status.success(), "--interactive with message must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
!stdout.contains( "--print" ),
"--interactive must suppress --print default. Got:\n{stdout}"
);
}
#[ test ]
fn t44_interactive_flag_alone_accepted()
{
let out = run_cli( &[ "--dry-run", "--interactive" ] );
assert!(
out.status.success(),
"--interactive alone must be accepted (exit 0). stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
!stdout.contains( "--print" ),
"--interactive with no message must not add --print. Got:\n{stdout}"
);
}
#[ test ]
fn t45_interactive_flag_in_help()
{
let out = run_cli( &[ "--help" ] );
assert!( out.status.success(), "--help must exit 0" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--interactive" ),
"--interactive must appear in --help output. Got:\n{stdout}"
);
}
#[ test ]
fn t46_no_skip_permissions_disables_default()
{
let out = run_cli( &[ "--dry-run", "--no-skip-permissions", "test" ] );
assert!( out.status.success(), "exit={} stderr={}", out.status.code().unwrap_or( -1 ), String::from_utf8_lossy( &out.stderr ) );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
!stdout.contains( "--dangerously-skip-permissions" ),
"--no-skip-permissions must suppress automatic bypass. Got:\n{stdout}"
);
}
#[ test ]
fn t47_explicit_dangerously_skip_permissions_rejected()
{
let out = run_cli( &[ "--dry-run", "--dangerously-skip-permissions", "test" ] );
assert!(
!out.status.success(),
"--dangerously-skip-permissions explicit must exit non-zero (now hidden; always-on by default)"
);
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "unknown option" ),
"explicit --dangerously-skip-permissions must report 'unknown option'. Got:\n{stderr}"
);
}
#[ test ]
fn t49_help_options_column_aligned()
{
let out = run_cli( &[ "--help" ] );
assert!( out.status.success(), "--help must exit 0" );
let stdout = String::from_utf8_lossy( &out.stdout );
let mut col_by_line : Vec< ( usize, String ) > = Vec::new();
for line in stdout.lines()
{
if !line.starts_with( " -" ) { continue; }
let bytes = line.as_bytes();
let mut i = 2; while i < bytes.len()
{
if bytes[ i ] == b' '
{
let gap_start = i;
while i < bytes.len() && bytes[ i ] == b' ' { i += 1; }
if i - gap_start >= 2
{
col_by_line.push( ( i, line.to_string() ) );
break;
}
}
else { i += 1; }
}
}
assert!( !col_by_line.is_empty(), "--help must contain option lines" );
let expected_col = col_by_line[ 0 ].0;
for ( col, line ) in &col_by_line
{
assert_eq!(
*col, expected_col,
"all option descriptions must start at column {expected_col}. Misaligned line:\n {line}"
);
}
}
#[ test ]
fn s58_strip_fences_flag_accepted()
{
let out = run_cli( &[ "--dry-run", "--strip-fences", "t" ] );
assert!( out.status.success(), "--strip-fences must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
}
#[ test ]
fn s59_keep_claudecode_flag_accepted()
{
let out = run_cli( &[ "--dry-run", "--keep-claudecode", "t" ] );
assert!( out.status.success(), "--keep-claudecode must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
}
#[ test ]
fn s60_file_requires_a_value()
{
let out = run_cli( &[ "--dry-run", "--file" ] );
assert!( !out.status.success(), "--file without value must fail" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!( stderr.contains( "requires a value" ), "stderr must mention 'requires a value'. Got: {stderr}" );
}
#[ test ]
fn s61_file_with_path_accepted()
{
let out = run_cli( &[ "--dry-run", "--file", "/tmp/x.txt", "t" ] );
assert!( out.status.success(), "--file with path must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
}
#[ test ]
fn s62_strip_fences_absent_by_default()
{
let out = run_cli( &[ "--dry-run", "t" ] );
assert!( out.status.success(), "default must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
}
#[ test ]
fn s63_keep_claudecode_absent_by_default()
{
let out = run_cli( &[ "--dry-run", "t" ] );
assert!( out.status.success(), "default must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
}
#[ test ]
fn s64_file_and_strip_fences_together()
{
let out = run_cli( &[ "--dry-run", "--file", "/tmp/x.txt", "--strip-fences", "t" ] );
assert!( out.status.success(), "combo must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
}
#[ test ]
fn s65_file_and_keep_claudecode_together()
{
let out = run_cli( &[ "--dry-run", "--file", "/tmp/x.txt", "--keep-claudecode", "t" ] );
assert!( out.status.success(), "combo must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
}
#[ test ]
fn s66_strip_fences_and_keep_claudecode_together()
{
let out = run_cli( &[ "--dry-run", "--strip-fences", "--keep-claudecode", "t" ] );
assert!( out.status.success(), "combo must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
}
#[ test ]
fn s67_all_three_new_flags_together()
{
let out = run_cli( &[ "--dry-run", "--file", "/tmp/x.txt", "--strip-fences", "--keep-claudecode", "t" ] );
assert!( out.status.success(), "all three must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
}
#[ test ]
fn s68_help_includes_file()
{
let out = run_cli( &[ "--help" ] );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "--file" ), "help must mention --file. Got:\n{stdout}" );
}
#[ test ]
fn s69_help_includes_strip_fences()
{
let out = run_cli( &[ "--help" ] );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "--strip-fences" ), "help must mention --strip-fences. Got:\n{stdout}" );
}
#[ test ]
fn s79_help_includes_keep_claudecode()
{
let out = run_cli( &[ "--help" ] );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "--keep-claudecode" ), "help must mention --keep-claudecode. Got:\n{stdout}" );
}
#[ test ]
fn bug_reproducer_212_run_subcommand_strips_token()
{
let with_run = run_cli( &[ "run", "--dry-run", "Fix bug" ] );
assert!(
with_run.status.success(),
"clr run must exit 0. stderr: {}",
String::from_utf8_lossy( &with_run.stderr )
);
let without_run = run_cli( &[ "--dry-run", "Fix bug" ] );
assert!( without_run.status.success(), "clr without run must exit 0" );
let out_with = String::from_utf8_lossy( &with_run.stdout );
let out_without = String::from_utf8_lossy( &without_run.stdout );
assert!(
out_with.contains( "\"Fix bug\n\nultrathink\"" ),
"message must be 'Fix bug' (not 'run Fix bug'). Got:\n{out_with}"
);
assert_eq!(
out_with.trim(), out_without.trim(),
"`clr run <args>` and `clr <args>` must produce identical dry-run output"
);
}
#[ test ]
fn bug_reproducer_212_run_subcommand_args()
{
let out = run_cli( &[ "run", "hello", "--dry-run" ] );
assert!(
out.status.success(),
"clr run hello --dry-run must exit 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "\"hello\n\nultrathink\"" ),
"message must be 'hello' with ultrathink suffix. Got:\n{stdout}"
);
assert!(
!stdout.contains( "run hello" ),
"'run' must NOT appear in the message (bug: treated as first positional word). Got:\n{stdout}"
);
let with_run = run_cli( &[ "run", "--dry-run" ] );
let without_run = run_cli( &[ "--dry-run" ] );
assert!( with_run.status.success(), "clr run --dry-run must exit 0" );
assert_eq!(
String::from_utf8_lossy( &with_run.stdout ).trim(),
String::from_utf8_lossy( &without_run.stdout ).trim(),
"clr run --dry-run must produce same output as clr --dry-run (bare command form)"
);
let out = run_cli( &[ "run", "--model", "sonnet", "--dry-run" ] );
assert!(
out.status.success(),
"clr run --model sonnet --dry-run must exit 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--model sonnet" ),
"--model sonnet must appear in assembled command. Got:\n{stdout}"
);
}
#[ test ]
fn t48_no_skip_permissions_new_session_combination()
{
let out = run_cli( &[ "--dry-run", "--no-skip-permissions", "--new-session", "--no-ultrathink", "hello" ] );
assert!(
out.status.success(),
"--no-skip-permissions --new-session must exit 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
!stdout.contains( "--dangerously-skip-permissions" ),
"--no-skip-permissions must suppress automatic bypass. Got:\n{stdout}"
);
assert!(
!stdout.contains( " -c" ),
"--new-session must suppress automatic continuation. Got:\n{stdout}"
);
assert!(
stdout.contains( "\"hello\"" ),
"message must still appear. Got:\n{stdout}"
);
}
#[ test ]
fn bug_reproducer_215_run_help_dispatches_help()
{
let with_run = run_cli( &[ "run", "help" ] );
assert!(
with_run.status.success(),
"`clr run help` must exit 0 (BUG-215: was hanging invoking claude). stderr: {}",
String::from_utf8_lossy( &with_run.stderr )
);
let out_run = String::from_utf8_lossy( &with_run.stdout );
assert!(
out_run.contains( "USAGE" ),
"`clr run help` must print USAGE. Got:\n{out_run}"
);
let bare_help = run_cli( &[ "help" ] );
assert!(
bare_help.status.success(),
"`clr help` must exit 0"
);
assert_eq!(
out_run.trim(), String::from_utf8_lossy( &bare_help.stdout ).trim(),
"`clr run help` output must be identical to `clr help`"
);
}
#[ test ]
fn bug_reproducer_302_prefix_guard_false_positive_is()
{
let out = run_cli( &[ "is", "it", "so?" ] );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
!stderr.contains( "unknown subcommand" ),
"BUG-302: `clr is it so?` must not emit 'unknown subcommand'. stderr:\n{stderr}"
);
assert!(
out.status.code() != Some( 1 ) || !stderr.contains( "Did you mean" ),
"BUG-302: `clr is it so?` must not exit 1 via guard. stderr:\n{stderr}"
);
}
#[ test ]
fn bug_reproducer_302_false_positive_prevention()
{
for word in &[ "is", "asked", "running", "he", "a" ]
{
let out = run_cli( &[ word, "--dry-run" ] );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
!stderr.contains( "unknown subcommand" ),
"BUG-302: `clr {word}` must not be rejected by the guard. stderr:\n{stderr}"
);
}
}
#[ test ]
fn bug_302_regression_kil_still_caught_by_close_typo()
{
let out = run_cli( &[ "kil" ] );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
out.status.code() == Some( 1 ),
"regression: `clr kil` must still exit 1 via is_close_typo after BUG-302 fix. stderr:\n{stderr}"
);
assert!(
stderr.contains( "Did you mean" ),
"regression: `clr kil` must still emit 'Did you mean'. stderr:\n{stderr}"
);
}
#[ test ]
fn bug_302_regression_isolat_still_caught_by_prefix()
{
let out = run_cli( &[ "isolat" ] );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
out.status.code() == Some( 1 ),
"regression (IN-8): `clr isolat` must still exit 1 via prefix guard after BUG-302 fix. stderr:\n{stderr}"
);
assert!(
stderr.contains( "Did you mean" ) && stderr.contains( "isolated" ),
"regression (IN-8): `clr isolat` must emit 'Did you mean ... isolated'. stderr:\n{stderr}"
);
}