@@ -178,7 +178,7 @@ fn prepare_codex_config_toml_writes_fresh_config() {
178178 let working_dir = tmp. path ( ) . join ( "workspace/proj" ) ;
179179 fs:: create_dir_all ( & working_dir) . unwrap ( ) ;
180180
181- prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) ) . unwrap ( ) ;
181+ prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) , None ) . unwrap ( ) ;
182182
183183 let canonical = working_dir. canonicalize ( ) . unwrap ( ) ;
184184 let key = canonical. to_string_lossy ( ) . into_owned ( ) ;
@@ -202,7 +202,8 @@ fn prepare_codex_config_toml_preserves_unrelated_keys() {
202202 )
203203 . unwrap ( ) ;
204204
205- prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) ) . unwrap ( ) ;
205+ // Pass `None` for the model id so the helper preserves the user's existing `model = ...`.
206+ prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) , None ) . unwrap ( ) ;
206207
207208 let canonical = working_dir. canonicalize ( ) . unwrap ( ) ;
208209 let key = canonical. to_string_lossy ( ) . into_owned ( ) ;
@@ -225,9 +226,9 @@ fn prepare_codex_config_toml_is_idempotent() {
225226 let working_dir = tmp. path ( ) . join ( "workspace" ) ;
226227 fs:: create_dir_all ( & working_dir) . unwrap ( ) ;
227228
228- prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) ) . unwrap ( ) ;
229+ prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) , None ) . unwrap ( ) ;
229230 let after_first = fs:: read_to_string ( & config_path) . unwrap ( ) ;
230- prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) ) . unwrap ( ) ;
231+ prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) , None ) . unwrap ( ) ;
231232 let after_second = fs:: read_to_string ( & config_path) . unwrap ( ) ;
232233
233234 assert_eq ! ( after_first, after_second) ;
@@ -256,7 +257,7 @@ fn prepare_codex_config_toml_upgrades_untrusted_entry() {
256257 )
257258 . unwrap ( ) ;
258259
259- prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) ) . unwrap ( ) ;
260+ prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) , None ) . unwrap ( ) ;
260261
261262 let cfg = read_codex_config ( & config_path) ;
262263 assert_eq ! (
@@ -275,7 +276,7 @@ fn prepare_codex_config_toml_trusts_multiple_child_repos() {
275276 fs:: create_dir_all ( repo_a. join ( ".git" ) ) . unwrap ( ) ;
276277 fs:: create_dir_all ( repo_b. join ( ".git" ) ) . unwrap ( ) ;
277278
278- prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) ) . unwrap ( ) ;
279+ prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) , None ) . unwrap ( ) ;
279280
280281 let cfg = read_codex_config ( & config_path) ;
281282 let projects = cfg[ "projects" ] . as_table ( ) . unwrap ( ) ;
@@ -303,7 +304,7 @@ fn prepare_codex_config_toml_overwrites_stale_openai_base_url() {
303304 )
304305 . unwrap ( ) ;
305306
306- prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) ) . unwrap ( ) ;
307+ prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) , None ) . unwrap ( ) ;
307308
308309 let cfg = read_codex_config ( & config_path) ;
309310 assert_eq ! ( cfg[ "openai_base_url" ] . as_str( ) , Some ( CODEX_OPENAI_BASE_URL ) ) ;
@@ -327,7 +328,7 @@ fn write_codex_mcp_servers_cli_server() {
327328 } ,
328329 } ,
329330 ) ] ) ;
330- prepare_codex_config_toml ( & config_path, & working_dir, & servers) . unwrap ( ) ;
331+ prepare_codex_config_toml ( & config_path, & working_dir, & servers, None ) . unwrap ( ) ;
331332
332333 let cfg = read_codex_config ( & config_path) ;
333334 let mcp = & cfg[ "mcp_servers" ] [ "my-mcp" ] ;
@@ -358,7 +359,7 @@ fn write_codex_mcp_servers_sse_server() {
358359 } ,
359360 } ,
360361 ) ] ) ;
361- prepare_codex_config_toml ( & config_path, & working_dir, & servers) . unwrap ( ) ;
362+ prepare_codex_config_toml ( & config_path, & working_dir, & servers, None ) . unwrap ( ) ;
362363
363364 let cfg = read_codex_config ( & config_path) ;
364365 let mcp = & cfg[ "mcp_servers" ] [ "remote-mcp" ] ;
@@ -384,7 +385,7 @@ fn write_codex_mcp_servers_cli_server_with_cwd() {
384385 } ,
385386 } ,
386387 ) ] ) ;
387- prepare_codex_config_toml ( & config_path, & working_dir, & servers) . unwrap ( ) ;
388+ prepare_codex_config_toml ( & config_path, & working_dir, & servers, None ) . unwrap ( ) ;
388389
389390 let cfg = read_codex_config ( & config_path) ;
390391 let mcp = & cfg[ "mcp_servers" ] [ "my-mcp" ] ;
@@ -410,13 +411,100 @@ fn write_codex_mcp_servers_cli_server_without_cwd_omits_key() {
410411 } ,
411412 } ,
412413 ) ] ) ;
413- prepare_codex_config_toml ( & config_path, & working_dir, & servers) . unwrap ( ) ;
414+ prepare_codex_config_toml ( & config_path, & working_dir, & servers, None ) . unwrap ( ) ;
414415
415416 let cfg = read_codex_config ( & config_path) ;
416417 let mcp = & cfg[ "mcp_servers" ] [ "my-mcp" ] ;
417418 assert ! ( mcp. get( "cwd" ) . is_none( ) ) ;
418419}
419420
421+ #[ test]
422+ fn prepare_codex_config_toml_writes_model_when_specified ( ) {
423+ // A non-default model id is written to the top-level `model` key so Codex pins it
424+ // for new sessions launched from this `~/.codex/config.toml`. Even for the
425+ // current target model, we stamp a self-referential migration entry so the
426+ // upgrade prompt is suppressed regardless of what the user selected.
427+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
428+ let config_path = tmp. path ( ) . join ( "config.toml" ) ;
429+ let working_dir = tmp. path ( ) . join ( "workspace" ) ;
430+ fs:: create_dir_all ( & working_dir) . unwrap ( ) ;
431+
432+ prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) , Some ( "gpt-5.5" ) )
433+ . unwrap ( ) ;
434+
435+ let cfg = read_codex_config ( & config_path) ;
436+ assert_eq ! ( cfg[ "model" ] . as_str( ) , Some ( "gpt-5.5" ) ) ;
437+ assert_eq ! (
438+ cfg[ "notice" ] [ "model_migrations" ] [ "gpt-5.5" ] . as_str( ) ,
439+ Some ( CODEX_MODEL_MIGRATIONS_TARGET ) ,
440+ ) ;
441+ }
442+
443+ #[ test]
444+ fn prepare_codex_config_toml_writes_model_migration_for_older_model ( ) {
445+ // For an older model id, the migration entry maps it to the current target
446+ // so Codex's "choose a newer model" prompt is suppressed at session launch.
447+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
448+ let config_path = tmp. path ( ) . join ( "config.toml" ) ;
449+ let working_dir = tmp. path ( ) . join ( "workspace" ) ;
450+ fs:: create_dir_all ( & working_dir) . unwrap ( ) ;
451+
452+ prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) , Some ( "gpt-5.2" ) )
453+ . unwrap ( ) ;
454+
455+ let cfg = read_codex_config ( & config_path) ;
456+ assert_eq ! ( cfg[ "model" ] . as_str( ) , Some ( "gpt-5.2" ) ) ;
457+ assert_eq ! (
458+ cfg[ "notice" ] [ "model_migrations" ] [ "gpt-5.2" ] . as_str( ) ,
459+ Some ( CODEX_MODEL_MIGRATIONS_TARGET ) ,
460+ ) ;
461+ }
462+
463+ #[ test]
464+ fn prepare_codex_config_toml_skips_model_for_default_sentinel ( ) {
465+ // The literal "default" sentinel means "let Codex pick its own default model";
466+ // we should NOT write a `model` key (or a migration entry) in that case.
467+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
468+ let config_path = tmp. path ( ) . join ( "config.toml" ) ;
469+ let working_dir = tmp. path ( ) . join ( "workspace" ) ;
470+ fs:: create_dir_all ( & working_dir) . unwrap ( ) ;
471+
472+ prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) , Some ( "default" ) )
473+ . unwrap ( ) ;
474+
475+ let cfg = read_codex_config ( & config_path) ;
476+ assert ! (
477+ cfg. get( "model" ) . is_none( ) ,
478+ "`model` should not be written for the default sentinel"
479+ ) ;
480+ assert ! (
481+ cfg. get( "notice" ) . is_none( ) ,
482+ "`[notice]` table should not be written without a pinned model id"
483+ ) ;
484+ }
485+
486+ #[ test]
487+ fn prepare_codex_config_toml_skips_model_when_none ( ) {
488+ // No model id supplied means the user didn't pick one; we should not write a
489+ // `model` key or any `[notice.model_migrations]` entries.
490+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
491+ let config_path = tmp. path ( ) . join ( "config.toml" ) ;
492+ let working_dir = tmp. path ( ) . join ( "workspace" ) ;
493+ fs:: create_dir_all ( & working_dir) . unwrap ( ) ;
494+
495+ prepare_codex_config_toml ( & config_path, & working_dir, & HashMap :: new ( ) , None ) . unwrap ( ) ;
496+
497+ let cfg = read_codex_config ( & config_path) ;
498+ assert ! (
499+ cfg. get( "model" ) . is_none( ) ,
500+ "`model` should not be written when no override is supplied"
501+ ) ;
502+ assert ! (
503+ cfg. get( "notice" ) . is_none( ) ,
504+ "`[notice]` table should not be written without a pinned model id"
505+ ) ;
506+ }
507+
420508#[ test]
421509fn find_child_git_repos_returns_only_repo_children ( ) {
422510 let tmp = TempDir :: new ( ) . unwrap ( ) ;
0 commit comments