test: add vscode Uri/RelativePattern mocks and fix extension.spec; fix: guard dev FS watchers in activate() and MCP watchers in tests; core(task): safe provider logging in batched webview updates

This commit is contained in:
Hannes Rudolph 2025-10-12 01:15:33 -06:00
parent 458aac2eb5
commit 62557dcc06
5 changed files with 65 additions and 31 deletions

View file

@ -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,

View file

@ -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", () => ({

View file

@ -303,6 +303,17 @@ export class Task extends EventEmitter<TaskEvents> 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<TaskEvents> 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<TaskEvents> 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<TaskEvents> 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<TaskEvents> 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<TaskEvents> 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<TaskEvents> 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}'`,
)
}

View file

@ -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" },

View file

@ -152,6 +152,10 @@ export class McpHub {
private refCount: number = 0 // Reference counter for active clients
private configChangeDebounceTimers: Map<string, NodeJS.Timeout> = 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<void> {
// 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<void> {
// 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
}