#![ cfg( unix ) ]
mod cli_binary_test_helpers;
use cli_binary_test_helpers::{ fake_claude, run_cli, run_with_path };
use std::os::unix::fs::PermissionsExt;
#[ test ]
fn t01_expect_match_exits_0()
{
let ( _tmp, path ) = fake_claude( "#!/bin/sh\necho 'yes'" );
let out = run_with_path( &[ "-p", "--expect", "yes|no", "answer" ], &path );
assert_eq!(
out.status.code(),
Some( 0 ),
"match must exit 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn t02_expect_mismatch_default_fail_exits_3()
{
let ( _tmp, path ) = fake_claude( "#!/bin/sh\necho 'maybe'" );
let out = run_with_path( &[ "-p", "--expect", "yes|no", "answer" ], &path );
assert_eq!(
out.status.code(),
Some( 3 ),
"mismatch with default strategy must exit 3. stdout: {}",
String::from_utf8_lossy( &out.stdout )
);
}
#[ test ]
fn t03_expect_case_insensitive_match()
{
let ( _tmp, path ) = fake_claude( "#!/bin/sh\necho 'YES'" );
let out = run_with_path( &[ "-p", "--expect", "yes|no", "answer" ], &path );
assert_eq!(
out.status.code(),
Some( 0 ),
"case-insensitive match must exit 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn t04_expect_whitespace_trimmed()
{
let ( _tmp, path ) = fake_claude( "#!/bin/sh\nprintf ' yes '" );
let out = run_with_path( &[ "-p", "--expect", "yes|no", "answer" ], &path );
assert_eq!(
out.status.code(),
Some( 0 ),
"whitespace trim must exit 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn t05_expect_dry_run_exits_0()
{
let out = run_cli( &[ "--dry-run", "--expect", "yes|no", "answer" ] );
assert!(
out.status.success(),
"dry-run with --expect must exit 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn t06_help_lists_all_expect_params()
{
let out = run_cli( &[ "--help" ] );
assert!( out.status.success(), "clr --help must exit 0" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--expect" ),
"--help must list --expect. Got:\n{stdout}"
);
assert!(
stdout.contains( "--expect-strategy" ),
"--help must list --expect-strategy. Got:\n{stdout}"
);
assert!(
stdout.contains( "--retry-on-validation" ),
"--help must list --retry-on-validation. Got:\n{stdout}"
);
assert!(
!stdout.contains( "--expect-retries" ),
"--help must NOT list --expect-retries. Got:\n{stdout}"
);
}
#[ test ]
fn t07_retry_matches_on_second_attempt()
{
let tmp = tempfile::tempdir().expect( "create temp dir" );
let count_path = tmp.path().join( "count.txt" );
let fake = tmp.path().join( "claude" );
let script = format!(
"#!/bin/sh\nCF={}\nN=0\n[ -f \"$CF\" ] && N=$(cat \"$CF\")\nN=$((N+1))\necho $N > \"$CF\"\n[ \"$N\" -eq 1 ] && echo 'maybe' || echo 'yes'\n",
count_path.display()
);
std::fs::write( &fake, &script ).expect( "write fake claude" );
std::fs::set_permissions( &fake, std::fs::Permissions::from_mode( 0o755 ) )
.expect( "chmod fake claude" );
let old_path = std::env::var( "PATH" ).unwrap_or_default();
let new_path = format!( "{}:{old_path}", tmp.path().display() );
let out = run_with_path(
&[ "-p", "--expect", "yes|no", "--expect-strategy", "retry", "--retry-on-validation", "1", "--validation-delay", "0", "answer" ],
&new_path,
);
assert_eq!(
out.status.code(),
Some( 0 ),
"retry must succeed on 2nd attempt. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn t08_retry_exhausted_exits_3()
{
let ( _tmp, path ) = fake_claude( "#!/bin/sh\necho 'maybe'" );
let out = run_with_path(
&[ "-p", "--expect", "yes|no", "--expect-strategy", "retry", "--retry-on-validation", "2", "--validation-delay", "0", "answer" ],
&path,
);
assert_eq!(
out.status.code(),
Some( 3 ),
"exhausted retries must exit 3. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn t09_default_strategy_outputs_fallback_exits_0()
{
let ( _tmp, path ) = fake_claude( "#!/bin/sh\necho 'maybe'" );
let out = run_with_path(
&[ "-p", "--expect", "yes|no", "--expect-strategy", "default:no", "answer" ],
&path,
);
assert_eq!(
out.status.code(),
Some( 0 ),
"default strategy must exit 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let stdout = String::from_utf8_lossy( &out.stdout );
assert_eq!(
stdout.trim(),
"no",
"stdout must be the fallback 'no'. Got:\n{stdout}"
);
}
#[ test ]
fn t10_invalid_strategy_exits_1()
{
let out = run_cli( &[ "--expect", "yes|no", "--expect-strategy", "bogus", "answer" ] );
assert_eq!(
out.status.code(),
Some( 1 ),
"invalid strategy must exit 1. Got: {:?}",
out.status.code()
);
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "strategy" ) || stderr.contains( "Error" ),
"stderr must contain error message. Got:\n{stderr}"
);
}
#[ test ]
fn t12_strategy_without_expect_silently_ignored()
{
let out = run_cli( &[ "--dry-run", "--expect-strategy", "fail", "task" ] );
assert!(
out.status.success(),
"--expect-strategy without --expect must exit 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn t13_retries_0_means_single_attempt()
{
let tmp = tempfile::tempdir().expect( "create temp dir" );
let count_path = tmp.path().join( "count.txt" );
let fake = tmp.path().join( "claude" );
let script = format!(
"#!/bin/sh\nCF={}\nN=0\n[ -f \"$CF\" ] && N=$(cat \"$CF\")\nN=$((N+1))\necho $N > \"$CF\"\necho 'maybe'\n",
count_path.display()
);
std::fs::write( &fake, &script ).expect( "write fake claude" );
std::fs::set_permissions( &fake, std::fs::Permissions::from_mode( 0o755 ) )
.expect( "chmod fake claude" );
let old_path = std::env::var( "PATH" ).unwrap_or_default();
let new_path = format!( "{}:{old_path}", tmp.path().display() );
let out = run_with_path(
&[ "-p", "--expect", "yes|no", "--expect-strategy", "retry", "--retry-on-validation", "0", "answer" ],
&new_path,
);
assert_eq!(
out.status.code(),
Some( 3 ),
"retries=0 mismatch must exit 3. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let count = std::fs::read_to_string( &count_path )
.expect( "count file must exist — subprocess must have run" )
.trim()
.parse::<u32>()
.expect( "count is a number" );
assert_eq!( count, 1, "must invoke exactly 1 time (0 retries). Got: {count}" );
}
#[ test ]
fn t15_retries_without_retry_strategy_ignored()
{
let out = run_cli( &[
"--dry-run",
"--expect", "yes|no",
"--expect-strategy", "fail",
"--retry-on-validation", "5",
"task",
] );
assert!(
out.status.success(),
"--retry-on-validation with fail strategy in dry-run must exit 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn t16_retries_3_makes_4_total_attempts()
{
let tmp = tempfile::tempdir().expect( "create temp dir" );
let count_path = tmp.path().join( "count.txt" );
let fake = tmp.path().join( "claude" );
let script = format!(
"#!/bin/sh\nCF={}\nN=0\n[ -f \"$CF\" ] && N=$(cat \"$CF\")\nN=$((N+1))\necho $N > \"$CF\"\necho 'maybe'\n",
count_path.display()
);
std::fs::write( &fake, &script ).expect( "write fake claude" );
std::fs::set_permissions( &fake, std::fs::Permissions::from_mode( 0o755 ) )
.expect( "chmod fake claude" );
let old_path = std::env::var( "PATH" ).unwrap_or_default();
let new_path = format!( "{}:{old_path}", tmp.path().display() );
let out = run_with_path(
&[ "-p", "--expect", "yes|no", "--expect-strategy", "retry", "--retry-on-validation", "3", "--validation-delay", "0", "answer" ],
&new_path,
);
assert_eq!(
out.status.code(),
Some( 3 ),
"exhausted 3 retries must exit 3. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let count = std::fs::read_to_string( &count_path )
.expect( "count file must exist" )
.trim()
.parse::<u32>()
.expect( "count is a number" );
assert_eq!( count, 4, "must invoke exactly 4 times (1 initial + 3 retries). Got: {count}" );
}
#[ test ]
fn t18_default_strategy_empty_value_accepted()
{
let out = run_cli( &[
"--dry-run",
"--expect", "yes",
"--expect-strategy", "default:",
"test",
] );
assert!(
out.status.success(),
"default: with empty VALUE must exit 0 at parse time. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}