use std::process::Command;
fn run_cli( args : &[ &str ] ) -> std::process::Output
{
let bin = env!( "CARGO_BIN_EXE_clr" );
Command::new( bin )
.args( args )
.output()
.expect( "Failed to invoke clr binary" )
}
#[ test ]
fn t01_message_accepted()
{
let out = run_cli( &[ "--dry-run", "hello" ] );
assert!( out.status.success(), "positional message must be accepted" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "\"hello\n\nultrathink\"" ), "message must be suffixed with \"\\n\\nultrathink\" and appear quoted. Got:\n{stdout}" );
}
#[ test ]
fn t02_model_flag_accepted()
{
let out = run_cli( &[ "--dry-run", "--model", "claude-opus-4-6", "test" ] );
assert!( out.status.success(), "--model must be accepted" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "claude-opus-4-6" ), "model must appear in command. Got:\n{stdout}" );
}
#[ test ]
fn t03_max_tokens_flag_accepted()
{
let out = run_cli( &[ "--dry-run", "--max-tokens", "1000", "test" ] );
assert!( out.status.success(), "--max-tokens must be accepted" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "CLAUDE_CODE_MAX_OUTPUT_TOKENS=1000" ), "token env var must appear. Got:\n{stdout}" );
}
#[ test ]
fn t04_dry_run_always_contains_continue()
{
let out = run_cli( &[ "--dry-run", "test" ] );
assert!( out.status.success() );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( " -c" ),
"dry-run output must always contain -c (automatic continuation). Got:\n{stdout}"
);
}
#[ test ]
fn t05_skip_permissions_default_on()
{
let out = run_cli( &[ "--dry-run", "test" ] );
assert!( out.status.success() );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--dangerously-skip-permissions" ),
"Must produce --dangerously-skip-permissions by default. Got:\n{stdout}"
);
}
#[ test ]
fn t06_verbose_flag_passed_to_claude()
{
let out = run_cli( &[ "--dry-run", "--verbose", "test" ] );
assert!( out.status.success() );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--verbose" ),
"--verbose must appear in claude command. Got:\n{stdout}"
);
}
#[ test ]
fn t07_session_dir_flag()
{
let out = run_cli( &[ "--dry-run", "--session-dir", "/tmp/sess", "test" ] );
assert!( out.status.success() );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "CLAUDE_CODE_SESSION_DIR=/tmp/sess" ),
"--session-dir must set env var. Got:\n{stdout}"
);
}
#[ test ]
fn t08_dir_flag()
{
let out = run_cli( &[ "--dry-run", "--dir", "/tmp/test-dir", "test" ] );
assert!( out.status.success() );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "cd /tmp/test-dir" ), "--dir must produce cd prefix. Got:\n{stdout}" );
}
#[ test ]
fn t09_dry_run_without_message()
{
let out = run_cli( &[ "--dry-run" ] );
assert!( out.status.success(), "--dry-run without message must exit 0" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "claude" ), "dry-run output must contain 'claude'. Got:\n{stdout}" );
}
#[ test ]
fn t10_multiple_flags_combined()
{
let out = run_cli( &[
"--dry-run", "--dir", "/tmp",
"--model", "claude-sonnet-4-6", "fix it",
] );
assert!( out.status.success(), "multiple flags must be accepted" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "cd /tmp" ), "Must have cd line" );
assert!( stdout.contains( "--dangerously-skip-permissions" ), "Must have skip-permissions (default-on)" );
assert!( stdout.contains( " -c" ), "Must have -c (automatic)" );
assert!( stdout.contains( "claude-sonnet-4-6" ), "Must have model" );
assert!( stdout.contains( "\"fix it\n\nultrathink\"" ), "Must have ultrathink-suffixed quoted message" );
}
#[ test ]
fn t11_unknown_flag_rejected()
{
let out = run_cli( &[ "--unknown-flag-xyz" ] );
assert!( !out.status.success(), "unknown flag must exit non-zero" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!( stderr.contains( "Error:" ), "error must go to stderr. Got: {stderr}" );
}
#[ test ]
fn t12_max_tokens_non_numeric_rejected()
{
let out = run_cli( &[ "--dry-run", "--max-tokens", "not-a-number", "test" ] );
assert!( !out.status.success(), "non-numeric --max-tokens must exit non-zero" );
}
#[ test ]
fn t13_print_without_message_rejected()
{
let out = run_cli( &[ "--print" ] );
assert!( !out.status.success(), "--print without message must exit non-zero" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "--print requires a message" ),
"--print without message must give specific error. Got:\n{stderr}"
);
}
#[ test ]
fn t14_help_flag_exits_zero_with_usage()
{
let out = run_cli( &[ "--help" ] );
assert!( out.status.success(), "--help must exit 0" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "USAGE:" ), "--help must print USAGE" );
}
#[ test ]
fn t15_short_help_flag_works()
{
let out = run_cli( &[ "-h" ] );
assert!( out.status.success(), "-h must exit 0" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "USAGE:" ) );
}
#[ test ]
fn t16_help_lists_all_options()
{
let out = run_cli( &[ "--help" ] );
assert!( out.status.success() );
let stdout = String::from_utf8_lossy( &out.stdout );
for opt in &[
"--print", "--new-session", "--model", "--verbose",
"--no-skip-permissions", "--max-tokens", "--session-dir",
"--dir", "--dry-run", "--verbosity", "--help", "[MESSAGE]",
"--system-prompt", "--append-system-prompt", "--no-ultrathink",
] {
assert!( stdout.contains( opt ), "--help missing option {opt}. Got:\n{stdout}" );
}
assert!(
!stdout.contains( "--continue" ),
"--help must NOT list --continue (removed; continuation is automatic). Got:\n{stdout}"
);
}
#[ test ]
fn t17_error_output_goes_to_stderr_not_stdout()
{
let out = run_cli( &[ "--unknown-flag" ] );
assert!( !out.status.success() );
assert!(
out.stdout.is_empty(),
"stdout must be empty on error; got: {}",
String::from_utf8_lossy( &out.stdout )
);
let stderr = String::from_utf8_lossy( &out.stderr );
assert!( stderr.contains( "Error:" ), "stderr must contain 'Error:'; got: {stderr}" );
}
#[ test ]
fn t18_max_tokens_zero_accepted()
{
let out = run_cli( &[ "--dry-run", "--max-tokens", "0", "test" ] );
assert!( out.status.success(), "--max-tokens 0 must be accepted" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "CLAUDE_CODE_MAX_OUTPUT_TOKENS=0" ), "must set token env to 0. Got:\n{stdout}" );
}
#[ test ]
fn t19_max_tokens_u32_max_accepted()
{
let out = run_cli( &[ "--dry-run", "--max-tokens", "4294967295", "test" ] );
assert!( out.status.success(), "--max-tokens u32::MAX must be accepted" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "CLAUDE_CODE_MAX_OUTPUT_TOKENS=4294967295" ), "must set correct token env. Got:\n{stdout}" );
}
#[ test ]
fn t20_max_tokens_overflow_rejected()
{
let out = run_cli( &[ "--dry-run", "--max-tokens", "4294967296", "test" ] );
assert!( !out.status.success(), "--max-tokens u32::MAX+1 must exit non-zero" );
}
#[ test ]
fn t21_max_tokens_negative_rejected()
{
let out = run_cli( &[ "--dry-run", "--max-tokens", "-1", "test" ] );
assert!( !out.status.success(), "--max-tokens -1 must exit non-zero" );
}
#[ test ]
fn t22_duplicate_dir_uses_last_value()
{
let out = run_cli( &[ "--dry-run", "--dir", "/first", "--dir", "/last", "test" ] );
assert!( out.status.success(), "duplicate --dir must exit 0 (last wins)" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "cd /last" ), "last --dir value must win. Got:\n{stdout}" );
assert!( !stdout.contains( "cd /first" ), "first --dir must be overridden. Got:\n{stdout}" );
}
#[ test ]
fn t23_duplicate_model_uses_last_value()
{
let out = run_cli( &[ "--dry-run", "--model", "first-model", "--model", "last-model", "test" ] );
assert!( out.status.success(), "duplicate --model must exit 0 (last wins)" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "last-model" ), "last --model value must win. Got:\n{stdout}" );
assert!( !stdout.contains( "first-model" ), "first --model must be overridden. Got:\n{stdout}" );
}
#[ test ]
fn t24_duplicate_session_dir_uses_last_value()
{
let out = run_cli( &[ "--dry-run", "--session-dir", "/first", "--session-dir", "/last", "test" ] );
assert!( out.status.success(), "duplicate --session-dir must exit 0 (last wins)" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!( stdout.contains( "CLAUDE_CODE_SESSION_DIR=/last" ), "last --session-dir must win. Got:\n{stdout}" );
assert!( !stdout.contains( "CLAUDE_CODE_SESSION_DIR=/first" ), "first must be overridden. Got:\n{stdout}" );
}
#[ test ]
fn t25_duplicate_max_tokens_uses_last_value()
{
let out = run_cli( &[ "--dry-run", "--max-tokens", "100", "--max-tokens", "50000", "test" ] );
assert!( out.status.success(), "duplicate --max-tokens must exit 0 (last wins)" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "CLAUDE_CODE_MAX_OUTPUT_TOKENS=50000" ),
"last --max-tokens must win. Got:\n{stdout}"
);
}
#[ test ]
fn t26_help_after_flags_shows_help()
{
let out = run_cli( &[ "--dir", "/tmp", "--help" ] );
assert!( out.status.success(), "--help must exit 0 even after valid flags" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "USAGE:" ),
"--help must print USAGE even after valid flags. Got:\n{stdout}"
);
}
#[ test ]
fn t27_double_dash_separator()
{
let out = run_cli( &[ "--dry-run", "--", "--not-a-flag" ] );
assert!( out.status.success(), "-- separator must allow --not-a-flag as message" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "\"--not-a-flag\n\nultrathink\"" ),
"Text after -- must become message with ultrathink suffix. Got:\n{stdout}"
);
}
#[ test ]
fn t28_verbosity_six_rejected()
{
let out = run_cli( &[ "--verbosity", "6", "--dry-run", "test" ] );
assert!( !out.status.success(), "--verbosity 6 must be rejected" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!( stderr.contains( "verbosity" ), "error must mention verbosity. Got:\n{stderr}" );
}
#[ test ]
fn t29_dry_run_does_not_invoke_claude()
{
let out = run_cli( &[ "--dry-run", "test" ] );
assert!(
out.status.success(),
"--dry-run must not fail due to missing claude binary"
);
}
#[ test ]
fn t30_print_with_message_parsed()
{
let out = run_cli( &[ "--dry-run", "-p", "test" ] );
assert!( out.status.success(), "-p with message must parse OK" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--print" ),
"-p must add --print to command. Got:\n{stdout}"
);
}
#[ test ]
fn t31_flag_missing_value_rejected()
{
let out = run_cli( &[ "--dry-run", "--model" ] );
assert!( !out.status.success(), "--model without value must exit non-zero" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!( stderr.contains( "requires a value" ), "must mention missing value. Got:\n{stderr}" );
}
#[ test ]
fn t32_flag_value_consumed_as_flag_name()
{
let out = run_cli( &[ "--dry-run", "--model", "--verbose", "msg" ] );
assert!( out.status.success(), "must accept --verbose as model value" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--model --verbose" ),
"--verbose must be the model value, not a flag. Got:\n{stdout}"
);
}
#[ test ]
fn t33_interleaved_positional_and_flags()
{
let out = run_cli( &[ "--dry-run", "hello", "--dir", "/tmp", "world" ] );
assert!( out.status.success(), "interleaved positional must be accepted" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "\"hello world\n\nultrathink\"" ),
"positional args must join as ultrathink-suffixed message. Got:\n{stdout}"
);
assert!( stdout.contains( "cd /tmp" ), "dir flag must still work. Got:\n{stdout}" );
}
#[ test ]
fn t34_equals_syntax_rejected()
{
let out = run_cli( &[ "--model=sonnet" ] );
assert!( !out.status.success(), "--model=sonnet must be rejected" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!( stderr.contains( "unknown option" ), "must report unknown option. Got:\n{stderr}" );
}
#[ test ]
fn t35_combined_short_flags_rejected()
{
let out = run_cli( &[ "-pc" ] );
assert!( !out.status.success(), "-pc must be rejected" );
let stderr = String::from_utf8_lossy( &out.stderr );
assert!( stderr.contains( "unknown option" ), "must report unknown option. Got:\n{stderr}" );
}
#[ 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 out = run_cli( &[ "--dry-run", "--" ] );
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,
"claude --dangerously-skip-permissions --chrome -c",
"-- with nothing after must produce command with default bypass and continuation. 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() );
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 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 t50_default_message_gets_ultrathink_suffix()
{
let out = run_cli( &[ "--dry-run", "hello" ] );
assert!(
out.status.success(),
"dry-run with message 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 suffixed with \"\\n\\nultrathink\". Got:\n{stdout}"
);
}
#[ test ]
fn t51_no_ultrathink_suppresses_suffix()
{
let out = run_cli( &[ "--dry-run", "--no-ultrathink", "hello" ] );
assert!(
out.status.success(),
"--no-ultrathink must be a known flag (exit 0). stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "\"hello\"" ),
"message must appear verbatim with --no-ultrathink. Got:\n{stdout}"
);
assert!(
!stdout.contains( "ultrathink" ),
"suffix must be suppressed with --no-ultrathink. Got:\n{stdout}"
);
}
#[ test ]
fn t52_idempotent_guard_no_double_suffix()
{
let out = run_cli( &[ "--dry-run", "fix the bug ultrathink" ] );
assert!(
out.status.success(),
"ultrathink-suffixed message must be accepted. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "\"fix the bug ultrathink\"" ),
"message must appear verbatim (guard fires, no re-suffix). Got:\n{stdout}"
);
assert!(
!stdout.contains( "ultrathink\n\nultrathink" ),
"double-suffix must not appear. Got:\n{stdout}"
);
}
#[ test ]
fn t53_help_lists_no_ultrathink()
{
let out = run_cli( &[ "--help" ] );
assert!( out.status.success(), "--help must exit 0" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--no-ultrathink" ),
"--help must list --no-ultrathink flag. Got:\n{stdout}"
);
}
#[ test ]
fn t54_empty_positional_arg_ignored()
{
let out = run_cli( &[ "--dry-run", "" ] );
assert!(
out.status.success(),
"empty positional arg must not error. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let stdout = String::from_utf8_lossy( &out.stdout );
let last_line = stdout.trim_end().lines().last().unwrap_or_default();
assert_eq!(
last_line,
"claude --dangerously-skip-permissions --chrome -c",
"empty positional arg must produce bare command (no --print, no message). Got:\n{stdout}"
);
assert!(
!stdout.contains( "\"ultrathink \"" ),
"empty positional arg must NOT produce degenerate 'ultrathink ' message. Got:\n{stdout}"
);
}
#[ test ]
fn t55_help_wins_over_subsequent_unknown_flag()
{
let out = run_cli( &[ "--help", "--not-a-real-flag" ] );
assert!(
out.status.success(),
"--help before unknown flag must exit 0 (help wins). stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "USAGE:" ),
"--help before unknown flag must show USAGE. Got:\n{stdout}"
);
}
#[ test ]
fn t56_help_wins_over_preceding_unknown_flag()
{
let out = run_cli( &[ "--not-a-real-flag", "--help" ] );
assert!(
out.status.success(),
"--help after unknown flag must exit 0 (help wins). stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "USAGE:" ),
"--help after unknown flag must show USAGE. Got:\n{stdout}"
);
}
#[ test ]
fn t57_empty_positional_after_double_dash_ignored()
{
let out = run_cli( &[ "--dry-run", "--", "" ] );
assert!(
out.status.success(),
"empty arg after -- must not error. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let stdout = String::from_utf8_lossy( &out.stdout );
let last_line = stdout.trim_end().lines().last().unwrap_or_default();
assert_eq!(
last_line,
"claude --dangerously-skip-permissions --chrome -c",
"empty arg after -- must produce bare command (no --print, no message). Got:\n{stdout}"
);
assert!(
!stdout.contains( "\"ultrathink \"" ),
"empty arg after -- must NOT produce degenerate 'ultrathink ' message. Got:\n{stdout}"
);
}
#[ test ]
fn t58_default_message_gets_ultrathink_suffix()
{
let out = run_cli( &[ "--dry-run", "hello" ] );
assert!(
out.status.success(),
"dry-run with message 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 suffixed with \"\\n\\nultrathink\". Got:\n{stdout}"
);
assert!(
!stdout.contains( "\"ultrathink hello\"" ),
"prefix form must be absent after fix. Got:\n{stdout}"
);
}