@@ -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,126 @@ describe("config hook", () => {
285289 expect ( config . provider ?. coreinfra ?. name ) . toBe ( "CoreInfra AI Hub" ) ;
286290 expect ( config . provider ?. coreinfra ?. models ) . toBeUndefined ( ) ;
287291 } ) ;
292+
293+ it ( "silently retries anonymously when the hub rejects the key with 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 ( 2 ) ;
343+ expect ( hubCalls [ 0 ] ?. headers ) . toEqual ( { "X-CoreInfra-Api-Key" : "bad-key" } ) ;
344+ expect ( hubCalls [ 1 ] ?. headers ) . toBeUndefined ( ) ;
345+
346+ const models = config . provider ?. coreinfra ?. models ?? { } ;
347+ expect ( models [ "gpt-5.4-nano" ] ) . toBeDefined ( ) ;
348+ expect ( models [ "claude-sonnet-4-20250514" ] ) . toBeDefined ( ) ;
349+
350+ expect ( log ) . not . toHaveBeenCalledWith (
351+ expect . objectContaining ( {
352+ body : expect . objectContaining ( {
353+ level : expect . stringMatching ( / ^ ( w a r n | e r r o r ) $ / ) ,
354+ } ) ,
355+ } ) ,
356+ ) ;
357+ } ) ;
358+
359+ it ( "does not retry when the hub accepts the key" , async ( ) => {
360+ vi . stubEnv ( "COREINFRA_API_KEY" , "good-key" ) ;
361+ const { readFile } = await import ( "node:fs/promises" ) ;
362+ vi . mocked ( readFile ) . mockResolvedValue ( JSON . stringify ( MODELS_DEV_DATA ) ) ;
363+
364+ const hubCalls : Array < { headers ?: Record < string , string > } > = [ ] ;
365+ vi . stubGlobal (
366+ "fetch" ,
367+ vi
368+ . fn ( )
369+ . mockImplementation (
370+ ( url : string , init ?: { headers ?: Record < string , string > } ) => {
371+ if ( url . includes ( "models.dev" ) ) {
372+ return Promise . resolve ( {
373+ ok : true ,
374+ json : ( ) => Promise . resolve ( MODELS_DEV_DATA ) ,
375+ } ) ;
376+ }
377+ hubCalls . push ( { headers : init ?. headers } ) ;
378+ return Promise . resolve ( {
379+ ok : true ,
380+ json : ( ) => Promise . resolve ( hubResponse ( ) ) ,
381+ } ) ;
382+ } ,
383+ ) ,
384+ ) ;
385+
386+ const { input, log } = mockInput ( ) ;
387+ const hooks : Hooks = await plugin ( input ) ;
388+ const config = { provider : { } } as Parameters <
389+ NonNullable < Hooks [ "config" ] >
390+ > [ 0 ] ;
391+
392+ await hooks . config ?.( config ) ;
393+
394+ expect ( hubCalls ) . toHaveLength ( 1 ) ;
395+ expect ( hubCalls [ 0 ] ?. headers ) . toEqual ( {
396+ "X-CoreInfra-Api-Key" : "good-key" ,
397+ } ) ;
398+ expect ( config . provider ?. coreinfra ?. models ?. [ "gpt-5.4-nano" ] ) . toBeDefined ( ) ;
399+ expect ( log ) . not . toHaveBeenCalledWith (
400+ expect . objectContaining ( {
401+ body : expect . objectContaining ( { level : "warn" } ) ,
402+ } ) ,
403+ ) ;
404+ } ) ;
288405} ) ;
289406
290407describe ( "auth hook" , ( ) => {
291408 it ( "returns apiKey from auth loader" , async ( ) => {
292409 await setupFetchMocks ( { hubData : { providers : { } } } ) ;
293- const hooks : Hooks = await plugin ( mockInput ( ) ) ;
410+ const { input } = mockInput ( ) ;
411+ const hooks : Hooks = await plugin ( input ) ;
294412 const getAuth = vi . fn ( ) . mockResolvedValue ( { type : "api" , key : "sk-test" } ) ;
295413 const providerArg = { } as Parameters <
296414 NonNullable < Hooks [ "auth" ] > [ "loader" ]
@@ -301,7 +419,8 @@ describe("auth hook", () => {
301419
302420 it ( "returns empty object when no auth" , async ( ) => {
303421 await setupFetchMocks ( { hubData : { providers : { } } } ) ;
304- const hooks : Hooks = await plugin ( mockInput ( ) ) ;
422+ const { input } = mockInput ( ) ;
423+ const hooks : Hooks = await plugin ( input ) ;
305424 const getAuth = vi . fn ( ) . mockResolvedValue ( null ) ;
306425 const providerArg = { } as Parameters <
307426 NonNullable < Hooks [ "auth" ] > [ "loader" ]
0 commit comments