diff --git a/src/__mocks__/vscode.js b/src/__mocks__/vscode.js index 7fc82f559f..1715e49ca1 100644 --- a/src/__mocks__/vscode.js +++ b/src/__mocks__/vscode.js @@ -36,6 +36,8 @@ const mockSelection = class extends mockRange { } } +const mockRelativePattern = (base, pattern) => ({ base, pattern }) + export const workspace = { workspaceFolders: [], getWorkspaceFolder: () => null, @@ -107,6 +109,7 @@ export const env = { } export const Uri = mockUri +export const RelativePattern = mockRelativePattern export const Range = mockRange export const Position = mockPosition export const Selection = mockSelection @@ -160,6 +163,7 @@ export default { extensions, env, Uri, + RelativePattern, Range, Position, Selection, diff --git a/src/__tests__/extension.spec.ts b/src/__tests__/extension.spec.ts index c39854f697..a4827157b9 100644 --- a/src/__tests__/extension.spec.ts +++ b/src/__tests__/extension.spec.ts @@ -40,6 +40,14 @@ vi.mock("vscode", () => ({ ExtensionMode: { Production: 1, }, + // Provide minimal implementations required by extension.ts watchers in dev + Uri: { + file: vi.fn().mockImplementation((p: string) => ({ fsPath: p })), + }, + RelativePattern: vi.fn().mockImplementation((base: { fsPath?: string } | string, pattern: string) => ({ + base: typeof base === "string" ? base : (base?.fsPath ?? ""), + pattern, + })), })) vi.mock("@dotenvx/dotenvx", () => ({ diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 3bd9729831..9f8ccb5462 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -303,6 +303,17 @@ export class Task extends EventEmitter implements TaskLike { private tokenUsageSnapshot?: TokenUsage private tokenUsageSnapshotAt?: number + private safeProviderLog(message: string): void { + try { + const provider = this.providerRef.deref() + if (provider && typeof (provider as any).log === "function") { + ;(provider as any).log(message) + } + } catch { + // no-op (tests/mocks may not implement log) + } + } + constructor({ provider, apiConfiguration, @@ -466,7 +477,7 @@ export class Task extends EventEmitter implements TaskLike { this._taskMode = defaultModeSlug // Use the provider's log method for better error visibility const errorMessage = `Failed to initialize task mode: ${error instanceof Error ? error.message : String(error)}` - provider.log(errorMessage) + this.safeProviderLog(errorMessage) } } @@ -688,20 +699,16 @@ export class Task extends EventEmitter implements TaskLike { const providerNow = this.providerRef.deref() if (!providerNow) { - this.providerRef - .deref() - ?.log( - `[Task#updateClineMessage] Dropping ${batch.length} messageUpdated deltas: provider unavailable`, - ) + this.safeProviderLog( + `[Task#updateClineMessage] Dropping ${batch.length} messageUpdated deltas: provider unavailable`, + ) return } if (!providerNow.isVisible()) { // Drop deltas while hidden; UI will receive a full state sync on visibility - this.providerRef - .deref() - ?.log( - `[Task#updateClineMessage] Dropping ${batch.length} messageUpdated deltas while hidden`, - ) + this.safeProviderLog( + `[Task#updateClineMessage] Dropping ${batch.length} messageUpdated deltas while hidden`, + ) return } @@ -709,13 +716,11 @@ export class Task extends EventEmitter implements TaskLike { await providerNow.postMessageToWebview({ type: "messageUpdated", clineMessage: m }) } } catch (e) { - this.providerRef - .deref() - ?.log( - `[Task#updateClineMessage] Failed to flush message updates: ${ - e instanceof Error ? e.message : String(e) - }`, - ) + this.safeProviderLog( + `[Task#updateClineMessage] Failed to flush message updates: ${ + e instanceof Error ? e.message : String(e) + }`, + ) } }, this.MESSAGE_UPDATE_THROTTLE_MS) } @@ -1741,9 +1746,9 @@ export class Task extends EventEmitter implements TaskLike { // including the subtask result, not just from before the subtask was created this.skipPrevResponseIdOnce = true } catch (error) { - this.providerRef - .deref() - ?.log(`Error failed to add reply from subtask into conversation of parent task, error: ${error}`) + this.safeProviderLog( + `Error failed to add reply from subtask into conversation of parent task, error: ${error}`, + ) throw error } @@ -1834,9 +1839,9 @@ export class Task extends EventEmitter implements TaskLike { const provider = this.providerRef.deref() if (this.isPaused && provider) { - provider.log(`[subtasks] paused ${this.taskId}.${this.instanceId}`) + this.safeProviderLog(`[subtasks] paused ${this.taskId}.${this.instanceId}`) await this.waitForSubtask() - provider.log(`[subtasks] resumed ${this.taskId}.${this.instanceId}`) + this.safeProviderLog(`[subtasks] resumed ${this.taskId}.${this.instanceId}`) const currentMode = (await provider.getState())?.mode ?? defaultModeSlug if (currentMode !== this.pausedModeSlug) { @@ -1846,7 +1851,7 @@ export class Task extends EventEmitter implements TaskLike { // Delay to allow mode change to take effect before next tool is executed. await delay(500) - provider.log( + this.safeProviderLog( `[subtasks] task ${this.taskId}.${this.instanceId} has switched back to '${this.pausedModeSlug}' from '${currentMode}'`, ) } diff --git a/src/extension.ts b/src/extension.ts index 5db0996ad6..5ebfa23297 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -273,7 +273,12 @@ export async function activate(context: vscode.ExtensionContext) { const enableLogging = typeof socketPath === "string" // Watch the core files and automatically reload the extension host. - if (process.env.NODE_ENV === "development") { + if ( + process.env.NODE_ENV === "development" && + (vscode as any).RelativePattern && + (vscode as any).Uri?.file && + vscode.workspace?.createFileSystemWatcher + ) { const watchPaths = [ { path: context.extensionPath, pattern: "**/*.ts" }, { path: path.join(context.extensionPath, "../packages/types"), pattern: "**/*.ts" }, diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index caca5ddb39..96daf1dd39 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -152,6 +152,10 @@ export class McpHub { private refCount: number = 0 // Reference counter for active clients private configChangeDebounceTimers: Map = new Map() + private isTestEnvironment(): boolean { + return process.env.NODE_ENV === "test" || !!process.env.VITEST || process.env.JEST === "true" + } + constructor(provider: ClineProvider) { this.providerRef = new WeakRef(provider) this.watchMcpSettingsFile() @@ -261,8 +265,7 @@ export class McpHub { } public setupWorkspaceFoldersWatcher(): void { - // Skip if test environment is detected - if (process.env.NODE_ENV === "test") { + if (this.isTestEnvironment() || !vscode.workspace?.onDidChangeWorkspaceFolders) { return } @@ -334,8 +337,12 @@ export class McpHub { } private async watchProjectMcpFile(): Promise { - // Skip if test environment is detected or VSCode APIs are not available - if (process.env.NODE_ENV === "test" || !vscode.workspace.createFileSystemWatcher) { + // Skip in test environments or when required VSCode APIs are unavailable + if ( + this.isTestEnvironment() || + !vscode.workspace?.createFileSystemWatcher || + !(vscode as any).RelativePattern + ) { return } @@ -467,8 +474,13 @@ export class McpHub { } private async watchMcpSettingsFile(): Promise { - // Skip if test environment is detected or VSCode APIs are not available - if (process.env.NODE_ENV === "test" || !vscode.workspace.createFileSystemWatcher) { + // Skip in test environments or when required VSCode APIs are unavailable + if ( + this.isTestEnvironment() || + !vscode.workspace?.createFileSystemWatcher || + !(vscode as any).Uri?.file || + !(vscode as any).RelativePattern + ) { return }