use crate::cli_runner::{ run_cs, run_cs_with_env, stdout, stderr, assert_exit };
use tempfile::TempDir;
fn read_settings_model( home : &std::path::Path ) -> Option< String >
{
let content = std::fs::read_to_string(
home.join( ".claude" ).join( "settings.json" ),
).ok()?;
let val : serde_json::Value = serde_json::from_str( &content ).ok()?;
val.get( "model" )?.as_str().map( std::string::ToString::to_string )
}
#[ test ]
fn ft01_get_model_opus()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let claude_dir = dir.path().join( ".claude" );
std::fs::create_dir_all( &claude_dir ).unwrap();
std::fs::write( claude_dir.join( "settings.json" ), r#"{"model":"opus"}"# ).unwrap();
let out = run_cs_with_env( &[ ".model" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert_eq!(
text, "model: opus\n",
"FT-01: expected `model: opus\\n`; got: {text:?}",
);
}
#[ test ]
fn ft02_get_model_sonnet()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let claude_dir = dir.path().join( ".claude" );
std::fs::create_dir_all( &claude_dir ).unwrap();
std::fs::write( claude_dir.join( "settings.json" ), r#"{"model":"sonnet"}"# ).unwrap();
let out = run_cs_with_env( &[ ".model" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert_eq!(
text, "model: sonnet\n",
"FT-02: expected `model: sonnet\\n`; got: {text:?}",
);
}
#[ test ]
fn ft03_get_model_unset_key_absent()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let claude_dir = dir.path().join( ".claude" );
std::fs::create_dir_all( &claude_dir ).unwrap();
std::fs::write( claude_dir.join( "settings.json" ), "{}" ).unwrap();
let out = run_cs_with_env( &[ ".model" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert_eq!(
text, "model: (unset)\n",
"FT-03: expected `model: (unset)\\n` when key absent; got: {text:?}",
);
}
#[ test ]
fn ft04_get_model_unset_file_absent()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".model" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert_eq!(
text, "model: (unset)\n",
"FT-04: expected `model: (unset)\\n` when file absent; got: {text:?}\nstderr: {}",
stderr( &out ),
);
}
#[ test ]
fn ft05_set_opus_writes_full_id()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".model", "set::opus" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(), Some( "claude-opus-4-6" ),
"FT-05: set::opus must write `claude-opus-4-6`; got: {model:?}\nstderr: {}",
stderr( &out ),
);
}
#[ test ]
fn ft06_set_sonnet_writes_full_id()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".model", "set::sonnet" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(), Some( "claude-sonnet-4-6" ),
"FT-06: set::sonnet must write `claude-sonnet-4-6`; got: {model:?}",
);
}
#[ test ]
fn ft07_set_haiku_writes_full_id()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".model", "set::haiku" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(), Some( "claude-haiku-4-5-20251001" ),
"FT-07: set::haiku must write `claude-haiku-4-5-20251001`; got: {model:?}",
);
}
#[ test ]
fn ft08_set_default_removes_key_preserves_others()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let claude_dir = dir.path().join( ".claude" );
std::fs::create_dir_all( &claude_dir ).unwrap();
std::fs::write(
claude_dir.join( "settings.json" ),
r#"{"model":"claude-opus-4-6","theme":"dark"}"#,
).unwrap();
let out = run_cs_with_env( &[ ".model", "set::default" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let content = std::fs::read_to_string( claude_dir.join( "settings.json" ) )
.expect( "settings.json must exist after set::default" );
assert!(
!content.contains( "\"model\"" ),
"FT-08: settings.json must not contain `model` key after set::default; got: {content}",
);
assert!(
content.contains( "\"theme\"" ),
"FT-08: settings.json must preserve `theme` key after set::default; got: {content}",
);
}
#[ test ]
fn ft09_set_bad_value_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".model", "set::bad" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "opus" ) && err.contains( "sonnet" )
&& err.contains( "haiku" ) && err.contains( "default" ),
"FT-09: stderr must name all four valid set:: values; got:\n{err}",
);
}
#[ test ]
fn ft10_set_creates_file_when_absent()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
assert!(
!dir.path().join( ".claude" ).join( "settings.json" ).exists(),
"test precondition: settings.json must not exist before the run",
);
let out = run_cs_with_env( &[ ".model", "set::opus" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(), Some( "claude-opus-4-6" ),
"FT-10: set::opus must create settings.json with `claude-opus-4-6` when file was absent; got: {model:?}\nstderr: {}",
stderr( &out ),
);
}
#[ test ]
fn ft11_set_preserves_existing_keys()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let claude_dir = dir.path().join( ".claude" );
std::fs::create_dir_all( &claude_dir ).unwrap();
std::fs::write(
claude_dir.join( "settings.json" ),
r#"{"theme":"dark","autoUpdaterStatus":"disabled"}"#,
).unwrap();
let out = run_cs_with_env( &[ ".model", "set::opus" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let content = std::fs::read_to_string( claude_dir.join( "settings.json" ) )
.expect( "settings.json must exist after set::opus" );
assert!(
content.contains( "\"model\"" ) && content.contains( "claude-opus-4-6" ),
"FT-11: settings.json must contain model key after set::opus; got: {content}",
);
assert!(
content.contains( "\"theme\"" ) && content.contains( "\"autoUpdaterStatus\"" ),
"FT-11: pre-existing keys must be preserved after set::opus; got: {content}",
);
}
#[ test ]
fn ft12_get_json_format()
{
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let claude_dir = dir.path().join( ".claude" );
std::fs::create_dir_all( &claude_dir ).unwrap();
std::fs::write( claude_dir.join( "settings.json" ), r#"{"model":"opus"}"# ).unwrap();
let out = run_cs_with_env( &[ ".model", "format::json" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert_eq!(
text, "{\"model\":\"opus\"}\n",
"FT-12 (present): expected `{{\"model\":\"opus\"}}\\n`; got: {text:?}",
);
}
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".model", "format::json" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert_eq!(
text, "{\"model\":null}\n",
"FT-12 (absent): expected `{{\"model\":null}}\\n`; got: {text:?}",
);
}
}
#[ test ]
fn it13_model_listed_in_help_output()
{
let out = run_cs( &[ ".help" ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
text.contains( ".model" ),
"IT-13: `clp .help` must list `.model`; got:\n{text}",
);
}
#[ test ]
fn ec1_set_absent_get_mode_reads_and_prints()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let claude_dir = dir.path().join( ".claude" );
std::fs::create_dir_all( &claude_dir ).unwrap();
let settings_path = claude_dir.join( "settings.json" );
let original = r#"{"model":"sonnet"}"#;
std::fs::write( &settings_path, original ).unwrap();
let out = run_cs_with_env( &[ ".model" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert_eq!(
text, "model: sonnet\n",
"EC-1: get mode must print `model: sonnet\\n`; got: {text:?}",
);
let after = std::fs::read_to_string( &settings_path )
.expect( "settings.json must still exist after get mode" );
assert_eq!(
after, original,
"EC-1: get mode must not write settings.json; content changed from {original:?} to {after:?}",
);
}
#[ test ]
fn cc_a_set_uppercase_opus_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".model", "set::Opus" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "opus" ),
"CC-A: `set::Opus` must exit 1 and name the valid lowercase `opus` in stderr; got:\n{err}",
);
}
#[ test ]
fn cc_b_format_table_rejected()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".model", "format::table" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 1 );
}
#[ test ]
fn cc_c_malformed_settings_json_get_returns_unset()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let claude_dir = dir.path().join( ".claude" );
std::fs::create_dir_all( &claude_dir ).unwrap();
std::fs::write( claude_dir.join( "settings.json" ), b"{ INVALID JSON garbage" ).unwrap();
let out = run_cs_with_env( &[ ".model" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert_eq!(
text, "model: (unset)\n",
"CC-C: malformed settings.json must yield `model: (unset)\\n`; got: {text:?}",
);
}
#[ test ]
fn cc_d_malformed_settings_json_set_writes_model()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let claude_dir = dir.path().join( ".claude" );
std::fs::create_dir_all( &claude_dir ).unwrap();
std::fs::write( claude_dir.join( "settings.json" ), b"{ INVALID JSON garbage" ).unwrap();
let out = run_cs_with_env( &[ ".model", "set::opus" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(), Some( "claude-opus-4-6" ),
"CC-D: malformed settings.json must be treated as {{}} — model must be written; got: {model:?}\nstderr: {}",
stderr( &out ),
);
}
#[ test ]
fn cc_e_set_default_absent_settings_exits_0()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
assert!(
!dir.path().join( ".claude" ).join( "settings.json" ).exists(),
"CC-E precondition: settings.json must be absent before the run",
);
let out = run_cs_with_env( &[ ".model", "set::default" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
if let Ok( content ) = std::fs::read_to_string( dir.path().join( ".claude" ).join( "settings.json" ) )
{
assert!(
!content.contains( "\"model\"" ),
"CC-E: if settings.json was created by set::default, it must not contain a model key; got: {content}",
);
}
}
#[ test ]
fn cc_f_model_null_value_treated_as_unset()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let claude_dir = dir.path().join( ".claude" );
std::fs::create_dir_all( &claude_dir ).unwrap();
std::fs::write( claude_dir.join( "settings.json" ), r#"{"model":null}"# ).unwrap();
let out = run_cs_with_env( &[ ".model" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert_eq!(
text, "model: (unset)\n",
"CC-F: null model value must be treated as unset; got: {text:?}",
);
}
#[ test ]
fn cc_g_set_mode_stdout_format()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env( &[ ".model", "set::opus" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let text = stdout( &out );
assert_eq!(
text, "model set: opus\n",
"CC-G: set mode stdout must be `model set: opus\\n`; got: {text:?}",
);
}
#[ test ]
fn cc_h_set_overwrites_existing_model()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let claude_dir = dir.path().join( ".claude" );
std::fs::create_dir_all( &claude_dir ).unwrap();
std::fs::write(
claude_dir.join( "settings.json" ),
r#"{"model":"claude-opus-4-6"}"#,
).unwrap();
let out = run_cs_with_env( &[ ".model", "set::haiku" ], &[ ( "HOME", home ) ] );
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(), Some( "claude-haiku-4-5-20251001" ),
"CC-H: set::haiku must overwrite `claude-opus-4-6` with `claude-haiku-4-5-20251001`; got: {model:?}",
);
}