Skip to content

Commit 34fb5b7

Browse files
roomote[bot]roomotedaniel-lxs
authored
fix: recover from error state when Qdrant becomes available (RooCodeInc#6661)
* 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 RooCodeInc#6660 * fix: address PR review comments for code indexing error recovery - Add race condition protection for multiple rapid clicks on Start Indexing button - Add error handling for setSystemState in recoverFromError method - Enhance JSDoc documentation for recoverFromError method - Add test cases for recoverFromError idempotency and error handling * refactor: move error recovery logic into startIndexing method - Moved error recovery from webviewMessageHandler into CodeIndexManager.startIndexing() - This ensures error recovery happens whenever indexing is started, not just from UI - Added race condition prevention flag within CodeIndexManager - Simplified webviewMessageHandler by removing error state checking - The startIndexing method now automatically recovers from error state before proceeding * fix: remove await from startIndexing calls and update JSDoc - startIndexing should never be awaited as it's a long-running background process - Added JSDoc warning to never await this method - Updated webviewMessageHandler to not await startIndexing calls * fix: use platform-agnostic paths in code-index manager tests for Windows compatibility --------- Co-authored-by: Roo Code <[email protected]> Co-authored-by: Daniel Riccio <[email protected]>
1 parent 142cdb5 commit 34fb5b7

File tree

3 files changed

+300
-24
lines changed

3 files changed

+300
-24
lines changed

src/core/webview/webviewMessageHandler.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2224,7 +2224,15 @@ export const webviewMessageHandler = async (
22242224
await manager.initialize(provider.contextProxy)
22252225
}
22262226

2227+
// startIndexing now handles error recovery internally
22272228
manager.startIndexing()
2229+
2230+
// If startIndexing recovered from error, we need to reinitialize
2231+
if (!manager.isInitialized) {
2232+
await manager.initialize(provider.contextProxy)
2233+
// Try starting again after initialization
2234+
manager.startIndexing()
2235+
}
22282236
}
22292237
} catch (error) {
22302238
provider.log(`Error starting indexing: ${error instanceof Error ? error.message : String(error)}`)

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

Lines changed: 234 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,36 @@
11
import { CodeIndexManager } from "../manager"
22
import { CodeIndexServiceFactory } from "../service-factory"
33
import type { MockedClass } from "vitest"
4+
import * as path from "path"
45

56
// Mock vscode module
6-
vi.mock("vscode", () => ({
7-
window: {
8-
activeTextEditor: null,
9-
},
10-
workspace: {
11-
workspaceFolders: [
12-
{
13-
uri: { fsPath: "/test/workspace" },
14-
name: "test",
15-
index: 0,
16-
},
17-
],
18-
},
19-
}))
7+
vi.mock("vscode", () => {
8+
const testPath = require("path")
9+
const testWorkspacePath = testPath.join(testPath.sep, "test", "workspace")
10+
return {
11+
window: {
12+
activeTextEditor: null,
13+
},
14+
workspace: {
15+
workspaceFolders: [
16+
{
17+
uri: { fsPath: testWorkspacePath },
18+
name: "test",
19+
index: 0,
20+
},
21+
],
22+
},
23+
}
24+
})
2025

2126
// Mock only the essential dependencies
22-
vi.mock("../../../utils/path", () => ({
23-
getWorkspacePath: vi.fn(() => "/test/workspace"),
24-
}))
27+
vi.mock("../../../utils/path", () => {
28+
const testPath = require("path")
29+
const testWorkspacePath = testPath.join(testPath.sep, "test", "workspace")
30+
return {
31+
getWorkspacePath: vi.fn(() => testWorkspacePath),
32+
}
33+
})
2534

2635
vi.mock("../state-manager", () => ({
2736
CodeIndexStateManager: vi.fn().mockImplementation(() => ({
@@ -48,6 +57,13 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
4857
let mockContext: any
4958
let manager: CodeIndexManager
5059

60+
// Define test paths for use in tests
61+
const testWorkspacePath = path.join(path.sep, "test", "workspace")
62+
const testExtensionPath = path.join(path.sep, "test", "extension")
63+
const testStoragePath = path.join(path.sep, "test", "storage")
64+
const testGlobalStoragePath = path.join(path.sep, "test", "global-storage")
65+
const testLogPath = path.join(path.sep, "test", "log")
66+
5167
beforeEach(() => {
5268
// Clear all instances before each test
5369
CodeIndexManager.disposeAll()
@@ -57,14 +73,14 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
5773
workspaceState: {} as any,
5874
globalState: {} as any,
5975
extensionUri: {} as any,
60-
extensionPath: "/test/extension",
76+
extensionPath: testExtensionPath,
6177
asAbsolutePath: vi.fn(),
6278
storageUri: {} as any,
63-
storagePath: "/test/storage",
79+
storagePath: testStoragePath,
6480
globalStorageUri: {} as any,
65-
globalStoragePath: "/test/global-storage",
81+
globalStoragePath: testGlobalStoragePath,
6682
logUri: {} as any,
67-
logPath: "/test/log",
83+
logPath: testLogPath,
6884
extensionMode: 3, // vscode.ExtensionMode.Test
6985
secrets: {} as any,
7086
environmentVariableCollection: {} as any,
@@ -118,7 +134,7 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
118134
// Mock service factory to handle _recreateServices call
119135
const mockServiceFactoryInstance = {
120136
configManager: mockConfigManager,
121-
workspacePath: "/test/workspace",
137+
workspacePath: testWorkspacePath,
122138
cacheManager: mockCacheManager,
123139
createEmbedder: vi.fn().mockReturnValue({ embedderInfo: { name: "openai" } }),
124140
createVectorStore: vi.fn().mockReturnValue({}),
@@ -192,7 +208,7 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
192208
// Mock service factory to handle _recreateServices call
193209
const mockServiceFactoryInstance = {
194210
configManager: mockConfigManager,
195-
workspacePath: "/test/workspace",
211+
workspacePath: testWorkspacePath,
196212
cacheManager: mockCacheManager,
197213
createEmbedder: vi.fn().mockReturnValue({ embedderInfo: { name: "openai" } }),
198214
createVectorStore: vi.fn().mockReturnValue({}),
@@ -370,4 +386,199 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
370386
expect(mockServiceFactoryInstance.validateEmbedder).not.toHaveBeenCalled()
371387
})
372388
})
389+
390+
describe("recoverFromError", () => {
391+
let mockConfigManager: any
392+
let mockCacheManager: any
393+
let mockStateManager: any
394+
395+
beforeEach(() => {
396+
// Mock config manager
397+
mockConfigManager = {
398+
loadConfiguration: vi.fn().mockResolvedValue({ requiresRestart: false }),
399+
isFeatureConfigured: true,
400+
isFeatureEnabled: true,
401+
getConfig: vi.fn().mockReturnValue({
402+
isConfigured: true,
403+
embedderProvider: "openai",
404+
modelId: "text-embedding-3-small",
405+
openAiOptions: { openAiNativeApiKey: "test-key" },
406+
qdrantUrl: "http://localhost:6333",
407+
qdrantApiKey: "test-key",
408+
searchMinScore: 0.4,
409+
}),
410+
}
411+
;(manager as any)._configManager = mockConfigManager
412+
413+
// Mock cache manager
414+
mockCacheManager = {
415+
initialize: vi.fn(),
416+
clearCacheFile: vi.fn(),
417+
}
418+
;(manager as any)._cacheManager = mockCacheManager
419+
420+
// Mock state manager
421+
mockStateManager = (manager as any)._stateManager
422+
mockStateManager.setSystemState = vi.fn()
423+
mockStateManager.getCurrentStatus = vi.fn().mockReturnValue({
424+
systemStatus: "Error",
425+
message: "Failed during initial scan: fetch failed",
426+
processedItems: 0,
427+
totalItems: 0,
428+
currentItemUnit: "items",
429+
})
430+
431+
// Mock orchestrator and search service to simulate initialized state
432+
;(manager as any)._orchestrator = { stopWatcher: vi.fn(), state: "Error" }
433+
;(manager as any)._searchService = {}
434+
;(manager as any)._serviceFactory = {}
435+
})
436+
437+
it("should clear error state when recoverFromError is called", async () => {
438+
// Act
439+
await manager.recoverFromError()
440+
441+
// Assert
442+
expect(mockStateManager.setSystemState).toHaveBeenCalledWith("Standby", "")
443+
})
444+
445+
it("should reset internal service instances", async () => {
446+
// Verify initial state
447+
expect((manager as any)._configManager).toBeDefined()
448+
expect((manager as any)._serviceFactory).toBeDefined()
449+
expect((manager as any)._orchestrator).toBeDefined()
450+
expect((manager as any)._searchService).toBeDefined()
451+
452+
// Act
453+
await manager.recoverFromError()
454+
455+
// Assert - all service instances should be undefined
456+
expect((manager as any)._configManager).toBeUndefined()
457+
expect((manager as any)._serviceFactory).toBeUndefined()
458+
expect((manager as any)._orchestrator).toBeUndefined()
459+
expect((manager as any)._searchService).toBeUndefined()
460+
})
461+
462+
it("should make manager report as not initialized after recovery", async () => {
463+
// Verify initial state
464+
expect(manager.isInitialized).toBe(true)
465+
466+
// Act
467+
await manager.recoverFromError()
468+
469+
// Assert
470+
expect(manager.isInitialized).toBe(false)
471+
})
472+
473+
it("should allow re-initialization after recovery", async () => {
474+
// Setup mock for re-initialization
475+
const mockServiceFactoryInstance = {
476+
createServices: vi.fn().mockReturnValue({
477+
embedder: { embedderInfo: { name: "openai" } },
478+
vectorStore: {},
479+
scanner: {},
480+
fileWatcher: {
481+
onDidStartBatchProcessing: vi.fn(),
482+
onBatchProgressUpdate: vi.fn(),
483+
watch: vi.fn(),
484+
stopWatcher: vi.fn(),
485+
dispose: vi.fn(),
486+
},
487+
}),
488+
validateEmbedder: vi.fn().mockResolvedValue({ valid: true }),
489+
}
490+
MockedCodeIndexServiceFactory.mockImplementation(() => mockServiceFactoryInstance as any)
491+
492+
// Act - recover from error
493+
await manager.recoverFromError()
494+
495+
// Verify manager is not initialized
496+
expect(manager.isInitialized).toBe(false)
497+
498+
// Mock context proxy for initialization
499+
const mockContextProxy = {
500+
getValue: vi.fn(),
501+
setValue: vi.fn(),
502+
storeSecret: vi.fn(),
503+
getSecret: vi.fn(),
504+
refreshSecrets: vi.fn().mockResolvedValue(undefined),
505+
getGlobalState: vi.fn().mockReturnValue({
506+
codebaseIndexEnabled: true,
507+
codebaseIndexQdrantUrl: "http://localhost:6333",
508+
codebaseIndexEmbedderProvider: "openai",
509+
codebaseIndexEmbedderModelId: "text-embedding-3-small",
510+
codebaseIndexEmbedderModelDimension: 1536,
511+
codebaseIndexSearchMaxResults: 10,
512+
codebaseIndexSearchMinScore: 0.4,
513+
}),
514+
}
515+
516+
// Re-initialize
517+
await manager.initialize(mockContextProxy as any)
518+
519+
// Assert - manager should be initialized again
520+
expect(manager.isInitialized).toBe(true)
521+
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
522+
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalled()
523+
})
524+
525+
it("should be safe to call when not in error state (idempotent)", async () => {
526+
// Setup manager in non-error state
527+
mockStateManager.getCurrentStatus.mockReturnValue({
528+
systemStatus: "Standby",
529+
message: "",
530+
processedItems: 0,
531+
totalItems: 0,
532+
currentItemUnit: "items",
533+
})
534+
535+
// Verify initial state is not error
536+
const initialStatus = manager.getCurrentStatus()
537+
expect(initialStatus.systemStatus).not.toBe("Error")
538+
539+
// Act - call recoverFromError when not in error state
540+
await expect(manager.recoverFromError()).resolves.not.toThrow()
541+
542+
// Assert - should still clear state and service instances
543+
expect(mockStateManager.setSystemState).toHaveBeenCalledWith("Standby", "")
544+
expect((manager as any)._configManager).toBeUndefined()
545+
expect((manager as any)._serviceFactory).toBeUndefined()
546+
expect((manager as any)._orchestrator).toBeUndefined()
547+
expect((manager as any)._searchService).toBeUndefined()
548+
})
549+
550+
it("should continue recovery even if setSystemState throws", async () => {
551+
// Setup state manager to throw on setSystemState
552+
mockStateManager.setSystemState.mockImplementation(() => {
553+
throw new Error("State update failed")
554+
})
555+
556+
// Setup manager with service instances
557+
;(manager as any)._configManager = mockConfigManager
558+
;(manager as any)._serviceFactory = {}
559+
;(manager as any)._orchestrator = { stopWatcher: vi.fn() }
560+
;(manager as any)._searchService = {}
561+
562+
// Spy on console.error
563+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
564+
565+
// Act - should not throw despite setSystemState error
566+
await expect(manager.recoverFromError()).resolves.not.toThrow()
567+
568+
// Assert - error should be logged
569+
expect(consoleErrorSpy).toHaveBeenCalledWith(
570+
"Failed to clear error state during recovery:",
571+
expect.any(Error),
572+
)
573+
574+
// Assert - service instances should still be cleared
575+
expect((manager as any)._configManager).toBeUndefined()
576+
expect((manager as any)._serviceFactory).toBeUndefined()
577+
expect((manager as any)._orchestrator).toBeUndefined()
578+
expect((manager as any)._searchService).toBeUndefined()
579+
580+
// Cleanup
581+
consoleErrorSpy.mockRestore()
582+
})
583+
})
373584
})

0 commit comments

Comments
 (0)