fix: enhance indexing error recovery to prevent workspace reload requirement

- Add resetFromError() and canStartIndexing() methods to CodeIndexStateManager
- Improve startIndexing() in CodeIndexOrchestrator to handle error state recovery
- Enhance CodeIndexManager.startIndexing() with better error handling and recovery
- Improve webview message handler for startIndexing with detailed error feedback
- Add comprehensive error recovery tests

Fixes #5826: Users can now restart indexing after errors without workspace reload
This commit is contained in:
Roo Code 2025-07-17 17:46:47 +00:00
parent 80fcd1ba74
commit aa9c89cfb2
5 changed files with 298 additions and 14 deletions

View file

@ -2073,15 +2073,94 @@ export const webviewMessageHandler = async (
case "startIndexing": {
try {
const manager = provider.codeIndexManager!
if (manager.isFeatureEnabled && manager.isFeatureConfigured) {
if (!manager.isInitialized) {
await manager.initialize(provider.contextProxy)
}
if (!manager.isFeatureEnabled) {
await provider.postMessageToWebview({
type: "indexingStatusUpdate",
values: {
systemStatus: "Standby",
message: "Code indexing is disabled. Please enable it in settings.",
processedItems: 0,
totalItems: 0,
currentItemUnit: "blocks",
},
})
break
}
manager.startIndexing()
if (!manager.isFeatureConfigured) {
await provider.postMessageToWebview({
type: "indexingStatusUpdate",
values: {
systemStatus: "Standby",
message: "Code indexing is not configured. Please configure your embedder settings.",
processedItems: 0,
totalItems: 0,
currentItemUnit: "blocks",
},
})
break
}
// Initialize manager if needed
if (!manager.isInitialized) {
try {
await manager.initialize(provider.contextProxy)
} catch (initError) {
const errorMessage = initError instanceof Error ? initError.message : String(initError)
provider.log(`Error initializing code index manager: ${errorMessage}`)
await provider.postMessageToWebview({
type: "indexingStatusUpdate",
values: {
systemStatus: "Error",
message: `Failed to initialize: ${errorMessage}`,
processedItems: 0,
totalItems: 0,
currentItemUnit: "blocks",
},
})
break
}
}
// Attempt to start indexing with enhanced error recovery
try {
await manager.startIndexing()
// Send updated status to webview
await provider.postMessageToWebview({
type: "indexingStatusUpdate",
values: manager.getCurrentStatus(),
})
} catch (startError) {
const errorMessage = startError instanceof Error ? startError.message : String(startError)
provider.log(`Error starting indexing: ${errorMessage}`)
// Send error status with recovery suggestion to webview
await provider.postMessageToWebview({
type: "indexingStatusUpdate",
values: {
systemStatus: "Error",
message: `Failed to start indexing: ${errorMessage}. You can try starting indexing again.`,
processedItems: 0,
totalItems: 0,
currentItemUnit: "blocks",
},
})
}
} catch (error) {
provider.log(`Error starting indexing: ${error instanceof Error ? error.message : String(error)}`)
const errorMessage = error instanceof Error ? error.message : String(error)
provider.log(`Unexpected error in startIndexing handler: ${errorMessage}`)
// Send generic error status to webview
await provider.postMessageToWebview({
type: "indexingStatusUpdate",
values: {
systemStatus: "Error",
message: `An unexpected error occurred. Please try again or check your configuration.`,
processedItems: 0,
totalItems: 0,
currentItemUnit: "blocks",
},
})
}
break
}

View file

@ -0,0 +1,161 @@
import { describe, it, expect, beforeEach, vi } from "vitest"
import { CodeIndexStateManager } from "../state-manager"
import { CodeIndexOrchestrator } from "../orchestrator"
import { CodeIndexManager } from "../manager"
describe("Code Index Error Recovery", () => {
let stateManager: CodeIndexStateManager
let mockConfigManager: any
let mockVectorStore: any
let mockScanner: any
let mockFileWatcher: any
let mockCacheManager: any
beforeEach(() => {
stateManager = new CodeIndexStateManager()
mockConfigManager = {
isFeatureConfigured: true,
isFeatureEnabled: true,
}
mockVectorStore = {
initialize: vi.fn().mockResolvedValue(false),
clearCollection: vi.fn().mockResolvedValue(undefined),
}
mockScanner = {
scanDirectory: vi.fn().mockResolvedValue({
stats: { totalFiles: 5, processedFiles: 5 }
}),
}
mockFileWatcher = {
initialize: vi.fn().mockResolvedValue(undefined),
onDidStartBatchProcessing: vi.fn().mockReturnValue({ dispose: vi.fn() }),
onBatchProgressUpdate: vi.fn().mockReturnValue({ dispose: vi.fn() }),
onDidFinishBatchProcessing: vi.fn().mockReturnValue({ dispose: vi.fn() }),
dispose: vi.fn(),
}
mockCacheManager = {
clearCacheFile: vi.fn().mockResolvedValue(undefined),
}
})
describe("CodeIndexStateManager", () => {
it("should allow resetting from error state", () => {
// Set to error state
stateManager.setSystemState("Error", "Test error")
expect(stateManager.state).toBe("Error")
// Reset from error
stateManager.resetFromError()
expect(stateManager.state).toBe("Standby")
})
it("should not reset if not in error state", () => {
stateManager.setSystemState("Indexed", "All good")
stateManager.resetFromError()
expect(stateManager.state).toBe("Indexed")
})
it("should correctly identify when indexing can start", () => {
// Test all valid states
stateManager.setSystemState("Standby")
expect(stateManager.canStartIndexing()).toBe(true)
stateManager.setSystemState("Error")
expect(stateManager.canStartIndexing()).toBe(true)
stateManager.setSystemState("Indexed")
expect(stateManager.canStartIndexing()).toBe(true)
// Test invalid state
stateManager.setSystemState("Indexing")
expect(stateManager.canStartIndexing()).toBe(false)
})
})
describe("CodeIndexOrchestrator", () => {
let orchestrator: CodeIndexOrchestrator
beforeEach(() => {
orchestrator = new CodeIndexOrchestrator(
mockConfigManager,
stateManager,
"/test/workspace",
mockCacheManager,
mockVectorStore,
mockScanner,
mockFileWatcher
)
})
it("should recover from error state when starting indexing", async () => {
// Set to error state
stateManager.setSystemState("Error", "Previous error")
expect(stateManager.state).toBe("Error")
// Start indexing should recover from error
await orchestrator.startIndexing()
// Should have reset from error and started indexing
expect(stateManager.state).toBe("Indexed")
})
it("should not start if already processing", async () => {
// Start indexing once
const firstStart = orchestrator.startIndexing()
// Try to start again while processing
await orchestrator.startIndexing()
// Wait for first start to complete
await firstStart
// Should only have been called once
expect(mockVectorStore.initialize).toHaveBeenCalledTimes(1)
})
it("should handle configuration errors gracefully", async () => {
mockConfigManager.isFeatureConfigured = false
await orchestrator.startIndexing()
expect(stateManager.state).toBe("Standby")
expect(mockVectorStore.initialize).not.toHaveBeenCalled()
})
})
describe("Integration Test", () => {
it("should allow restarting indexing after error without workspace reload", async () => {
const orchestrator = new CodeIndexOrchestrator(
mockConfigManager,
stateManager,
"/test/workspace",
mockCacheManager,
mockVectorStore,
mockScanner,
mockFileWatcher
)
// Simulate an error during indexing
mockVectorStore.initialize.mockRejectedValueOnce(new Error("Connection failed"))
// First attempt should fail
await orchestrator.startIndexing()
expect(stateManager.state).toBe("Error")
// Fix the mock to succeed
mockVectorStore.initialize.mockResolvedValueOnce(false)
// Second attempt should succeed without requiring workspace reload
await orchestrator.startIndexing()
expect(stateManager.state).toBe("Indexed")
// Should have attempted initialization twice
expect(mockVectorStore.initialize).toHaveBeenCalledTimes(2)
})
})
})

View file

@ -157,14 +157,36 @@ export class CodeIndexManager {
/**
* Initiates the indexing process (initial scan and starts watcher).
* Enhanced with error recovery to prevent users from needing to reload workspace.
*/
public async startIndexing(): Promise<void> {
if (!this.isFeatureEnabled) {
return
}
this.assertInitialized()
await this._orchestrator!.startIndexing()
// If we're in an error state, attempt to recover by resetting the state
if (this._stateManager.state === "Error") {
console.log("[CodeIndexManager] Attempting to recover from error state before starting indexing.")
this._stateManager.resetFromError()
}
try {
await this._orchestrator!.startIndexing()
} catch (error) {
// Enhanced error handling with recovery suggestions
const errorMessage = error instanceof Error ? error.message : String(error)
console.error("[CodeIndexManager] Failed to start indexing:", errorMessage)
// Set a more informative error state that suggests recovery options
this._stateManager.setSystemState(
"Error",
`Failed to start indexing: ${errorMessage}. Try starting indexing again or check your configuration.`
)
// Re-throw the error so callers can handle it appropriately
throw error
}
}
/**

View file

@ -100,18 +100,22 @@ export class CodeIndexOrchestrator {
return
}
if (
this._isProcessing ||
(this.stateManager.state !== "Standby" &&
this.stateManager.state !== "Error" &&
this.stateManager.state !== "Indexed")
) {
// Check if we can start indexing using the state manager's logic
if (this._isProcessing || !this.stateManager.canStartIndexing()) {
console.warn(
`[CodeIndexOrchestrator] Start rejected: Already processing or in state ${this.stateManager.state}.`,
)
return
}
// If we're recovering from an error state, reset it first
if (this.stateManager.state === "Error") {
console.log("[CodeIndexOrchestrator] Recovering from error state, resetting to standby.")
this.stateManager.resetFromError()
// Stop any existing watcher to ensure clean state
this.stopWatcher()
}
this._isProcessing = true
this.stateManager.setSystemState("Indexing", "Initializing services...")

View file

@ -55,6 +55,24 @@ export class CodeIndexStateManager {
}
}
/**
* Resets the system from an error state to standby, allowing indexing to be restarted.
* This method provides a clean recovery path from error states without requiring workspace reload.
*/
public resetFromError(): void {
if (this._systemStatus === "Error") {
this.setSystemState("Standby", "Ready to start indexing.")
}
}
/**
* Checks if the system is in a state that allows starting indexing.
* @returns true if indexing can be started, false otherwise
*/
public canStartIndexing(): boolean {
return this._systemStatus === "Standby" || this._systemStatus === "Error" || this._systemStatus === "Indexed"
}
public reportBlockIndexingProgress(processedItems: number, totalItems: number): void {
const progressChanged = processedItems !== this._processedItems || totalItems !== this._totalItems