From 50242a816cb79039d959041bb461da23462329b6 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Thu, 1 Oct 2026 13:27:07 +0300 Subject: [PATCH 01/10] fix(code-index): make startup embedder validation non-blocking --- .../code-index-status-manager.spec.ts | 5 +- .../embedder-readiness-manager.spec.ts | 100 ++++++++++++++++++ .../code-index/__tests__/manager.spec.ts | 63 +++-------- .../code-index/code-index-workspace-scope.ts | 3 + .../code-index/embedder-readiness-manager.ts | 35 ++++++ src/services/code-index/manager.ts | 13 +-- 6 files changed, 159 insertions(+), 60 deletions(-) create mode 100644 src/services/code-index/__tests__/embedder-readiness-manager.spec.ts create mode 100644 src/services/code-index/embedder-readiness-manager.ts diff --git a/src/services/code-index/__tests__/code-index-status-manager.spec.ts b/src/services/code-index/__tests__/code-index-status-manager.spec.ts index c73fb4f03b..4b1a585a09 100644 --- a/src/services/code-index/__tests__/code-index-status-manager.spec.ts +++ b/src/services/code-index/__tests__/code-index-status-manager.spec.ts @@ -8,6 +8,7 @@ import { } from "../../../test-utils/vscode" import { CodeIndexManager } from "../manager" import { CodeIndexStateManager } from "../state-manager" +import { EmbedderReadinessManager } from "../embedder-readiness-manager" import { CodeIndexStatusManager, type CodeIndexStatus } from "../code-index-status-manager" // Reload the real workspace resolver against this suite's VS Code mock, @@ -40,11 +41,13 @@ function makeSource(workspacePath: string) { emit = () => listener(status) return subscription } + const stateManager = new CodeIndexStateManager() const manager = new CodeIndexManager( workspacePath, makeUri(workspacePath), makeExtensionContext(), - new CodeIndexStateManager(), + stateManager, + new EmbedderReadinessManager(stateManager), ) Object.defineProperty(manager, "onProgressUpdate", { value: vi.fn(event), configurable: true }) return Object.assign(manager, { diff --git a/src/services/code-index/__tests__/embedder-readiness-manager.spec.ts b/src/services/code-index/__tests__/embedder-readiness-manager.spec.ts new file mode 100644 index 0000000000..86f24ecaed --- /dev/null +++ b/src/services/code-index/__tests__/embedder-readiness-manager.spec.ts @@ -0,0 +1,100 @@ +import { EmbedderReadinessManager } from "../embedder-readiness-manager" +import type { IndexingState } from "../state-manager" +import type { CodeIndexServiceFactory } from "../service-factory" +import type { IEmbedder } from "../interfaces" + +describe("EmbedderReadinessManager", () => { + const setup = () => { + const stateManager = { + state: "Standby" as IndexingState, + setSystemState: vi.fn(), + } + const embedder: IEmbedder = { + embedderInfo: { name: "ollama" }, + createEmbeddings: vi.fn(), + validateConfiguration: vi.fn(), + } + const serviceFactory = { + validateEmbedder: vi.fn(), + } as unknown as CodeIndexServiceFactory + return { stateManager, manager: new EmbedderReadinessManager(stateManager), serviceFactory, embedder } + } + + it("reports the latest validation failure while the manager remains in standby", async () => { + const { stateManager, manager, serviceFactory, embedder } = setup() + vi.mocked(serviceFactory.validateEmbedder).mockResolvedValue({ + valid: false, + error: "Current validation failure", + }) + + void manager.validate(serviceFactory, embedder) + await Promise.resolve() + + expect(stateManager.setSystemState).toHaveBeenCalledExactlyOnceWith("Error", "Current validation failure") + }) + + it("reports an unexpected validation rejection while the manager remains in standby", async () => { + const { stateManager, manager, serviceFactory, embedder } = setup() + vi.mocked(serviceFactory.validateEmbedder).mockRejectedValue(new Error("Validation crashed")) + + void manager.validate(serviceFactory, embedder) + await Promise.resolve() + await Promise.resolve() + + expect(stateManager.setSystemState).toHaveBeenCalledExactlyOnceWith("Error", "Validation crashed") + }) + + it("ignores validation results after another operation changes the status", async () => { + const { stateManager, manager, serviceFactory, embedder } = setup() + let finishValidation!: (result: { valid: boolean; error?: string }) => void + vi.mocked(serviceFactory.validateEmbedder).mockReturnValue( + new Promise((resolve) => { + finishValidation = resolve + }), + ) + void manager.validate(serviceFactory, embedder) + + stateManager.state = "Indexed" + finishValidation({ valid: false, error: "Stale validation failure" }) + await Promise.resolve() + + expect(stateManager.setSystemState).not.toHaveBeenCalled() + }) + + it("ignores an older result when a newer validation has started", async () => { + const { stateManager, manager, serviceFactory, embedder } = setup() + let finishFirstValidation!: (result: { valid: boolean; error?: string }) => void + vi.mocked(serviceFactory.validateEmbedder) + .mockReturnValueOnce( + new Promise((resolve) => { + finishFirstValidation = resolve + }), + ) + .mockResolvedValueOnce({ valid: true }) + void manager.validate(serviceFactory, embedder) + void manager.validate(serviceFactory, embedder) + await Promise.resolve() + + finishFirstValidation({ valid: false, error: "Older validation failure" }) + await Promise.resolve() + + expect(stateManager.setSystemState).not.toHaveBeenCalled() + }) + + it("ignores a pending result after invalidation", async () => { + const { stateManager, manager, serviceFactory, embedder } = setup() + let finishValidation!: (result: { valid: boolean; error?: string }) => void + vi.mocked(serviceFactory.validateEmbedder).mockReturnValue( + new Promise((resolve) => { + finishValidation = resolve + }), + ) + void manager.validate(serviceFactory, embedder) + + manager.invalidate() + finishValidation({ valid: false, error: "Invalidated validation failure" }) + await Promise.resolve() + + expect(stateManager.setSystemState).not.toHaveBeenCalled() + }) +}) diff --git a/src/services/code-index/__tests__/manager.spec.ts b/src/services/code-index/__tests__/manager.spec.ts index 952e205fdd..ab98005bfc 100644 --- a/src/services/code-index/__tests__/manager.spec.ts +++ b/src/services/code-index/__tests__/manager.spec.ts @@ -474,63 +474,24 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { ;(manager as any)._configManager = mockConfigManager }) - it("should validate embedder during _recreateServices when validation succeeds", async () => { - // Arrange - mockServiceFactoryInstance.validateEmbedder.mockResolvedValue({ valid: true }) + it("should create indexing services without waiting for startup embedder validation", async () => { + let finishValidation!: (result: { valid: boolean; error?: string }) => void + mockServiceFactoryInstance.validateEmbedder.mockReturnValue( + new Promise((resolve) => { + finishValidation = resolve + }), + ) - // Act - directly call the private method for testing await (manager as any)._recreateServices() - // Assert expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled() - const createdEmbedder = mockServiceFactoryInstance.createServices.mock.results[0].value.embedder - expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(createdEmbedder) + expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(mockEmbedder) expect(mockStateManager.setSystemState).not.toHaveBeenCalledWith("Error", expect.any(String)) - }) - - it("should set error state when embedder validation fails", async () => { - // Arrange - mockServiceFactoryInstance.validateEmbedder.mockResolvedValue({ - valid: false, - error: "embeddings:validation.authenticationFailed", - }) - - // Act & Assert - await expect((manager as any)._recreateServices()).rejects.toThrow( - "embeddings:validation.authenticationFailed", - ) - - // Assert other expectations - expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled() - const createdEmbedder = mockServiceFactoryInstance.createServices.mock.results[0].value.embedder - expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(createdEmbedder) - expect(mockStateManager.setSystemState).toHaveBeenCalledWith( - "Error", - "embeddings:validation.authenticationFailed", - ) - }) - - it("should set generic error state when embedder validation throws", async () => { - // Arrange - // Since the real service factory catches exceptions, we should mock it to resolve with an error - mockServiceFactoryInstance.validateEmbedder.mockResolvedValue({ - valid: false, - error: "embeddings:validation.configurationError", - }) - - // Act & Assert - await expect((manager as any)._recreateServices()).rejects.toThrow( - "embeddings:validation.configurationError", - ) + expect((manager as any)._orchestrator).toBeDefined() + expect((manager as any)._searchService).toBeDefined() - // Assert other expectations - expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled() - const createdEmbedder = mockServiceFactoryInstance.createServices.mock.results[0].value.embedder - expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(createdEmbedder) - expect(mockStateManager.setSystemState).toHaveBeenCalledWith( - "Error", - "embeddings:validation.configurationError", - ) + finishValidation({ valid: true }) + await Promise.resolve() }) it("should handle embedder creation failure", async () => { diff --git a/src/services/code-index/code-index-workspace-scope.ts b/src/services/code-index/code-index-workspace-scope.ts index c8481f6595..b6f348b1ff 100644 --- a/src/services/code-index/code-index-workspace-scope.ts +++ b/src/services/code-index/code-index-workspace-scope.ts @@ -3,6 +3,7 @@ import type * as vscode from "vscode" import { CodeIndexManager } from "./manager" import { CodeIndexStateManager } from "./state-manager" import { WorkspaceIndexingEnablementManager } from "./workspace-indexing-enablement-manager" +import { EmbedderReadinessManager } from "./embedder-readiness-manager" /** Owns code-index services for one workspace; initialization remains with existing callers. */ export class CodeIndexWorkspaceScope implements vscode.Disposable { @@ -38,11 +39,13 @@ export class CodeIndexWorkspaceScope implements vscode.Disposable { throw new Error("Code index workspace scope is already initialized") } this._stateManager = new CodeIndexStateManager() + const embedderReadinessManager = new EmbedderReadinessManager(this._stateManager) this._codeIndexManager = new CodeIndexManager( this.workspacePath, this.folderUri, this.context, this._stateManager, + embedderReadinessManager, ) this._workspaceIndexingEnablementManager = new WorkspaceIndexingEnablementManager(this._codeIndexManager) this._isInitialized = true diff --git a/src/services/code-index/embedder-readiness-manager.ts b/src/services/code-index/embedder-readiness-manager.ts new file mode 100644 index 0000000000..38cd45fb77 --- /dev/null +++ b/src/services/code-index/embedder-readiness-manager.ts @@ -0,0 +1,35 @@ +import type { CodeIndexStateManager } from "./state-manager" +import type { IEmbedder } from "./interfaces" +import type { CodeIndexServiceFactory } from "./service-factory" + +type ValidationResult = { valid: boolean; error?: string } + +/** Tracks embedder readiness without blocking initialization and discards stale results. */ +export class EmbedderReadinessManager { + private generation = 0 + + public constructor(private readonly stateManager: Pick) {} + + public async validate(serviceFactory: CodeIndexServiceFactory, embedder: IEmbedder): Promise { + const generation = ++this.generation + try { + const result: ValidationResult = await serviceFactory.validateEmbedder(embedder) + if (!this.canApply(generation) || result.valid) return + this.stateManager.setSystemState("Error", result.error || "Embedder configuration validation failed") + } catch (error) { + if (!this.canApply(generation)) return + this.stateManager.setSystemState( + "Error", + error instanceof Error ? error.message : "Embedder configuration validation failed", + ) + } + } + + public invalidate(): void { + this.generation++ + } + + private canApply(generation: number): boolean { + return generation === this.generation && this.stateManager.state === "Standby" + } +} diff --git a/src/services/code-index/manager.ts b/src/services/code-index/manager.ts index bb186dc105..fb7a7496a2 100644 --- a/src/services/code-index/manager.ts +++ b/src/services/code-index/manager.ts @@ -16,6 +16,7 @@ import path from "path" import { t } from "../../i18n" import { TelemetryService } from "@roo-code/telemetry" import { TelemetryEventName } from "@roo-code/types" +import { EmbedderReadinessManager } from "./embedder-readiness-manager" export class CodeIndexManager { // Specialized class instances @@ -40,6 +41,7 @@ export class CodeIndexManager { folderUri: vscode.Uri, context: vscode.ExtensionContext, stateManager: CodeIndexStateManager, + private readonly embedderReadinessManager: EmbedderReadinessManager, ) { this.workspacePath = workspacePath this._folderUri = folderUri @@ -234,6 +236,7 @@ export class CodeIndexManager { * Stops any in-progress indexing operation and the file watcher. */ public stopIndexing(): void { + this.embedderReadinessManager.invalidate() if (this._sembleProvider) { this._sembleProvider.stopIndexing() return @@ -276,6 +279,7 @@ export class CodeIndexManager { } this._isRecoveringFromError = true + this.embedderReadinessManager.invalidate() try { // Clear error state this._stateManager.setSystemState("Standby", "") @@ -415,14 +419,7 @@ export class CodeIndexManager { ignoreInstance, rooIgnoreController, ) - - // Validate embedder configuration before proceeding - const validationResult = await this._serviceFactory.validateEmbedder(embedder) - if (!validationResult.valid) { - const errorMessage = validationResult.error || "Embedder configuration validation failed" - this._stateManager.setSystemState("Error", errorMessage) - throw new Error(errorMessage) - } + void this.embedderReadinessManager.validate(this._serviceFactory, embedder) // (Re)Initialize orchestrator this._orchestrator = new CodeIndexOrchestrator( From d5b4b34953996e417433286ee917fcf8f2cde752 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Thu, 1 Oct 2026 13:38:30 +0300 Subject: [PATCH 02/10] test(code-index): update scope constructor expectations for readiness manager --- .../code-index-manager-registry.spec.ts | 50 ++++++++++++++++--- .../code-index-workspace-scope.spec.ts | 8 +++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/services/code-index/__tests__/code-index-manager-registry.spec.ts b/src/services/code-index/__tests__/code-index-manager-registry.spec.ts index 0d5012a641..2db00a8300 100644 --- a/src/services/code-index/__tests__/code-index-manager-registry.spec.ts +++ b/src/services/code-index/__tests__/code-index-manager-registry.spec.ts @@ -4,6 +4,7 @@ import { CodeIndexManager } from "../manager" import { CodeIndexManagerRegistry } from "../code-index-manager-registry" import { CodeIndexWorkspaceScope } from "../code-index-workspace-scope" import { CodeIndexStateManager } from "../state-manager" +import { EmbedderReadinessManager } from "../embedder-readiness-manager" vi.mock("../state-manager") @@ -60,7 +61,13 @@ describe("CodeIndexManagerRegistry", () => { it("uses the first workspace when there is no active editor", () => { CodeIndexManagerRegistry.getOrCreate(context) - expect(CodeIndexManager).toHaveBeenCalledWith("/first", first.uri, context, expect.any(CodeIndexStateManager)) + expect(CodeIndexManager).toHaveBeenCalledWith( + "/first", + first.uri, + context, + expect.any(CodeIndexStateManager), + expect.any(EmbedderReadinessManager), + ) }) it("prefers the active editor's workspace", () => { @@ -68,20 +75,38 @@ describe("CodeIndexManagerRegistry", () => { Object.defineProperty(vscode.window, "activeTextEditor", { configurable: true, value: editor }) vi.mocked(vscode.workspace.getWorkspaceFolder).mockReturnValue(second) expect(CodeIndexManagerRegistry.getOrCreate(context)).toBeDefined() - expect(CodeIndexManager).toHaveBeenCalledWith("/second", second.uri, context, expect.any(CodeIndexStateManager)) + expect(CodeIndexManager).toHaveBeenCalledWith( + "/second", + second.uri, + context, + expect.any(CodeIndexStateManager), + expect.any(EmbedderReadinessManager), + ) }) it("falls back to the first workspace for an editor outside all folders", () => { Object.defineProperty(vscode.window, "activeTextEditor", { configurable: true, value: makeTextEditor() }) CodeIndexManagerRegistry.getOrCreate(context) - expect(CodeIndexManager).toHaveBeenCalledWith("/first", first.uri, context, expect.any(CodeIndexStateManager)) + expect(CodeIndexManager).toHaveBeenCalledWith( + "/first", + first.uri, + context, + expect.any(CodeIndexStateManager), + expect.any(EmbedderReadinessManager), + ) }) it("gives an explicit path priority over the active editor", () => { Object.defineProperty(vscode.window, "activeTextEditor", { configurable: true, value: makeTextEditor() }) vi.mocked(vscode.workspace.getWorkspaceFolder).mockReturnValue(first) expect(CodeIndexManagerRegistry.getOrCreate(context, "/second")).toBeDefined() - expect(CodeIndexManager).toHaveBeenCalledWith("/second", second.uri, context, expect.any(CodeIndexStateManager)) + expect(CodeIndexManager).toHaveBeenCalledWith( + "/second", + second.uri, + context, + expect.any(CodeIndexStateManager), + expect.any(EmbedderReadinessManager), + ) }) it("preserves the actual remote workspace URI", () => { @@ -91,7 +116,13 @@ describe("CodeIndexManagerRegistry", () => { value: [{ uri, name: "remote", index: 0 }], }) CodeIndexManagerRegistry.getOrCreate(context, "/remote") - expect(CodeIndexManager).toHaveBeenCalledWith("/remote", uri, context, expect.any(CodeIndexStateManager)) + expect(CodeIndexManager).toHaveBeenCalledWith( + "/remote", + uri, + context, + expect.any(CodeIndexStateManager), + expect.any(EmbedderReadinessManager), + ) expect(vi.mocked(CodeIndexManager).mock.calls[0][1]).toBe(uri) expect(vscode.Uri.file).not.toHaveBeenCalled() }) @@ -102,7 +133,13 @@ describe("CodeIndexManagerRegistry", () => { vi.mocked(vscode.Uri.file).mockReturnValue(uri) CodeIndexManagerRegistry.getOrCreate(context, uri.fsPath) expect(vscode.Uri.file).toHaveBeenCalledWith(uri.fsPath) - expect(CodeIndexManager).toHaveBeenCalledWith(uri.fsPath, uri, context, expect.any(CodeIndexStateManager)) + expect(CodeIndexManager).toHaveBeenCalledWith( + uri.fsPath, + uri, + context, + expect.any(CodeIndexStateManager), + expect.any(EmbedderReadinessManager), + ) }) it("constructs a file URI for an explicit path not matching any open workspace folder", () => { @@ -116,6 +153,7 @@ describe("CodeIndexManagerRegistry", () => { uri, context, expect.any(CodeIndexStateManager), + expect.any(EmbedderReadinessManager), ) }) diff --git a/src/services/code-index/__tests__/code-index-workspace-scope.spec.ts b/src/services/code-index/__tests__/code-index-workspace-scope.spec.ts index 417d7284c1..a1f4a297bf 100644 --- a/src/services/code-index/__tests__/code-index-workspace-scope.spec.ts +++ b/src/services/code-index/__tests__/code-index-workspace-scope.spec.ts @@ -2,6 +2,7 @@ import { makeExtensionContext, makeUri } from "../../../test-utils/vscode" import { CodeIndexManager } from "../manager" import { CodeIndexWorkspaceScope } from "../code-index-workspace-scope" import { CodeIndexStateManager } from "../state-manager" +import { EmbedderReadinessManager } from "../embedder-readiness-manager" import { WorkspaceIndexingEnablementManager } from "../workspace-indexing-enablement-manager" vi.mock("../state-manager") @@ -57,6 +58,7 @@ describe("CodeIndexWorkspaceScope", () => { uri, context, vi.mocked(CodeIndexStateManager).mock.instances[0], + expect.any(EmbedderReadinessManager), ) expect(scope.codeIndexManager).toBe(manager) expect(() => scope.init()).toThrow("Code index workspace scope is already initialized") @@ -98,6 +100,9 @@ describe("CodeIndexWorkspaceScope", () => { const calls = vi.mocked(CodeIndexManager).mock.calls expect(CodeIndexStateManager).toHaveBeenCalledTimes(2) expect(calls[1][3]).not.toBe(calls[0][3]) + expect(calls[1][4]).not.toBe(calls[0][4]) + expect(calls[0][4]["stateManager"]).toBe(calls[0][3]) + expect(calls[1][4]["stateManager"]).toBe(calls[1][3]) expect(scope["_stateManager"]).toBe(calls[1][3]) }) @@ -113,6 +118,9 @@ describe("CodeIndexWorkspaceScope", () => { expect(calls[0][3]).toBe(vi.mocked(CodeIndexStateManager).mock.instances[0]) expect(calls[1][3]).toBe(vi.mocked(CodeIndexStateManager).mock.instances[1]) expect(calls[1][3]).not.toBe(calls[0][3]) + expect(calls[1][4]).not.toBe(calls[0][4]) + expect(calls[0][4]["stateManager"]).toBe(calls[0][3]) + expect(calls[1][4]["stateManager"]).toBe(calls[1][3]) }) it("clears its reference even when manager disposal throws", () => { From 72c72af6cc56d5966877a3a8f9ab8c7d5d53ff5a Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Thu, 1 Oct 2026 13:44:51 +0300 Subject: [PATCH 03/10] test(code-index): include readiness dependency in search workspace assertion --- src/core/tools/__tests__/CodebaseSearchTool.workspace.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/tools/__tests__/CodebaseSearchTool.workspace.spec.ts b/src/core/tools/__tests__/CodebaseSearchTool.workspace.spec.ts index f2c4cc2aed..b3bce8efc7 100644 --- a/src/core/tools/__tests__/CodebaseSearchTool.workspace.spec.ts +++ b/src/core/tools/__tests__/CodebaseSearchTool.workspace.spec.ts @@ -7,6 +7,7 @@ import { CodebaseSearchTool } from "../CodebaseSearchTool" import { CodeIndexManagerRegistry } from "../../../services/code-index/code-index-manager-registry" import { CodeIndexManager } from "../../../services/code-index/manager" import { CodeIndexStateManager } from "../../../services/code-index/state-manager" +import { EmbedderReadinessManager } from "../../../services/code-index/embedder-readiness-manager" import { getWorkspacePath } from "../../../utils/path" import { makeExtensionContext, makeTextDocument, makeTextEditor, makeUri } from "../../../test-utils/vscode" @@ -199,6 +200,7 @@ describe("CodebaseSearchTool workspace selection", () => { expect.objectContaining({ fsPath: "/external-task" }), provider.context, expect.any(CodeIndexStateManager), + expect.any(EmbedderReadinessManager), ) expect(vscode.Uri.file).toHaveBeenCalledExactlyOnceWith("/external-task") const manager = CodeIndexManagerRegistry.getOrCreate(provider.context, "/external-task")! From aaeb28ca3159489242ae7543f92a8124bf544cd1 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Thu, 1 Oct 2026 14:02:46 +0300 Subject: [PATCH 04/10] test(code-index): cover stale validation rejections and strengthen service assertions --- src/eslint-suppressions.json | 2 +- .../embedder-readiness-manager.spec.ts | 27 +++++++++++++++++++ .../code-index/__tests__/manager.spec.ts | 8 +++--- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index 24db0bf433..6ca8a4e134 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -1301,7 +1301,7 @@ }, "services/code-index/__tests__/manager.spec.ts": { "@typescript-eslint/no-explicit-any": { - "count": 87 + "count": 84 } }, "services/code-index/__tests__/orchestrator.spec.ts": { diff --git a/src/services/code-index/__tests__/embedder-readiness-manager.spec.ts b/src/services/code-index/__tests__/embedder-readiness-manager.spec.ts index 86f24ecaed..b9b3f986cb 100644 --- a/src/services/code-index/__tests__/embedder-readiness-manager.spec.ts +++ b/src/services/code-index/__tests__/embedder-readiness-manager.spec.ts @@ -81,6 +81,33 @@ describe("EmbedderReadinessManager", () => { expect(stateManager.setSystemState).not.toHaveBeenCalled() }) + it.each(["invalidation", "newer validation", "status change"] as const)( + "ignores a pending rejection after %s", + async (scenario) => { + const { stateManager, manager, serviceFactory, embedder } = setup() + let rejectValidation!: (error: Error) => void + vi.mocked(serviceFactory.validateEmbedder).mockReturnValueOnce( + new Promise((_, reject) => { + rejectValidation = reject + }), + ) + const validation = manager.validate(serviceFactory, embedder) + + if (scenario === "invalidation") { + manager.invalidate() + } else if (scenario === "newer validation") { + vi.mocked(serviceFactory.validateEmbedder).mockResolvedValueOnce({ valid: true }) + await manager.validate(serviceFactory, embedder) + } else { + stateManager.state = "Indexed" + } + + rejectValidation(new Error("Stale rejection")) + await expect(validation).resolves.toBeUndefined() + expect(stateManager.setSystemState).not.toHaveBeenCalled() + }, + ) + it("ignores a pending result after invalidation", async () => { const { stateManager, manager, serviceFactory, embedder } = setup() let finishValidation!: (result: { valid: boolean; error?: string }) => void diff --git a/src/services/code-index/__tests__/manager.spec.ts b/src/services/code-index/__tests__/manager.spec.ts index ab98005bfc..e537a6be6d 100644 --- a/src/services/code-index/__tests__/manager.spec.ts +++ b/src/services/code-index/__tests__/manager.spec.ts @@ -3,6 +3,8 @@ import { makeExtensionContext } from "../../../test-utils/vscode" import { CodeIndexManager } from "../manager" import { CodeIndexManagerRegistry } from "../code-index-manager-registry" import { CodeIndexServiceFactory } from "../service-factory" +import { CodeIndexOrchestrator } from "../orchestrator" +import { CodeIndexSearchService } from "../search-service" import type { MockedClass } from "vitest" import * as path from "path" import { providerIdentifiers } from "@roo-code/types/provider-identifiers" @@ -482,13 +484,13 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { }), ) - await (manager as any)._recreateServices() + await manager["_recreateServices"]() expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled() expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(mockEmbedder) expect(mockStateManager.setSystemState).not.toHaveBeenCalledWith("Error", expect.any(String)) - expect((manager as any)._orchestrator).toBeDefined() - expect((manager as any)._searchService).toBeDefined() + expect(manager["_orchestrator"]).toBeInstanceOf(CodeIndexOrchestrator) + expect(manager["_searchService"]).toBeInstanceOf(CodeIndexSearchService) finishValidation({ valid: true }) await Promise.resolve() From 50cbd4c5b9a8e2af49a84e4e44a8d41c590d6112 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Thu, 1 Oct 2026 14:38:30 +0300 Subject: [PATCH 05/10] test(code-index): cover readiness validation fallback errors --- .../embedder-readiness-manager.spec.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/services/code-index/__tests__/embedder-readiness-manager.spec.ts b/src/services/code-index/__tests__/embedder-readiness-manager.spec.ts index b9b3f986cb..123c105295 100644 --- a/src/services/code-index/__tests__/embedder-readiness-manager.spec.ts +++ b/src/services/code-index/__tests__/embedder-readiness-manager.spec.ts @@ -33,6 +33,30 @@ describe("EmbedderReadinessManager", () => { expect(stateManager.setSystemState).toHaveBeenCalledExactlyOnceWith("Error", "Current validation failure") }) + it.each([undefined, ""])("uses the fallback for a validation failure with message %j", async (error) => { + const { stateManager, manager, serviceFactory, embedder } = setup() + vi.mocked(serviceFactory.validateEmbedder).mockResolvedValue({ valid: false, error }) + + await manager.validate(serviceFactory, embedder) + + expect(stateManager.setSystemState).toHaveBeenCalledExactlyOnceWith( + "Error", + "Embedder configuration validation failed", + ) + }) + + it("uses the fallback for a non-Error rejection", async () => { + const { stateManager, manager, serviceFactory, embedder } = setup() + vi.mocked(serviceFactory.validateEmbedder).mockRejectedValue("Unexpected rejection") + + await expect(manager.validate(serviceFactory, embedder)).resolves.toBeUndefined() + + expect(stateManager.setSystemState).toHaveBeenCalledExactlyOnceWith( + "Error", + "Embedder configuration validation failed", + ) + }) + it("reports an unexpected validation rejection while the manager remains in standby", async () => { const { stateManager, manager, serviceFactory, embedder } = setup() vi.mocked(serviceFactory.validateEmbedder).mockRejectedValue(new Error("Validation crashed")) From 505a7992427d9bae797a0a4c9544dbc22954cde4 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Fri, 2 Oct 2026 01:25:23 +0300 Subject: [PATCH 06/10] fix(code-index): remove automatic embedder preflight validation --- .../CodebaseSearchTool.workspace.spec.ts | 2 - .../code-index-manager-registry.spec.ts | 50 +----- .../code-index-status-manager.spec.ts | 5 +- .../code-index-workspace-scope.spec.ts | 8 - .../embedder-readiness-manager.spec.ts | 151 ------------------ .../code-index/__tests__/manager.spec.ts | 19 +-- .../code-index/code-index-workspace-scope.ts | 3 - .../code-index/embedder-readiness-manager.ts | 35 ---- src/services/code-index/manager.ts | 5 - 9 files changed, 14 insertions(+), 264 deletions(-) delete mode 100644 src/services/code-index/__tests__/embedder-readiness-manager.spec.ts delete mode 100644 src/services/code-index/embedder-readiness-manager.ts diff --git a/src/core/tools/__tests__/CodebaseSearchTool.workspace.spec.ts b/src/core/tools/__tests__/CodebaseSearchTool.workspace.spec.ts index b3bce8efc7..f2c4cc2aed 100644 --- a/src/core/tools/__tests__/CodebaseSearchTool.workspace.spec.ts +++ b/src/core/tools/__tests__/CodebaseSearchTool.workspace.spec.ts @@ -7,7 +7,6 @@ import { CodebaseSearchTool } from "../CodebaseSearchTool" import { CodeIndexManagerRegistry } from "../../../services/code-index/code-index-manager-registry" import { CodeIndexManager } from "../../../services/code-index/manager" import { CodeIndexStateManager } from "../../../services/code-index/state-manager" -import { EmbedderReadinessManager } from "../../../services/code-index/embedder-readiness-manager" import { getWorkspacePath } from "../../../utils/path" import { makeExtensionContext, makeTextDocument, makeTextEditor, makeUri } from "../../../test-utils/vscode" @@ -200,7 +199,6 @@ describe("CodebaseSearchTool workspace selection", () => { expect.objectContaining({ fsPath: "/external-task" }), provider.context, expect.any(CodeIndexStateManager), - expect.any(EmbedderReadinessManager), ) expect(vscode.Uri.file).toHaveBeenCalledExactlyOnceWith("/external-task") const manager = CodeIndexManagerRegistry.getOrCreate(provider.context, "/external-task")! diff --git a/src/services/code-index/__tests__/code-index-manager-registry.spec.ts b/src/services/code-index/__tests__/code-index-manager-registry.spec.ts index 2db00a8300..0d5012a641 100644 --- a/src/services/code-index/__tests__/code-index-manager-registry.spec.ts +++ b/src/services/code-index/__tests__/code-index-manager-registry.spec.ts @@ -4,7 +4,6 @@ import { CodeIndexManager } from "../manager" import { CodeIndexManagerRegistry } from "../code-index-manager-registry" import { CodeIndexWorkspaceScope } from "../code-index-workspace-scope" import { CodeIndexStateManager } from "../state-manager" -import { EmbedderReadinessManager } from "../embedder-readiness-manager" vi.mock("../state-manager") @@ -61,13 +60,7 @@ describe("CodeIndexManagerRegistry", () => { it("uses the first workspace when there is no active editor", () => { CodeIndexManagerRegistry.getOrCreate(context) - expect(CodeIndexManager).toHaveBeenCalledWith( - "/first", - first.uri, - context, - expect.any(CodeIndexStateManager), - expect.any(EmbedderReadinessManager), - ) + expect(CodeIndexManager).toHaveBeenCalledWith("/first", first.uri, context, expect.any(CodeIndexStateManager)) }) it("prefers the active editor's workspace", () => { @@ -75,38 +68,20 @@ describe("CodeIndexManagerRegistry", () => { Object.defineProperty(vscode.window, "activeTextEditor", { configurable: true, value: editor }) vi.mocked(vscode.workspace.getWorkspaceFolder).mockReturnValue(second) expect(CodeIndexManagerRegistry.getOrCreate(context)).toBeDefined() - expect(CodeIndexManager).toHaveBeenCalledWith( - "/second", - second.uri, - context, - expect.any(CodeIndexStateManager), - expect.any(EmbedderReadinessManager), - ) + expect(CodeIndexManager).toHaveBeenCalledWith("/second", second.uri, context, expect.any(CodeIndexStateManager)) }) it("falls back to the first workspace for an editor outside all folders", () => { Object.defineProperty(vscode.window, "activeTextEditor", { configurable: true, value: makeTextEditor() }) CodeIndexManagerRegistry.getOrCreate(context) - expect(CodeIndexManager).toHaveBeenCalledWith( - "/first", - first.uri, - context, - expect.any(CodeIndexStateManager), - expect.any(EmbedderReadinessManager), - ) + expect(CodeIndexManager).toHaveBeenCalledWith("/first", first.uri, context, expect.any(CodeIndexStateManager)) }) it("gives an explicit path priority over the active editor", () => { Object.defineProperty(vscode.window, "activeTextEditor", { configurable: true, value: makeTextEditor() }) vi.mocked(vscode.workspace.getWorkspaceFolder).mockReturnValue(first) expect(CodeIndexManagerRegistry.getOrCreate(context, "/second")).toBeDefined() - expect(CodeIndexManager).toHaveBeenCalledWith( - "/second", - second.uri, - context, - expect.any(CodeIndexStateManager), - expect.any(EmbedderReadinessManager), - ) + expect(CodeIndexManager).toHaveBeenCalledWith("/second", second.uri, context, expect.any(CodeIndexStateManager)) }) it("preserves the actual remote workspace URI", () => { @@ -116,13 +91,7 @@ describe("CodeIndexManagerRegistry", () => { value: [{ uri, name: "remote", index: 0 }], }) CodeIndexManagerRegistry.getOrCreate(context, "/remote") - expect(CodeIndexManager).toHaveBeenCalledWith( - "/remote", - uri, - context, - expect.any(CodeIndexStateManager), - expect.any(EmbedderReadinessManager), - ) + expect(CodeIndexManager).toHaveBeenCalledWith("/remote", uri, context, expect.any(CodeIndexStateManager)) expect(vi.mocked(CodeIndexManager).mock.calls[0][1]).toBe(uri) expect(vscode.Uri.file).not.toHaveBeenCalled() }) @@ -133,13 +102,7 @@ describe("CodeIndexManagerRegistry", () => { vi.mocked(vscode.Uri.file).mockReturnValue(uri) CodeIndexManagerRegistry.getOrCreate(context, uri.fsPath) expect(vscode.Uri.file).toHaveBeenCalledWith(uri.fsPath) - expect(CodeIndexManager).toHaveBeenCalledWith( - uri.fsPath, - uri, - context, - expect.any(CodeIndexStateManager), - expect.any(EmbedderReadinessManager), - ) + expect(CodeIndexManager).toHaveBeenCalledWith(uri.fsPath, uri, context, expect.any(CodeIndexStateManager)) }) it("constructs a file URI for an explicit path not matching any open workspace folder", () => { @@ -153,7 +116,6 @@ describe("CodeIndexManagerRegistry", () => { uri, context, expect.any(CodeIndexStateManager), - expect.any(EmbedderReadinessManager), ) }) diff --git a/src/services/code-index/__tests__/code-index-status-manager.spec.ts b/src/services/code-index/__tests__/code-index-status-manager.spec.ts index 4b1a585a09..c73fb4f03b 100644 --- a/src/services/code-index/__tests__/code-index-status-manager.spec.ts +++ b/src/services/code-index/__tests__/code-index-status-manager.spec.ts @@ -8,7 +8,6 @@ import { } from "../../../test-utils/vscode" import { CodeIndexManager } from "../manager" import { CodeIndexStateManager } from "../state-manager" -import { EmbedderReadinessManager } from "../embedder-readiness-manager" import { CodeIndexStatusManager, type CodeIndexStatus } from "../code-index-status-manager" // Reload the real workspace resolver against this suite's VS Code mock, @@ -41,13 +40,11 @@ function makeSource(workspacePath: string) { emit = () => listener(status) return subscription } - const stateManager = new CodeIndexStateManager() const manager = new CodeIndexManager( workspacePath, makeUri(workspacePath), makeExtensionContext(), - stateManager, - new EmbedderReadinessManager(stateManager), + new CodeIndexStateManager(), ) Object.defineProperty(manager, "onProgressUpdate", { value: vi.fn(event), configurable: true }) return Object.assign(manager, { diff --git a/src/services/code-index/__tests__/code-index-workspace-scope.spec.ts b/src/services/code-index/__tests__/code-index-workspace-scope.spec.ts index a1f4a297bf..417d7284c1 100644 --- a/src/services/code-index/__tests__/code-index-workspace-scope.spec.ts +++ b/src/services/code-index/__tests__/code-index-workspace-scope.spec.ts @@ -2,7 +2,6 @@ import { makeExtensionContext, makeUri } from "../../../test-utils/vscode" import { CodeIndexManager } from "../manager" import { CodeIndexWorkspaceScope } from "../code-index-workspace-scope" import { CodeIndexStateManager } from "../state-manager" -import { EmbedderReadinessManager } from "../embedder-readiness-manager" import { WorkspaceIndexingEnablementManager } from "../workspace-indexing-enablement-manager" vi.mock("../state-manager") @@ -58,7 +57,6 @@ describe("CodeIndexWorkspaceScope", () => { uri, context, vi.mocked(CodeIndexStateManager).mock.instances[0], - expect.any(EmbedderReadinessManager), ) expect(scope.codeIndexManager).toBe(manager) expect(() => scope.init()).toThrow("Code index workspace scope is already initialized") @@ -100,9 +98,6 @@ describe("CodeIndexWorkspaceScope", () => { const calls = vi.mocked(CodeIndexManager).mock.calls expect(CodeIndexStateManager).toHaveBeenCalledTimes(2) expect(calls[1][3]).not.toBe(calls[0][3]) - expect(calls[1][4]).not.toBe(calls[0][4]) - expect(calls[0][4]["stateManager"]).toBe(calls[0][3]) - expect(calls[1][4]["stateManager"]).toBe(calls[1][3]) expect(scope["_stateManager"]).toBe(calls[1][3]) }) @@ -118,9 +113,6 @@ describe("CodeIndexWorkspaceScope", () => { expect(calls[0][3]).toBe(vi.mocked(CodeIndexStateManager).mock.instances[0]) expect(calls[1][3]).toBe(vi.mocked(CodeIndexStateManager).mock.instances[1]) expect(calls[1][3]).not.toBe(calls[0][3]) - expect(calls[1][4]).not.toBe(calls[0][4]) - expect(calls[0][4]["stateManager"]).toBe(calls[0][3]) - expect(calls[1][4]["stateManager"]).toBe(calls[1][3]) }) it("clears its reference even when manager disposal throws", () => { diff --git a/src/services/code-index/__tests__/embedder-readiness-manager.spec.ts b/src/services/code-index/__tests__/embedder-readiness-manager.spec.ts deleted file mode 100644 index 123c105295..0000000000 --- a/src/services/code-index/__tests__/embedder-readiness-manager.spec.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { EmbedderReadinessManager } from "../embedder-readiness-manager" -import type { IndexingState } from "../state-manager" -import type { CodeIndexServiceFactory } from "../service-factory" -import type { IEmbedder } from "../interfaces" - -describe("EmbedderReadinessManager", () => { - const setup = () => { - const stateManager = { - state: "Standby" as IndexingState, - setSystemState: vi.fn(), - } - const embedder: IEmbedder = { - embedderInfo: { name: "ollama" }, - createEmbeddings: vi.fn(), - validateConfiguration: vi.fn(), - } - const serviceFactory = { - validateEmbedder: vi.fn(), - } as unknown as CodeIndexServiceFactory - return { stateManager, manager: new EmbedderReadinessManager(stateManager), serviceFactory, embedder } - } - - it("reports the latest validation failure while the manager remains in standby", async () => { - const { stateManager, manager, serviceFactory, embedder } = setup() - vi.mocked(serviceFactory.validateEmbedder).mockResolvedValue({ - valid: false, - error: "Current validation failure", - }) - - void manager.validate(serviceFactory, embedder) - await Promise.resolve() - - expect(stateManager.setSystemState).toHaveBeenCalledExactlyOnceWith("Error", "Current validation failure") - }) - - it.each([undefined, ""])("uses the fallback for a validation failure with message %j", async (error) => { - const { stateManager, manager, serviceFactory, embedder } = setup() - vi.mocked(serviceFactory.validateEmbedder).mockResolvedValue({ valid: false, error }) - - await manager.validate(serviceFactory, embedder) - - expect(stateManager.setSystemState).toHaveBeenCalledExactlyOnceWith( - "Error", - "Embedder configuration validation failed", - ) - }) - - it("uses the fallback for a non-Error rejection", async () => { - const { stateManager, manager, serviceFactory, embedder } = setup() - vi.mocked(serviceFactory.validateEmbedder).mockRejectedValue("Unexpected rejection") - - await expect(manager.validate(serviceFactory, embedder)).resolves.toBeUndefined() - - expect(stateManager.setSystemState).toHaveBeenCalledExactlyOnceWith( - "Error", - "Embedder configuration validation failed", - ) - }) - - it("reports an unexpected validation rejection while the manager remains in standby", async () => { - const { stateManager, manager, serviceFactory, embedder } = setup() - vi.mocked(serviceFactory.validateEmbedder).mockRejectedValue(new Error("Validation crashed")) - - void manager.validate(serviceFactory, embedder) - await Promise.resolve() - await Promise.resolve() - - expect(stateManager.setSystemState).toHaveBeenCalledExactlyOnceWith("Error", "Validation crashed") - }) - - it("ignores validation results after another operation changes the status", async () => { - const { stateManager, manager, serviceFactory, embedder } = setup() - let finishValidation!: (result: { valid: boolean; error?: string }) => void - vi.mocked(serviceFactory.validateEmbedder).mockReturnValue( - new Promise((resolve) => { - finishValidation = resolve - }), - ) - void manager.validate(serviceFactory, embedder) - - stateManager.state = "Indexed" - finishValidation({ valid: false, error: "Stale validation failure" }) - await Promise.resolve() - - expect(stateManager.setSystemState).not.toHaveBeenCalled() - }) - - it("ignores an older result when a newer validation has started", async () => { - const { stateManager, manager, serviceFactory, embedder } = setup() - let finishFirstValidation!: (result: { valid: boolean; error?: string }) => void - vi.mocked(serviceFactory.validateEmbedder) - .mockReturnValueOnce( - new Promise((resolve) => { - finishFirstValidation = resolve - }), - ) - .mockResolvedValueOnce({ valid: true }) - void manager.validate(serviceFactory, embedder) - void manager.validate(serviceFactory, embedder) - await Promise.resolve() - - finishFirstValidation({ valid: false, error: "Older validation failure" }) - await Promise.resolve() - - expect(stateManager.setSystemState).not.toHaveBeenCalled() - }) - - it.each(["invalidation", "newer validation", "status change"] as const)( - "ignores a pending rejection after %s", - async (scenario) => { - const { stateManager, manager, serviceFactory, embedder } = setup() - let rejectValidation!: (error: Error) => void - vi.mocked(serviceFactory.validateEmbedder).mockReturnValueOnce( - new Promise((_, reject) => { - rejectValidation = reject - }), - ) - const validation = manager.validate(serviceFactory, embedder) - - if (scenario === "invalidation") { - manager.invalidate() - } else if (scenario === "newer validation") { - vi.mocked(serviceFactory.validateEmbedder).mockResolvedValueOnce({ valid: true }) - await manager.validate(serviceFactory, embedder) - } else { - stateManager.state = "Indexed" - } - - rejectValidation(new Error("Stale rejection")) - await expect(validation).resolves.toBeUndefined() - expect(stateManager.setSystemState).not.toHaveBeenCalled() - }, - ) - - it("ignores a pending result after invalidation", async () => { - const { stateManager, manager, serviceFactory, embedder } = setup() - let finishValidation!: (result: { valid: boolean; error?: string }) => void - vi.mocked(serviceFactory.validateEmbedder).mockReturnValue( - new Promise((resolve) => { - finishValidation = resolve - }), - ) - void manager.validate(serviceFactory, embedder) - - manager.invalidate() - finishValidation({ valid: false, error: "Invalidated validation failure" }) - await Promise.resolve() - - expect(stateManager.setSystemState).not.toHaveBeenCalled() - }) -}) diff --git a/src/services/code-index/__tests__/manager.spec.ts b/src/services/code-index/__tests__/manager.spec.ts index e537a6be6d..5038cc247e 100644 --- a/src/services/code-index/__tests__/manager.spec.ts +++ b/src/services/code-index/__tests__/manager.spec.ts @@ -476,24 +476,19 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { ;(manager as any)._configManager = mockConfigManager }) - it("should create indexing services without waiting for startup embedder validation", async () => { - let finishValidation!: (result: { valid: boolean; error?: string }) => void - mockServiceFactoryInstance.validateEmbedder.mockReturnValue( - new Promise((resolve) => { - finishValidation = resolve - }), - ) + it("should create indexing services without a startup embedder validation request", async () => { + mockServiceFactoryInstance.validateEmbedder.mockResolvedValue({ + valid: false, + error: "Embedder unavailable", + }) await manager["_recreateServices"]() expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled() - expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(mockEmbedder) + expect(mockServiceFactoryInstance.validateEmbedder).not.toHaveBeenCalled() expect(mockStateManager.setSystemState).not.toHaveBeenCalledWith("Error", expect.any(String)) expect(manager["_orchestrator"]).toBeInstanceOf(CodeIndexOrchestrator) expect(manager["_searchService"]).toBeInstanceOf(CodeIndexSearchService) - - finishValidation({ valid: true }) - await Promise.resolve() }) it("should handle embedder creation failure", async () => { @@ -647,7 +642,7 @@ describe("CodeIndexManager - handleSettingsChange regression", () => { // Assert - manager should be initialized again expect(manager.isInitialized).toBe(true) expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled() - expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalled() + expect(mockServiceFactoryInstance.validateEmbedder).not.toHaveBeenCalled() }) it("should be safe to call when not in error state (idempotent)", async () => { diff --git a/src/services/code-index/code-index-workspace-scope.ts b/src/services/code-index/code-index-workspace-scope.ts index b6f348b1ff..c8481f6595 100644 --- a/src/services/code-index/code-index-workspace-scope.ts +++ b/src/services/code-index/code-index-workspace-scope.ts @@ -3,7 +3,6 @@ import type * as vscode from "vscode" import { CodeIndexManager } from "./manager" import { CodeIndexStateManager } from "./state-manager" import { WorkspaceIndexingEnablementManager } from "./workspace-indexing-enablement-manager" -import { EmbedderReadinessManager } from "./embedder-readiness-manager" /** Owns code-index services for one workspace; initialization remains with existing callers. */ export class CodeIndexWorkspaceScope implements vscode.Disposable { @@ -39,13 +38,11 @@ export class CodeIndexWorkspaceScope implements vscode.Disposable { throw new Error("Code index workspace scope is already initialized") } this._stateManager = new CodeIndexStateManager() - const embedderReadinessManager = new EmbedderReadinessManager(this._stateManager) this._codeIndexManager = new CodeIndexManager( this.workspacePath, this.folderUri, this.context, this._stateManager, - embedderReadinessManager, ) this._workspaceIndexingEnablementManager = new WorkspaceIndexingEnablementManager(this._codeIndexManager) this._isInitialized = true diff --git a/src/services/code-index/embedder-readiness-manager.ts b/src/services/code-index/embedder-readiness-manager.ts deleted file mode 100644 index 38cd45fb77..0000000000 --- a/src/services/code-index/embedder-readiness-manager.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { CodeIndexStateManager } from "./state-manager" -import type { IEmbedder } from "./interfaces" -import type { CodeIndexServiceFactory } from "./service-factory" - -type ValidationResult = { valid: boolean; error?: string } - -/** Tracks embedder readiness without blocking initialization and discards stale results. */ -export class EmbedderReadinessManager { - private generation = 0 - - public constructor(private readonly stateManager: Pick) {} - - public async validate(serviceFactory: CodeIndexServiceFactory, embedder: IEmbedder): Promise { - const generation = ++this.generation - try { - const result: ValidationResult = await serviceFactory.validateEmbedder(embedder) - if (!this.canApply(generation) || result.valid) return - this.stateManager.setSystemState("Error", result.error || "Embedder configuration validation failed") - } catch (error) { - if (!this.canApply(generation)) return - this.stateManager.setSystemState( - "Error", - error instanceof Error ? error.message : "Embedder configuration validation failed", - ) - } - } - - public invalidate(): void { - this.generation++ - } - - private canApply(generation: number): boolean { - return generation === this.generation && this.stateManager.state === "Standby" - } -} diff --git a/src/services/code-index/manager.ts b/src/services/code-index/manager.ts index fb7a7496a2..aa382e2131 100644 --- a/src/services/code-index/manager.ts +++ b/src/services/code-index/manager.ts @@ -16,7 +16,6 @@ import path from "path" import { t } from "../../i18n" import { TelemetryService } from "@roo-code/telemetry" import { TelemetryEventName } from "@roo-code/types" -import { EmbedderReadinessManager } from "./embedder-readiness-manager" export class CodeIndexManager { // Specialized class instances @@ -41,7 +40,6 @@ export class CodeIndexManager { folderUri: vscode.Uri, context: vscode.ExtensionContext, stateManager: CodeIndexStateManager, - private readonly embedderReadinessManager: EmbedderReadinessManager, ) { this.workspacePath = workspacePath this._folderUri = folderUri @@ -236,7 +234,6 @@ export class CodeIndexManager { * Stops any in-progress indexing operation and the file watcher. */ public stopIndexing(): void { - this.embedderReadinessManager.invalidate() if (this._sembleProvider) { this._sembleProvider.stopIndexing() return @@ -279,7 +276,6 @@ export class CodeIndexManager { } this._isRecoveringFromError = true - this.embedderReadinessManager.invalidate() try { // Clear error state this._stateManager.setSystemState("Standby", "") @@ -419,7 +415,6 @@ export class CodeIndexManager { ignoreInstance, rooIgnoreController, ) - void this.embedderReadinessManager.validate(this._serviceFactory, embedder) // (Re)Initialize orchestrator this._orchestrator = new CodeIndexOrchestrator( From 23ba04193b749ff88935c092e3812338b7c4dc28 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Sat, 3 Oct 2026 21:44:00 +0300 Subject: [PATCH 07/10] fix(code-index): report incremental scan errors without clearing existing index --- src/eslint-suppressions.json | 2 +- .../code-index-scan-executor.spec.ts | 15 +++- .../code-index/__tests__/orchestrator.spec.ts | 71 +++++++++++-------- .../code-index/code-index-scan-executor.ts | 10 ++- src/services/code-index/orchestrator.ts | 20 ++---- 5 files changed, 70 insertions(+), 48 deletions(-) diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index 6ca8a4e134..8130c33191 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -1306,7 +1306,7 @@ }, "services/code-index/__tests__/orchestrator.spec.ts": { "@typescript-eslint/no-explicit-any": { - "count": 25 + "count": 23 } }, "services/code-index/__tests__/service-factory.spec.ts": { diff --git a/src/services/code-index/__tests__/code-index-scan-executor.spec.ts b/src/services/code-index/__tests__/code-index-scan-executor.spec.ts index 33b71584ff..bcc63f7c00 100644 --- a/src/services/code-index/__tests__/code-index-scan-executor.spec.ts +++ b/src/services/code-index/__tests__/code-index-scan-executor.spec.ts @@ -136,14 +136,23 @@ describe("CodeIndexScanExecutor", () => { }, ) - it.each([0, 2])("preserves incremental batch-error tolerance with %s indexed blocks", async (indexed) => { + it.each([0, 2])("rejects incremental batch errors with %s indexed blocks", async (indexed) => { const { executor, scanner } = setup() + const failures = [new Error("embedding failure"), new Error("upsert failure"), new Error("embedding failure")] scanner.scanDirectory.mockImplementation(async (_path, onError, onIndexed, onParsed) => { onParsed?.(3) onIndexed?.(indexed) - onError?.(new Error("batch failure")) + for (const failure of failures) onError?.(failure) return { stats: { processed: 1, skipped: 0 }, totalBlockCount: 3 } }) - await expect(executor.runIncrementalScan(new AbortController().signal)).resolves.toBe(true) + const result = executor.runIncrementalScan(new AbortController().signal) + await expect(result).rejects.toBeInstanceOf(AggregateError) + await expect(result).rejects.toMatchObject({ + errors: failures, + message: "Incremental scan failed with 3 errors:\nembedding failure\nupsert failure", + }) + await result.catch((error: AggregateError) => { + for (const [index, failure] of failures.entries()) expect(error.errors[index]).toBe(failure) + }) }) }) diff --git a/src/services/code-index/__tests__/orchestrator.spec.ts b/src/services/code-index/__tests__/orchestrator.spec.ts index bad3d69be3..3ddd59ec34 100644 --- a/src/services/code-index/__tests__/orchestrator.spec.ts +++ b/src/services/code-index/__tests__/orchestrator.spec.ts @@ -42,8 +42,8 @@ vi.mock("@roo-code/telemetry", () => ({ })) // Mock i18n translator used in orchestrator messages -vi.mock("../../i18n", () => ({ - t: (key: string, params?: any) => { +vi.mock("../../../i18n", () => ({ + t: (key: string, params?: { errorMessage?: string }) => { if (key === "embeddings:orchestrator.failedDuringInitialScan" && params?.errorMessage) { return `Failed during initial scan: ${params.errorMessage}` } @@ -259,36 +259,49 @@ describe("CodeIndexOrchestrator - error path cleanup gating", () => { expect(calls[calls.length - 1]).toBe("Error") }) - it("collects batch errors from incremental scan and still completes indexing", async () => { - const batchError = new Error("incremental batch failure") - vectorStore.initialize.mockResolvedValue(false) // existing collection - vectorStore.hasIndexedData.mockResolvedValue(true) // force incremental scan path - vectorStore.markIndexingIncomplete.mockResolvedValue(undefined) - vectorStore.markIndexingComplete.mockResolvedValue(undefined) - - // Incremental scan reports a batch error but returns a result — orchestrator completes normally - scanner.scanDirectory.mockImplementation(async (_dir: string, onBatchError: (e: Error) => void) => { - onBatchError(batchError) - return { stats: { processed: 0, skipped: 0 }, totalBlockCount: 0 } - }) + it.each([0, 2])( + "preserves the existing index on incremental batch failure after %s indexed blocks", + async (indexed) => { + const batchError = new Error("incremental batch failure") + vectorStore.initialize.mockResolvedValue(false) // existing collection + vectorStore.hasIndexedData.mockResolvedValue(true) // force incremental scan path + vectorStore.markIndexingIncomplete.mockResolvedValue(undefined) + vectorStore.markIndexingComplete.mockResolvedValue(undefined) + + scanner.scanDirectory.mockImplementation( + async (_dir: string, onBatchError: (e: Error) => void, onIndexed: (count: number) => void) => { + onIndexed(indexed) + onBatchError(batchError) + return { stats: { processed: 1, skipped: 0 }, totalBlockCount: 3 } + }, + ) - const orchestrator = new CodeIndexOrchestrator( - configManager, - stateManager, - workspacePath, - cacheManager, - vectorStore, - scanner, - fileWatcher, - ) + const orchestrator = new CodeIndexOrchestrator( + configManager, + stateManager, + workspacePath, + cacheManager, + vectorStore, + scanner, + fileWatcher, + ) - await orchestrator.startIndexing() + await orchestrator.startIndexing() - // Incremental scan doesn't gate on batch errors — Indexed state is still reached - const calls = stateManager.setSystemState.mock.calls.map((c: any[]) => c[0]) - expect(calls[calls.length - 1]).toBe("Indexed") - expect(calls).not.toContain("Error") - }) + expect(orchestrator.state).toBe("Error") + expect(stateManager.setSystemState).toHaveBeenLastCalledWith( + "Error", + expect.stringContaining(batchError.message), + ) + expect(stateManager.setSystemState).not.toHaveBeenCalledWith("Indexed", expect.any(String)) + expect(vectorStore.markIndexingIncomplete).toHaveBeenCalledOnce() + expect(vectorStore.markIndexingComplete).not.toHaveBeenCalled() + expect(vectorStore.clearCollection).not.toHaveBeenCalled() + expect(cacheManager.clearCacheFile).not.toHaveBeenCalled() + expect(fileWatcher.initialize).not.toHaveBeenCalled() + expect(fileWatcher.dispose).toHaveBeenCalledOnce() + }, + ) }) describe("CodeIndexOrchestrator - stopIndexing", () => { diff --git a/src/services/code-index/code-index-scan-executor.ts b/src/services/code-index/code-index-scan-executor.ts index 2ff55454d9..54760c50f2 100644 --- a/src/services/code-index/code-index-scan-executor.ts +++ b/src/services/code-index/code-index-scan-executor.ts @@ -20,7 +20,15 @@ export class CodeIndexScanExecutor { const summary = await this.scanWorkspace(signal, "incremental") if (!summary) return false - // Preserve the existing incremental policy: reported batch errors do not prevent completion. + // Do not mark an existing index complete when some updates failed. + if (summary.batchErrors.length > 0) { + const messages = [...new Set(summary.batchErrors.map((error) => error.message))] + throw new AggregateError( + summary.batchErrors, + `Incremental scan failed with ${summary.batchErrors.length} errors:\n${messages.join("\n")}`, + ) + } + if (summary.found > 0) { console.log( `[CodeIndexOrchestrator] Incremental scan completed: ${summary.indexed} blocks indexed from new/changed files`, diff --git a/src/services/code-index/orchestrator.ts b/src/services/code-index/orchestrator.ts index c055fff370..872efac362 100644 --- a/src/services/code-index/orchestrator.ts +++ b/src/services/code-index/orchestrator.ts @@ -130,15 +130,14 @@ export class CodeIndexOrchestrator { const signal = this._abortController.signal this.stateManager.setSystemState("Indexing", "Initializing services...") - // Track whether we successfully connected to Qdrant and started indexing - // This helps us decide whether to preserve cache on error - let indexingStarted = false + // Preserve data on connection failures and incremental updates; clean up failed rebuilds. + let clearIndexOnError = false try { const collectionCreated = await this.vectorStore.initialize() // Successfully connected to Qdrant - indexingStarted = true + clearIndexOnError = true if (collectionCreated) { await this.cacheManager.clearCacheFile() @@ -149,6 +148,7 @@ export class CodeIndexOrchestrator { const hasExistingData = await this.vectorStore.hasIndexedData() if (hasExistingData && !collectionCreated) { + clearIndexOnError = false if (!(await this.scanExecutor.runIncrementalScan(signal))) { await this.cacheManager.flush() this.stopWatcher() @@ -193,7 +193,7 @@ export class CodeIndexOrchestrator { stack: error instanceof Error ? error.stack : undefined, location: "startIndexing", }) - if (indexingStarted) { + if (clearIndexOnError) { try { await this.vectorStore.clearCollection() } catch (cleanupError) { @@ -204,21 +204,13 @@ export class CodeIndexOrchestrator { location: "startIndexing.cleanup", }) } - } - - // Only clear cache if indexing had started (Qdrant connection succeeded) - // If we never connected to Qdrant, preserve cache for incremental scan when it comes back - if (indexingStarted) { // Indexing started but failed mid-way - clear cache to avoid cache-Qdrant mismatch await this.cacheManager.clearCacheFile() console.log( "[CodeIndexOrchestrator] Indexing failed after starting. Clearing cache to avoid inconsistency.", ) } else { - // Never connected to Qdrant - preserve cache for future incremental scan - console.log( - "[CodeIndexOrchestrator] Failed to connect to Qdrant. Preserving cache for future incremental scan.", - ) + console.log("[CodeIndexOrchestrator] Preserving existing index and cache for a retry.") } this.stateManager.setSystemState( From 5284ffdbfdda6b7ee11d3459d980443b647429be Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Sat, 3 Oct 2026 22:28:45 +0300 Subject: [PATCH 08/10] fix(code-index): preserve existing index across failed retries --- .../code-index/__tests__/orchestrator.spec.ts | 74 +++++++++++++++---- src/services/code-index/orchestrator.ts | 6 +- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/src/services/code-index/__tests__/orchestrator.spec.ts b/src/services/code-index/__tests__/orchestrator.spec.ts index 3ddd59ec34..a48ccb942e 100644 --- a/src/services/code-index/__tests__/orchestrator.spec.ts +++ b/src/services/code-index/__tests__/orchestrator.spec.ts @@ -200,12 +200,48 @@ describe("CodeIndexOrchestrator - error path cleanup gating", () => { 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")) + it.each([false, true])( + "only cleans up a failed full scan when the collection was created by this run: %s", + async (created) => { + vectorStore.initialize.mockResolvedValue(created) + 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() + + expect(vectorStore.clearCollection).toHaveBeenCalledTimes(created ? 1 : 0) + // A new collection clears stale cache at initialization and again on failure. + expect(cacheManager.clearCacheFile).toHaveBeenCalledTimes(created ? 2 : 0) + + // 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("preserves an existing index after an incremental failure and a failed full-scan retry", async () => { + let complete = true + vectorStore.initialize.mockResolvedValue(false) + vectorStore.hasIndexedData.mockImplementation(async () => complete) + vectorStore.markIndexingIncomplete.mockImplementation(async () => { + complete = false + }) + scanner.scanDirectory.mockImplementation(async (_dir: string, onError: (error: Error) => void) => { + onError(new Error("embedding failed")) + return { stats: { processed: 0, skipped: 0 }, totalBlockCount: 0 } + }) const orchestrator = new CodeIndexOrchestrator( configManager, stateManager, @@ -216,17 +252,23 @@ describe("CodeIndexOrchestrator - error path cleanup gating", () => { 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") + for (let attempt = 1; attempt <= 2; attempt++) { + await orchestrator.startIndexing() + expect(complete).toBe(false) + expect(orchestrator.state).toBe("Error") + expect(scanner.scanDirectory).toHaveBeenCalledTimes(attempt) + expect(vectorStore.markIndexingIncomplete).toHaveBeenCalledTimes(attempt) + expect(vectorStore.clearCollection).not.toHaveBeenCalled() + expect(cacheManager.clearCacheFile).not.toHaveBeenCalled() + expect(vectorStore.markIndexingComplete).not.toHaveBeenCalled() + expect(fileWatcher.initialize).not.toHaveBeenCalled() + } + expect(stateManager.setSystemState).toHaveBeenCalledWith("Indexing", "Checking for new or modified files...") + expect(stateManager.setSystemState).toHaveBeenCalledWith( + "Indexing", + "Services ready. Starting workspace scan...", + ) + expect(stateManager.setSystemState).not.toHaveBeenCalledWith("Indexed", expect.any(String)) }) it("collects batch errors from full scan and transitions to Error when all blocks fail", async () => { diff --git a/src/services/code-index/orchestrator.ts b/src/services/code-index/orchestrator.ts index 872efac362..bc1f5deaed 100644 --- a/src/services/code-index/orchestrator.ts +++ b/src/services/code-index/orchestrator.ts @@ -130,14 +130,13 @@ export class CodeIndexOrchestrator { const signal = this._abortController.signal this.stateManager.setSystemState("Indexing", "Initializing services...") - // Preserve data on connection failures and incremental updates; clean up failed rebuilds. + // Only clean up collections created by this run; existing data must survive failed retries. let clearIndexOnError = false try { const collectionCreated = await this.vectorStore.initialize() - // Successfully connected to Qdrant - clearIndexOnError = true + clearIndexOnError = collectionCreated if (collectionCreated) { await this.cacheManager.clearCacheFile() @@ -148,7 +147,6 @@ export class CodeIndexOrchestrator { const hasExistingData = await this.vectorStore.hasIndexedData() if (hasExistingData && !collectionCreated) { - clearIndexOnError = false if (!(await this.scanExecutor.runIncrementalScan(signal))) { await this.cacheManager.flush() this.stopWatcher() From 701260cdc599eb5810bbf029d7c72d4b83429d8a Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Sat, 3 Oct 2026 23:00:46 +0300 Subject: [PATCH 09/10] test(code-index): cover cleanup after partial full-scan progress --- .../code-index/__tests__/orchestrator.spec.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/services/code-index/__tests__/orchestrator.spec.ts b/src/services/code-index/__tests__/orchestrator.spec.ts index a48ccb942e..614d61d999 100644 --- a/src/services/code-index/__tests__/orchestrator.spec.ts +++ b/src/services/code-index/__tests__/orchestrator.spec.ts @@ -231,6 +231,55 @@ describe("CodeIndexOrchestrator - error path cleanup gating", () => { }, ) + it.each([false, true])( + "only cleans up after partial full-scan progress when the collection was created by this run: %s", + async (created) => { + const failure = new Error("batch failure after partial progress") + vectorStore.initialize.mockResolvedValue(created) + vectorStore.hasIndexedData.mockResolvedValue(false) + vectorStore.markIndexingIncomplete.mockResolvedValue(undefined) + scanner.scanDirectory.mockImplementation( + async ( + _dir: string, + onError: (error: Error) => void, + onIndexed: (count: number) => void, + onParsed: (count: number) => void, + ) => { + onParsed(3) + onIndexed(1) + onError(failure) + return { stats: { processed: 1, skipped: 0 }, totalBlockCount: 3 } + }, + ) + const orchestrator = new CodeIndexOrchestrator( + configManager, + stateManager, + workspacePath, + cacheManager, + vectorStore, + scanner, + fileWatcher, + ) + + await orchestrator.startIndexing() + + expect(scanner.scanDirectory).toHaveBeenCalledOnce() + expect(stateManager.reportBlockIndexingProgress).toHaveBeenLastCalledWith(1, 3) + expect(stateManager.setSystemState).toHaveBeenLastCalledWith( + "Error", + expect.stringContaining(failure.message), + ) + expect(stateManager.setSystemState).not.toHaveBeenCalledWith("Indexed", expect.any(String)) + expect(vectorStore.clearCollection).toHaveBeenCalledTimes(created ? 1 : 0) + // New collections clear stale cache before scanning and again during error cleanup. + expect(cacheManager.clearCacheFile).toHaveBeenCalledTimes(created ? 2 : 0) + expect(vectorStore.markIndexingIncomplete).toHaveBeenCalledOnce() + expect(vectorStore.markIndexingComplete).not.toHaveBeenCalled() + expect(fileWatcher.initialize).not.toHaveBeenCalled() + expect(fileWatcher.dispose).toHaveBeenCalledOnce() + }, + ) + it("preserves an existing index after an incremental failure and a failed full-scan retry", async () => { let complete = true vectorStore.initialize.mockResolvedValue(false) From abb3a5b0f591e739e3df3a57964460f0b498e0a7 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Sat, 3 Oct 2026 23:36:37 +0300 Subject: [PATCH 10/10] fix(code-index): exclude private error details from indexing telemetry --- .../code-index/__tests__/orchestrator.spec.ts | 55 +++++++++++++++++++ src/services/code-index/orchestrator.ts | 8 +-- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/services/code-index/__tests__/orchestrator.spec.ts b/src/services/code-index/__tests__/orchestrator.spec.ts index 614d61d999..1a81c1abfa 100644 --- a/src/services/code-index/__tests__/orchestrator.spec.ts +++ b/src/services/code-index/__tests__/orchestrator.spec.ts @@ -1,5 +1,7 @@ import { describe, it, expect, beforeEach, vi } from "vitest" import { CodeIndexOrchestrator } from "../orchestrator" +import { TelemetryService } from "@roo-code/telemetry" +import { TelemetryEventName } from "@roo-code/types" import { clearAllMocks } from "../../../test-utils/reset" @@ -280,6 +282,59 @@ describe("CodeIndexOrchestrator - error path cleanup gating", () => { }, ) + it.each([false, true])( + "keeps private error details out of telemetry (cleanup failure: %s)", + async (cleanupFails) => { + const messages = [ + "Embedding failed (Workspace: /Users/private-user/Secret Project, File: /Users/private-user/Secret Project/src/private.ts)", + "Embedding failed (Workspace: C:\\Users\\private-user\\Secret Project, File: C:\\Users\\private-user\\Secret Project\\private.ts)", + ] + const failures = messages.map((message) => { + const error = new Error(message) + error.stack = `${message}\n at privateFunction (/Users/private-user/Secret Project/private.ts:12:3)` + return error + }) + vectorStore.initialize.mockResolvedValue(cleanupFails) + vectorStore.hasIndexedData.mockResolvedValue(!cleanupFails) + vectorStore.markIndexingIncomplete.mockResolvedValue(undefined) + scanner.scanDirectory.mockImplementation(async (_dir: string, onError: (error: Error) => void) => { + for (const failure of failures) onError(failure) + return { stats: { processed: 0, skipped: 0 }, totalBlockCount: 0 } + }) + if (cleanupFails) vectorStore.clearCollection.mockRejectedValue(failures[1]) + const orchestrator = new CodeIndexOrchestrator( + configManager, + stateManager, + workspacePath, + cacheManager, + vectorStore, + scanner, + fileWatcher, + ) + + await orchestrator.startIndexing() + + const expectedEvents = [ + [TelemetryEventName.CODE_INDEX_ERROR, { error: "Indexing failed", location: "startIndexing" }], + ] + if (cleanupFails) { + expectedEvents.push([ + TelemetryEventName.CODE_INDEX_ERROR, + { error: "Index cleanup failed", location: "startIndexing.cleanup" }, + ]) + } + // Exact payload assertions also exclude stacks, nested causes and aggregate error objects. + expect(vi.mocked(TelemetryService.instance.captureEvent).mock.calls).toEqual(expectedEvents) + expect(stateManager.setSystemState).toHaveBeenLastCalledWith("Error", expect.stringContaining(messages[0])) + if (!cleanupFails) { + expect(stateManager.setSystemState).toHaveBeenLastCalledWith( + "Error", + expect.stringContaining(messages[1]), + ) + } + }, + ) + it("preserves an existing index after an incremental failure and a failed full-scan retry", async () => { let complete = true vectorStore.initialize.mockResolvedValue(false) diff --git a/src/services/code-index/orchestrator.ts b/src/services/code-index/orchestrator.ts index bc1f5deaed..8d58b7fb8b 100644 --- a/src/services/code-index/orchestrator.ts +++ b/src/services/code-index/orchestrator.ts @@ -186,9 +186,10 @@ export class CodeIndexOrchestrator { } console.error("[CodeIndexOrchestrator] Error during indexing:", error) + // Scanner/provider errors and stacks may contain local paths or other private data. + // Keep details local; send only a fixed failure category across the telemetry boundary. TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, + error: "Indexing failed", location: "startIndexing", }) if (clearIndexOnError) { @@ -197,8 +198,7 @@ export class CodeIndexOrchestrator { } catch (cleanupError) { console.error("[CodeIndexOrchestrator] Failed to clean up after error:", cleanupError) TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, { - error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError), - stack: cleanupError instanceof Error ? cleanupError.stack : undefined, + error: "Index cleanup failed", location: "startIndexing.cleanup", }) }