diff --git a/src/services/code-index/__tests__/orchestrator.spec.ts b/src/services/code-index/__tests__/orchestrator.spec.ts index aab1ef888d..443e73b115 100644 --- a/src/services/code-index/__tests__/orchestrator.spec.ts +++ b/src/services/code-index/__tests__/orchestrator.spec.ts @@ -157,4 +157,102 @@ describe("CodeIndexOrchestrator - error path cleanup gating", () => { const lastCall = stateManager.setSystemState.mock.calls[stateManager.setSystemState.mock.calls.length - 1] expect(lastCall[0]).toBe("Error") }) + + it("should perform incremental scan when indexed data already exists (window reload scenario)", async () => { + // Arrange: simulate window reload scenario where index already exists + vectorStore.initialize.mockResolvedValue(false) // existing collection (not newly created) + vectorStore.hasIndexedData.mockResolvedValue(true) // index data exists + vectorStore.markIndexingIncomplete.mockResolvedValue(undefined) + vectorStore.markIndexingComplete.mockResolvedValue(undefined) + + // Mock scanner to return successful result for incremental scan + scanner.scanDirectory.mockResolvedValue({ + stats: { + filesProcessed: 5, + blocksFound: 10, + blocksIndexed: 10, + }, + }) + + const orchestrator = new CodeIndexOrchestrator( + configManager, + stateManager, + workspacePath, + cacheManager, + vectorStore, + scanner, + fileWatcher, + ) + + // Act + await orchestrator.startIndexing() + + // Assert + // Should check for existing data + expect(vectorStore.hasIndexedData).toHaveBeenCalledTimes(1) + + // Should perform incremental scan (scanner called once) + expect(scanner.scanDirectory).toHaveBeenCalledTimes(1) + + // Should NOT clear collection or cache (preserving existing index) + expect(vectorStore.clearCollection).not.toHaveBeenCalled() + expect(cacheManager.clearCacheFile).not.toHaveBeenCalled() + + // Should mark indexing as incomplete at start and complete at end + expect(vectorStore.markIndexingIncomplete).toHaveBeenCalledTimes(1) + expect(vectorStore.markIndexingComplete).toHaveBeenCalledTimes(1) + + // Should end in Indexed state + expect(stateManager.state).toBe("Indexed") + }) + + it("should perform full scan when collection is newly created", async () => { + // Arrange: new collection created + vectorStore.initialize.mockResolvedValue(true) // new collection created + vectorStore.hasIndexedData.mockResolvedValue(false) // no data yet + vectorStore.markIndexingIncomplete.mockResolvedValue(undefined) + vectorStore.markIndexingComplete.mockResolvedValue(undefined) + + // Mock scanner to return successful result for full scan + scanner.scanDirectory.mockResolvedValue({ + stats: { + filesProcessed: 100, + blocksFound: 500, + blocksIndexed: 500, + }, + }) + + // Clear cache when new collection is created + cacheManager.clearCacheFile.mockResolvedValue(undefined) + + const orchestrator = new CodeIndexOrchestrator( + configManager, + stateManager, + workspacePath, + cacheManager, + vectorStore, + scanner, + fileWatcher, + ) + + // Act + await orchestrator.startIndexing() + + // Assert + // Should check for existing data + expect(vectorStore.hasIndexedData).toHaveBeenCalledTimes(1) + + // Should clear cache since collection was newly created + expect(cacheManager.clearCacheFile).toHaveBeenCalledTimes(1) + + // Should perform full scan + expect(scanner.scanDirectory).toHaveBeenCalledTimes(1) + + // Should mark indexing as incomplete at start and complete at end + expect(vectorStore.markIndexingIncomplete).toHaveBeenCalledTimes(1) + expect(vectorStore.markIndexingComplete).toHaveBeenCalledTimes(1) + + // Should end in Indexed state + expect(stateManager.state).toBe("Indexed") + }) }) diff --git a/src/services/code-index/manager.ts b/src/services/code-index/manager.ts index dd79a3f161..f03cb1f19d 100644 --- a/src/services/code-index/manager.ts +++ b/src/services/code-index/manager.ts @@ -154,11 +154,30 @@ export class CodeIndexManager { // 5. Handle Indexing Start/Restart // The enhanced vectorStore.initialize() in startIndexing() now handles dimension changes automatically // by detecting incompatible collections and recreating them, so we rely on that for dimension changes - const shouldStartOrRestartIndexing = - requiresRestart || - (needsServiceRecreation && (!this._orchestrator || this._orchestrator.state !== "Indexing")) + + // Determine if we should start or restart indexing + // We need to be careful here to avoid unnecessary re-indexing on window reload + let shouldStartOrRestartIndexing = false + + if (requiresRestart) { + // Configuration changed in a way that requires restart (e.g., model change) + shouldStartOrRestartIndexing = true + } else if (needsServiceRecreation && this._orchestrator) { + // Services were recreated but orchestrator exists + // Only restart if not currently indexing + shouldStartOrRestartIndexing = + this._orchestrator.state !== "Indexing" && this._orchestrator.state !== "Indexed" + } else if (needsServiceRecreation && !this._orchestrator) { + // Services were recreated and orchestrator doesn't exist (e.g., after window reload) + // This is the common case that was causing unnecessary re-indexing + // The orchestrator's startIndexing() will check for existing data and do incremental scan if needed + shouldStartOrRestartIndexing = true + } if (shouldStartOrRestartIndexing) { + // Note: startIndexing() internally checks for existing indexed data via hasIndexedData() + // and will perform an incremental scan instead of a full rebuild when data exists + // This prevents unnecessary re-indexing when the window is reloaded this._orchestrator?.startIndexing() // This method is async, but we don't await it here } diff --git a/src/services/code-index/orchestrator.ts b/src/services/code-index/orchestrator.ts index 99f317882b..38b19eed23 100644 --- a/src/services/code-index/orchestrator.ts +++ b/src/services/code-index/orchestrator.ts @@ -141,6 +141,10 @@ export class CodeIndexOrchestrator { // If it does, we can skip the full scan and just start the watcher const hasExistingData = await this.vectorStore.hasIndexedData() + console.log( + `[CodeIndexOrchestrator] Checking existing data: hasExistingData=${hasExistingData}, collectionCreated=${collectionCreated}`, + ) + if (hasExistingData && !collectionCreated) { // Collection exists with data - run incremental scan to catch any new/changed files // This handles files added while workspace was closed or Qdrant was inactive @@ -201,6 +205,9 @@ export class CodeIndexOrchestrator { this.stateManager.setSystemState("Indexed", t("embeddings:orchestrator.fileWatcherStarted")) } else { // No existing data or collection was just created - do a full scan + console.log( + `[CodeIndexOrchestrator] No existing data or collection was just created. Starting full workspace scan...`, + ) this.stateManager.setSystemState("Indexing", "Services ready. Starting workspace scan...") // Mark as incomplete at the start of full scan diff --git a/src/services/code-index/vector-store/qdrant-client.ts b/src/services/code-index/vector-store/qdrant-client.ts index ba62afc5f8..eb996569e7 100644 --- a/src/services/code-index/vector-store/qdrant-client.ts +++ b/src/services/code-index/vector-store/qdrant-client.ts @@ -588,10 +588,13 @@ export class QdrantVectorStore implements IVectorStore { try { const collectionInfo = await this.getCollectionInfo() if (!collectionInfo) { + console.log("[QdrantVectorStore] hasIndexedData: No collection info found") return false } // Check if the collection has any points indexed const pointsCount = collectionInfo.points_count ?? 0 + console.log(`[QdrantVectorStore] hasIndexedData: Collection has ${pointsCount} points`) + if (pointsCount === 0) { return false } @@ -605,17 +608,21 @@ export class QdrantVectorStore implements IVectorStore { // If marker exists, use it to determine completion status if (metadataPoints.length > 0) { - return metadataPoints[0].payload?.indexing_complete === true + const isComplete = metadataPoints[0].payload?.indexing_complete === true + console.log( + `[QdrantVectorStore] hasIndexedData: Found metadata marker, indexing_complete=${isComplete}`, + ) + return isComplete } // Backward compatibility: No marker exists (old index or pre-marker version) // Fall back to old logic - assume complete if collection has points console.log( - "[QdrantVectorStore] No indexing metadata marker found. Using backward compatibility mode (checking points_count > 0).", + `[QdrantVectorStore] hasIndexedData: No indexing metadata marker found. Using backward compatibility mode (returning ${pointsCount > 0} based on points_count)`, ) return pointsCount > 0 } catch (error) { - console.warn("[QdrantVectorStore] Failed to check if collection has data:", error) + console.warn("[QdrantVectorStore] hasIndexedData: Failed to check if collection has data:", error) return false } }