mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
fix: resolve codebase indexing ignore rules issues (#5655)
- Fix .gitignore files having no effect on indexing by replacing broken single-file logic with proper findGitignoreFiles() usage - Fix .rooignore inconsistency by using global RooIgnoreController instance instead of creating new instances per scan - Fix .rooignore rules being ignored after 'Clear Index Data' by ensuring patterns are reloaded during service recreation - Add comprehensive integration tests covering all three reported issues - Fix VSCode test mocks to support RelativePattern and createFileSystemWatcher - Ensure backward compatibility and proper disposal handling All 346 code-index tests passing with new integration test coverage.
This commit is contained in:
parent
e84dd0a2cf
commit
d38a99671f
7 changed files with 591 additions and 19 deletions
|
|
@ -60,7 +60,7 @@ export class RooIgnoreController {
|
|||
/**
|
||||
* Load custom patterns from .rooignore if it exists
|
||||
*/
|
||||
private async loadRooIgnore(): Promise<void> {
|
||||
public async loadRooIgnore(): Promise<void> {
|
||||
try {
|
||||
// Reset ignore instance to prevent duplicate patterns
|
||||
this.ignoreInstance = ignore()
|
||||
|
|
@ -183,7 +183,7 @@ export class RooIgnoreController {
|
|||
* Clean up resources when the controller is no longer needed
|
||||
*/
|
||||
dispose(): void {
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
this.disposables.forEach((d) => d?.dispose?.())
|
||||
this.disposables = []
|
||||
}
|
||||
|
||||
|
|
|
|||
492
src/services/code-index/__tests__/ignore-integration.spec.ts
Normal file
492
src/services/code-index/__tests__/ignore-integration.spec.ts
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
||||
import { CodeIndexManager } from "../manager"
|
||||
import { CodeIndexServiceFactory } from "../service-factory"
|
||||
import type { MockedClass } from "vitest"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
// Mock vscode module
|
||||
vi.mock("vscode", () => ({
|
||||
workspace: {
|
||||
workspaceFolders: [
|
||||
{
|
||||
uri: { fsPath: "/test/workspace" },
|
||||
name: "test",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
createFileSystemWatcher: vi.fn(() => ({
|
||||
onDidCreate: vi.fn(),
|
||||
onDidChange: vi.fn(),
|
||||
onDidDelete: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
})),
|
||||
},
|
||||
RelativePattern: vi.fn().mockImplementation((base, pattern) => ({
|
||||
base,
|
||||
pattern,
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock path utilities
|
||||
vi.mock("../../../utils/path", () => ({
|
||||
getWorkspacePath: vi.fn(() => "/test/workspace"),
|
||||
}))
|
||||
|
||||
// Mock fs operations
|
||||
vi.mock("fs/promises")
|
||||
const mockFs = fs as any
|
||||
|
||||
// Mock file existence check
|
||||
vi.mock("../../../utils/fs", () => ({
|
||||
fileExistsAtPath: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock glob utilities
|
||||
vi.mock("../../glob/list-files", () => ({
|
||||
findGitignoreFiles: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock state manager
|
||||
vi.mock("../state-manager", () => ({
|
||||
CodeIndexStateManager: vi.fn().mockImplementation(() => ({
|
||||
onProgressUpdate: vi.fn(),
|
||||
getCurrentStatus: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
setSystemState: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock telemetry
|
||||
vi.mock("@roo-code/telemetry", () => ({
|
||||
TelemetryService: {
|
||||
instance: {
|
||||
captureEvent: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock service factory
|
||||
vi.mock("../service-factory")
|
||||
const MockedCodeIndexServiceFactory = CodeIndexServiceFactory as MockedClass<typeof CodeIndexServiceFactory>
|
||||
|
||||
describe("CodeIndexManager - Ignore Integration Tests", () => {
|
||||
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()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("gitignore integration", () => {
|
||||
it("should load and apply .gitignore patterns during service recreation", async () => {
|
||||
// Mock .gitignore file discovery and content
|
||||
const { findGitignoreFiles } = await import("../../glob/list-files")
|
||||
const mockFindGitignoreFiles = findGitignoreFiles as any
|
||||
mockFindGitignoreFiles.mockResolvedValue(["/test/workspace/.gitignore"])
|
||||
|
||||
// Mock .gitignore content
|
||||
mockFs.readFile.mockResolvedValue("node_modules/\n*.log\n.env")
|
||||
|
||||
// Mock config manager
|
||||
const mockConfigManager = {
|
||||
loadConfiguration: vi.fn().mockResolvedValue({ requiresRestart: false }),
|
||||
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 service factory
|
||||
const mockServiceFactoryInstance = {
|
||||
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)
|
||||
|
||||
// Act - call _recreateServices which should load .gitignore
|
||||
await (manager as any)._recreateServices()
|
||||
|
||||
// Assert
|
||||
expect(mockFindGitignoreFiles).toHaveBeenCalledWith("/test/workspace")
|
||||
expect(mockFs.readFile).toHaveBeenCalledWith("/test/workspace/.gitignore", "utf8")
|
||||
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
|
||||
|
||||
// Verify that the ignore instance was passed to createServices with .gitignore patterns
|
||||
const createServicesCall = mockServiceFactoryInstance.createServices.mock.calls[0]
|
||||
const ignoreInstance = createServicesCall[2] // Third parameter is the ignore instance
|
||||
|
||||
// Test that the ignore instance has the expected patterns
|
||||
expect(ignoreInstance.ignores("node_modules/package.json")).toBe(true)
|
||||
expect(ignoreInstance.ignores("debug.log")).toBe(true)
|
||||
expect(ignoreInstance.ignores(".env")).toBe(true)
|
||||
expect(ignoreInstance.ignores(".gitignore")).toBe(true) // Should always ignore .gitignore files
|
||||
expect(ignoreInstance.ignores("src/main.ts")).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle missing .gitignore files gracefully", async () => {
|
||||
// Mock no .gitignore files found
|
||||
const { findGitignoreFiles } = await import("../../glob/list-files")
|
||||
const mockFindGitignoreFiles = findGitignoreFiles as any
|
||||
mockFindGitignoreFiles.mockResolvedValue([])
|
||||
|
||||
// Mock config manager
|
||||
const mockConfigManager = {
|
||||
loadConfiguration: vi.fn().mockResolvedValue({ requiresRestart: false }),
|
||||
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 service factory
|
||||
const mockServiceFactoryInstance = {
|
||||
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)
|
||||
|
||||
// Act - should not throw even with no .gitignore files
|
||||
await expect((manager as any)._recreateServices()).resolves.not.toThrow()
|
||||
|
||||
// Assert
|
||||
expect(mockFindGitignoreFiles).toHaveBeenCalledWith("/test/workspace")
|
||||
expect(mockFs.readFile).not.toHaveBeenCalled()
|
||||
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
|
||||
|
||||
// Verify that an ignore instance was still created (even if empty)
|
||||
const createServicesCall = mockServiceFactoryInstance.createServices.mock.calls[0]
|
||||
const ignoreInstance = createServicesCall[2]
|
||||
|
||||
// Should still ignore .gitignore files themselves
|
||||
expect(ignoreInstance.ignores(".gitignore")).toBe(true)
|
||||
expect(ignoreInstance.ignores("src/main.ts")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("rooignore integration", () => {
|
||||
it("should preserve RooIgnoreController instance across service recreations", async () => {
|
||||
// Mock file existence check
|
||||
const { fileExistsAtPath } = await import("../../../utils/fs")
|
||||
const mockFileExistsAtPath = fileExistsAtPath as any
|
||||
mockFileExistsAtPath.mockResolvedValue(true)
|
||||
|
||||
// Mock .rooignore content
|
||||
mockFs.readFile.mockResolvedValue("*.secret\ntemp/")
|
||||
|
||||
// Mock config manager
|
||||
const mockConfigManager = {
|
||||
loadConfiguration: vi.fn().mockResolvedValue({ requiresRestart: false }),
|
||||
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 .gitignore discovery
|
||||
const { findGitignoreFiles } = await import("../../glob/list-files")
|
||||
const mockFindGitignoreFiles = findGitignoreFiles as any
|
||||
mockFindGitignoreFiles.mockResolvedValue([])
|
||||
|
||||
// Mock service factory
|
||||
const mockServiceFactoryInstance = {
|
||||
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)
|
||||
|
||||
// Act - call _recreateServices twice to simulate "Clear Index Data" scenario
|
||||
await (manager as any)._recreateServices()
|
||||
const firstRooIgnoreController = (manager as any)._rooIgnoreController
|
||||
|
||||
await (manager as any)._recreateServices()
|
||||
const secondRooIgnoreController = (manager as any)._rooIgnoreController
|
||||
|
||||
// Assert - should be the same instance (preserved across recreations)
|
||||
expect(firstRooIgnoreController).toBe(secondRooIgnoreController)
|
||||
expect(firstRooIgnoreController).toBeDefined()
|
||||
|
||||
// Verify that loadRooIgnore was called on both recreations
|
||||
expect(mockFileExistsAtPath).toHaveBeenCalledWith("/test/workspace/.rooignore")
|
||||
expect(mockFs.readFile).toHaveBeenCalledWith("/test/workspace/.rooignore", "utf8")
|
||||
|
||||
// Verify that the RooIgnoreController was passed to createServices
|
||||
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalledTimes(2)
|
||||
const firstCall = mockServiceFactoryInstance.createServices.mock.calls[0]
|
||||
const secondCall = mockServiceFactoryInstance.createServices.mock.calls[1]
|
||||
|
||||
// Fourth parameter should be the RooIgnoreController
|
||||
expect(firstCall[3]).toBe(firstRooIgnoreController)
|
||||
expect(secondCall[3]).toBe(secondRooIgnoreController)
|
||||
expect(firstCall[3]).toBe(secondCall[3]) // Same instance
|
||||
})
|
||||
|
||||
it("should reload .rooignore patterns on each service recreation", async () => {
|
||||
// Mock file existence check
|
||||
const { fileExistsAtPath } = await import("../../../utils/fs")
|
||||
const mockFileExistsAtPath = fileExistsAtPath as any
|
||||
mockFileExistsAtPath.mockResolvedValue(true)
|
||||
|
||||
// Mock .rooignore content
|
||||
mockFs.readFile.mockResolvedValue("*.secret\ntemp/")
|
||||
|
||||
// Create a spy on the RooIgnoreController's loadRooIgnore method
|
||||
const mockLoadRooIgnore = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
// Mock config manager
|
||||
const mockConfigManager = {
|
||||
loadConfiguration: vi.fn().mockResolvedValue({ requiresRestart: false }),
|
||||
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 .gitignore discovery
|
||||
const { findGitignoreFiles } = await import("../../glob/list-files")
|
||||
const mockFindGitignoreFiles = findGitignoreFiles as any
|
||||
mockFindGitignoreFiles.mockResolvedValue([])
|
||||
|
||||
// Pre-set a mock RooIgnoreController to test reloading
|
||||
;(manager as any)._rooIgnoreController = {
|
||||
loadRooIgnore: mockLoadRooIgnore,
|
||||
dispose: vi.fn(),
|
||||
}
|
||||
|
||||
// Mock service factory
|
||||
const mockServiceFactoryInstance = {
|
||||
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)
|
||||
|
||||
// Act - call _recreateServices
|
||||
await (manager as any)._recreateServices()
|
||||
|
||||
// Assert - loadRooIgnore should have been called to reload patterns
|
||||
expect(mockLoadRooIgnore).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("integration with service factory", () => {
|
||||
it("should pass both gitignore and rooignore controllers to service factory", async () => {
|
||||
// Mock .gitignore discovery
|
||||
const { findGitignoreFiles } = await import("../../glob/list-files")
|
||||
const mockFindGitignoreFiles = findGitignoreFiles as any
|
||||
mockFindGitignoreFiles.mockResolvedValue(["/test/workspace/.gitignore"])
|
||||
|
||||
// Mock .gitignore content
|
||||
mockFs.readFile.mockImplementation((filePath: string) => {
|
||||
if (filePath === "/test/workspace/.gitignore") {
|
||||
return Promise.resolve("node_modules/\n*.log")
|
||||
}
|
||||
if (filePath === "/test/workspace/.rooignore") {
|
||||
return Promise.resolve("*.secret\ntemp/")
|
||||
}
|
||||
return Promise.reject(new Error("File not found"))
|
||||
})
|
||||
|
||||
// Mock file existence for .rooignore
|
||||
const { fileExistsAtPath } = await import("../../../utils/fs")
|
||||
const mockFileExistsAtPath = fileExistsAtPath as any
|
||||
mockFileExistsAtPath.mockResolvedValue(true)
|
||||
|
||||
// Mock config manager
|
||||
const mockConfigManager = {
|
||||
loadConfiguration: vi.fn().mockResolvedValue({ requiresRestart: false }),
|
||||
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 service factory
|
||||
const mockServiceFactoryInstance = {
|
||||
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)
|
||||
|
||||
// Act
|
||||
await (manager as any)._recreateServices()
|
||||
|
||||
// Assert
|
||||
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalledTimes(1)
|
||||
const createServicesCall = mockServiceFactoryInstance.createServices.mock.calls[0]
|
||||
|
||||
// Verify parameters: context, cacheManager, ignoreInstance, rooIgnoreController
|
||||
expect(createServicesCall).toHaveLength(4)
|
||||
expect(createServicesCall[0]).toBe(mockContext) // context
|
||||
expect(createServicesCall[1]).toBe(mockCacheManager) // cacheManager
|
||||
|
||||
// Third parameter should be ignore instance with .gitignore patterns
|
||||
const ignoreInstance = createServicesCall[2]
|
||||
expect(ignoreInstance.ignores("node_modules/package.json")).toBe(true)
|
||||
expect(ignoreInstance.ignores("debug.log")).toBe(true)
|
||||
expect(ignoreInstance.ignores(".gitignore")).toBe(true)
|
||||
|
||||
// Fourth parameter should be RooIgnoreController
|
||||
const rooIgnoreController = createServicesCall[3]
|
||||
expect(rooIgnoreController).toBeDefined()
|
||||
expect(rooIgnoreController).toBe((manager as any)._rooIgnoreController)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -12,7 +12,17 @@ vi.mock("vscode", () => ({
|
|||
index: 0,
|
||||
},
|
||||
],
|
||||
createFileSystemWatcher: vi.fn(() => ({
|
||||
onDidCreate: vi.fn(),
|
||||
onDidChange: vi.fn(),
|
||||
onDidDelete: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
})),
|
||||
},
|
||||
RelativePattern: vi.fn().mockImplementation((base, pattern) => ({
|
||||
base,
|
||||
pattern,
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock only the essential dependencies
|
||||
|
|
@ -178,6 +188,10 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
|
|||
// Simulate an initialized manager by setting the required properties
|
||||
;(manager as any)._orchestrator = { stopWatcher: vi.fn() }
|
||||
;(manager as any)._searchService = {}
|
||||
;(manager as any)._rooIgnoreController = {
|
||||
dispose: vi.fn(),
|
||||
loadRooIgnore: vi.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
|
||||
// Verify manager is considered initialized
|
||||
expect(manager.isInitialized).toBe(true)
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ import { CacheManager } from "./cache-manager"
|
|||
import fs from "fs/promises"
|
||||
import ignore from "ignore"
|
||||
import path from "path"
|
||||
import { findGitignoreFiles } from "../glob/list-files"
|
||||
import { t } from "../../i18n"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
import { TelemetryEventName } from "@roo-code/types"
|
||||
import { RooIgnoreController } from "../../core/ignore/RooIgnoreController"
|
||||
|
||||
export class CodeIndexManager {
|
||||
// --- Singleton Implementation ---
|
||||
|
|
@ -27,6 +29,7 @@ export class CodeIndexManager {
|
|||
private _orchestrator: CodeIndexOrchestrator | undefined
|
||||
private _searchService: CodeIndexSearchService | undefined
|
||||
private _cacheManager: CacheManager | undefined
|
||||
private _rooIgnoreController: RooIgnoreController | undefined
|
||||
|
||||
public static getInstance(context: vscode.ExtensionContext): CodeIndexManager | undefined {
|
||||
// Use first workspace folder consistently
|
||||
|
|
@ -70,7 +73,13 @@ export class CodeIndexManager {
|
|||
}
|
||||
|
||||
private assertInitialized() {
|
||||
if (!this._configManager || !this._orchestrator || !this._searchService || !this._cacheManager) {
|
||||
if (
|
||||
!this._configManager ||
|
||||
!this._orchestrator ||
|
||||
!this._searchService ||
|
||||
!this._cacheManager ||
|
||||
!this._rooIgnoreController
|
||||
) {
|
||||
throw new Error("CodeIndexManager not initialized. Call initialize() first.")
|
||||
}
|
||||
}
|
||||
|
|
@ -100,6 +109,13 @@ export class CodeIndexManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global RooIgnoreController instance
|
||||
*/
|
||||
public get rooIgnoreController(): RooIgnoreController | undefined {
|
||||
return this._rooIgnoreController
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the manager with configuration and dependent services.
|
||||
* Must be called before using any other methods.
|
||||
|
|
@ -186,6 +202,9 @@ export class CodeIndexManager {
|
|||
if (this._orchestrator) {
|
||||
this.stopWatcher()
|
||||
}
|
||||
if (this._rooIgnoreController) {
|
||||
this._rooIgnoreController.dispose()
|
||||
}
|
||||
this._stateManager.dispose()
|
||||
}
|
||||
|
||||
|
|
@ -228,6 +247,7 @@ export class CodeIndexManager {
|
|||
// Clear existing services to ensure clean state
|
||||
this._orchestrator = undefined
|
||||
this._searchService = undefined
|
||||
// Note: Keep _rooIgnoreController to preserve file watchers unless it doesn't exist
|
||||
|
||||
// (Re)Initialize service factory
|
||||
this._serviceFactory = new CodeIndexServiceFactory(
|
||||
|
|
@ -244,26 +264,41 @@ export class CodeIndexManager {
|
|||
return
|
||||
}
|
||||
|
||||
const ignorePath = path.join(workspacePath, ".gitignore")
|
||||
// Load all .gitignore files from workspace root up to parent directories
|
||||
try {
|
||||
const content = await fs.readFile(ignorePath, "utf8")
|
||||
ignoreInstance.add(content)
|
||||
const gitignoreFiles = await findGitignoreFiles(workspacePath)
|
||||
for (const gitignoreFile of gitignoreFiles) {
|
||||
try {
|
||||
const content = await fs.readFile(gitignoreFile, "utf8")
|
||||
ignoreInstance.add(content)
|
||||
} catch (error) {
|
||||
console.warn(`Failed to read .gitignore file at ${gitignoreFile}:`, error)
|
||||
}
|
||||
}
|
||||
// Always ignore .gitignore files themselves
|
||||
ignoreInstance.add(".gitignore")
|
||||
} catch (error) {
|
||||
// Should never happen: reading file failed even though it exists
|
||||
console.error("Unexpected error loading .gitignore:", error)
|
||||
console.error("Error finding .gitignore files:", error)
|
||||
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
location: "_recreateServices",
|
||||
location: "_recreateServices.gitignore",
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize or reuse RooIgnoreController with file watchers
|
||||
if (!this._rooIgnoreController) {
|
||||
this._rooIgnoreController = new RooIgnoreController(workspacePath)
|
||||
}
|
||||
// Always reload .rooignore patterns to ensure they're up to date
|
||||
await this._rooIgnoreController.loadRooIgnore()
|
||||
|
||||
// (Re)Create shared service instances
|
||||
const { embedder, vectorStore, scanner, fileWatcher } = this._serviceFactory.createServices(
|
||||
this.context,
|
||||
this._cacheManager!,
|
||||
ignoreInstance,
|
||||
this._rooIgnoreController,
|
||||
)
|
||||
|
||||
// Validate embedder configuration before proceeding
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export class DirectoryScanner implements IDirectoryScanner {
|
|||
private readonly codeParser: ICodeParser,
|
||||
private readonly cacheManager: CacheManager,
|
||||
private readonly ignoreInstance: Ignore,
|
||||
private readonly rooIgnoreController?: RooIgnoreController,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
|
@ -62,10 +63,15 @@ export class DirectoryScanner implements IDirectoryScanner {
|
|||
// Filter out directories (marked with trailing '/')
|
||||
const filePaths = allPaths.filter((p) => !p.endsWith("/"))
|
||||
|
||||
// Initialize RooIgnoreController if not provided
|
||||
const ignoreController = new RooIgnoreController(directoryPath)
|
||||
|
||||
await ignoreController.initialize()
|
||||
// Use injected RooIgnoreController or create a fallback
|
||||
let ignoreController: RooIgnoreController
|
||||
if (this.rooIgnoreController) {
|
||||
ignoreController = this.rooIgnoreController
|
||||
} else {
|
||||
// Fallback for backward compatibility
|
||||
ignoreController = new RooIgnoreController(directoryPath)
|
||||
await ignoreController.initialize()
|
||||
}
|
||||
|
||||
// Filter paths using .rooignore
|
||||
const allowedPaths = ignoreController.filterPaths(filePaths)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { ICodeParser, IEmbedder, IFileWatcher, IVectorStore } from "./interfaces
|
|||
import { CodeIndexConfigManager } from "./config-manager"
|
||||
import { CacheManager } from "./cache-manager"
|
||||
import { Ignore } from "ignore"
|
||||
import { RooIgnoreController } from "../../core/ignore/RooIgnoreController"
|
||||
import { t } from "../../i18n"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
import { TelemetryEventName } from "@roo-code/types"
|
||||
|
|
@ -145,8 +146,16 @@ export class CodeIndexServiceFactory {
|
|||
vectorStore: IVectorStore,
|
||||
parser: ICodeParser,
|
||||
ignoreInstance: Ignore,
|
||||
rooIgnoreController?: RooIgnoreController,
|
||||
): DirectoryScanner {
|
||||
return new DirectoryScanner(embedder, vectorStore, parser, this.cacheManager, ignoreInstance)
|
||||
return new DirectoryScanner(
|
||||
embedder,
|
||||
vectorStore,
|
||||
parser,
|
||||
this.cacheManager,
|
||||
ignoreInstance,
|
||||
rooIgnoreController,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -158,8 +167,17 @@ export class CodeIndexServiceFactory {
|
|||
vectorStore: IVectorStore,
|
||||
cacheManager: CacheManager,
|
||||
ignoreInstance: Ignore,
|
||||
rooIgnoreController?: RooIgnoreController,
|
||||
): IFileWatcher {
|
||||
return new FileWatcher(this.workspacePath, context, cacheManager, embedder, vectorStore, ignoreInstance)
|
||||
return new FileWatcher(
|
||||
this.workspacePath,
|
||||
context,
|
||||
cacheManager,
|
||||
embedder,
|
||||
vectorStore,
|
||||
ignoreInstance,
|
||||
rooIgnoreController,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -170,6 +188,7 @@ export class CodeIndexServiceFactory {
|
|||
context: vscode.ExtensionContext,
|
||||
cacheManager: CacheManager,
|
||||
ignoreInstance: Ignore,
|
||||
rooIgnoreController?: RooIgnoreController,
|
||||
): {
|
||||
embedder: IEmbedder
|
||||
vectorStore: IVectorStore
|
||||
|
|
@ -184,8 +203,15 @@ export class CodeIndexServiceFactory {
|
|||
const embedder = this.createEmbedder()
|
||||
const vectorStore = this.createVectorStore()
|
||||
const parser = codeParser
|
||||
const scanner = this.createDirectoryScanner(embedder, vectorStore, parser, ignoreInstance)
|
||||
const fileWatcher = this.createFileWatcher(context, embedder, vectorStore, cacheManager, ignoreInstance)
|
||||
const scanner = this.createDirectoryScanner(embedder, vectorStore, parser, ignoreInstance, rooIgnoreController)
|
||||
const fileWatcher = this.createFileWatcher(
|
||||
context,
|
||||
embedder,
|
||||
vectorStore,
|
||||
cacheManager,
|
||||
ignoreInstance,
|
||||
rooIgnoreController,
|
||||
)
|
||||
|
||||
return {
|
||||
embedder,
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ async function createIgnoreInstance(dirPath: string): Promise<ReturnType<typeof
|
|||
/**
|
||||
* Find all .gitignore files from the given directory up to the workspace root
|
||||
*/
|
||||
async function findGitignoreFiles(startPath: string): Promise<string[]> {
|
||||
export async function findGitignoreFiles(startPath: string): Promise<string[]> {
|
||||
const gitignoreFiles: string[] = []
|
||||
let currentPath = startPath
|
||||
|
||||
|
|
@ -312,7 +312,6 @@ function isDirectoryExplicitlyIgnored(dirName: string): boolean {
|
|||
return false
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Combine file and directory results and format them properly
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue