mod cli_binary_test_helpers;
use cli_binary_test_helpers::run_cli;
#[ test ]
fn s34_no_chrome_suppresses_chrome_flag()
{
let out = run_cli( &[ "--dry-run", "--no-chrome", "Fix bug" ] );
assert!( out.status.success(), "--no-chrome must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
!stdout.contains( "--chrome" ),
"--no-chrome must suppress --chrome. Got:\n{stdout}"
);
}
#[ test ]
fn s35_default_chrome_injected()
{
let out = run_cli( &[ "--dry-run", "Fix bug" ] );
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( "--chrome" ),
"default assembled command must contain --chrome. Got:\n{stdout}"
);
}
#[ test ]
fn s36_no_chrome_without_message_accepted()
{
let out = run_cli( &[ "--dry-run", "--no-chrome" ] );
assert!(
out.status.success(),
"--no-chrome without message must exit 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
!stdout.contains( "--chrome" ),
"--no-chrome must suppress --chrome even without a message. Got:\n{stdout}"
);
}
#[ test ]
fn s37_help_lists_no_chrome()
{
let out = run_cli( &[ "--help" ] );
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( "--no-chrome" ),
"--help must mention --no-chrome. Got:\n{stdout}"
);
}
#[ test ]
fn s38_no_chrome_with_no_skip_permissions_both_suppressed()
{
let out = run_cli( &[ "--dry-run", "--no-chrome", "--no-skip-permissions", "Fix bug" ] );
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( "--chrome" ),
"--no-chrome must suppress --chrome. Got:\n{stdout}"
);
assert!(
!stdout.contains( "--dangerously-skip-permissions" ),
"--no-skip-permissions must suppress --dangerously-skip-permissions. Got:\n{stdout}"
);
}
#[ test ]
fn s39_no_chrome_with_dry_run_preview_clean()
{
let out = run_cli( &[ "--dry-run", "--no-chrome", "Fix bug" ] );
assert!( out.status.success(), "must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
!stdout.contains( "--chrome" ),
"--no-chrome must suppress --chrome in dry-run preview. Got:\n{stdout}"
);
assert!(
out.stderr.is_empty(),
"--dry-run --no-chrome must produce no stderr. Got:\n{}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn s40_no_persist_forwards_no_session_persistence()
{
let out = run_cli( &[ "--dry-run", "--no-persist", "Fix bug" ] );
assert!( out.status.success(), "--no-persist must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--no-session-persistence" ),
"--no-persist must forward --no-session-persistence. Got:\n{stdout}"
);
}
#[ test ]
fn s41_default_no_session_persistence_absent()
{
let out = run_cli( &[ "--dry-run", "Fix bug" ] );
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( "--no-session-persistence" ),
"default command must not contain --no-session-persistence. Got:\n{stdout}"
);
}
#[ test ]
fn s42_no_persist_without_message_accepted()
{
let out = run_cli( &[ "--dry-run", "--no-persist" ] );
assert!(
out.status.success(),
"--no-persist without message must exit 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--no-session-persistence" ),
"--no-persist must forward --no-session-persistence. Got:\n{stdout}"
);
}
#[ test ]
fn s43_help_lists_no_persist()
{
let out = run_cli( &[ "--help" ] );
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( "--no-persist" ),
"--help must mention --no-persist. Got:\n{stdout}"
);
}
#[ test ]
fn s44_no_persist_with_new_session_accepted()
{
let out = run_cli( &[ "--dry-run", "--no-persist", "--new-session", "Fix bug" ] );
assert!( out.status.success(), "must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--no-session-persistence" ),
"--no-persist must forward --no-session-persistence. Got:\n{stdout}"
);
assert!(
!stdout.contains( " -c" ),
"--new-session must suppress -c. Got:\n{stdout}"
);
}
#[ test ]
fn s45_no_persist_with_dry_run_preview_shows_flag()
{
let out = run_cli( &[ "--dry-run", "--no-persist", "Fix bug" ] );
assert!( out.status.success(), "must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--no-session-persistence" ),
"--no-persist must appear in dry-run preview. Got:\n{stdout}"
);
assert!(
out.stderr.is_empty(),
"--dry-run --no-persist must produce no stderr. Got:\n{}",
String::from_utf8_lossy( &out.stderr )
);
}
#[ test ]
fn s46_json_schema_forwarded()
{
let out = run_cli( &[ "--dry-run", "--json-schema", r#"{"type":"object"}"#, "task" ] );
assert!( out.status.success(), "--json-schema must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--json-schema" ),
"--json-schema must appear in assembled command. Got:\n{stdout}"
);
assert!(
stdout.contains( r#"{"type":"object"}"# ),
"--json-schema value must be forwarded. Got:\n{stdout}"
);
}
#[ test ]
fn s47_default_json_schema_absent()
{
let out = run_cli( &[ "--dry-run", "task" ] );
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( "--json-schema" ),
"default command must not contain --json-schema. Got:\n{stdout}"
);
}
#[ test ]
fn s48_json_schema_complex_forwarded_verbatim()
{
let schema = r#"{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}"#;
let out = run_cli( &[ "--dry-run", "--json-schema", schema, "task" ] );
assert!( out.status.success(), "must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( schema ),
"complex schema must be forwarded verbatim. Got:\n{stdout}"
);
}
#[ test ]
fn s49_help_lists_json_schema()
{
let out = run_cli( &[ "--help" ] );
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( "--json-schema" ),
"--help must mention --json-schema. Got:\n{stdout}"
);
}
#[ test ]
fn s50_json_schema_with_model_both_forwarded()
{
let out = run_cli( &[ "--dry-run", "--json-schema", r#"{"type":"string"}"#, "--model", "sonnet", "task" ] );
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( "--json-schema" ),
"--json-schema must appear. Got:\n{stdout}"
);
assert!(
stdout.contains( "--model sonnet" ),
"--model must appear. Got:\n{stdout}"
);
}
#[ test ]
fn s51_json_schema_without_message_accepted()
{
let out = run_cli( &[ "--dry-run", "--json-schema", r#"{"type":"string"}"# ] );
assert!(
out.status.success(),
"--json-schema without message must exit 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--json-schema" ),
"--json-schema must appear in assembled command. Got:\n{stdout}"
);
}
#[ test ]
fn s52_mcp_config_forwarded()
{
let out = run_cli( &[ "--dry-run", "--mcp-config", "/tmp/mcp.json", "task" ] );
assert!( out.status.success(), "--mcp-config must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--mcp-config /tmp/mcp.json" ),
"--mcp-config value must appear in assembled command. Got:\n{stdout}"
);
}
#[ test ]
fn s53_default_mcp_config_absent()
{
let out = run_cli( &[ "--dry-run", "task" ] );
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( "--mcp-config" ),
"default command must not contain --mcp-config. Got:\n{stdout}"
);
}
#[ test ]
fn s54_mcp_config_multiple_forwarded_individually()
{
let out = run_cli( &[
"--dry-run", "--mcp-config", "/tmp/s1.json", "--mcp-config", "/tmp/s2.json", "task",
] );
assert!( out.status.success(), "must exit 0. stderr: {}", String::from_utf8_lossy( &out.stderr ) );
let stdout = String::from_utf8_lossy( &out.stdout );
let count = stdout.matches( "--mcp-config" ).count();
assert!(
count >= 2,
"multiple --mcp-config flags must each appear in assembled command (found {count}). Got:\n{stdout}"
);
assert!(
stdout.contains( "/tmp/s1.json" ),
"first mcp-config path must appear. Got:\n{stdout}"
);
assert!(
stdout.contains( "/tmp/s2.json" ),
"second mcp-config path must appear. Got:\n{stdout}"
);
}
#[ test ]
fn s55_help_lists_mcp_config()
{
let out = run_cli( &[ "--help" ] );
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( "--mcp-config" ),
"--help must mention --mcp-config. Got:\n{stdout}"
);
}
#[ test ]
fn s56_mcp_config_with_model_both_forwarded()
{
let out = run_cli( &[ "--dry-run", "--mcp-config", "/tmp/mcp.json", "--model", "sonnet", "task" ] );
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( "--mcp-config" ),
"--mcp-config must appear. Got:\n{stdout}"
);
assert!(
stdout.contains( "--model sonnet" ),
"--model must appear. Got:\n{stdout}"
);
}
#[ test ]
fn s57_mcp_config_without_message_accepted()
{
let out = run_cli( &[ "--dry-run", "--mcp-config", "/tmp/mcp.json" ] );
assert!(
out.status.success(),
"--mcp-config without message must exit 0. stderr: {}",
String::from_utf8_lossy( &out.stderr )
);
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "--mcp-config /tmp/mcp.json" ),
"--mcp-config must appear in assembled command. Got:\n{stdout}"
);
}
#[ test ]
fn s81_default_no_subdir_no_hyphen_prefix()
{
let out = run_cli( &[ "--dry-run", "task" ] );
assert!( out.status.success(), "must exit 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
!stdout.contains( "/-" ),
"without --subdir, no /- path component must appear. Got:\n{stdout}"
);
}
#[ test ]
fn s82_subdir_name_appends_hyphen_prefix()
{
let out = run_cli( &[ "--dry-run", "--subdir", "build", "task" ] );
assert!( out.status.success(), "must exit 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "/-build" ),
"--subdir build must produce path ending in /-build. Got:\n{stdout}"
);
}
#[ test ]
fn s83_subdir_dot_identity_no_suffix()
{
let out = run_cli( &[ "--dry-run", "--subdir", ".", "task" ] );
assert!( out.status.success(), "must exit 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
!stdout.contains( "/-" ),
"--subdir . must not append any /- suffix. Got:\n{stdout}"
);
}
#[ test ]
fn s84_help_lists_subdir()
{
let out = run_cli( &[ "--help" ] );
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( "--subdir" ),
"--help must mention --subdir. Got:\n{stdout}"
);
}
#[ test ]
fn s85_subdir_with_dir_combined()
{
let out = run_cli( &[ "--dry-run", "--dir", "/tmp/project", "--subdir", "debug", "task" ] );
assert!( out.status.success(), "must exit 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
stdout.contains( "/tmp/project/-debug" ),
"--dir /tmp/project --subdir debug must produce /tmp/project/-debug. Got:\n{stdout}"
);
}
#[ test ]
fn s86_subdir_empty_string_is_identity()
{
let out = run_cli( &[ "--dry-run", "--subdir", "", "task" ] );
assert!( out.status.success(), "must exit 0: {out:?}" );
let stdout = String::from_utf8_lossy( &out.stdout );
assert!(
!stdout.contains( "/-" ),
"--subdir '' (empty) must be identity — no /- suffix. Got:\n{stdout}"
);
}
#[ test ]
fn s87_subdir_rejects_slash()
{
let out = run_cli( &[ "--dry-run", "--subdir", "a/b", "task" ] );
assert!(
!out.status.success(),
"--subdir a/b must be rejected (contains '/'). Got exit: {:?}",
out.status.code()
);
let stderr = String::from_utf8_lossy( &out.stderr );
assert!(
stderr.contains( "no '/' separators" ),
"--subdir a/b error must mention slash constraint. Got:\n{stderr}"
);
}
#[ test ]
fn s88_dryrun_subdir_no_mkdir()
{
let unique = format!( "clr_drytest_{}", std::process::id() );
let base = std::env::temp_dir().join( &unique );
let expected_dir = base.join( "-probe" );
let _ = std::fs::remove_dir_all( &base );
let out = run_cli( &[
"--dry-run",
"--dir", base.to_str().unwrap(),
"--subdir", "probe",
"task",
] );
assert!( out.status.success(), "must exit 0: {out:?}" );
assert!(
!expected_dir.exists(),
"--dry-run must not create directory {expected_dir:?}"
);
let _ = std::fs::remove_dir_all( &base );
}