use std::fs;
use std::io::ErrorKind;
use claude_version::settings_io::{ infer_type, StoredAs, get_setting, set_setting, set_env_var, remove_env_var, read_all_settings };
use tempfile::TempDir;
#[ test ]
fn tc027_true_inferred_as_bool()
{
assert_eq!( infer_type( "true" ), StoredAs::Bool );
}
#[ test ]
fn tc028_false_inferred_as_bool()
{
assert_eq!( infer_type( "false" ), StoredAs::Bool );
}
#[ test ]
fn tc029_zero_inferred_as_number_not_bool()
{
assert_eq!( infer_type( "0" ), StoredAs::Number );
}
#[ test ]
fn tc030_one_inferred_as_number_not_bool()
{
assert_eq!( infer_type( "1" ), StoredAs::Number );
}
#[ test ]
fn tc031_integer_inferred_as_number()
{
assert_eq!( infer_type( "42" ), StoredAs::Number );
}
#[ test ]
fn tc032_float_inferred_as_number()
{
assert_eq!( infer_type( "3.14" ), StoredAs::Number );
}
#[ test ]
fn tc033_plain_string_inferred_as_str()
{
assert_eq!( infer_type( "hello" ), StoredAs::Str );
}
#[ test ]
fn tc034_empty_string_inferred_as_str()
{
assert_eq!( infer_type( "" ), StoredAs::Str );
}
#[ test ]
fn tc035_uppercase_true_inferred_as_str()
{
assert_eq!( infer_type( "TRUE" ), StoredAs::Str );
}
#[ test ]
fn tc036_mixed_case_false_inferred_as_str()
{
assert_eq!( infer_type( "False" ), StoredAs::Str );
}
#[ test ]
fn tc037_whitespace_only_inferred_as_str()
{
assert_eq!( infer_type( " " ), StoredAs::Str );
}
#[ test ]
fn tc038_negative_integer_inferred_as_number()
{
assert_eq!( infer_type( "-1" ), StoredAs::Number );
}
#[ test ]
fn tc061_nan_inferred_as_str()
{
assert_eq!( infer_type( "NaN" ), StoredAs::Str );
}
#[ test ]
fn tc062_inf_inferred_as_str()
{
assert_eq!( infer_type( "inf" ), StoredAs::Str );
}
#[ test ]
fn tc063_infinity_inferred_as_str()
{
assert_eq!( infer_type( "infinity" ), StoredAs::Str );
}
#[ test ]
fn tc064_negative_inf_inferred_as_str()
{
assert_eq!( infer_type( "-inf" ), StoredAs::Str );
}
#[ test ]
fn tc065_non_finite_case_variants_inferred_as_str()
{
for val in &[ "nan", "NAN", "Inf", "INF", "Infinity", "INFINITY", "-Infinity", "+inf" ]
{
assert_eq!(
infer_type( val ),
StoredAs::Str,
"'{val}' must be Str, not Number"
);
}
}
#[ test ]
fn tc066_set_nan_produces_valid_json_roundtrip()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_setting( &path, "x", "NaN" ).unwrap();
let pairs = read_all_settings( &path ).unwrap();
assert_eq!( pairs.len(), 1 );
assert_eq!( pairs[ 0 ].0, "x" );
assert_eq!( pairs[ 0 ].1, "NaN" );
}
#[ test ]
fn tc067_set_inf_produces_valid_json_roundtrip()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_setting( &path, "x", "inf" ).unwrap();
let pairs = read_all_settings( &path ).unwrap();
assert_eq!( pairs.len(), 1 );
assert_eq!( pairs[ 0 ].0, "x" );
assert_eq!( pairs[ 0 ].1, "inf" );
}
#[ test ]
fn tc039_set_setting_creates_file_when_absent()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
assert!( !path.exists() );
set_setting( &path, "key1", "val1" ).unwrap();
assert!( path.exists(), "settings.json must be created" );
}
#[ test ]
fn tc040_set_setting_adds_new_key_to_existing_file()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_setting( &path, "a", "1" ).unwrap();
set_setting( &path, "b", "2" ).unwrap();
let v = get_setting( &path, "a" ).unwrap();
assert_eq!( v.as_deref(), Some( "1" ) );
let v2 = get_setting( &path, "b" ).unwrap();
assert_eq!( v2.as_deref(), Some( "2" ) );
}
#[ test ]
fn tc041_set_setting_overwrites_existing_key()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_setting( &path, "k", "old" ).unwrap();
set_setting( &path, "k", "new" ).unwrap();
let v = get_setting( &path, "k" ).unwrap();
assert_eq!( v.as_deref(), Some( "new" ) );
}
#[ test ]
fn tc042_bool_true_written_without_quotes()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_setting( &path, "flag", "true" ).unwrap();
let raw = fs::read_to_string( &path ).unwrap();
assert!( raw.contains( "true" ), "JSON must contain bare true" );
assert!( !raw.contains( "\"true\"" ), "JSON must not quote boolean" );
}
#[ test ]
fn tc043_integer_written_without_quotes()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_setting( &path, "count", "42" ).unwrap();
let raw = fs::read_to_string( &path ).unwrap();
assert!( raw.contains( "42" ), "JSON must contain bare 42" );
assert!( !raw.contains( "\"42\"" ), "JSON must not quote integer" );
}
#[ test ]
fn tc044_zero_written_as_number_not_false()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_setting( &path, "n", "0" ).unwrap();
let raw = fs::read_to_string( &path ).unwrap();
assert!( raw.contains( ": 0" ) || raw.contains( ":0" ), "JSON must contain 0 as number" );
assert!( !raw.contains( "false" ), "0 must not be written as false" );
}
#[ test ]
fn tc045_empty_string_written_as_empty_json_string()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_setting( &path, "s", "" ).unwrap();
let raw = fs::read_to_string( &path ).unwrap();
assert!( raw.contains( "\"\"" ), "empty string must appear as \"\" in JSON" );
}
#[ test ]
fn tc046_value_with_double_quote_escaped()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_setting( &path, "s", r#"say "hi""# ).unwrap();
let v = get_setting( &path, "s" ).unwrap();
assert_eq!( v.as_deref(), Some( r#"say "hi""# ) );
}
#[ test ]
fn tc047_value_with_backslash_escaped()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_setting( &path, "s", r"back\slash" ).unwrap();
let v = get_setting( &path, "s" ).unwrap();
assert_eq!( v.as_deref(), Some( r"back\slash" ) );
}
#[ test ]
fn tc048_key_with_dot_stored_as_literal()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_setting( &path, "foo.bar", "val" ).unwrap();
let v = get_setting( &path, "foo.bar" ).unwrap();
assert_eq!( v.as_deref(), Some( "val" ) );
}
#[ test ]
fn tc049_atomic_write_leaves_no_tmp_file()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_setting( &path, "k", "v" ).unwrap();
let tmp = dir.path().join( "settings.json.tmp" );
assert!( !tmp.exists(), "temp file must not remain after successful write" );
}
#[ cfg( unix ) ]
#[ test ]
fn tc050_write_to_read_only_dir_returns_error()
{
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
let mut perms = fs::metadata( dir.path() ).unwrap().permissions();
perms.set_mode( 0o555 );
fs::set_permissions( dir.path(), perms ).unwrap();
let result = set_setting( &path, "k", "v" );
let mut perms2 = fs::metadata( dir.path() ).unwrap().permissions();
perms2.set_mode( 0o755 );
fs::set_permissions( dir.path(), perms2 ).unwrap();
assert!( result.is_err(), "write to read-only dir must fail" );
}
#[ test ]
fn tc051_get_setting_returns_value_for_existing_key()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_setting( &path, "mykey", "myval" ).unwrap();
let v = get_setting( &path, "mykey" ).unwrap();
assert_eq!( v.as_deref(), Some( "myval" ) );
}
#[ test ]
fn tc052_get_setting_returns_none_for_missing_key()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_setting( &path, "existing", "1" ).unwrap();
let v = get_setting( &path, "nonexistent" ).unwrap();
assert_eq!( v, None );
}
#[ test ]
fn tc053_get_setting_on_missing_file_returns_not_found()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "no_such_file.json" );
let result = get_setting( &path, "k" );
assert!( result.is_err() );
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::NotFound,
"missing file must return NotFound error"
);
}
#[ test ]
fn tc054_read_all_settings_empty_object_returns_empty_vec()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
fs::write( &path, "{}" ).unwrap();
let pairs = read_all_settings( &path ).unwrap();
assert!( pairs.is_empty(), "empty JSON object must produce empty vec" );
}
#[ test ]
fn tc055_read_all_settings_returns_all_key_value_pairs()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
fs::write( &path, r#"{"a": "hello", "b": "world"}"# ).unwrap();
let pairs = read_all_settings( &path ).unwrap();
assert_eq!( pairs.len(), 2 );
let map : std::collections::HashMap< _, _ > = pairs.into_iter().collect();
assert_eq!( map.get( "a" ).map( String::as_str ), Some( "hello" ) );
assert_eq!( map.get( "b" ).map( String::as_str ), Some( "world" ) );
}
#[ test ]
fn tc056_read_all_settings_missing_file_returns_not_found()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "no_such.json" );
let result = read_all_settings( &path );
assert!( result.is_err() );
assert_eq!( result.unwrap_err().kind(), ErrorKind::NotFound );
}
#[ test ]
fn tc057_read_all_settings_malformed_json_returns_error()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
fs::write( &path, "{ not valid json" ).unwrap();
let result = read_all_settings( &path );
assert!( result.is_err(), "malformed JSON must return error" );
}
#[ test ]
fn tc058_set_then_get_roundtrip()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
for ( key, val ) in &[ ( "k1", "hello" ), ( "k2", "42" ), ( "k3", "true" ), ( "k4", "" ) ]
{
set_setting( &path, key, val ).unwrap();
let v = get_setting( &path, key ).unwrap();
assert_eq!(
v.as_deref(),
Some( *val ),
"roundtrip failed for key={key} val={val}"
);
}
}
#[ test ]
fn tc059_set_new_key_preserves_existing_keys()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_setting( &path, "existing", "alpha" ).unwrap();
set_setting( &path, "new_key", "beta" ).unwrap();
let v = get_setting( &path, "existing" ).unwrap();
assert_eq!( v.as_deref(), Some( "alpha" ), "existing key must be preserved" );
}
#[ test ]
fn tc060_value_with_newline_stored_safely()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_setting( &path, "s", "line1\nline2" ).unwrap();
let v = get_setting( &path, "s" ).unwrap();
assert_eq!( v.as_deref(), Some( "line1\nline2" ) );
}
#[ test ]
fn tc076_read_all_settings_preserves_nested_objects()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
fs::write( &path, r#"{"env": {"TZ": "Europe/Kyiv"}, "autoUpdates": false}"# ).unwrap();
let pairs = read_all_settings( &path ).unwrap();
assert_eq!( pairs.len(), 2 );
assert_eq!( pairs[ 1 ].0, "autoUpdates" );
assert_eq!( pairs[ 1 ].1, "false" );
assert_eq!( pairs[ 0 ].0, "env" );
assert!( pairs[ 0 ].1.contains( "TZ" ), "env must contain TZ: {}", pairs[ 0 ].1 );
}
#[ test ]
fn tc077_set_setting_preserves_nested_objects()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
fs::write( &path, r#"{"env": {"TZ": "Europe/Kyiv"}, "autoUpdates": false}"# ).unwrap();
set_setting( &path, "autoUpdates", "true" ).unwrap();
let content = fs::read_to_string( &path ).unwrap();
assert!( content.contains( "TZ" ), "env block must survive round-trip: {content}" );
assert!( content.contains( "true" ), "autoUpdates must be updated: {content}" );
}
#[ test ]
fn tc078_set_setting_preserves_plugins_object()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
fs::write( &path, r#"{"enabledPlugins": {"foo@bar": true}, "key1": "val1"}"# ).unwrap();
set_setting( &path, "key1", "val2" ).unwrap();
let content = fs::read_to_string( &path ).unwrap();
assert!( content.contains( "enabledPlugins" ), "plugins must survive: {content}" );
assert!( content.contains( "foo@bar" ), "plugin entry must survive: {content}" );
assert!( content.contains( "val2" ), "key1 must be updated: {content}" );
}
#[ test ]
fn tc079_infer_type_detects_raw_object()
{
assert_eq!( infer_type( r#"{"key": "value"}"# ), StoredAs::Raw );
}
#[ test ]
fn tc080_infer_type_detects_raw_array()
{
assert_eq!( infer_type( "[1, 2, 3]" ), StoredAs::Raw );
}
#[ test ]
fn tc081_set_env_var_creates_env_block()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
fs::write( &path, r#"{"autoUpdates": false}"# ).unwrap();
set_env_var( &path, "DISABLE_AUTOUPDATER", "1" ).unwrap();
let content = fs::read_to_string( &path ).unwrap();
assert!( content.contains( "DISABLE_AUTOUPDATER" ), "must contain env var: {content}" );
assert!( content.contains( "\"1\"" ), "value must be quoted string: {content}" );
assert!( content.contains( "autoUpdates" ), "existing keys preserved: {content}" );
}
#[ test ]
fn tc082_set_env_var_updates_existing_env()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
fs::write( &path, r#"{"env": {"TZ": "Europe/Kyiv"}, "autoUpdates": false}"# ).unwrap();
set_env_var( &path, "DISABLE_AUTOUPDATER", "1" ).unwrap();
let content = fs::read_to_string( &path ).unwrap();
assert!( content.contains( "TZ" ), "existing env vars preserved: {content}" );
assert!( content.contains( "DISABLE_AUTOUPDATER" ), "new env var added: {content}" );
}
#[ test ]
fn tc068_set_env_var_overwrites_existing()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
fs::write( &path, r#"{"env": {"DISABLE_AUTOUPDATER": "0"}}"# ).unwrap();
set_env_var( &path, "DISABLE_AUTOUPDATER", "1" ).unwrap();
let content = fs::read_to_string( &path ).unwrap();
assert!( content.contains( "\"1\"" ), "value must be updated to 1: {content}" );
}
#[ test ]
fn tc069_set_env_var_creates_file()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
set_env_var( &path, "DISABLE_AUTOUPDATER", "1" ).unwrap();
assert!( path.exists(), "file must be created" );
let content = fs::read_to_string( &path ).unwrap();
assert!( content.contains( "DISABLE_AUTOUPDATER" ), "must contain var: {content}" );
}
#[ test ]
fn tc070_remove_env_var_removes_key()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
fs::write( &path, r#"{"env": {"TZ": "UTC", "DISABLE_AUTOUPDATER": "1"}}"# ).unwrap();
remove_env_var( &path, "DISABLE_AUTOUPDATER" ).unwrap();
let content = fs::read_to_string( &path ).unwrap();
assert!( !content.contains( "DISABLE_AUTOUPDATER" ), "var must be removed: {content}" );
assert!( content.contains( "TZ" ), "other env vars preserved: {content}" );
}
#[ test ]
fn tc071_remove_env_var_noop_when_absent()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
fs::write( &path, r#"{"env": {"TZ": "UTC"}}"# ).unwrap();
remove_env_var( &path, "NONEXISTENT" ).unwrap();
let content = fs::read_to_string( &path ).unwrap();
assert!( content.contains( "TZ" ), "original content preserved: {content}" );
}
#[ test ]
fn tc072_remove_env_var_noop_when_file_absent()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
assert!( remove_env_var( &path, "FOO" ).is_ok() );
}
#[ test ]
fn tc074_infer_type_null_is_raw()
{
assert_eq!( infer_type( "null" ), StoredAs::Raw );
}
#[ test ]
fn tc075_null_value_survives_roundtrip()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
fs::write( &path, r#"{"existing": null, "other": "hello"}"# ).unwrap();
set_setting( &path, "other", "world" ).unwrap();
let content = fs::read_to_string( &path ).unwrap();
assert!(
content.contains( ": null" ) || content.contains( ":null" ),
"null must survive as bare null, not become \"null\": {content}"
);
assert!(
!content.contains( "\"null\"" ),
"null must not be quoted: {content}"
);
}
#[ test ]
fn tc073_real_world_settings_roundtrip()
{
let dir = TempDir::new().unwrap();
let path = dir.path().join( "settings.json" );
let real_json = r#"{
"env": {
"TZ": "Europe/Kyiv"
},
"enabledPlugins": {
"rust-analyzer-lsp@claude-plugins-official": true
},
"skipDangerousModePermissionPrompt": true,
"autoUpdates": false
}"#;
fs::write( &path, real_json ).unwrap();
set_setting( &path, "autoUpdates", "true" ).unwrap();
let content = fs::read_to_string( &path ).unwrap();
assert!( content.contains( "TZ" ), "env.TZ must survive: {content}" );
assert!( content.contains( "enabledPlugins" ), "plugins must survive: {content}" );
assert!( content.contains( "skipDangerousModePermissionPrompt" ), "skip must survive: {content}" );
}