fn run( args : &[ &str ] ) -> std::process::Output
{
let bin = env!( "CARGO_BIN_EXE_claude_version" );
std::process::Command::new( bin )
.args( args )
.output()
.expect( "failed to run cm" )
}
fn out_stdout( out : &std::process::Output ) -> String
{
String::from_utf8_lossy( &out.stdout ).into_owned()
}
fn out_stderr( out : &std::process::Output ) -> String
{
String::from_utf8_lossy( &out.stderr ).into_owned()
}
fn code( out : &std::process::Output ) -> i32
{
out.status.code().unwrap_or( -1 )
}
#[ test ]
fn tc001_empty_argv_shows_help()
{
let out = run( &[] );
assert_eq!( code( &out ), 0, "empty argv must exit 0" );
assert!( out_stdout( &out ).contains( "Available commands:" ), "must show help" );
}
#[ test ]
fn tc002_dot_help()
{
let out = run( &[ ".help" ] );
assert_eq!( code( &out ), 0 );
assert!( out_stdout( &out ).contains( "Available commands:" ), "must show help" );
}
#[ test ]
fn tc004_unknown_param_exits_1()
{
let out = run( &[ ".status", "bogus::1" ] );
assert_eq!( code( &out ), 1 );
let err = out_stderr( &out );
assert!( err.to_lowercase().contains( "unknown parameter" ), "must mention unknown parameter: {err}" );
}
#[ test ]
fn tc005_verbosity_empty_value()
{
let out = run( &[ ".status", "v::" ] );
assert_eq!( code( &out ), 1 );
let err = out_stderr( &out );
assert!( err.contains( "v::" ), "must mention v::: {err}" );
}
#[ test ]
fn tc006_verbosity_out_of_range()
{
let out = run( &[ ".status", "v::3" ] );
assert_eq!( code( &out ), 1 );
let err = out_stderr( &out );
assert!(
err.contains( "out of range" ) || err.contains( "0, 1, or 2" ),
"must mention range: {err}"
);
}
#[ test ]
fn tc007_verbosity_non_integer()
{
let out = run( &[ ".status", "v::abc" ] );
assert_eq!( code( &out ), 1 );
let err = out_stderr( &out );
assert!( err.contains( "v::" ), "must mention v::: {err}" );
}
#[ test ]
fn tc008_verbosity_0_accepted()
{
let out = run( &[ ".status", "v::0" ] );
assert_eq!( code( &out ), 0 );
}
#[ test ]
fn tc010_last_verbosity_wins()
{
let out = run( &[ ".status", "v::2", "v::0" ] );
assert_eq!( code( &out ), 0 );
let text = out_stdout( &out );
assert!( !text.contains( "Version:" ), "last v::0 must win: {text}" );
}
#[ test ]
fn tc011_single_word_subcommand()
{
let out = run( &[ ".status" ] );
assert_eq!( code( &out ), 0 );
}
#[ test ]
fn tc012_two_word_subcommand()
{
let out = run( &[ ".version.list" ] );
assert_eq!( code( &out ), 0 );
}
#[ test ]
fn tc014_unknown_command()
{
let out = run( &[ ".nonexistent" ] );
assert_eq!( code( &out ), 1 );
let err = out_stderr( &out );
assert!( err.contains( "not found" ), "must mention not found: {err}" );
}
#[ test ]
fn tc015_format_empty_value()
{
let out = run( &[ ".status", "format::" ] );
assert_eq!( code( &out ), 1 );
}
#[ test ]
fn tc016_version_param_empty_value()
{
let out = run( &[ ".version.install", "version::" ] );
assert_eq!( code( &out ), 1 );
}
#[ test ]
fn tc020_dry_run_param()
{
let out = run( &[ ".version.install", "dry::1" ] );
assert_eq!( code( &out ), 0 );
let text = out_stdout( &out );
assert!( text.contains( "[dry-run]" ), "must show dry-run: {text}" );
}
#[ test ]
fn tc021_force_param()
{
let out = run( &[ ".version.install", "dry::1", "force::1" ] );
assert_eq!( code( &out ), 0 );
}
#[ test ]
fn tc022_v_param_consistent()
{
let out_a = run( &[ ".version.list", "v::0" ] );
let out_b = run( &[ ".version.list", "v::0" ] );
assert_eq!( code( &out_a ), 0 );
assert_eq!( code( &out_b ), 0 );
assert_eq!(
out_stdout( &out_a ), out_stdout( &out_b ),
"v::0 must produce identical output"
);
}
#[ test ]
fn tc024_bare_token_after_command_rejected()
{
let out = run( &[ ".version.show", "extra" ] );
assert_eq!( code( &out ), 1, "bare token after command must exit 1" );
let err = out_stderr( &out );
assert!( err.contains( "param::value" ), "must mention param::value syntax: {err}" );
}
#[ test ]
fn tc025_bare_token_without_dot_prefix()
{
let out = run( &[ "status" ] );
assert_eq!( code( &out ), 1 );
let err = out_stderr( &out );
assert!( err.contains( "'.'" ), "must mention dot prefix requirement: {err}" );
}
#[ test ]
fn tc026_help_subcommand_explicitly()
{
let out = run( &[ ".help" ] );
assert_eq!( code( &out ), 0 );
assert!( out_stdout( &out ).contains( "Available commands:" ), "must show help" );
}
#[ test ]
fn tc027_double_dash_rejected()
{
let out = run( &[ ".status", "--" ] );
assert_eq!( code( &out ), 1 );
let err = out_stderr( &out );
assert!( err.contains( "param::value" ), "-- must be rejected as non-param::value: {err}" );
}
#[ test ]
fn tc028_four_part_semver_rejected()
{
let out = run( &[ ".version.install", "version::1.2.3.4" ] );
assert_eq!( code( &out ), 1 );
}
#[ test ]
fn tc029_leading_zero_semver_rejected()
{
let out = run( &[ ".version.install", "version::01.02.03", "dry::1" ] );
assert_eq!( code( &out ), 1, "leading-zero semver must be rejected" );
}
#[ test ]
fn tc030_format_text_wrong_case_rejected()
{
let out = run( &[ ".status", "format::TEXT" ] );
assert_eq!( code( &out ), 1 );
}
#[ test ]
fn tc031_command_without_dot_prefix()
{
let out = run( &[ "version" ] );
assert_eq!( code( &out ), 1 );
let err = out_stderr( &out );
assert!( err.contains( "'.'" ), "must mention dot prefix: {err}" );
}
#[ test ]
fn tc032_unknown_param_key()
{
let out = run( &[ ".status", "nope::x" ] );
assert_eq!( code( &out ), 1 );
let err = out_stderr( &out );
assert!( err.to_lowercase().contains( "unknown parameter" ), "must mention unknown parameter: {err}" );
}
#[ test ]
fn tc033_dry_true_rejected()
{
let out = run( &[ ".version.install", "dry::true" ] );
assert_eq!( code( &out ), 1, "dry::true must be rejected" );
let err = out_stderr( &out );
assert!( err.contains( "dry::" ), "error must mention dry::: {err}" );
}
#[ test ]
fn tc034_dry_yes_rejected()
{
let out = run( &[ ".version.install", "dry::yes" ] );
assert_eq!( code( &out ), 1, "dry::yes must be rejected" );
}
#[ test ]
fn tc035_force_true_rejected()
{
let out = run( &[ ".version.install", "dry::1", "force::true" ] );
assert_eq!( code( &out ), 1, "force::true must be rejected" );
let err = out_stderr( &out );
assert!( err.contains( "force::" ), "error must mention force::: {err}" );
}
#[ test ]
fn tc036_dry_0_accepted()
{
let out = run( &[ ".version.install", "dry::0" ] );
assert_ne!( code( &out ), 1, "dry::0 is valid, must not exit 1" );
}
#[ test ]
fn tc037_force_0_accepted()
{
let out = run( &[ ".version.install", "dry::1", "force::0" ] );
assert_eq!( code( &out ), 0, "force::0 is valid" );
}
#[ test ]
fn tc038_help_in_second_position()
{
let out = run( &[ ".status", ".help" ] );
assert_eq!( code( &out ), 0, "`.status .help` must exit 0" );
let stdout = out_stdout( &out );
assert!( stdout.contains( "Available commands:" ), "must show help listing: {stdout}" );
}
#[ test ]
fn tc039_help_after_multi_part_command()
{
let out = run( &[ ".version.install", ".help" ] );
assert_eq!( code( &out ), 0, "`.version.install .help` must exit 0" );
let stdout = out_stdout( &out );
assert!( stdout.contains( "Available commands:" ), "must show help listing: {stdout}" );
}
#[ test ]
fn tc040_help_after_params()
{
let out = run( &[ ".version.guard", "dry::1", ".help" ] );
assert_eq!( code( &out ), 0, "`.version.guard dry::1 .help` must exit 0" );
let stdout = out_stdout( &out );
assert!( stdout.contains( "Available commands:" ), "must show help listing: {stdout}" );
}
#[ test ]
fn tc484_verbosity_canonical_out_of_range_rejected()
{
let out = run( &[ ".status", "verbosity::3" ] );
assert_eq!( code( &out ), 1, "verbosity::3 must be rejected (exit 1)" );
let err = out_stderr( &out );
assert!(
err.contains( "out of range" ) || err.contains( "0, 1, or 2" ) || err.contains( "verbosity::" ),
"error must mention range or verbosity: {err}"
);
}
#[ test ]
fn tc485_verbosity_canonical_negative_rejected()
{
let out = run( &[ ".status", "verbosity::-1" ] );
assert_eq!( code( &out ), 1, "verbosity::-1 must be rejected (exit 1)" );
let err = out_stderr( &out );
assert!(
err.contains( "verbosity::" ),
"error must mention verbosity: {err}"
);
}
#[ test ]
fn tc486_verbosity_canonical_zero_accepted()
{
let out = run( &[ ".status", "verbosity::0" ] );
assert_eq!( code( &out ), 0, "verbosity::0 is valid, must exit 0" );
assert!( !out_stdout( &out ).contains( "Version:" ), "verbosity::0 must not show labels" );
}
#[ test ]
fn tc487_count_u64_max_rejected_with_clear_error()
{
let out = run( &[ ".version.history", "count::18446744073709551615" ] );
assert_eq!( code( &out ), 1, "count::u64_max must be rejected (exit 1)" );
let err = out_stderr( &out );
assert!(
err.contains( "count::" ),
"error must mention count: {err}"
);
assert!(
!err.contains( "fit in target type" ),
"must not expose internal type error: {err}"
);
}
#[ test ]
fn tc488_count_i64_max_accepted()
{
let out = run( &[ ".version.history", "count::9223372036854775807" ] );
assert_ne!( code( &out ), 1, "count::i64_max must not be rejected by adapter (exit must not be 1)" );
}
#[ test ]
fn tc489_bare_help_after_command_routes_to_help()
{
let out = run( &[ ".version.show", "help" ] );
assert_eq!( code( &out ), 0, "`.version.show help` must exit 0" );
let stdout = out_stdout( &out );
assert!( stdout.contains( "Available commands:" ), "must show help listing: {stdout}" );
}
#[ test ]
fn tc490_bare_help_after_params_routes_to_help()
{
let out = run( &[ ".version.history", "count::3", "help" ] );
assert_eq!( code( &out ), 0, "`.version.history count::3 help` must exit 0" );
let stdout = out_stdout( &out );
assert!( stdout.contains( "Available commands:" ), "must show help listing: {stdout}" );
}
#[ test ]
fn tc491_interval_u64_max_rejected_with_clear_error()
{
let out = run( &[ ".version.guard", "interval::18446744073709551615" ] );
assert_eq!( code( &out ), 1, "interval::u64_max must be rejected (exit 1)" );
let err = out_stderr( &out );
assert!(
err.contains( "interval::" ),
"error must mention interval: {err}"
);
assert!(
!err.contains( "fit in target type" ),
"must not expose internal type error: {err}"
);
}
#[ test ]
fn tc493_dry_0_then_1_last_wins_dry_active()
{
let out = run( &[ ".version.install", "dry::0", "dry::1" ] );
assert_eq!( code( &out ), 0, "dry::0 dry::1 must exit 0" );
let text = out_stdout( &out );
assert!(
text.contains( "[dry-run]" ),
"dry::1 (last) must win: output must contain [dry-run]: {text}"
);
}
#[ test ]
fn tc494_dry_1_then_0_last_wins_dry_inactive()
{
let dir = tempfile::TempDir::new().expect( "failed to create tmpdir" );
let out = std::process::Command::new( env!( "CARGO_BIN_EXE_claude_version" ) )
.args( [ ".settings.set", "key::probe", "value::check", "dry::1", "dry::0" ] )
.env( "HOME", dir.path() )
.output()
.expect( "failed to run cm" );
let settings_file = dir.path().join( ".claude/settings.json" );
assert!(
settings_file.exists(),
"dry::0 (last) must win: settings file must be written"
);
let text = String::from_utf8_lossy( &out.stdout ).into_owned();
assert!(
!text.contains( "[dry-run]" ),
"dry::0 (last) must win: output must NOT contain [dry-run]: {text}"
);
}
#[ test ]
fn tc495_format_text_then_json_last_wins_json()
{
let out = run( &[ ".version.list", "format::text", "format::json" ] );
assert_eq!( code( &out ), 0, "format::text format::json must exit 0" );
let text = out_stdout( &out );
assert!(
text.trim_start().starts_with( '[' ),
"format::json (last) must win, output must start with '[': {text}"
);
}