use crate::cli_runner::{
run_cs, run_cs_with_env,
stdout, stderr, assert_exit,
write_credentials, write_account,
write_settings_json, write_account_settings_json,
FAR_FUTURE_MS,
};
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_set_model_opus_writes_full_id()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".account.use", "name::alice@example.com", "set_model::opus", "touch::0" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(),
Some( "claude-opus-4-6" ),
"set_model::opus must write `claude-opus-4-6` to settings.json, got: {model:?}\nstdout: {}\nstderr: {}",
stdout( &out ), stderr( &out ),
);
}
#[ test ]
fn ft02_set_model_sonnet_writes_full_id()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".account.use", "name::alice@example.com", "set_model::sonnet", "touch::0" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(),
Some( "claude-sonnet-4-6" ),
"set_model::sonnet must write `claude-sonnet-4-6` to settings.json, got: {model:?}",
);
}
#[ test ]
fn ft03_set_model_haiku_writes_full_id()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".account.use", "name::alice@example.com", "set_model::haiku", "touch::0" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(),
Some( "claude-haiku-4-5-20251001" ),
"set_model::haiku must write `claude-haiku-4-5-20251001` to settings.json, got: {model:?}",
);
}
#[ test ]
fn ft04_set_model_default_removes_key_preserves_others()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
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(
&[ ".account.use", "name::alice@example.com", "set_model::default", "touch::0" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let content = std::fs::read_to_string(
dir.path().join( ".claude" ).join( "settings.json" ),
).expect( "settings.json must exist after set_model::default" );
assert!(
!content.contains( "\"model\"" ),
"settings.json must not contain `model` key after set_model::default, got: {content}",
);
assert!(
content.contains( "\"theme\"" ),
"settings.json must preserve `theme` key after set_model::default, got: {content}",
);
}
#[ test ]
fn ft05_explicit_set_model_wins_over_switch_restore()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
write_account_settings_json( dir.path(), "alice@example.com", "claude-opus-4-6" );
let out = run_cs_with_env(
&[ ".account.use", "name::alice@example.com", "set_model::sonnet", "touch::0" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(),
Some( "claude-sonnet-4-6" ),
"explicit set_model::sonnet must win over switch_account model restore (opus), got: {model:?}",
);
}
#[ test ]
fn ft06_trace_line_emitted_with_set_model()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".account.use", "name::alice@example.com", "set_model::opus", "trace::1", "touch::0" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let err = stderr( &out );
assert!(
err.contains( "[trace] account.use" ),
"trace::1 + set_model::opus must emit `[trace] account.use` line to stderr, got:\n{err}",
);
assert!(
err.contains( "set_model: opus" ),
"trace line must contain `set_model: opus`, got stderr:\n{err}",
);
}
#[ test ]
fn ft07_set_model_bad_value_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env(
&[ ".usage", "set_model::bad" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "opus" ) && err.contains( "sonnet" )
&& err.contains( "haiku" ) && err.contains( "default" ),
"stderr must name all four valid set_model:: values; got:\n{err}",
);
}
#[ test ]
fn ft08_set_model_appears_in_help_output()
{
let use_out = run_cs( &[ ".account.use.help" ] );
assert_exit( &use_out, 0 );
let use_text = stdout( &use_out );
assert!(
use_text.contains( "set_model" ),
"`.account.use.help` must list `set_model` param, got:\n{use_text}",
);
let usage_out = run_cs( &[ ".usage.help" ] );
assert_exit( &usage_out, 0 );
let usage_text = stdout( &usage_out );
assert!(
usage_text.contains( "set_model" ),
"`.usage.help` must list `set_model` param, got:\n{usage_text}",
);
}
#[ test ]
fn ft09_set_model_no_set_model_key_in_json()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "set_model::opus", "format::json" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let text = stdout( &out );
assert!(
!text.contains( "\"set_model\"" ),
"format::json output must not contain a `set_model` key, got:\n{text}",
);
let parsed : serde_json::Value = serde_json::from_str( text.trim() )
.unwrap_or_else( |e| panic!( "output must be valid JSON: {e}\ngot:\n{text}" ) );
assert!( parsed.is_array(), "JSON output must be an array, got:\n{text}" );
}
#[ test ]
fn ec1_set_model_opus_accepted_no_unrecognized_error()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".account.use", "name::alice@example.com", "set_model::opus", "touch::0" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let err = stderr( &out );
assert!(
!err.contains( "unrecognized" ),
"set_model::opus must not produce `unrecognized` error; got stderr:\n{err}",
);
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(),
Some( "claude-opus-4-6" ),
"set_model::opus must write `claude-opus-4-6`, got: {model:?}",
);
}
#[ test ]
fn ec2_set_model_sonnet_accepted_writes_full_id()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".account.use", "name::alice@example.com", "set_model::sonnet", "touch::0" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(),
Some( "claude-sonnet-4-6" ),
"set_model::sonnet must write `claude-sonnet-4-6`, got: {model:?}",
);
}
#[ test ]
fn ec3_set_model_haiku_accepted_writes_full_id()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".account.use", "name::alice@example.com", "set_model::haiku", "touch::0" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(),
Some( "claude-haiku-4-5-20251001" ),
"set_model::haiku must write `claude-haiku-4-5-20251001`, got: {model:?}",
);
}
#[ test ]
fn ec4_set_model_default_accepted_removes_key()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
write_settings_json( dir.path(), "claude-opus-4-6" );
let out = run_cs_with_env(
&[ ".account.use", "name::alice@example.com", "set_model::default", "touch::0" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let content = std::fs::read_to_string(
dir.path().join( ".claude" ).join( "settings.json" ),
).expect( "settings.json must exist after set_model::default" );
assert!(
!content.contains( "\"model\"" ),
"settings.json must not contain `model` key after set_model::default, got: {content}",
);
}
#[ test ]
fn ec5_set_model_bad_exits_1_all_valid_values_named()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env(
&[ ".usage", "set_model::bad" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "opus" ) && err.contains( "sonnet" )
&& err.contains( "haiku" ) && err.contains( "default" ),
"stderr must name all four valid set_model:: values; got:\n{err}",
);
}
#[ test ]
fn ec6_account_use_set_model_wins_over_switch_restore()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
write_account_settings_json( dir.path(), "alice@example.com", "claude-opus-4-6" );
let out = run_cs_with_env(
&[ ".account.use", "name::alice@example.com", "set_model::sonnet", "touch::0" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(),
Some( "claude-sonnet-4-6" ),
"set_model::sonnet must win over switch_account model restore (opus), got: {model:?}",
);
}
#[ test ]
fn ec7_usage_set_model_writes_to_settings()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
write_settings_json( dir.path(), "claude-opus-4-6" );
let out = run_cs_with_env(
&[ ".usage", "set_model::sonnet" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(),
Some( "claude-sonnet-4-6" ),
"`.usage set_model::sonnet` must write `claude-sonnet-4-6` to settings.json, got: {model:?}",
);
}
#[ test ]
fn cc1_account_use_set_model_bad_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".account.use", "name::alice@example.com", "set_model::bad" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "opus" ) && err.contains( "sonnet" )
&& err.contains( "haiku" ) && err.contains( "default" ),
"`.account.use set_model::bad` must name all four valid values in stderr; got:\n{err}",
);
}
#[ test ]
fn cc2_account_use_dry_run_does_not_write_settings()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_credentials( dir.path(), "max", "default", FAR_FUTURE_MS );
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".account.use", "name::alice@example.com", "set_model::opus", "dry::1" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert!(
model.is_none(),
"dry-run must not write settings.json; got model={model:?}\nstdout: {}",
stdout( &out ),
);
let out_text = stdout( &out );
assert!(
out_text.contains( "dry-run" ),
"stdout must contain `dry-run`; got: {out_text}",
);
}
#[ test ]
fn cc3_usage_set_model_format_json_also_writes_settings()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
let out = run_cs_with_env(
&[ ".usage", "set_model::opus", "format::json" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(),
Some( "claude-opus-4-6" ),
"`.usage set_model::opus format::json` must write `claude-opus-4-6` to settings.json even when ~/.claude/ was absent; got: {model:?}\nstderr: {}",
stderr( &out ),
);
}
#[ test ]
fn cc4_set_model_uppercase_exits_1()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
let out = run_cs_with_env(
&[ ".usage", "set_model::Opus" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 1 );
let err = stderr( &out );
assert!(
err.contains( "opus" ),
"`set_model::Opus` must exit 1 and mention the valid value `opus` in stderr; got:\n{err}",
);
}
#[ test ]
fn cc6_usage_set_model_default_removes_key()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
write_settings_json( dir.path(), "claude-opus-4-6" );
let out = run_cs_with_env(
&[ ".usage", "set_model::default" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let content = std::fs::read_to_string(
dir.path().join( ".claude" ).join( "settings.json" ),
).expect( "settings.json must exist after set_model::default" );
assert!(
!content.contains( "\"model\"" ),
"`.usage set_model::default` must remove the `model` key; got: {content}",
);
}
#[ test ]
fn cc7_usage_set_model_haiku_overwrites_existing_opus()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
write_settings_json( dir.path(), "claude-opus-4-6" );
let out = run_cs_with_env(
&[ ".usage", "set_model::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" ),
"`.usage set_model::haiku` must overwrite `claude-opus-4-6` with `claude-haiku-4-5-20251001`; got: {model:?}",
);
}
#[ doc = "bug_reproducer(BUG-258)" ]
#[ test ]
fn cc8_usage_set_model_creates_dir_when_absent()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
assert!(
!dir.path().join( ".claude" ).exists(),
"test precondition: ~/.claude/ must not exist before the run",
);
let out = run_cs_with_env(
&[ ".usage", "set_model::opus" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
assert!(
dir.path().join( ".claude" ).exists(),
"`~/.claude/` must be created by `set_session_model` when parent dir was absent",
);
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(),
Some( "claude-opus-4-6" ),
"`.usage set_model::opus` must write settings.json even when `~/.claude/` was absent; got: {model:?}\nstderr: {}",
stderr( &out ),
);
}
#[ test ]
fn cc9_set_model_recovers_from_malformed_settings_json()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
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(
&[ ".usage", "set_model::opus" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let model = read_settings_model( dir.path() );
assert_eq!(
model.as_deref(),
Some( "claude-opus-4-6" ),
"malformed `settings.json` must be treated as `{{}}` — `set_model::opus` must still \
write the model key; got: {model:?}\nstderr: {}",
stderr( &out ),
);
}
#[ test ]
fn cc13_usage_set_model_no_trace_line_emitted()
{
let dir = TempDir::new().unwrap();
let home = dir.path().to_str().unwrap();
write_account( dir.path(), "alice@example.com", "max", "default", FAR_FUTURE_MS, false );
write_settings_json( dir.path(), "claude-sonnet-4-6" );
let out = run_cs_with_env(
&[ ".usage", "set_model::opus", "trace::1" ],
&[ ( "HOME", home ) ],
);
assert_exit( &out, 0 );
let err = stderr( &out );
assert!(
!err.contains( "set_model: opus" ),
"`.usage set_model::opus trace::1` must NOT emit a `set_model: opus` trace line; got stderr:\n{err}",
);
}