Skip to content

Commit d33082c

Browse files
heysethdaniel-lxs
authored andcommitted
Fix: Enhanced codebase index recovery and reuse ('Start Indexing' button now reuses existing Qdrant index) (RooCodeInc#8588)
Co-authored-by: daniel-lxs <[email protected]>
1 parent 1f8cd29 commit d33082c

File tree

6 files changed

+562
-158
lines changed

6 files changed

+562
-158
lines changed

src/core/webview/webviewMessageHandler.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2801,18 +2801,26 @@ export const webviewMessageHandler = async (
28012801
return
28022802
}
28032803
if (manager.isFeatureEnabled && manager.isFeatureConfigured) {
2804-
if (!manager.isInitialized) {
2805-
await manager.initialize(provider.contextProxy)
2806-
}
2807-
2808-
// startIndexing now handles error recovery internally
2809-
manager.startIndexing()
2810-
2811-
// If startIndexing recovered from error, we need to reinitialize
2812-
if (!manager.isInitialized) {
2813-
await manager.initialize(provider.contextProxy)
2814-
// Try starting again after initialization
2804+
// Mimic extension startup behavior: initialize first, which will
2805+
// check if Qdrant container is active and reuse existing collection
2806+
await manager.initialize(provider.contextProxy)
2807+
2808+
// Only call startIndexing if we're in a state that requires it
2809+
// (e.g., Standby or Error). If already Indexed or Indexing, the
2810+
// initialize() call above will have already started the watcher.
2811+
const currentState = manager.state
2812+
if (currentState === "Standby" || currentState === "Error") {
2813+
// startIndexing now handles error recovery internally
28152814
manager.startIndexing()
2815+
2816+
// If startIndexing recovered from error, we need to reinitialize
2817+
if (!manager.isInitialized) {
2818+
await manager.initialize(provider.contextProxy)
2819+
// Try starting again after initialization
2820+
if (manager.state === "Standby" || manager.state === "Error") {
2821+
manager.startIndexing()
2822+
}
2823+
}
28162824
}
28172825
}
28182826
} catch (error) {
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { describe, it, expect, beforeEach, vi } from "vitest"
2+
import { CodeIndexOrchestrator } from "../orchestrator"
3+
4+
// Mock vscode workspace so startIndexing passes workspace check
5+
vi.mock("vscode", () => {
6+
const path = require("path")
7+
const testWorkspacePath = path.join(path.sep, "test", "workspace")
8+
return {
9+
window: {
10+
activeTextEditor: null,
11+
},
12+
workspace: {
13+
workspaceFolders: [
14+
{
15+
uri: { fsPath: testWorkspacePath },
16+
name: "test",
17+
index: 0,
18+
},
19+
],
20+
createFileSystemWatcher: vi.fn().mockReturnValue({
21+
onDidCreate: vi.fn().mockReturnValue({ dispose: vi.fn() }),
22+
onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }),
23+
onDidDelete: vi.fn().mockReturnValue({ dispose: vi.fn() }),
24+
dispose: vi.fn(),
25+
}),
26+
},
27+
RelativePattern: vi.fn().mockImplementation((base: string, pattern: string) => ({ base, pattern })),
28+
}
29+
})
30+
31+
// Mock TelemetryService
32+
vi.mock("@roo-code/telemetry", () => ({
33+
TelemetryService: {
34+
instance: {
35+
captureEvent: vi.fn(),
36+
},
37+
},
38+
}))
39+
40+
// Mock i18n translator used in orchestrator messages
41+
vi.mock("../../i18n", () => ({
42+
t: (key: string, params?: any) => {
43+
if (key === "embeddings:orchestrator.failedDuringInitialScan" && params?.errorMessage) {
44+
return `Failed during initial scan: ${params.errorMessage}`
45+
}
46+
return key
47+
},
48+
}))
49+
50+
describe("CodeIndexOrchestrator - error path cleanup gating", () => {
51+
const workspacePath = "/test/workspace"
52+
53+
let configManager: any
54+
let stateManager: any
55+
let cacheManager: any
56+
let vectorStore: any
57+
let scanner: any
58+
let fileWatcher: any
59+
60+
beforeEach(() => {
61+
vi.clearAllMocks()
62+
63+
configManager = {
64+
isFeatureConfigured: true,
65+
}
66+
67+
// Minimal state manager that tracks state transitions
68+
let currentState = "Standby"
69+
stateManager = {
70+
get state() {
71+
return currentState
72+
},
73+
setSystemState: vi.fn().mockImplementation((state: string, _msg: string) => {
74+
currentState = state
75+
}),
76+
reportFileQueueProgress: vi.fn(),
77+
reportBlockIndexingProgress: vi.fn(),
78+
}
79+
80+
cacheManager = {
81+
clearCacheFile: vi.fn().mockResolvedValue(undefined),
82+
}
83+
84+
vectorStore = {
85+
initialize: vi.fn(),
86+
hasIndexedData: vi.fn(),
87+
markIndexingIncomplete: vi.fn(),
88+
markIndexingComplete: vi.fn(),
89+
clearCollection: vi.fn().mockResolvedValue(undefined),
90+
}
91+
92+
scanner = {
93+
scanDirectory: vi.fn(),
94+
}
95+
96+
fileWatcher = {
97+
initialize: vi.fn().mockResolvedValue(undefined),
98+
onDidStartBatchProcessing: vi.fn().mockReturnValue({ dispose: vi.fn() }),
99+
onBatchProgressUpdate: vi.fn().mockReturnValue({ dispose: vi.fn() }),
100+
onDidFinishBatchProcessing: vi.fn().mockReturnValue({ dispose: vi.fn() }),
101+
dispose: vi.fn(),
102+
}
103+
})
104+
105+
it("should not call clearCollection() or clear cache when initialize() fails (indexing not started)", async () => {
106+
// Arrange: fail at initialize()
107+
vectorStore.initialize.mockRejectedValue(new Error("Qdrant unreachable"))
108+
109+
const orchestrator = new CodeIndexOrchestrator(
110+
configManager,
111+
stateManager,
112+
workspacePath,
113+
cacheManager,
114+
vectorStore,
115+
scanner,
116+
fileWatcher,
117+
)
118+
119+
// Act
120+
await orchestrator.startIndexing()
121+
122+
// Assert
123+
expect(vectorStore.clearCollection).not.toHaveBeenCalled()
124+
expect(cacheManager.clearCacheFile).not.toHaveBeenCalled()
125+
126+
// Error state should be set
127+
expect(stateManager.setSystemState).toHaveBeenCalled()
128+
const lastCall = stateManager.setSystemState.mock.calls[stateManager.setSystemState.mock.calls.length - 1]
129+
expect(lastCall[0]).toBe("Error")
130+
})
131+
132+
it("should call clearCollection() and clear cache when an error occurs after initialize() succeeds (indexing started)", async () => {
133+
// Arrange: initialize succeeds; fail soon after to enter error path with indexingStarted=true
134+
vectorStore.initialize.mockResolvedValue(false) // existing collection
135+
vectorStore.hasIndexedData.mockResolvedValue(false) // force full scan path
136+
vectorStore.markIndexingIncomplete.mockRejectedValue(new Error("mark incomplete failure"))
137+
138+
const orchestrator = new CodeIndexOrchestrator(
139+
configManager,
140+
stateManager,
141+
workspacePath,
142+
cacheManager,
143+
vectorStore,
144+
scanner,
145+
fileWatcher,
146+
)
147+
148+
// Act
149+
await orchestrator.startIndexing()
150+
151+
// Assert: cleanup gated behind indexingStarted should have happened
152+
expect(vectorStore.clearCollection).toHaveBeenCalledTimes(1)
153+
expect(cacheManager.clearCacheFile).toHaveBeenCalledTimes(1)
154+
155+
// Error state should be set
156+
expect(stateManager.setSystemState).toHaveBeenCalled()
157+
const lastCall = stateManager.setSystemState.mock.calls[stateManager.setSystemState.mock.calls.length - 1]
158+
expect(lastCall[0]).toBe("Error")
159+
})
160+
})

src/services/code-index/interfaces/vector-store.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,24 @@ export interface IVectorStore {
6262
* @returns Promise resolving to boolean indicating if the collection exists
6363
*/
6464
collectionExists(): Promise<boolean>
65+
66+
/**
67+
* Checks if the collection exists and has indexed points
68+
* @returns Promise resolving to boolean indicating if the collection exists and has points
69+
*/
70+
hasIndexedData(): Promise<boolean>
71+
72+
/**
73+
* Marks the indexing process as complete by storing metadata
74+
* Should be called after a successful full workspace scan or incremental scan
75+
*/
76+
markIndexingComplete(): Promise<void>
77+
78+
/**
79+
* Marks the indexing process as incomplete by storing metadata
80+
* Should be called at the start of indexing to indicate work in progress
81+
*/
82+
markIndexingIncomplete(): Promise<void>
6583
}
6684

6785
export interface VectorStoreSearchResult {

0 commit comments

Comments
 (0)