Roo-Code/src/services/code-index/__tests__/manager.spec.ts
NaccOll d90bab71ff
feat: code indexing support multiple folder similar with task history (#6204)
* feat: Implement code indexing support multi-folder workspaces similar to task history

* fix: add missing mock for onDidChangeActiveTextEditor in tests
2025-08-04 19:39:37 -07:00

373 lines
12 KiB
TypeScript

import { CodeIndexManager } from "../manager"
import { CodeIndexServiceFactory } from "../service-factory"
import type { MockedClass } from "vitest"
// Mock vscode module
vi.mock("vscode", () => ({
window: {
activeTextEditor: null,
},
workspace: {
workspaceFolders: [
{
uri: { fsPath: "/test/workspace" },
name: "test",
index: 0,
},
],
},
}))
// Mock only the essential dependencies
vi.mock("../../../utils/path", () => ({
getWorkspacePath: vi.fn(() => "/test/workspace"),
}))
vi.mock("../state-manager", () => ({
CodeIndexStateManager: vi.fn().mockImplementation(() => ({
onProgressUpdate: vi.fn(),
getCurrentStatus: vi.fn(),
dispose: vi.fn(),
setSystemState: vi.fn(),
})),
}))
// Mock TelemetryService
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureEvent: vi.fn(),
},
},
}))
vi.mock("../service-factory")
const MockedCodeIndexServiceFactory = CodeIndexServiceFactory as MockedClass<typeof CodeIndexServiceFactory>
describe("CodeIndexManager - handleSettingsChange regression", () => {
let mockContext: any
let manager: CodeIndexManager
beforeEach(() => {
// Clear all instances before each test
CodeIndexManager.disposeAll()
mockContext = {
subscriptions: [],
workspaceState: {} as any,
globalState: {} as any,
extensionUri: {} as any,
extensionPath: "/test/extension",
asAbsolutePath: vi.fn(),
storageUri: {} as any,
storagePath: "/test/storage",
globalStorageUri: {} as any,
globalStoragePath: "/test/global-storage",
logUri: {} as any,
logPath: "/test/log",
extensionMode: 3, // vscode.ExtensionMode.Test
secrets: {} as any,
environmentVariableCollection: {} as any,
extension: {} as any,
languageModelAccessInformation: {} as any,
}
manager = CodeIndexManager.getInstance(mockContext)!
})
afterEach(() => {
CodeIndexManager.disposeAll()
})
describe("handleSettingsChange", () => {
it("should not throw when called on uninitialized manager (regression test)", async () => {
// This is the core regression test: handleSettingsChange() should not throw
// when called before the manager is initialized (during first-time configuration)
// Ensure manager is not initialized
expect(manager.isInitialized).toBe(false)
// Mock a minimal config manager that simulates first-time configuration
const mockConfigManager = {
loadConfiguration: vi.fn().mockResolvedValue({ requiresRestart: true }),
isFeatureConfigured: true,
isFeatureEnabled: true,
getConfig: vi.fn().mockReturnValue({
isConfigured: true,
embedderProvider: "openai",
modelId: "text-embedding-3-small",
openAiOptions: { openAiNativeApiKey: "test-key" },
qdrantUrl: "http://localhost:6333",
qdrantApiKey: "test-key",
searchMinScore: 0.4,
}),
}
;(manager as any)._configManager = mockConfigManager
// Mock cache manager
const mockCacheManager = {
initialize: vi.fn(),
clearCacheFile: vi.fn(),
}
;(manager as any)._cacheManager = mockCacheManager
// Mock the feature state to simulate valid configuration that would normally trigger restart
vi.spyOn(manager, "isFeatureEnabled", "get").mockReturnValue(true)
vi.spyOn(manager, "isFeatureConfigured", "get").mockReturnValue(true)
// Mock service factory to handle _recreateServices call
const mockServiceFactoryInstance = {
configManager: mockConfigManager,
workspacePath: "/test/workspace",
cacheManager: mockCacheManager,
createEmbedder: vi.fn().mockReturnValue({ embedderInfo: { name: "openai" } }),
createVectorStore: vi.fn().mockReturnValue({}),
createDirectoryScanner: vi.fn().mockReturnValue({}),
createFileWatcher: vi.fn().mockReturnValue({
onDidStartBatchProcessing: vi.fn(),
onBatchProgressUpdate: vi.fn(),
watch: vi.fn(),
stopWatcher: vi.fn(),
dispose: vi.fn(),
}),
createServices: vi.fn().mockReturnValue({
embedder: { embedderInfo: { name: "openai" } },
vectorStore: {},
scanner: {},
fileWatcher: {
onDidStartBatchProcessing: vi.fn(),
onBatchProgressUpdate: vi.fn(),
watch: vi.fn(),
stopWatcher: vi.fn(),
dispose: vi.fn(),
},
}),
validateEmbedder: vi.fn().mockResolvedValue({ valid: true }),
}
MockedCodeIndexServiceFactory.mockImplementation(() => mockServiceFactoryInstance as any)
// The key test: this should NOT throw "CodeIndexManager not initialized" error
await expect(manager.handleSettingsChange()).resolves.not.toThrow()
// Verify that loadConfiguration was called (the method should still work)
expect(mockConfigManager.loadConfiguration).toHaveBeenCalled()
})
it("should work normally when manager is initialized", async () => {
// Mock a complete config manager with all required properties
const mockConfigManager = {
loadConfiguration: vi.fn().mockResolvedValue({ requiresRestart: true }),
isFeatureConfigured: true,
isFeatureEnabled: true,
getConfig: vi.fn().mockReturnValue({
isConfigured: true,
embedderProvider: "openai",
modelId: "text-embedding-3-small",
openAiOptions: { openAiNativeApiKey: "test-key" },
qdrantUrl: "http://localhost:6333",
qdrantApiKey: "test-key",
searchMinScore: 0.4,
}),
}
;(manager as any)._configManager = mockConfigManager
// Mock cache manager
const mockCacheManager = {
initialize: vi.fn(),
clearCacheFile: vi.fn(),
}
;(manager as any)._cacheManager = mockCacheManager
// Simulate an initialized manager by setting the required properties
;(manager as any)._orchestrator = { stopWatcher: vi.fn() }
;(manager as any)._searchService = {}
// Verify manager is considered initialized
expect(manager.isInitialized).toBe(true)
// Mock the feature state
vi.spyOn(manager, "isFeatureEnabled", "get").mockReturnValue(true)
vi.spyOn(manager, "isFeatureConfigured", "get").mockReturnValue(true)
// Mock service factory to handle _recreateServices call
const mockServiceFactoryInstance = {
configManager: mockConfigManager,
workspacePath: "/test/workspace",
cacheManager: mockCacheManager,
createEmbedder: vi.fn().mockReturnValue({ embedderInfo: { name: "openai" } }),
createVectorStore: vi.fn().mockReturnValue({}),
createDirectoryScanner: vi.fn().mockReturnValue({}),
createFileWatcher: vi.fn().mockReturnValue({
onDidStartBatchProcessing: vi.fn(),
onBatchProgressUpdate: vi.fn(),
watch: vi.fn(),
stopWatcher: vi.fn(),
dispose: vi.fn(),
}),
createServices: vi.fn().mockReturnValue({
embedder: { embedderInfo: { name: "openai" } },
vectorStore: {},
scanner: {},
fileWatcher: {
onDidStartBatchProcessing: vi.fn(),
onBatchProgressUpdate: vi.fn(),
watch: vi.fn(),
stopWatcher: vi.fn(),
dispose: vi.fn(),
},
}),
validateEmbedder: vi.fn().mockResolvedValue({ valid: true }),
}
MockedCodeIndexServiceFactory.mockImplementation(() => mockServiceFactoryInstance as any)
// Mock the methods that would be called during restart
const recreateServicesSpy = vi.spyOn(manager as any, "_recreateServices")
await manager.handleSettingsChange()
// Verify that the restart sequence was called
expect(mockConfigManager.loadConfiguration).toHaveBeenCalled()
// _recreateServices should be called when requiresRestart is true
expect(recreateServicesSpy).toHaveBeenCalled()
// Note: startIndexing is NOT called by handleSettingsChange - it's only called by initialize()
})
it("should handle case when config manager is not set", async () => {
// Ensure config manager is not set (edge case)
;(manager as any)._configManager = undefined
// This should not throw an error
await expect(manager.handleSettingsChange()).resolves.not.toThrow()
})
})
describe("embedder validation integration", () => {
let mockServiceFactoryInstance: any
let mockStateManager: any
let mockEmbedder: any
let mockVectorStore: any
let mockScanner: any
let mockFileWatcher: any
beforeEach(() => {
// Mock service factory objects
mockEmbedder = { embedderInfo: { name: "openai" } }
mockVectorStore = {}
mockScanner = {}
mockFileWatcher = {
onDidStartBatchProcessing: vi.fn(),
onBatchProgressUpdate: vi.fn(),
watch: vi.fn(),
stopWatcher: vi.fn(),
dispose: vi.fn(),
}
// Mock service factory instance
mockServiceFactoryInstance = {
createServices: vi.fn().mockReturnValue({
embedder: mockEmbedder,
vectorStore: mockVectorStore,
scanner: mockScanner,
fileWatcher: mockFileWatcher,
}),
validateEmbedder: vi.fn(),
}
// Mock the ServiceFactory constructor
MockedCodeIndexServiceFactory.mockImplementation(() => mockServiceFactoryInstance)
// Mock state manager methods directly on the existing instance
mockStateManager = (manager as any)._stateManager
mockStateManager.setSystemState = vi.fn()
// Mock config manager
const mockConfigManager = {
loadConfiguration: vitest.fn().mockResolvedValue({ requiresRestart: false }),
isFeatureConfigured: true,
isFeatureEnabled: true,
getConfig: vitest.fn().mockReturnValue({
isConfigured: true,
embedderProvider: "openai",
modelId: "text-embedding-3-small",
openAiOptions: { openAiNativeApiKey: "test-key" },
qdrantUrl: "http://localhost:6333",
qdrantApiKey: "test-key",
searchMinScore: 0.4,
}),
}
;(manager as any)._configManager = mockConfigManager
})
it("should validate embedder during _recreateServices when validation succeeds", async () => {
// Arrange
mockServiceFactoryInstance.validateEmbedder.mockResolvedValue({ valid: true })
// 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(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",
)
// 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",
)
})
it("should handle embedder creation failure", async () => {
// Arrange
mockServiceFactoryInstance.createServices.mockImplementation(() => {
throw new Error("Invalid configuration")
})
// Act & Assert - should throw the error
await expect((manager as any)._recreateServices()).rejects.toThrow("Invalid configuration")
// Should not attempt validation if embedder creation fails
expect(mockServiceFactoryInstance.validateEmbedder).not.toHaveBeenCalled()
})
})
})