@@ -7,6 +7,10 @@ import {
77 codexProfilesToCatalogEntries ,
88 codexProvidersAsSettings ,
99} from "./codex-providers.js" ;
10+ import {
11+ xaiProfilesToCatalogEntries ,
12+ xaiProvidersAsSettings ,
13+ } from "./xai-providers.js" ;
1014import {
1115 dropOrphanedOAuthEntries ,
1216 mergeOAuthCatalog ,
@@ -241,4 +245,158 @@ describe("CL-6728: OAuth projections do not overwrite hand-named provider entrie
241245 expect ( persisted . providers [ "codex/mine" ] ?. apiKey ) . toBe ( "hand-named-key" ) ;
242246 expect ( JSON . stringify ( persisted ) ) . not . toContain ( "live-token" ) ;
243247 } ) ;
248+
249+ test ( "mergeOAuthCatalog keeps the marked live entry when settings is null and resolved is codex/<slug>" , ( ) => {
250+ const resolvedCodexMine : ResolvedProvider = {
251+ providerName : "codex/mine" ,
252+ baseURL : CODEX_BASE_URL ,
253+ apiKey : "live-token" ,
254+ model : "gpt-5.1-codex-max" ,
255+ } ;
256+ const merged = mergeOAuthCatalog ( null , resolvedCodexMine , [ liveMine ] , [ ] ) ;
257+ const rows = merged . filter ( ( p ) => p . name === "codex/mine" ) ;
258+ expect ( rows ) . toHaveLength ( 1 ) ;
259+ expect ( rows [ 0 ] ?. codexProfile ) . toBe ( "mine" ) ;
260+ expect ( rows [ 0 ] ?. apiKey ) . toBe ( "live-token" ) ;
261+ } ) ;
262+
263+ test ( "mergeOAuthCatalog keeps the marked live entry when settings is empty and resolved is codex/<slug>" , ( ) => {
264+ const resolvedCodexMine : ResolvedProvider = {
265+ providerName : "codex/mine" ,
266+ baseURL : CODEX_BASE_URL ,
267+ apiKey : "live-token" ,
268+ model : "gpt-5.1-codex-max" ,
269+ } ;
270+ const merged = mergeOAuthCatalog (
271+ settingsWith ( { } ) ,
272+ resolvedCodexMine ,
273+ [ liveMine ] ,
274+ [ ] ,
275+ ) ;
276+ const rows = merged . filter ( ( p ) => p . name === "codex/mine" ) ;
277+ expect ( rows ) . toHaveLength ( 1 ) ;
278+ expect ( rows [ 0 ] ?. codexProfile ) . toBe ( "mine" ) ;
279+ expect ( rows [ 0 ] ?. apiKey ) . toBe ( "live-token" ) ;
280+ } ) ;
281+
282+ test ( "persist round-trip from the empty-settings merge contains no live token" , ( ) => {
283+ const resolvedCodexMine : ResolvedProvider = {
284+ providerName : "codex/mine" ,
285+ baseURL : CODEX_BASE_URL ,
286+ apiKey : "live-token" ,
287+ model : "gpt-5.1-codex-max" ,
288+ } ;
289+ const merged = mergeOAuthCatalog ( null , resolvedCodexMine , [ liveMine ] , [ ] ) ;
290+ const persisted = providerCatalogToSettings ( merged , undefined ) ;
291+ expect ( JSON . stringify ( persisted ) ) . not . toContain ( "live-token" ) ;
292+ } ) ;
293+
294+ test ( "mergeOAuthCatalog keeps a hand-named xai/<slug> entry when its profile is live" , ( ) => {
295+ const handNamedXai = ( ) : ProviderSettings => ( {
296+ baseURL : "https://hand-named-xai.example.com/v1" ,
297+ apiKey : "hand-named-xai-key" ,
298+ models : [ "hand-xai-model" ] ,
299+ } ) ;
300+ const merged = mergeOAuthCatalog (
301+ settingsWith ( { "xai/work" : handNamedXai ( ) } ) ,
302+ resolved ,
303+ [ ] ,
304+ [ xaiWork ] ,
305+ ) ;
306+ const rows = merged . filter ( ( p ) => p . name === "xai/work" ) ;
307+ expect ( rows ) . toHaveLength ( 1 ) ;
308+ expect ( rows [ 0 ] ?. apiKey ) . toBe ( "hand-named-xai-key" ) ;
309+ expect ( rows [ 0 ] ?. xaiProfile ) . toBeUndefined ( ) ;
310+ } ) ;
311+
312+ test ( "overlayOAuthProjections keeps a hand-named xai/<slug> API-key entry" , ( ) => {
313+ const overlaid = overlayOAuthProjections (
314+ settingsWith ( {
315+ "xai/work" : {
316+ baseURL : "https://hand-named-xai.example.com/v1" ,
317+ apiKey : "hand-named-xai-key" ,
318+ models : [ "hand-xai-model" ] ,
319+ } ,
320+ } ) ,
321+ xaiProvidersAsSettings ( [ xaiWork ] ) ,
322+ ) ;
323+ expect ( overlaid ?. providers [ "xai/work" ] ?. apiKey ) . toBe ( "hand-named-xai-key" ) ;
324+ } ) ;
325+
326+ test ( "mergeOAuthCatalog keeps a keyless codex/<slug> entry when its profile is live" , ( ) => {
327+ const merged = mergeOAuthCatalog (
328+ settingsWith ( {
329+ "codex/mine" : {
330+ baseURL : "https://hand-named.example.com/v1" ,
331+ keyless : true ,
332+ models : [ "hand-model" ] ,
333+ } ,
334+ } ) ,
335+ resolved ,
336+ [ liveMine ] ,
337+ [ ] ,
338+ ) ;
339+ const rows = merged . filter ( ( p ) => p . name === "codex/mine" ) ;
340+ expect ( rows ) . toHaveLength ( 1 ) ;
341+ expect ( rows [ 0 ] ?. keyless ) . toBe ( true ) ;
342+ expect ( rows [ 0 ] ?. codexProfile ) . toBeUndefined ( ) ;
343+ } ) ;
344+
345+ test ( "runtimeSettingsWithCatalog keeps a keyless codex/<slug> entry" , ( ) => {
346+ const runtime = runtimeSettingsWithCatalog (
347+ settingsWith ( {
348+ "codex/mine" : {
349+ baseURL : "https://hand-named.example.com/v1" ,
350+ keyless : true ,
351+ models : [ "hand-model" ] ,
352+ } ,
353+ } ) ,
354+ liveCatalog ( ) ,
355+ ) ;
356+ expect ( runtime . providers [ "codex/mine" ] ?. keyless ) . toBe ( true ) ;
357+ expect ( runtime . providers [ "codex/mine" ] ?. apiKey ) . toBeUndefined ( ) ;
358+ } ) ;
359+
360+ test ( "overlayOAuthProjections treats a whitespace apiKey as hand-named" , ( ) => {
361+ const overlaid = overlayOAuthProjections (
362+ settingsWith ( {
363+ "codex/mine" : {
364+ baseURL : "https://hand-named.example.com/v1" ,
365+ apiKey : " " ,
366+ models : [ "hand-model" ] ,
367+ } ,
368+ } ) ,
369+ liveProjected ( ) ,
370+ ) ;
371+ expect ( overlaid ?. providers [ "codex/mine" ] ?. apiKey ) . toBe ( " " ) ;
372+ } ) ;
373+
374+ test ( "overlayOAuthProjections treats an empty-string apiKey as a placeholder" , ( ) => {
375+ const overlaid = overlayOAuthProjections (
376+ settingsWith ( {
377+ "codex/mine" : {
378+ baseURL : "https://hand-named.example.com/v1" ,
379+ apiKey : "" ,
380+ models : [ "hand-model" ] ,
381+ } ,
382+ } ) ,
383+ liveProjected ( ) ,
384+ ) ;
385+ expect ( overlaid ?. providers [ "codex/mine" ] ?. apiKey ) . toBe ( "live-token" ) ;
386+ } ) ;
387+
388+ test ( "runtimeSettingsWithCatalog resolves a keyless xai/<slug> entry from the catalog" , ( ) => {
389+ const runtime = runtimeSettingsWithCatalog (
390+ settingsWith ( {
391+ "xai/work" : {
392+ baseURL : "https://hand-named-xai.example.com/v1" ,
393+ keyless : true ,
394+ models : [ "hand-xai-model" ] ,
395+ } ,
396+ } ) ,
397+ xaiProfilesToCatalogEntries ( [ xaiWork ] ) ,
398+ ) ;
399+ expect ( runtime . providers [ "xai/work" ] ?. keyless ) . toBe ( true ) ;
400+ expect ( runtime . providers [ "xai/work" ] ?. apiKey ) . toBeUndefined ( ) ;
401+ } ) ;
244402} ) ;
0 commit comments