#![ allow( clippy::doc_markdown ) ] mod cli_binary_test_helpers;
use cli_binary_test_helpers::{ run_cli, run_cli_with_env };
use std::process::Command;
#[ cfg( unix ) ]
use std::os::unix::fs::PermissionsExt;
#[ test ]
fn ec1_retry_on_api_error_help_listed()
{
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( "--retry-on-api-error" ),
"`clr --help` must list --retry-on-api-error. Got:\n{stdout}"
);
}
#[ test ]
fn ec2_retry_on_api_error_zero_dry_run()
{
let out = run_cli( &[ "--retry-on-api-error", "0", "--dry-run", "task" ] );
assert!(
out.status.success(),
"exit must be 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
!stderr.contains( "retry" ),
"dry-run must emit no retry messages. stderr: {stderr}"
);
}
#[ test ]
fn ec3_retry_on_api_error_nonzero_dry_run()
{
let out = run_cli( &[ "--retry-on-api-error", "2", "--dry-run", "task" ] );
assert!(
out.status.success(),
"exit must be 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn ec4_clr_retry_on_api_error_env_var_accepted()
{
let out = run_cli_with_env(
&[ "--dry-run", "task" ],
&[ ( "CLR_RETRY_ON_API_ERROR", "2" ) ],
);
assert!(
out.status.success(),
"CLR_RETRY_ON_API_ERROR env var must be accepted. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn ec5_retry_on_api_error_cli_wins_over_env()
{
let out = run_cli_with_env(
&[ "--retry-on-api-error", "3", "--dry-run", "task" ],
&[ ( "CLR_RETRY_ON_API_ERROR", "1" ) ],
);
assert!(
out.status.success(),
"CLI value must win over env var. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn ec6_clr_retry_on_api_error_invalid_ignored()
{
let out = run_cli_with_env(
&[ "--dry-run", "task" ],
&[ ( "CLR_RETRY_ON_API_ERROR", "notanumber" ) ],
);
assert!(
out.status.success(),
"invalid CLR_RETRY_ON_API_ERROR must be silently ignored. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ cfg( unix ) ]
#[ test ]
fn ec7_retry_succeeds_after_one_api_error()
{
let tmp = tempfile::tempdir().expect( "create temp dir" );
let fake = tmp.path().join( "claude" );
let count = tmp.path().join( "count" );
let count_path = count.to_str().expect( "counter path utf-8" );
let script = format!(
"#!/bin/sh\n\
if [ -f \"{count_path}\" ]; then exit 0; fi\n\
touch \"{count_path}\"\n\
printf 'API Error: 500\\n' >&2\n\
exit 1\n"
);
std::fs::write( &fake, script.as_bytes() ).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 bin = env!( "CARGO_BIN_EXE_clr" );
let out = Command::new( bin )
.args( [
"-p", "--retry-on-api-error", "1", "--api-error-delay", "0",
"--max-sessions", "0", "x"
] )
.env( "PATH", &new_path )
.output()
.expect( "invoke clr" );
assert!(
out.status.success(),
"exit must be 0 after API error retry succeeds. exit={:?} stderr={}",
out.status.code(),
String::from_utf8_lossy( &out.stderr )
);
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.to_lowercase().contains( "api error" ) && stderr.to_lowercase().contains( "retry" ),
"stderr must contain API error retry message. Got:\n{stderr}"
);
}
#[ cfg( unix ) ]
#[ test ]
fn ec8_retry_exhausted_after_all_api_errors()
{
let tmp = tempfile::tempdir().expect( "create temp dir" );
let fake = tmp.path().join( "claude" );
std::fs::write(
&fake,
b"#!/bin/sh\nprintf 'API Error: 500\\n' >&2\nexit 1\n",
).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 bin = env!( "CARGO_BIN_EXE_clr" );
let out = Command::new( bin )
.args( [
"-p", "--retry-on-api-error", "2", "--api-error-delay", "0",
"--max-sessions", "0", "x"
] )
.env( "PATH", &new_path )
.output()
.expect( "invoke clr" );
assert!(
out.status.code() != Some( 0 ),
"exit must be nonzero after all retries exhausted. Got: {:?}", out.status.code()
);
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.to_lowercase().contains( "exhaust" ) || stderr.to_lowercase().contains( "fail" ),
"stderr must contain exhaustion message. Got:\n{stderr}"
);
}
#[ cfg( unix ) ]
#[ test ]
fn ec9_quota_exhausted_not_retried_as_api_error()
{
let tmp = tempfile::tempdir().expect( "create temp dir" );
let fake = tmp.path().join( "claude" );
std::fs::write(
&fake,
b"#!/bin/sh\nprintf \"You've hit your limit\\n\"\nexit 2\n",
).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 bin = env!( "CARGO_BIN_EXE_clr" );
let out = Command::new( bin )
.args( [
"-p", "--retry-on-api-error", "3", "--api-error-delay", "0",
"--max-sessions", "0", "x"
] )
.env( "PATH", &new_path )
.output()
.expect( "invoke clr" );
assert_eq!(
out.status.code(),
Some( 2 ),
"QuotaExhausted must exit 2, not be retried as ApiError. Got: {:?}", out.status.code()
);
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
!stderr.to_lowercase().contains( "retry" ),
"QuotaExhausted must not trigger retry messages. Got:\n{stderr}"
);
}
#[ cfg( unix ) ]
#[ test ]
fn ec10_default_no_retry_on_api_error()
{
let tmp = tempfile::tempdir().expect( "create temp dir" );
let fake = tmp.path().join( "claude" );
std::fs::write(
&fake,
b"#!/bin/sh\nprintf 'API Error: 500\\n' >&2\nexit 1\n",
).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 bin = env!( "CARGO_BIN_EXE_clr" );
let out = Command::new( bin )
.args( [ "-p", "--max-sessions", "0", "x" ] )
.env( "PATH", &new_path )
.env_remove( "CLR_RETRY_ON_API_ERROR" )
.output()
.expect( "invoke clr" );
assert!(
out.status.code() != Some( 0 ),
"exit must be nonzero (API error, default no-retry). Got: {:?}", out.status.code()
);
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
!stderr.to_lowercase().contains( "retrying" ),
"default retry=0 must emit no retry messages. Got:\n{stderr}"
);
}
#[ test ]
fn ec1_api_error_delay_help_listed()
{
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( "--api-error-delay" ),
"`clr --help` must list --api-error-delay. Got:\n{stdout}"
);
}
#[ test ]
fn ec2_api_error_delay_zero_dry_run()
{
let out = run_cli( &[ "--api-error-delay", "0", "--dry-run", "task" ] );
assert!(
out.status.success(),
"exit must be 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn ec3_api_error_delay_thirty_dry_run()
{
let out = run_cli( &[ "--api-error-delay", "30", "--dry-run", "task" ] );
assert!(
out.status.success(),
"exit must be 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn ec4_clr_api_error_delay_env_var_accepted()
{
let out = run_cli_with_env(
&[ "--dry-run", "task" ],
&[ ( "CLR_API_ERROR_DELAY", "5" ) ],
);
assert!(
out.status.success(),
"CLR_API_ERROR_DELAY env var must be accepted. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn ec5_api_error_delay_cli_wins_over_env()
{
let out = run_cli_with_env(
&[ "--api-error-delay", "30", "--dry-run", "task" ],
&[ ( "CLR_API_ERROR_DELAY", "10" ) ],
);
assert!(
out.status.success(),
"CLI value must win over env var. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn ec6_clr_api_error_delay_invalid_ignored()
{
let out = run_cli_with_env(
&[ "--dry-run", "task" ],
&[ ( "CLR_API_ERROR_DELAY", "abc" ) ],
);
assert!(
out.status.success(),
"invalid CLR_API_ERROR_DELAY must be silently ignored. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ cfg( unix ) ]
#[ test ]
fn ec7_api_error_delay_zero_immediate_retry()
{
let tmp = tempfile::tempdir().expect( "create temp dir" );
let fake = tmp.path().join( "claude" );
let count = tmp.path().join( "count" );
let count_path = count.to_str().expect( "counter path utf-8" );
let script = format!(
"#!/bin/sh\n\
if [ -f \"{count_path}\" ]; then exit 0; fi\n\
touch \"{count_path}\"\n\
printf 'API Error: 500\\n' >&2\n\
exit 1\n"
);
std::fs::write( &fake, script.as_bytes() ).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 bin = env!( "CARGO_BIN_EXE_clr" );
let start = std::time::Instant::now();
let out = Command::new( bin )
.args( [
"-p", "--retry-on-api-error", "1", "--api-error-delay", "0",
"--max-sessions", "0", "x"
] )
.env( "PATH", &new_path )
.output()
.expect( "invoke clr" );
let elapsed = start.elapsed();
assert!(
out.status.success(),
"exit must be 0 with delay=0 API error retry. exit={:?} stderr={}",
out.status.code(),
String::from_utf8_lossy( &out.stderr )
);
assert!(
elapsed.as_secs() < 5,
"delay=0 must retry immediately; elapsed {elapsed:?} is too long"
);
}