@@ -9,9 +9,9 @@ vi.mock("node:fs/promises", () => ({
99 readFile : vi . fn ( ) ,
1010} ) ) ;
1111
12- function mockInput ( ) : PluginInput {
12+ function mockInput ( ) {
1313 const log = vi . fn ( ) . mockResolvedValue ( undefined ) ;
14- return {
14+ const input = {
1515 client : {
1616 app : { log } ,
1717 } ,
@@ -20,7 +20,8 @@ function mockInput(): PluginInput {
2020 worktree : "/tmp" ,
2121 serverUrl : new URL ( "http://localhost:3000" ) ,
2222 $ : { } as PluginInput [ "$" ] ,
23- } ;
23+ } as PluginInput ;
24+ return { input, log } ;
2425}
2526
2627const MODELS_DEV_DATA = {
@@ -166,7 +167,8 @@ describe("config hook", () => {
166167 it ( "populates config with full capabilities from models.dev" , async ( ) => {
167168 await setupFetchMocks ( ) ;
168169
169- const hooks : Hooks = await plugin ( mockInput ( ) ) ;
170+ const { input } = mockInput ( ) ;
171+ const hooks : Hooks = await plugin ( input ) ;
170172 const config = { provider : { } } as Parameters <
171173 NonNullable < Hooks [ "config" ] >
172174 > [ 0 ] ;
@@ -255,7 +257,8 @@ describe("config hook", () => {
255257 it ( "uses defaults when models.dev has no matching model" , async ( ) => {
256258 await setupFetchMocks ( { modelsDevData : { } } ) ;
257259
258- const hooks : Hooks = await plugin ( mockInput ( ) ) ;
260+ const { input } = mockInput ( ) ;
261+ const hooks : Hooks = await plugin ( input ) ;
259262 const config = { provider : { } } as Parameters <
260263 NonNullable < Hooks [ "config" ] >
261264 > [ 0 ] ;
@@ -275,7 +278,8 @@ describe("config hook", () => {
275278 it ( "leaves models empty on fetchModels failure" , async ( ) => {
276279 await setupFetchMocks ( { fetchError : new Error ( "network down" ) } ) ;
277280
278- const hooks : Hooks = await plugin ( mockInput ( ) ) ;
281+ const { input } = mockInput ( ) ;
282+ const hooks : Hooks = await plugin ( input ) ;
279283 const config = { provider : { } } as Parameters <
280284 NonNullable < Hooks [ "config" ] >
281285 > [ 0 ] ;
@@ -285,12 +289,127 @@ describe("config hook", () => {
285289 expect ( config . provider ?. coreinfra ?. name ) . toBe ( "CoreInfra AI Hub" ) ;
286290 expect ( config . provider ?. coreinfra ?. models ) . toBeUndefined ( ) ;
287291 } ) ;
292+
293+ it ( "logs the failure without retrying when the hub returns 401" , async ( ) => {
294+ vi . stubEnv ( "COREINFRA_API_KEY" , "bad-key" ) ;
295+ const { readFile } = await import ( "node:fs/promises" ) ;
296+ vi . mocked ( readFile ) . mockResolvedValue ( JSON . stringify ( MODELS_DEV_DATA ) ) ;
297+
298+ const hubCalls : Array < { headers ?: Record < string , string > } > = [ ] ;
299+ vi . stubGlobal (
300+ "fetch" ,
301+ vi . fn ( ) . mockImplementation (
302+ (
303+ url : string ,
304+ init ?: { headers ?: Record < string , string > } ,
305+ ) : Promise < {
306+ ok : boolean ;
307+ status ?: number ;
308+ statusText ?: string ;
309+ json : ( ) => Promise < object > ;
310+ } > => {
311+ if ( url . includes ( "models.dev" ) ) {
312+ return Promise . resolve ( {
313+ ok : true ,
314+ json : ( ) => Promise . resolve ( MODELS_DEV_DATA ) ,
315+ } ) ;
316+ }
317+ hubCalls . push ( { headers : init ?. headers } ) ;
318+ if ( hubCalls . length === 1 ) {
319+ return Promise . resolve ( {
320+ ok : false ,
321+ status : 401 ,
322+ statusText : "Unauthorized" ,
323+ json : ( ) => Promise . resolve ( { } ) ,
324+ } ) ;
325+ }
326+ return Promise . resolve ( {
327+ ok : true ,
328+ json : ( ) => Promise . resolve ( hubResponse ( ) ) ,
329+ } ) ;
330+ } ,
331+ ) ,
332+ ) ;
333+
334+ const { input, log } = mockInput ( ) ;
335+ const hooks : Hooks = await plugin ( input ) ;
336+ const config = { provider : { } } as Parameters <
337+ NonNullable < Hooks [ "config" ] >
338+ > [ 0 ] ;
339+
340+ await hooks . config ?.( config ) ;
341+
342+ expect ( hubCalls ) . toHaveLength ( 1 ) ;
343+ expect ( hubCalls [ 0 ] ?. headers ) . toEqual ( { "X-CoreInfra-Api-Key" : "bad-key" } ) ;
344+
345+ expect ( config . provider ?. coreinfra ?. models ) . toBeUndefined ( ) ;
346+
347+ expect ( log ) . toHaveBeenCalledWith (
348+ expect . objectContaining ( {
349+ body : expect . objectContaining ( {
350+ level : "error" ,
351+ message : "fetchModels failed" ,
352+ extra : {
353+ error : "Failed to fetch CoreInfra prices: 401 Unauthorized" ,
354+ } ,
355+ } ) ,
356+ } ) ,
357+ ) ;
358+ } ) ;
359+
360+ it ( "does not retry when the hub accepts the key" , async ( ) => {
361+ vi . stubEnv ( "COREINFRA_API_KEY" , "good-key" ) ;
362+ const { readFile } = await import ( "node:fs/promises" ) ;
363+ vi . mocked ( readFile ) . mockResolvedValue ( JSON . stringify ( MODELS_DEV_DATA ) ) ;
364+
365+ const hubCalls : Array < { headers ?: Record < string , string > } > = [ ] ;
366+ vi . stubGlobal (
367+ "fetch" ,
368+ vi
369+ . fn ( )
370+ . mockImplementation (
371+ ( url : string , init ?: { headers ?: Record < string , string > } ) => {
372+ if ( url . includes ( "models.dev" ) ) {
373+ return Promise . resolve ( {
374+ ok : true ,
375+ json : ( ) => Promise . resolve ( MODELS_DEV_DATA ) ,
376+ } ) ;
377+ }
378+ hubCalls . push ( { headers : init ?. headers } ) ;
379+ return Promise . resolve ( {
380+ ok : true ,
381+ json : ( ) => Promise . resolve ( hubResponse ( ) ) ,
382+ } ) ;
383+ } ,
384+ ) ,
385+ ) ;
386+
387+ const { input, log } = mockInput ( ) ;
388+ const hooks : Hooks = await plugin ( input ) ;
389+ const config = { provider : { } } as Parameters <
390+ NonNullable < Hooks [ "config" ] >
391+ > [ 0 ] ;
392+
393+ await hooks . config ?.( config ) ;
394+
395+ expect ( hubCalls ) . toHaveLength ( 1 ) ;
396+ expect ( hubCalls [ 0 ] ?. headers ) . toEqual ( {
397+ "X-CoreInfra-Api-Key" : "good-key" ,
398+ } ) ;
399+ expect ( config . provider ?. coreinfra ?. models ?. [ "gpt-5.4-nano" ] ) . toBeDefined ( ) ;
400+ expect ( log ) . not . toHaveBeenCalledWith (
401+ expect . objectContaining ( {
402+ body : expect . objectContaining ( { level : "warn" } ) ,
403+ } ) ,
404+ ) ;
405+ } ) ;
288406} ) ;
289407
290408describe ( "auth hook" , ( ) => {
291409 it ( "returns apiKey from auth loader" , async ( ) => {
292410 await setupFetchMocks ( { hubData : { providers : { } } } ) ;
293- const hooks : Hooks = await plugin ( mockInput ( ) ) ;
411+ const { input } = mockInput ( ) ;
412+ const hooks : Hooks = await plugin ( input ) ;
294413 const getAuth = vi . fn ( ) . mockResolvedValue ( { type : "api" , key : "sk-test" } ) ;
295414 const providerArg = { } as Parameters <
296415 NonNullable < Hooks [ "auth" ] > [ "loader" ]
@@ -301,7 +420,8 @@ describe("auth hook", () => {
301420
302421 it ( "returns empty object when no auth" , async ( ) => {
303422 await setupFetchMocks ( { hubData : { providers : { } } } ) ;
304- const hooks : Hooks = await plugin ( mockInput ( ) ) ;
423+ const { input } = mockInput ( ) ;
424+ const hooks : Hooks = await plugin ( input ) ;
305425 const getAuth = vi . fn ( ) . mockResolvedValue ( null ) ;
306426 const providerArg = { } as Parameters <
307427 NonNullable < Hooks [ "auth" ] > [ "loader" ]
0 commit comments