mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-22 05:24:02 +00:00
* feat: Implement code indexing support multi-folder workspaces similar to task history * fix: add missing mock for onDidChangeActiveTextEditor in tests
373 lines
12 KiB
TypeScript
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()
|
|
})
|
|
})
|
|
})
|