Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2663,18 +2663,26 @@ export const webviewMessageHandler = async (
return
}
if (manager.isFeatureEnabled && manager.isFeatureConfigured) {
if (!manager.isInitialized) {
await manager.initialize(provider.contextProxy)
}

// startIndexing now handles error recovery internally
manager.startIndexing()

// If startIndexing recovered from error, we need to reinitialize
if (!manager.isInitialized) {
await manager.initialize(provider.contextProxy)
// Try starting again after initialization
// Mimic extension startup behavior: initialize first, which will
// check if Qdrant container is active and reuse existing collection
await manager.initialize(provider.contextProxy)

// Only call startIndexing if we're in a state that requires it
// (e.g., Standby or Error). If already Indexed or Indexing, the
// initialize() call above will have already started the watcher.
const currentState = manager.state
if (currentState === "Standby" || currentState === "Error") {
// startIndexing now handles error recovery internally
manager.startIndexing()

// If startIndexing recovered from error, we need to reinitialize
if (!manager.isInitialized) {
await manager.initialize(provider.contextProxy)
// Try starting again after initialization
if (manager.state === "Standby" || manager.state === "Error") {
manager.startIndexing()
}
}
}
}
} catch (error) {
Expand Down
160 changes: 160 additions & 0 deletions src/services/code-index/__tests__/orchestrator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { describe, it, expect, beforeEach, vi } from "vitest"
import { CodeIndexOrchestrator } from "../orchestrator"

// Mock vscode workspace so startIndexing passes workspace check
vi.mock("vscode", () => {
const path = require("path")
const testWorkspacePath = path.join(path.sep, "test", "workspace")
return {
window: {
activeTextEditor: null,
},
workspace: {
workspaceFolders: [
{
uri: { fsPath: testWorkspacePath },
name: "test",
index: 0,
},
],
createFileSystemWatcher: vi.fn().mockReturnValue({
onDidCreate: vi.fn().mockReturnValue({ dispose: vi.fn() }),
onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }),
onDidDelete: vi.fn().mockReturnValue({ dispose: vi.fn() }),
dispose: vi.fn(),
}),
},
RelativePattern: vi.fn().mockImplementation((base: string, pattern: string) => ({ base, pattern })),
}
})

// Mock TelemetryService
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureEvent: vi.fn(),
},
},
}))

// Mock i18n translator used in orchestrator messages
vi.mock("../../i18n", () => ({
t: (key: string, params?: any) => {
if (key === "embeddings:orchestrator.failedDuringInitialScan" && params?.errorMessage) {
return `Failed during initial scan: ${params.errorMessage}`
}
return key
},
}))

describe("CodeIndexOrchestrator - error path cleanup gating", () => {
const workspacePath = "/test/workspace"

let configManager: any
let stateManager: any
let cacheManager: any
let vectorStore: any
let scanner: any
let fileWatcher: any

beforeEach(() => {
vi.clearAllMocks()

configManager = {
isFeatureConfigured: true,
}

// Minimal state manager that tracks state transitions
let currentState = "Standby"
stateManager = {
get state() {
return currentState
},
setSystemState: vi.fn().mockImplementation((state: string, _msg: string) => {
currentState = state
}),
reportFileQueueProgress: vi.fn(),
reportBlockIndexingProgress: vi.fn(),
}

cacheManager = {
clearCacheFile: vi.fn().mockResolvedValue(undefined),
}

vectorStore = {
initialize: vi.fn(),
hasIndexedData: vi.fn(),
markIndexingIncomplete: vi.fn(),
markIndexingComplete: vi.fn(),
clearCollection: vi.fn().mockResolvedValue(undefined),
}

scanner = {
scanDirectory: vi.fn(),
}

fileWatcher = {
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(),
}
})

it("should not call clearCollection() or clear cache when initialize() fails (indexing not started)", async () => {
// Arrange: fail at initialize()
vectorStore.initialize.mockRejectedValue(new Error("Qdrant unreachable"))

const orchestrator = new CodeIndexOrchestrator(
configManager,
stateManager,
workspacePath,
cacheManager,
vectorStore,
scanner,
fileWatcher,
)

// Act
await orchestrator.startIndexing()

// Assert
expect(vectorStore.clearCollection).not.toHaveBeenCalled()
expect(cacheManager.clearCacheFile).not.toHaveBeenCalled()

// Error state should be set
expect(stateManager.setSystemState).toHaveBeenCalled()
const lastCall = stateManager.setSystemState.mock.calls[stateManager.setSystemState.mock.calls.length - 1]
expect(lastCall[0]).toBe("Error")
})

it("should call clearCollection() and clear cache when an error occurs after initialize() succeeds (indexing started)", async () => {
// Arrange: initialize succeeds; fail soon after to enter error path with indexingStarted=true
vectorStore.initialize.mockResolvedValue(false) // existing collection
vectorStore.hasIndexedData.mockResolvedValue(false) // force full scan path
vectorStore.markIndexingIncomplete.mockRejectedValue(new Error("mark incomplete failure"))

const orchestrator = new CodeIndexOrchestrator(
configManager,
stateManager,
workspacePath,
cacheManager,
vectorStore,
scanner,
fileWatcher,
)

// Act
await orchestrator.startIndexing()

// Assert: cleanup gated behind indexingStarted should have happened
expect(vectorStore.clearCollection).toHaveBeenCalledTimes(1)
expect(cacheManager.clearCacheFile).toHaveBeenCalledTimes(1)

// Error state should be set
expect(stateManager.setSystemState).toHaveBeenCalled()
const lastCall = stateManager.setSystemState.mock.calls[stateManager.setSystemState.mock.calls.length - 1]
expect(lastCall[0]).toBe("Error")
})
})
18 changes: 18 additions & 0 deletions src/services/code-index/interfaces/vector-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,24 @@ export interface IVectorStore {
* @returns Promise resolving to boolean indicating if the collection exists
*/
collectionExists(): Promise<boolean>

/**
* Checks if the collection exists and has indexed points
* @returns Promise resolving to boolean indicating if the collection exists and has points
*/
hasIndexedData(): Promise<boolean>

/**
* Marks the indexing process as complete by storing metadata
* Should be called after a successful full workspace scan or incremental scan
*/
markIndexingComplete(): Promise<void>

/**
* Marks the indexing process as incomplete by storing metadata
* Should be called at the start of indexing to indicate work in progress
*/
markIndexingIncomplete(): Promise<void>
}

export interface VectorStoreSearchResult {
Expand Down
Loading