Skip to content

Commit fe05a7e

Browse files
committed
fix: recover from error state when Qdrant becomes available
- Add recoverFromError method to CodeIndexManager to clear error state and reset internal services - Update startIndexing handler to check for error state and recover before initialization - Add comprehensive tests for error recovery functionality Fixes #6660
1 parent a921d05 commit fe05a7e

File tree

3 files changed

+157
-0
lines changed

3 files changed

+157
-0
lines changed

src/core/webview/webviewMessageHandler.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2199,6 +2199,12 @@ export const webviewMessageHandler = async (
21992199
return
22002200
}
22012201
if (manager.isFeatureEnabled && manager.isFeatureConfigured) {
2202+
// Check if in error state and recover
2203+
const currentStatus = manager.getCurrentStatus()
2204+
if (currentStatus.systemStatus === "Error") {
2205+
await manager.recoverFromError()
2206+
}
2207+
22022208
if (!manager.isInitialized) {
22032209
await manager.initialize(provider.contextProxy)
22042210
}

src/services/code-index/__tests__/manager.spec.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,4 +367,140 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
367367
expect(mockServiceFactoryInstance.validateEmbedder).not.toHaveBeenCalled()
368368
})
369369
})
370+
371+
describe("recoverFromError", () => {
372+
let mockConfigManager: any
373+
let mockCacheManager: any
374+
let mockStateManager: any
375+
376+
beforeEach(() => {
377+
// Mock config manager
378+
mockConfigManager = {
379+
loadConfiguration: vi.fn().mockResolvedValue({ requiresRestart: false }),
380+
isFeatureConfigured: true,
381+
isFeatureEnabled: true,
382+
getConfig: vi.fn().mockReturnValue({
383+
isConfigured: true,
384+
embedderProvider: "openai",
385+
modelId: "text-embedding-3-small",
386+
openAiOptions: { openAiNativeApiKey: "test-key" },
387+
qdrantUrl: "http://localhost:6333",
388+
qdrantApiKey: "test-key",
389+
searchMinScore: 0.4,
390+
}),
391+
}
392+
;(manager as any)._configManager = mockConfigManager
393+
394+
// Mock cache manager
395+
mockCacheManager = {
396+
initialize: vi.fn(),
397+
clearCacheFile: vi.fn(),
398+
}
399+
;(manager as any)._cacheManager = mockCacheManager
400+
401+
// Mock state manager
402+
mockStateManager = (manager as any)._stateManager
403+
mockStateManager.setSystemState = vi.fn()
404+
mockStateManager.getCurrentStatus = vi.fn().mockReturnValue({
405+
systemStatus: "Error",
406+
message: "Failed during initial scan: fetch failed",
407+
processedItems: 0,
408+
totalItems: 0,
409+
currentItemUnit: "items",
410+
})
411+
412+
// Mock orchestrator and search service to simulate initialized state
413+
;(manager as any)._orchestrator = { stopWatcher: vi.fn(), state: "Error" }
414+
;(manager as any)._searchService = {}
415+
;(manager as any)._serviceFactory = {}
416+
})
417+
418+
it("should clear error state when recoverFromError is called", async () => {
419+
// Act
420+
await manager.recoverFromError()
421+
422+
// Assert
423+
expect(mockStateManager.setSystemState).toHaveBeenCalledWith("Standby", "")
424+
})
425+
426+
it("should reset internal service instances", async () => {
427+
// Verify initial state
428+
expect((manager as any)._configManager).toBeDefined()
429+
expect((manager as any)._serviceFactory).toBeDefined()
430+
expect((manager as any)._orchestrator).toBeDefined()
431+
expect((manager as any)._searchService).toBeDefined()
432+
433+
// Act
434+
await manager.recoverFromError()
435+
436+
// Assert - all service instances should be undefined
437+
expect((manager as any)._configManager).toBeUndefined()
438+
expect((manager as any)._serviceFactory).toBeUndefined()
439+
expect((manager as any)._orchestrator).toBeUndefined()
440+
expect((manager as any)._searchService).toBeUndefined()
441+
})
442+
443+
it("should make manager report as not initialized after recovery", async () => {
444+
// Verify initial state
445+
expect(manager.isInitialized).toBe(true)
446+
447+
// Act
448+
await manager.recoverFromError()
449+
450+
// Assert
451+
expect(manager.isInitialized).toBe(false)
452+
})
453+
454+
it("should allow re-initialization after recovery", async () => {
455+
// Setup mock for re-initialization
456+
const mockServiceFactoryInstance = {
457+
createServices: vi.fn().mockReturnValue({
458+
embedder: { embedderInfo: { name: "openai" } },
459+
vectorStore: {},
460+
scanner: {},
461+
fileWatcher: {
462+
onDidStartBatchProcessing: vi.fn(),
463+
onBatchProgressUpdate: vi.fn(),
464+
watch: vi.fn(),
465+
stopWatcher: vi.fn(),
466+
dispose: vi.fn(),
467+
},
468+
}),
469+
validateEmbedder: vi.fn().mockResolvedValue({ valid: true }),
470+
}
471+
MockedCodeIndexServiceFactory.mockImplementation(() => mockServiceFactoryInstance as any)
472+
473+
// Act - recover from error
474+
await manager.recoverFromError()
475+
476+
// Verify manager is not initialized
477+
expect(manager.isInitialized).toBe(false)
478+
479+
// Mock context proxy for initialization
480+
const mockContextProxy = {
481+
getValue: vi.fn(),
482+
setValue: vi.fn(),
483+
storeSecret: vi.fn(),
484+
getSecret: vi.fn(),
485+
refreshSecrets: vi.fn().mockResolvedValue(undefined),
486+
getGlobalState: vi.fn().mockReturnValue({
487+
codebaseIndexEnabled: true,
488+
codebaseIndexQdrantUrl: "http://localhost:6333",
489+
codebaseIndexEmbedderProvider: "openai",
490+
codebaseIndexEmbedderModelId: "text-embedding-3-small",
491+
codebaseIndexEmbedderModelDimension: 1536,
492+
codebaseIndexSearchMaxResults: 10,
493+
codebaseIndexSearchMinScore: 0.4,
494+
}),
495+
}
496+
497+
// Re-initialize
498+
await manager.initialize(mockContextProxy as any)
499+
500+
// Assert - manager should be initialized again
501+
expect(manager.isInitialized).toBe(true)
502+
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
503+
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalled()
504+
})
505+
})
370506
})

src/services/code-index/manager.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,21 @@ export class CodeIndexManager {
179179
}
180180
}
181181

182+
/**
183+
* Recovers from error state by clearing the error and resetting internal state.
184+
* This allows the manager to be re-initialized after a recoverable error.
185+
*/
186+
public async recoverFromError(): Promise<void> {
187+
// Clear error state
188+
this._stateManager.setSystemState("Standby", "")
189+
190+
// Force re-initialization by clearing service instances
191+
this._configManager = undefined
192+
this._serviceFactory = undefined
193+
this._orchestrator = undefined
194+
this._searchService = undefined
195+
}
196+
182197
/**
183198
* Cleans up the manager instance.
184199
*/

0 commit comments

Comments
 (0)