@@ -320,6 +320,75 @@ describe("CodeIndexConfigManager", () => {
320320 } )
321321 } )
322322
323+ describe ( "empty/missing API key handling" , ( ) => {
324+ it ( "should not require restart when API keys are consistently empty" , async ( ) => {
325+ // Initial state with no API keys (undefined from secrets)
326+ mockContextProxy . getGlobalState . mockReturnValue ( {
327+ codebaseIndexEnabled : true ,
328+ codebaseIndexQdrantUrl : "http://qdrant.local" ,
329+ codebaseIndexEmbedderProvider : "openai" ,
330+ codebaseIndexEmbedderModelId : "text-embedding-3-small" ,
331+ } )
332+ mockContextProxy . getSecret . mockReturnValue ( undefined )
333+
334+ await configManager . loadConfiguration ( )
335+
336+ // Change an unrelated setting while keeping API keys empty
337+ mockContextProxy . getGlobalState . mockReturnValue ( {
338+ codebaseIndexEnabled : true ,
339+ codebaseIndexQdrantUrl : "http://qdrant.local" ,
340+ codebaseIndexEmbedderProvider : "openai" ,
341+ codebaseIndexEmbedderModelId : "text-embedding-3-small" ,
342+ codebaseIndexSearchMinScore : 0.5 , // Changed unrelated setting
343+ } )
344+
345+ const result = await configManager . loadConfiguration ( )
346+ // Should NOT require restart since API keys are consistently empty
347+ expect ( result . requiresRestart ) . toBe ( false )
348+ } )
349+
350+ it ( "should not require restart when API keys transition from undefined to empty string" , async ( ) => {
351+ // Initial state with undefined API keys
352+ mockContextProxy . getGlobalState . mockReturnValue ( {
353+ codebaseIndexEnabled : false , // Start disabled to avoid restart due to enable+configure
354+ codebaseIndexQdrantUrl : "http://qdrant.local" ,
355+ codebaseIndexEmbedderProvider : "openai" ,
356+ } )
357+ mockContextProxy . getSecret . mockReturnValue ( undefined )
358+
359+ await configManager . loadConfiguration ( )
360+
361+ // Change to empty string API keys (simulating what happens when secrets return "")
362+ mockContextProxy . getSecret . mockReturnValue ( "" )
363+
364+ const result = await configManager . loadConfiguration ( )
365+ // Should NOT require restart since undefined and "" are both "empty"
366+ expect ( result . requiresRestart ) . toBe ( false )
367+ } )
368+
369+ it ( "should require restart when API key actually changes from empty to non-empty" , async ( ) => {
370+ // Initial state with empty API key
371+ mockContextProxy . getGlobalState . mockReturnValue ( {
372+ codebaseIndexEnabled : true ,
373+ codebaseIndexQdrantUrl : "http://qdrant.local" ,
374+ codebaseIndexEmbedderProvider : "openai" ,
375+ } )
376+ mockContextProxy . getSecret . mockReturnValue ( "" )
377+
378+ await configManager . loadConfiguration ( )
379+
380+ // Add actual API key
381+ mockContextProxy . getSecret . mockImplementation ( ( key : string ) => {
382+ if ( key === "codeIndexOpenAiKey" ) return "actual-api-key"
383+ return ""
384+ } )
385+
386+ const result = await configManager . loadConfiguration ( )
387+ // Should require restart since we went from empty to actual key
388+ expect ( result . requiresRestart ) . toBe ( true )
389+ } )
390+ } )
391+
323392 describe ( "getRestartInfo public method" , ( ) => {
324393 it ( "should provide restart info without loading configuration" , async ( ) => {
325394 // Setup initial state
@@ -441,4 +510,73 @@ describe("CodeIndexConfigManager", () => {
441510 expect ( configManager . currentModelId ) . toBe ( "text-embedding-3-large" )
442511 } )
443512 } )
513+
514+ describe ( "initialization and restart prevention" , ( ) => {
515+ it ( "should not require restart when configuration hasn't changed between calls" , async ( ) => {
516+ // Setup initial configuration - start with enabled and configured to avoid initial transition restart
517+ mockContextProxy . getGlobalState . mockReturnValue ( {
518+ codebaseIndexEnabled : true ,
519+ codebaseIndexQdrantUrl : "http://qdrant.local" ,
520+ codebaseIndexEmbedderProvider : "openai" ,
521+ codebaseIndexEmbedderModelId : "text-embedding-3-small" ,
522+ } )
523+ mockContextProxy . getSecret . mockImplementation ( ( key : string ) => {
524+ if ( key === "codeIndexOpenAiKey" ) return "test-key"
525+ return undefined
526+ } )
527+
528+ // First load - this will initialize the config manager with current state
529+ await configManager . loadConfiguration ( )
530+
531+ // Second load with same configuration - should not require restart
532+ const secondResult = await configManager . loadConfiguration ( )
533+ expect ( secondResult . requiresRestart ) . toBe ( false )
534+ } )
535+
536+ it ( "should properly initialize with current config to prevent false restarts" , async ( ) => {
537+ // Setup configuration
538+ mockContextProxy . getGlobalState . mockReturnValue ( {
539+ codebaseIndexEnabled : false , // Start disabled to avoid transition restart
540+ codebaseIndexQdrantUrl : "http://qdrant.local" ,
541+ codebaseIndexEmbedderProvider : "openai" ,
542+ codebaseIndexEmbedderModelId : "text-embedding-3-small" ,
543+ } )
544+ mockContextProxy . getSecret . mockImplementation ( ( key : string ) => {
545+ if ( key === "codeIndexOpenAiKey" ) return "test-key"
546+ return undefined
547+ } )
548+
549+ // Create a new config manager (simulating what happens in CodeIndexManager.initialize)
550+ const newConfigManager = new CodeIndexConfigManager ( mockContextProxy )
551+
552+ // Load configuration - should not require restart since the manager should be initialized with current config
553+ const result = await newConfigManager . loadConfiguration ( )
554+ expect ( result . requiresRestart ) . toBe ( false )
555+ } )
556+
557+ it ( "should not require restart when settings are saved but code indexing config unchanged" , async ( ) => {
558+ // This test simulates the original issue: handleExternalSettingsChange() being called
559+ // when other settings are saved, but code indexing settings haven't changed
560+
561+ // Setup initial state - enabled and configured
562+ mockContextProxy . getGlobalState . mockReturnValue ( {
563+ codebaseIndexEnabled : true ,
564+ codebaseIndexQdrantUrl : "http://qdrant.local" ,
565+ codebaseIndexEmbedderProvider : "openai" ,
566+ codebaseIndexEmbedderModelId : "text-embedding-3-small" ,
567+ } )
568+ mockContextProxy . getSecret . mockImplementation ( ( key : string ) => {
569+ if ( key === "codeIndexOpenAiKey" ) return "test-key"
570+ return undefined
571+ } )
572+
573+ // First load to establish baseline
574+ await configManager . loadConfiguration ( )
575+
576+ // Simulate external settings change where code indexing config hasn't changed
577+ // (this is what happens when other settings are saved)
578+ const result = await configManager . loadConfiguration ( )
579+ expect ( result . requiresRestart ) . toBe ( false )
580+ } )
581+ } )
444582} )
0 commit comments