Skip to content

Commit e556eaf

Browse files
committed
fix: Enhance API key handling and restart logic in CodeIndexConfigManager
1 parent 53c4755 commit e556eaf

File tree

2 files changed

+154
-2
lines changed

2 files changed

+154
-2
lines changed

src/services/code-index/__tests__/config-manager.test.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
})

src/services/code-index/manager.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ export class CodeIndexManager {
9999
*/
100100
public async initialize(contextProxy: ContextProxy): Promise<{ requiresRestart: boolean }> {
101101
// 1. ConfigManager Initialization and Configuration Loading
102-
this._configManager = new CodeIndexConfigManager(contextProxy)
102+
if (!this._configManager) {
103+
this._configManager = new CodeIndexConfigManager(contextProxy)
104+
// For first initialization, load configuration to set up initial state
105+
await this._configManager.loadConfiguration()
106+
}
103107
const { requiresRestart } = await this._configManager.loadConfiguration()
104108

105109
// 2. Check if feature is enabled
@@ -249,10 +253,20 @@ export class CodeIndexManager {
249253
* Handles external settings changes by reloading configuration.
250254
* This method should be called when API provider settings are updated
251255
* to ensure the CodeIndexConfigManager picks up the new configuration.
256+
* If the configuration changes require a restart, the service will be restarted.
252257
*/
253258
public async handleExternalSettingsChange(): Promise<void> {
254259
if (this._configManager) {
255-
await this._configManager.loadConfiguration()
260+
const { requiresRestart } = await this._configManager.loadConfiguration()
261+
262+
const isFeatureEnabled = this.isFeatureEnabled
263+
const isFeatureConfigured = this.isFeatureConfigured
264+
265+
// If configuration changes require a restart, restart the service
266+
if (requiresRestart && isFeatureEnabled && isFeatureConfigured) {
267+
this.stopWatcher()
268+
await this.startIndexing()
269+
}
256270
}
257271
}
258272
}

0 commit comments

Comments
 (0)