Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 188 additions & 3 deletions src/__tests__/extension.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// npx vitest run __tests__/extension.spec.ts

import type * as vscode from "vscode"
import { makeUri } from "../test-utils/vscode"

vi.mock("vscode", () => ({
window: {
Expand All @@ -15,6 +16,7 @@ vi.mock("vscode", () => ({
onDidChangeActiveTextEditor: vi.fn(),
},
workspace: {
workspaceFolders: undefined,
registerTextDocumentContentProvider: vi.fn(),
getConfiguration: vi.fn().mockReturnValue({
get: vi.fn().mockReturnValue([]),
Expand Down Expand Up @@ -140,9 +142,9 @@ vi.mock("../services/mcp/McpServerManager", () => ({
}))

vi.mock("../services/code-index/manager", () => ({
CodeIndexManager: {
getInstance: vi.fn().mockReturnValue(null),
},
CodeIndexManager: vi.fn().mockImplementation(function () {
return { initialize: vi.fn().mockResolvedValue({ requiresRestart: false }), dispose: vi.fn() }
}),
}))

vi.mock("../services/mdm/MdmService", () => ({
Expand Down Expand Up @@ -267,6 +269,189 @@ describe("extension.ts", () => {
expect(dotenv.config).toHaveBeenCalledTimes(1)
})

describe("code index workspace activation", () => {
beforeEach(() => {
vi.resetModules()
})

afterEach(async () => {
const vscode = await import("vscode")
vi.mocked(vscode.workspace).workspaceFolders = undefined
const { codeIndexWorkspaceScopeRegistry } =
await import("../services/code-index/code-index-workspace-scope-registry")
codeIndexWorkspaceScopeRegistry.disposeAll()
})

test("initializes every workspace once with the shared context without blocking activation", async () => {
const vscode = await import("vscode")
const folders = ["/first", "/second"].map((name, index) => ({ name, index, uri: makeUri(name) }))
vi.mocked(vscode.workspace).workspaceFolders = folders
const { codeIndexWorkspaceScopeRegistry: registry } =
await import("../services/code-index/code-index-workspace-scope-registry")
const scopes = folders.map((folder) => registry.getScope(mockContext, folder.uri.fsPath)!)
let release!: () => void
const pending = new Promise<void>((resolve) => {
release = resolve
})
for (const scope of scopes) {
vi.mocked(scope.codeIndexManager.initialize).mockImplementationOnce(async () => {
await pending
return { requiresRestart: false }
})
}
const { ContextProxy } = await import("../core/config/ContextProxy")
const { activate } = await import("../extension")
let activated = false
const activation = activate(mockContext).then(() => {
activated = true
})
try {
await vi.waitFor(() => expect(activated).toBe(true))
const contextProxy = await ContextProxy.getInstance(mockContext)
for (const scope of scopes) {
expect(scope.codeIndexManager.initialize).toHaveBeenCalledExactlyOnceWith(contextProxy)
expect(scope.codeIndexManager.dispose).not.toHaveBeenCalled()
}
expect(vscode.commands.executeCommand).toHaveBeenCalledWith("test-extension.activationCompleted")
} finally {
release()
await activation
}
})

test.each([new Error("configuration failed"), "configuration failed"])(
"logs background rejection %s with its workspace and continues other initialization and cleanup",
async (error) => {
const vscode = await import("vscode")
vi.mocked(vscode.workspace).workspaceFolders = ["/broken", "/healthy"].map((name, index) => ({
name,
index,
uri: makeUri(name),
}))
const { codeIndexWorkspaceScopeRegistry: registry } =
await import("../services/code-index/code-index-workspace-scope-registry")
const broken = registry.getScope(mockContext, "/broken")!
const healthy = registry.getScope(mockContext, "/healthy")!
vi.mocked(broken.codeIndexManager.initialize).mockRejectedValueOnce(error)
const { activate } = await import("../extension")
await expect(activate(mockContext)).resolves.toBeDefined()

expect(healthy.codeIndexManager.initialize).toHaveBeenCalledTimes(1)
const channel = vi.mocked(vscode.window.createOutputChannel).mock.results.at(-1)?.value
expect(channel.appendLine).toHaveBeenCalledWith(
"[CodeIndexManager] Error during background CodeIndexManager configuration/indexing for /broken: configuration failed",
)
await Promise.all(mockContext.subscriptions.map((subscription) => subscription?.dispose?.()))
expect(broken.codeIndexManager.dispose).toHaveBeenCalledTimes(1)
expect(healthy.codeIndexManager.dispose).toHaveBeenCalledTimes(1)
expect(registry.getAllScopes()).toEqual([])
},
)

test.each([undefined, []])(
"activation without workspace folders (%s) still owns lazy cleanup",
async (folders) => {
const vscode = await import("vscode")
vi.mocked(vscode.workspace).workspaceFolders = folders
const { CodeIndexManager } = await import("../services/code-index/manager")
const { activate } = await import("../extension")
await expect(activate(mockContext)).resolves.toBeDefined()
expect(CodeIndexManager).not.toHaveBeenCalled()

vi.mocked(vscode.workspace).workspaceFolders = [{ name: "late", index: 0, uri: makeUri("/late") }]
const { codeIndexWorkspaceScopeRegistry: registry } =
await import("../services/code-index/code-index-workspace-scope-registry")
const lazy = registry.getScope(mockContext, "/late")!
await Promise.all(mockContext.subscriptions.map((subscription) => subscription?.dispose?.()))
expect(lazy.codeIndexManager.dispose).toHaveBeenCalledTimes(1)
expect(registry.getAllScopes()).toEqual([])
},
)

test("one registry cleanup owner disposes startup and lazy scopes exactly once", async () => {
const vscode = await import("vscode")
const first = { name: "first", index: 0, uri: makeUri("/first") }
const late = { name: "late", index: 1, uri: makeUri("/late") }
vi.mocked(vscode.workspace).workspaceFolders = [first]
const { codeIndexWorkspaceScopeRegistry: registry } =
await import("../services/code-index/code-index-workspace-scope-registry")
const { activate, deactivate } = await import("../extension")
await activate(mockContext)
const startup = registry.getScope(mockContext, first.uri.fsPath)!
vi.mocked(vscode.workspace).workspaceFolders = [first, late]
const lazy = registry.getScope(mockContext, late.uri.fsPath)!

await deactivate()
for (const subscription of mockContext.subscriptions) {
// Unrelated activation mocks do not all return a disposable.
await subscription?.dispose?.()
}

expect(startup.codeIndexManager.dispose).toHaveBeenCalledTimes(1)
expect(lazy.codeIndexManager.dispose).toHaveBeenCalledTimes(1)
expect(registry.getAllScopes()).toEqual([])
expect(mockContext.subscriptions).not.toContain(startup)
expect(mockContext.subscriptions).not.toContain(lazy)
registry.disposeAll()
expect(startup.codeIndexManager.dispose).toHaveBeenCalledTimes(1)
expect(lazy.codeIndexManager.dispose).toHaveBeenCalledTimes(1)
})

test.each([false, true])(
"cleanup waits for pending initialization (rejects=%s) before disposal",
async (rejects) => {
const vscode = await import("vscode")
vi.mocked(vscode.workspace).workspaceFolders = [{ name: "first", index: 0, uri: makeUri("/first") }]
const { codeIndexWorkspaceScopeRegistry: registry } =
await import("../services/code-index/code-index-workspace-scope-registry")
const scope = registry.getScope(mockContext, "/first")!
let release!: () => void
const pending = new Promise<void>((resolve) => {
release = resolve
})
vi.mocked(scope.codeIndexManager.initialize).mockImplementationOnce(async () => {
await pending
if (rejects) throw new Error("late initialization failure")
return { requiresRestart: false }
})
const { activate, deactivate } = await import("../extension")
await activate(mockContext)
const cleanup = Promise.all(mockContext.subscriptions.map((subscription) => subscription?.dispose?.()))
const disposedBeforeInitialization = vi.mocked(scope.codeIndexManager.dispose).mock.calls.length
release()
await cleanup
await deactivate()

expect(disposedBeforeInitialization).toBe(0)
expect(scope.codeIndexManager.dispose).toHaveBeenCalledTimes(1)
expect(registry.getAllScopes()).toEqual([])
},
)

test("skips an unavailable scope without skipping later workspace initialization", async () => {
const vscode = await import("vscode")
vi.mocked(vscode.workspace).workspaceFolders = ["/unavailable", "/healthy"].map((name, index) => ({
name,
index,
uri: makeUri(name),
}))
const { codeIndexWorkspaceScopeRegistry: registry } =
await import("../services/code-index/code-index-workspace-scope-registry")
const getScope = vi.spyOn(registry, "getScope").mockReturnValueOnce(undefined)
try {
const { activate } = await import("../extension")
await expect(activate(mockContext)).resolves.toBeDefined()
expect(getScope).toHaveBeenCalledWith(mockContext, "/unavailable")
expect(getScope).toHaveBeenCalledWith(mockContext, "/healthy")
const scopes = registry.getAllScopes()
expect(scopes).toHaveLength(1)
expect(scopes[0].codeIndexManager.initialize).toHaveBeenCalledTimes(1)
} finally {
getScope.mockRestore()
}
})
})

describe("cloud organization settings handling", () => {
beforeEach(() => {
vi.resetModules()
Expand Down
6 changes: 3 additions & 3 deletions src/activate/__tests__/registerCommands.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ vi.mock("../../core/config/importExport", () => ({
importSettingsWithFeedback: vi.fn(),
}))

vi.mock("../../services/code-index/manager", () => ({
CodeIndexManager: {
getInstance: vi.fn(),
vi.mock("../../services/code-index/code-index-workspace-scope-registry", () => ({
codeIndexWorkspaceScopeRegistry: {
getScope: vi.fn(),
},
}))

Expand Down
2 changes: 0 additions & 2 deletions src/activate/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { ClineProvider } from "../core/webview/ClineProvider"
import { ContextProxy } from "../core/config/ContextProxy"
import { focusPanel } from "../utils/focusPanel"
import { handleNewTask } from "./handleTask"
import { CodeIndexManager } from "../services/code-index/manager"
import { importSettingsWithFeedback } from "../core/config/importExport"
import { MdmService } from "../services/mdm/MdmService"
import { registerRipgrepDiagnosticCommand } from "../services/ripgrep/diagnostic"
Expand Down Expand Up @@ -227,7 +226,6 @@ export const openClineInNewTab = async ({ context, outputChannel }: Omit<Registe
// don't need to use that event).
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
const contextProxy = await ContextProxy.getInstance(context)
const codeIndexManager = CodeIndexManager.getInstance(context)

// Get the existing MDM service instance to ensure consistent policy enforcement
let mdmService: MdmService | undefined
Expand Down
3 changes: 0 additions & 3 deletions src/core/prompts/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { formatLanguage } from "../../shared/language"
import { isEmpty } from "../../utils/object"

import { McpHub } from "../../services/mcp/McpHub"
import { CodeIndexManager } from "../../services/code-index/manager"
import { SkillsManager } from "../../services/skills/SkillsManager"

import type { SystemPromptSettings } from "./types"
Expand Down Expand Up @@ -79,8 +78,6 @@ async function generatePrompt(
}
const shouldIncludeMcp = hasMcpGroup && hasMcpServers

const codeIndexManager = CodeIndexManager.getInstance(context, cwd)

// Tool calling is native-only.
const effectiveProtocol = "native"

Expand Down
Loading
Loading