@@ -2463,134 +2463,204 @@ describe("legacy model compat migrate", () => {
24632463 ] ) ;
24642464 } ) ;
24652465
2466- it ( "merges colliding model ref map keys instead of dropping per-key tuning " , ( ) => {
2466+ it ( "deep- merges colliding retired model refs and reports only unequal fields " , ( ) => {
24672467 const res = migrateLegacyConfigForTest ( {
24682468 agents : {
24692469 defaults : {
24702470 models : {
2471- "openai/gpt-4o" : { params : { reasoning_effort : "high" } , streaming : false } ,
2472- "openai/gpt-4" : { params : { reasoning_effort : "low" } , alias : "legacy-four" } ,
2471+ "openai/gpt-4o" : {
2472+ params : {
2473+ reasoning : { effort : "high" , budget : 100 } ,
2474+ tags : [ "stable" ] ,
2475+ } ,
2476+ streaming : false ,
2477+ } ,
2478+ "openai/gpt-4" : {
2479+ params : {
2480+ reasoning : { effort : "low" , summary : "auto" } ,
2481+ tags : [ "stable" ] ,
2482+ } ,
2483+ alias : "legacy-four" ,
2484+ } ,
24732485 } ,
24742486 } ,
24752487 } ,
24762488 } ) ;
24772489
24782490 expect ( res . config ?. agents ?. defaults ?. models ) . toEqual ( {
24792491 "openai/gpt-5.5" : {
2480- params : { reasoning_effort : "high" } ,
2492+ params : {
2493+ reasoning : { effort : "high" , budget : 100 , summary : "auto" } ,
2494+ tags : [ "stable" ] ,
2495+ } ,
24812496 streaming : false ,
24822497 alias : "legacy-four" ,
24832498 } ,
24842499 } ) ;
2485- const mergeChanges = res . changes . filter ( ( change ) =>
2486- change . includes ( "config.agents.defaults.models key" ) ,
2487- ) ;
2488- expect (
2489- mergeChanges . some (
2490- ( change ) =>
2491- change . includes ( "Merged" ) &&
2492- change . includes ( '"openai/gpt-4"' ) &&
2493- change . includes ( '"openai/gpt-5.5"' ) &&
2494- change . includes ( "conflicting fields" ) &&
2495- change . includes ( "params.reasoning_effort" ) ,
2496- ) ,
2497- ) . toBe ( true ) ;
2500+ expect ( res . changes . filter ( ( change ) => change . includes ( "Merged" ) ) ) . toEqual ( [
2501+ 'Merged config.agents.defaults.models key "openai/gpt-4" into "openai/gpt-5.5"; kept existing values for conflicting fields: params.reasoning.effort.' ,
2502+ ] ) ;
24982503 } ) ;
24992504
2500- it ( "merges when the canonical key follows the retired key " , ( ) => {
2505+ it ( "does not report conflicts between model refs that normalize to the same value " , ( ) => {
25012506 const res = migrateLegacyConfigForTest ( {
25022507 agents : {
25032508 defaults : {
25042509 models : {
2505- "openai/gpt-4 " : { alias : "legacy-four" } ,
2506- "openai/gpt-5.5 " : { streaming : false } ,
2510+ "openai/gpt-5.5 " : { params : { fallbacks : [ "openai/gpt-4" ] } } ,
2511+ "openai/gpt-4o " : { params : { fallbacks : [ "openai/gpt-5.5" ] } } ,
25072512 } ,
25082513 } ,
25092514 } ,
25102515 } ) ;
25112516
25122517 expect ( res . config ?. agents ?. defaults ?. models ) . toEqual ( {
2513- "openai/gpt-5.5" : {
2514- alias : "legacy-four" ,
2515- streaming : false ,
2516- } ,
2518+ "openai/gpt-5.5" : { params : { fallbacks : [ "openai/gpt-5.5" ] } } ,
25172519 } ) ;
2520+ expect ( res . changes . filter ( ( change ) => change . includes ( "Merged" ) ) ) . toEqual ( [
2521+ 'Merged config.agents.defaults.models key "openai/gpt-4o" into "openai/gpt-5.5".' ,
2522+ ] ) ;
25182523 } ) ;
25192524
2520- it ( "keeps canonical values when canonical and retired keys collide" , ( ) => {
2525+ it . each ( [ "first" , "last" ] as const ) (
2526+ "keeps canonical values when the canonical key appears %s" ,
2527+ ( canonicalPosition ) => {
2528+ const canonical = [
2529+ "openai/gpt-5.5" ,
2530+ {
2531+ alias : "canonical-five" ,
2532+ params : { reasoning : { effort : "medium" , canonicalOnly : true } } ,
2533+ agentRuntime : { id : "codex" } ,
2534+ } ,
2535+ ] as const ;
2536+ const retired = [
2537+ [
2538+ "openai/gpt-4" ,
2539+ {
2540+ alias : "legacy-four" ,
2541+ params : { reasoning : { effort : "low" , fourOnly : true } } ,
2542+ } ,
2543+ ] ,
2544+ [
2545+ "openai/gpt-4o" ,
2546+ {
2547+ streaming : false ,
2548+ params : { reasoning : { effort : "high" , fourOOnly : true } } ,
2549+ } ,
2550+ ] ,
2551+ ] as const ;
2552+ const entries =
2553+ canonicalPosition === "first" ? [ canonical , ...retired ] : [ ...retired , canonical ] ;
2554+ const res = migrateLegacyConfigForTest ( {
2555+ agents : { defaults : { models : Object . fromEntries ( entries ) } } ,
2556+ } ) ;
2557+
2558+ expect ( res . config ?. agents ?. defaults ?. models ) . toEqual ( {
2559+ "openai/gpt-5.5" : {
2560+ alias : "canonical-five" ,
2561+ params : {
2562+ reasoning : {
2563+ effort : "medium" ,
2564+ canonicalOnly : true ,
2565+ fourOnly : true ,
2566+ fourOOnly : true ,
2567+ } ,
2568+ } ,
2569+ agentRuntime : { id : "codex" } ,
2570+ streaming : false ,
2571+ } ,
2572+ } ) ;
2573+ expect ( res . changes . filter ( ( change ) => change . includes ( "Merged" ) ) ) . toEqual ( [
2574+ 'Merged config.agents.defaults.models key "openai/gpt-4" into "openai/gpt-5.5"; kept existing values for conflicting fields: alias, params.reasoning.effort.' ,
2575+ 'Merged config.agents.defaults.models key "openai/gpt-4o" into "openai/gpt-5.5"; kept existing values for conflicting fields: params.reasoning.effort.' ,
2576+ ] ) ;
2577+ } ,
2578+ ) ;
2579+
2580+ it ( "merges colliding model refs in per-agent model maps" , ( ) => {
25212581 const res = migrateLegacyConfigForTest ( {
25222582 agents : {
2523- defaults : {
2524- models : {
2525- "openai/gpt-4" : { alias : "legacy-four" , streaming : true } ,
2526- "openai/gpt-5.5" : { alias : "current-five" } ,
2583+ list : [
2584+ {
2585+ id : "research" ,
2586+ models : {
2587+ "openai/gpt-4" : { alias : "legacy-four" } ,
2588+ "openai/gpt-4o" : { streaming : false } ,
2589+ } ,
25272590 } ,
2528- } ,
2591+ ] ,
25292592 } ,
25302593 } ) ;
25312594
2532- expect ( res . config ?. agents ?. defaults ?. models ) . toEqual ( {
2533- "openai/gpt-5.5" : {
2534- alias : "current-five" ,
2535- streaming : true ,
2536- } ,
2595+ expect ( res . config ?. agents ?. list ?. [ 0 ] ?. models ) . toEqual ( {
2596+ "openai/gpt-5.5" : { alias : "legacy-four" , streaming : false } ,
25372597 } ) ;
2598+ expect ( res . changes . filter ( ( change ) => change . includes ( "Merged" ) ) ) . toEqual ( [
2599+ 'Merged config.agents.list.0.models key "openai/gpt-4o" into "openai/gpt-5.5".' ,
2600+ ] ) ;
25382601 } ) ;
25392602
2540- it ( "does not pollute prototypes when merging colliding model ref map keys" , ( ) => {
2541- const polluted = JSON . parse (
2542- '{"agents":{"defaults":{"models":{"openai/gpt-4o":{"streaming":false},"openai/gpt-4":{"__proto__":{"polluted":true},"alias":"legacy-four"}}}}}' ,
2543- ) ;
2544- const res = migrateLegacyConfigForTest ( polluted ) ;
2545-
2546- expect ( ( { } as Record < string , unknown > ) . polluted ) . toBeUndefined ( ) ;
2603+ it . each ( [
2604+ {
2605+ side : "canonical" ,
2606+ raw : '{"agents":{"defaults":{"models":{"openai/gpt-5.5":{"__proto__":{"polluted":true},"alias":"canonical-five","params":{"nested":{"__proto__":{"polluted":true},"model":"openai/gpt-4o"}}},"openai/gpt-4":{"streaming":false}}}}}' ,
2607+ } ,
2608+ {
2609+ side : "retired" ,
2610+ raw : '{"agents":{"defaults":{"models":{"openai/gpt-5.5":{"alias":"canonical-five"},"openai/gpt-4":{"__proto__":{"polluted":true},"streaming":false,"params":{"nested":{"__proto__":{"polluted":true},"model":"openai/gpt-4o"}}}}}}}' ,
2611+ } ,
2612+ ] ) ( "filters blocked keys recursively from the $side collision side" , ( { raw } ) => {
2613+ const res = migrateLegacyConfigForTest ( JSON . parse ( raw ) ) ;
25472614 const merged = res . config ?. agents ?. defaults ?. models ?. [ "openai/gpt-5.5" ] as Record <
25482615 string ,
25492616 unknown
25502617 > ;
2551- expect ( Object . getPrototypeOf ( merged ) ) . toBe ( Object . prototype ) ;
2552- expect ( merged . polluted ) . toBeUndefined ( ) ;
2553- expect ( merged ) . toEqual ( { streaming : false , alias : "legacy-four" } ) ;
2554- } ) ;
2555-
2556- it ( "does not pollute prototypes when the first colliding entry carries a blocked key" , ( ) => {
2557- const polluted = JSON . parse (
2558- '{"agents":{"defaults":{"models":{"openai/gpt-4o":{"__proto__":{"polluted":true},"fallback":["openai/gpt-4o"],"streaming":false},"openai/gpt-4":{"alias":"legacy-four"}}}}}' ,
2559- ) ;
2560- const res = migrateLegacyConfigForTest ( polluted ) ;
2618+ const params = merged . params as Record < string , unknown > ;
2619+ const nested = params . nested as Record < string , unknown > ;
25612620
2621+ for ( const record of [ merged , params , nested ] ) {
2622+ expect ( Object . getOwnPropertyNames ( record ) ) . not . toContain ( "__proto__" ) ;
2623+ expect ( Object . getPrototypeOf ( record ) ) . toBe ( Object . prototype ) ;
2624+ expect ( record . polluted ) . toBeUndefined ( ) ;
2625+ }
25622626 expect ( ( { } as Record < string , unknown > ) . polluted ) . toBeUndefined ( ) ;
2563- const merged = res . config ?. agents ?. defaults ?. models ?. [ "openai/gpt-5.5" ] as Record <
2564- string ,
2565- unknown
2566- > ;
2567- expect ( Object . getOwnPropertyNames ( merged ) ) . not . toContain ( "__proto__" ) ;
2568- expect ( Object . getPrototypeOf ( merged ) ) . toBe ( Object . prototype ) ;
2569- expect ( merged . polluted ) . toBeUndefined ( ) ;
25702627 expect ( merged ) . toEqual ( {
2571- fallback : [ "openai/gpt-5.5" ] ,
2628+ alias : "canonical-five" ,
2629+ params : { nested : { model : "openai/gpt-5.5" } } ,
25722630 streaming : false ,
2573- alias : "legacy-four" ,
25742631 } ) ;
25752632 } ) ;
25762633
2577- it ( "names the retired source key when the canonical key follows the retired key" , ( ) => {
2634+ it ( "does not invoke prototype setters when copying the rewritten config root" , ( ) => {
2635+ const raw = JSON . parse (
2636+ '{"__proto__":{"polluted":true},"agents":{"defaults":{"model":"openai/gpt-4"}}}' ,
2637+ ) as Record < string , unknown > ;
2638+ const res = migrateLegacyConfigForTest ( raw ) ;
2639+ const config = res . config as unknown as Record < string , unknown > ;
2640+
2641+ expect ( Object . getPrototypeOf ( config ) ) . toBe ( Object . prototype ) ;
2642+ expect ( Object . hasOwn ( config , "__proto__" ) ) . toBe ( true ) ;
2643+ expect ( config . polluted ) . toBeUndefined ( ) ;
2644+ expect ( res . config ?. agents ?. defaults ?. model ) . toBe ( "openai/gpt-5.5" ) ;
2645+ } ) ;
2646+
2647+ it ( "reports malformed scalar collisions without claiming equal values conflict" , ( ) => {
25782648 const res = migrateLegacyConfigForTest ( {
25792649 agents : {
25802650 defaults : {
25812651 models : {
2582- "openai/gpt-4" : { alias : "legacy-four" } ,
2583- "openai/gpt-5.5" : { streaming : false } ,
2652+ "openai/gpt-5.5" : false ,
2653+ "openai/gpt-4" : true ,
2654+ "openai/gpt-4o" : false ,
25842655 } ,
25852656 } ,
25862657 } ,
25872658 } ) ;
25882659
2589- const mergeChanges = res . changes . filter (
2590- ( change ) => change . includes ( "config.agents.defaults.models key" ) && change . includes ( "Merged" ) ,
2591- ) ;
2592- expect ( mergeChanges ) . toEqual ( [
2593- 'Merged config.agents.defaults.models key "openai/gpt-4" into "openai/gpt-5.5".' ,
2660+ expect ( res . config ?. agents ?. defaults ?. models ?. [ "openai/gpt-5.5" ] ) . toBe ( false ) ;
2661+ expect ( res . changes . filter ( ( change ) => change . includes ( "Merged" ) ) ) . toEqual ( [
2662+ 'Merged config.agents.defaults.models key "openai/gpt-4" into "openai/gpt-5.5"; kept existing values for conflicting fields: value.' ,
2663+ 'Merged config.agents.defaults.models key "openai/gpt-4o" into "openai/gpt-5.5".' ,
25942664 ] ) ;
25952665 } ) ;
25962666
0 commit comments