From 3bd4e43b91cb55b03dbaef70c2eb28e6fd4867b7 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 1 Nov 2026 03:21:40 +0000 Subject: [PATCH 1/2] refactor: remove no-explicit-any suppressions in small directories --- packages/types/src/task.ts | 3 + src/__mocks__/fs/promises.ts | 8 +- .../ClineProvider.delegation.spec.ts | 37 ++- ...Provider.history-resume-delegation.spec.ts | 215 ++++++++++-------- src/__tests__/abandonSubtask.spec.ts | 46 ++-- src/__tests__/api-subtask.spec.ts | 6 +- src/__tests__/delegation-concurrent.spec.ts | 12 +- src/__tests__/delegation-events.spec.ts | 16 +- src/__tests__/migrateSettings.spec.ts | 8 +- .../nested-delegation-resume.spec.ts | 54 +++-- src/__tests__/new-task-delegation.spec.ts | 19 +- src/activate/CodeActionProvider.ts | 2 +- .../__tests__/CodeActionProvider.spec.ts | 40 ++-- src/activate/__tests__/handleUri.spec.ts | 97 ++++---- .../__tests__/registerCommands.spec.ts | 11 +- src/activate/registerCodeActions.ts | 13 +- src/activate/registerCommands.ts | 9 +- src/activate/registerTerminalActions.ts | 2 +- src/eslint-suppressions.json | 165 -------------- .../api-delete-queued-message.spec.ts | 2 +- .../__tests__/api-send-message.spec.ts | 10 +- src/extension/api.ts | 18 +- src/i18n/index.ts | 2 +- src/i18n/setup.ts | 4 +- src/shared/__tests__/api.spec.ts | 16 +- src/shared/__tests__/embeddingModels.spec.ts | 6 +- .../modes-empty-prompt-component.spec.ts | 4 +- src/shared/api.ts | 6 +- src/shared/checkExistApiConfig.ts | 4 +- src/shared/cost.ts | 3 +- src/shared/parse-command.ts | 4 +- src/shared/support-prompt.ts | 20 +- src/shared/tools.ts | 2 +- src/shared/utils/__tests__/requesty.spec.ts | 6 +- src/vitest.setup.ts | 2 +- 35 files changed, 401 insertions(+), 471 deletions(-) diff --git a/packages/types/src/task.ts b/packages/types/src/task.ts index d243bffbb1..5004479342 100644 --- a/packages/types/src/task.ts +++ b/packages/types/src/task.ts @@ -147,6 +147,9 @@ export type TaskEvents = { [RooCodeEventName.TaskPaused]: [taskId: string] [RooCodeEventName.TaskUnpaused]: [taskId: string] [RooCodeEventName.TaskSpawned]: [taskId: string] + [RooCodeEventName.TaskDelegated]: [childTaskId: string] + [RooCodeEventName.TaskDelegationCompleted]: [childTaskId: string, summary: string] + [RooCodeEventName.TaskDelegationResumed]: [childTaskId: string] // Task Execution [RooCodeEventName.Message]: [{ action: "created" | "updated"; message: ClineMessage }] diff --git a/src/__mocks__/fs/promises.ts b/src/__mocks__/fs/promises.ts index daed479d4a..1ea7b56f3f 100644 --- a/src/__mocks__/fs/promises.ts +++ b/src/__mocks__/fs/promises.ts @@ -80,7 +80,7 @@ const mockFs = { // Handle file not found const error = new Error(`ENOENT: no such file or directory, open '${filePath}'`) - ;(error as any).code = "ENOENT" + ;(error as NodeJS.ErrnoException).code = "ENOENT" throw error }), @@ -113,7 +113,7 @@ const mockFs = { currentPath += "/" + parts[i] if (!mockDirectories.has(currentPath)) { const error = new Error(`ENOENT: no such file or directory, mkdir '${path}'`) - ;(error as any).code = "ENOENT" + ;(error as NodeJS.ErrnoException).code = "ENOENT" throw error } } @@ -130,7 +130,7 @@ const mockFs = { return Promise.resolve() } const error = new Error(`ENOENT: no such file or directory, access '${path}'`) - ;(error as any).code = "ENOENT" + ;(error as NodeJS.ErrnoException).code = "ENOENT" throw error }), @@ -146,7 +146,7 @@ const mockFs = { } // If old file doesn't exist, throw an error const error = new Error(`ENOENT: no such file or directory, rename '${oldPath}'`) - ;(error as any).code = "ENOENT" + ;(error as NodeJS.ErrnoException).code = "ENOENT" throw error }), diff --git a/src/__tests__/ClineProvider.delegation.spec.ts b/src/__tests__/ClineProvider.delegation.spec.ts index 68f39c6517..eceb4cbeb0 100644 --- a/src/__tests__/ClineProvider.delegation.spec.ts +++ b/src/__tests__/ClineProvider.delegation.spec.ts @@ -40,16 +40,15 @@ function makeStoreStub( * `parent`. Without flushPendingToolResultsToHistory the method hits its * non-fatal flush-error branch and never reaches the happy delegation path. */ -const makeParentTask = () => - ({ - taskId: "parent-1", - apiConfiguration: { apiProvider: providerIdentifiers.anthropic, anthropicApiKey: "task-local-key" }, - getTaskMode: vi.fn().mockResolvedValue("code"), - getTaskApiConfigName: vi.fn().mockResolvedValue("task-local-profile"), - emit: vi.fn(), - flushPendingToolResultsToHistory: vi.fn().mockResolvedValue(true), - retrySaveApiConversationHistory: vi.fn(), - }) as any +const makeParentTask = () => ({ + taskId: "parent-1", + apiConfiguration: { apiProvider: providerIdentifiers.anthropic, anthropicApiKey: "task-local-key" }, + getTaskMode: vi.fn().mockResolvedValue("code"), + getTaskApiConfigName: vi.fn().mockResolvedValue("task-local-profile"), + emit: vi.fn(), + flushPendingToolResultsToHistory: vi.fn().mockResolvedValue(true), + retrySaveApiConversationHistory: vi.fn(), +}) describe("ClineProvider.delegateParentAndOpenChild()", () => { it("rejects a stale restored action before delegation side effects", async () => { @@ -150,7 +149,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { } const parentTask = makeParentTask() const child = { taskId: "child-1", run: vi.fn().mockResolvedValue(undefined) } - const getCurrentTask = vi.fn(() => parentTask) + const getCurrentTask = vi.fn((): { taskId: string } => parentTask) const createTask = vi.fn(async () => { getCurrentTask.mockReturnValue(child) return child @@ -218,7 +217,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { taskHistoryStore, } as unknown as ClineProvider - const child = await (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, { + const child = await ClineProvider.prototype.delegateParentAndOpenChild.call(provider, { parentTaskId: "parent-1", message: "Do something", initialTodos: [], @@ -416,7 +415,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { taskHistoryStore, } as unknown as ClineProvider - await (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, { + await ClineProvider.prototype.delegateParentAndOpenChild.call(provider, { parentTaskId: "parent-1", message: "Do something", initialTodos: [], @@ -450,7 +449,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { taskHistoryStore, } as unknown as ClineProvider - await (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, { + await ClineProvider.prototype.delegateParentAndOpenChild.call(provider, { parentTaskId: "parent-1", message: "Do something", initialTodos: [], @@ -491,7 +490,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { taskHistoryStore, } as unknown as ClineProvider - await (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, { + await ClineProvider.prototype.delegateParentAndOpenChild.call(provider, { parentTaskId: "parent-1", message: "Do something", initialTodos: [], @@ -538,7 +537,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { taskHistoryStore, } as unknown as ClineProvider - await (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, { + await ClineProvider.prototype.delegateParentAndOpenChild.call(provider, { parentTaskId: "parent-1", message: "Continue", initialTodos: [], @@ -603,7 +602,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { } as unknown as ClineProvider await expect( - (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, { + ClineProvider.prototype.delegateParentAndOpenChild.call(provider, { parentTaskId: "parent-1", message: "Continue", initialTodos: [], @@ -614,7 +613,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { // The authoritative preflight rejects before either provider mutates its stack. expect(child.run).not.toHaveBeenCalled() expect(createTask).not.toHaveBeenCalled() - expect((provider as any).deleteTaskWithId).not.toHaveBeenCalled() + expect(provider.deleteTaskWithId).not.toHaveBeenCalled() }) it("rejects a delegated parent whose awaited-child identity is missing", async () => { @@ -744,7 +743,7 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => { } as unknown as ClineProvider await expect( - (ClineProvider.prototype as any).delegateParentAndOpenChild.call(provider, { + ClineProvider.prototype.delegateParentAndOpenChild.call(provider, { parentTaskId: "parent-1", message: "Do something", initialTodos: [], diff --git a/src/__tests__/ClineProvider.history-resume-delegation.spec.ts b/src/__tests__/ClineProvider.history-resume-delegation.spec.ts index ee87830da0..1283a2a59e 100644 --- a/src/__tests__/ClineProvider.history-resume-delegation.spec.ts +++ b/src/__tests__/ClineProvider.history-resume-delegation.spec.ts @@ -17,7 +17,7 @@ vi.mock("vscode", () => { } const workspace = { getConfiguration: vi.fn(() => ({ - get: vi.fn((_key: string, defaultValue: any) => defaultValue), + get: vi.fn((_key: string, defaultValue: unknown) => defaultValue), update: vi.fn(), })), workspaceFolders: [], @@ -57,19 +57,28 @@ import { ClineProvider } from "../core/webview/ClineProvider" import { readTaskMessages } from "../core/task-persistence/taskMessages" import { readApiMessages, saveApiMessages, saveTaskMessages } from "../core/task-persistence" import { makeProviderStub } from "./helpers/provider-stub" +import type { ToolUse } from "../shared/tools" +import type { AttemptCompletionCallbacks } from "../core/tools/AttemptCompletionTool" + +function firstContentBlock(message: ApiMessage) { + if (!Array.isArray(message.content) || message.content.length === 0) { + throw new Error("Expected a non-empty content block array") + } + return message.content[0] +} /** * Create a minimal taskHistoryStore stub whose atomicUpdatePair calls both updaters * with the provided items and resolves, simulating the happy-path atomic write. */ -function makeTaskHistoryStoreStub( - childItem: Record, - parentItem: Record, +function makeTaskHistoryStoreStub( + childItem: Child, + parentItem: Parent, overrides: { atomicUpdatePair?: ReturnType } = {}, ) { - const itemMap = new Map>([ - [childItem.id!, childItem], - [parentItem.id!, parentItem], + const itemMap = new Map & { id: string }>([ + [childItem.id, childItem as Partial & { id: string }], + [parentItem.id, parentItem as Partial & { id: string }], ]) const atomicUpdatePair = vi.fn( @@ -278,7 +287,7 @@ describe("History resume delegation - parent metadata transitions", () => { vi.mocked(readTaskMessages).mockResolvedValue([]) vi.mocked(readApiMessages).mockResolvedValue([]) - await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + await ClineProvider.prototype.reopenParentFromDelegation.call(provider, { parentTaskId: "parent-1", childTaskId: "child-1", completionResultSummary: "Child done", @@ -363,16 +372,18 @@ describe("History resume delegation - parent metadata transitions", () => { overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), }), taskHistoryStore, - } as any) + }) // Start with existing messages in history - const existingUiMessages = [{ type: "ask", ask: "tool", text: "Old tool", ts: 50 }] - const existingApiMessages = [{ role: "user", content: [{ type: "text", text: "Old request" }], ts: 50 }] + const existingUiMessages: ClineMessage[] = [{ type: "ask", ask: "tool", text: "Old tool", ts: 50 }] + const existingApiMessages: ApiMessage[] = [ + { role: "user", content: [{ type: "text", text: "Old request" }], ts: 50 }, + ] - vi.mocked(readTaskMessages).mockResolvedValue(existingUiMessages as any) - vi.mocked(readApiMessages).mockResolvedValue(existingApiMessages as any) + vi.mocked(readTaskMessages).mockResolvedValue(existingUiMessages) + vi.mocked(readApiMessages).mockResolvedValue(existingApiMessages) - await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + await ClineProvider.prototype.reopenParentFromDelegation.call(provider, { parentTaskId: "p1", childTaskId: "c1", completionResultSummary: "Subtask completed successfully", @@ -599,11 +610,11 @@ describe("History resume delegation - parent metadata transitions", () => { overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), }), taskHistoryStore, - } as any) + }) // Include an assistant message with new_task tool_use to exercise the tool_result path - const existingUiMessages = [{ type: "ask", ask: "tool", text: "new_task request", ts: 50 }] - const existingApiMessages = [ + const existingUiMessages: ClineMessage[] = [{ type: "ask", ask: "tool", text: "new_task request", ts: 50 }] + const existingApiMessages: ApiMessage[] = [ { role: "user", content: [{ type: "text", text: "Create a subtask" }], ts: 40 }, { role: "assistant", @@ -619,10 +630,10 @@ describe("History resume delegation - parent metadata transitions", () => { }, ] - vi.mocked(readTaskMessages).mockResolvedValue(existingUiMessages as any) - vi.mocked(readApiMessages).mockResolvedValue(existingApiMessages as any) + vi.mocked(readTaskMessages).mockResolvedValue(existingUiMessages) + vi.mocked(readApiMessages).mockResolvedValue(existingApiMessages) - await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + await ClineProvider.prototype.reopenParentFromDelegation.call(provider, { parentTaskId: "p-tool", childTaskId: "c-tool", completionResultSummary: "Subtask completed via tool_result", @@ -655,14 +666,17 @@ describe("History resume delegation - parent metadata transitions", () => { // Verify the injected message is a user message with tool_result type const injectedMsg = apiCall.messages[2] expect(injectedMsg.role).toBe("user") - expect((injectedMsg.content[0] as any).type).toBe("tool_result") - expect((injectedMsg.content[0] as any).tool_use_id).toBe("toolu_abc123") - - // Format contract with the e2e mock fixtures: the parent-resume fixtures in - // apps/vscode-e2e/src/fixtures/subtasks.ts match on this injected - // "completed.\n\nResult:" prefix (SUBTASK_RESULT_INJECTION). If this template - // changes, update the fixtures in the same PR or they silently never fire. - expect((injectedMsg.content[0] as any).content).toMatch(/^Subtask .+ completed\.\n\nResult:\n/) + const injectedBlock = firstContentBlock(injectedMsg) + expect(injectedBlock.type).toBe("tool_result") + if (injectedBlock.type === "tool_result") { + expect(injectedBlock.tool_use_id).toBe("toolu_abc123") + + // Format contract with the e2e mock fixtures: the parent-resume fixtures in + // apps/vscode-e2e/src/fixtures/subtasks.ts match on this injected + // "completed.\n\nResult:" prefix (SUBTASK_RESULT_INJECTION). If this template + // changes, update the fixtures in the same PR or they silently never fire. + expect(injectedBlock.content).toMatch(/^Subtask .+ completed\.\n\nResult:\n/) + } }) it("reopenParentFromDelegation injects plain text when no new_task tool_use exists in API history", async () => { @@ -691,16 +705,18 @@ describe("History resume delegation - parent metadata transitions", () => { overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), }), taskHistoryStore, - } as any) + }) // No assistant tool_use in history - const existingUiMessages = [{ type: "ask", ask: "tool", text: "subtask request", ts: 50 }] - const existingApiMessages = [{ role: "user", content: [{ type: "text", text: "Create a subtask" }], ts: 40 }] + const existingUiMessages: ClineMessage[] = [{ type: "ask", ask: "tool", text: "subtask request", ts: 50 }] + const existingApiMessages: ApiMessage[] = [ + { role: "user", content: [{ type: "text", text: "Create a subtask" }], ts: 40 }, + ] - vi.mocked(readTaskMessages).mockResolvedValue(existingUiMessages as any) - vi.mocked(readApiMessages).mockResolvedValue(existingApiMessages as any) + vi.mocked(readTaskMessages).mockResolvedValue(existingUiMessages) + vi.mocked(readApiMessages).mockResolvedValue(existingApiMessages) - await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + await ClineProvider.prototype.reopenParentFromDelegation.call(provider, { parentTaskId: "p-no-tool", childTaskId: "c-no-tool", completionResultSummary: "Subtask completed without tool_use", @@ -711,15 +727,20 @@ describe("History resume delegation - parent metadata transitions", () => { expect(apiCall.messages).toHaveLength(2) const injected = apiCall.messages[1] expect(injected.role).toBe("user") - expect((injected.content[0] as any).type).toBe("text") - expect((injected.content[0] as any).text).toContain("Subtask c-no-tool completed") + const injectedBlock = firstContentBlock(injected) + expect(injectedBlock.type).toBe("text") + if (injectedBlock.type === "text") { + expect(injectedBlock.text).toContain("Subtask c-no-tool completed") + } }) it("reopenParentFromDelegation sets skipPrevResponseIdOnce via resumeAfterDelegation", async () => { - const parentInstance: any = { + const parentInstance = { taskId: "parent-2", skipPrevResponseIdOnce: false, - resumeAfterDelegation: vi.fn().mockImplementation(async function (this: any) { + resumeAfterDelegation: vi.fn().mockImplementation(async function (this: { + skipPrevResponseIdOnce: boolean + }) { // Simulate what the real resumeAfterDelegation does this.skipPrevResponseIdOnce = true }), @@ -750,12 +771,12 @@ describe("History resume delegation - parent metadata transitions", () => { }), createTaskWithHistoryItem: vi.fn(async () => (currentTask = parentInstance)), taskHistoryStore, - } as any) + }) vi.mocked(readTaskMessages).mockResolvedValue([]) vi.mocked(readApiMessages).mockResolvedValue([]) - await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + await ClineProvider.prototype.reopenParentFromDelegation.call(provider, { parentTaskId: "parent-2", childTaskId: "child-2", completionResultSummary: "Done", @@ -799,12 +820,12 @@ describe("History resume delegation - parent metadata transitions", () => { }), createTaskWithHistoryItem: vi.fn(async () => (currentTask = parentInstance)), taskHistoryStore, - } as any) + }) vi.mocked(readTaskMessages).mockResolvedValue([]) vi.mocked(readApiMessages).mockResolvedValue([]) - await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + await ClineProvider.prototype.reopenParentFromDelegation.call(provider, { parentTaskId: "p3", childTaskId: "c3", completionResultSummary: "Summary", @@ -861,13 +882,13 @@ describe("History resume delegation - parent metadata transitions", () => { }), createTaskWithHistoryItem: vi.fn(async () => (currentTask = parentInstance)), taskHistoryStore, - } as any) + }) vi.mocked(readTaskMessages).mockResolvedValue([]) vi.mocked(readApiMessages).mockResolvedValue([]) await expect( - (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + ClineProvider.prototype.reopenParentFromDelegation.call(provider, { parentTaskId: "parent-rpd06", childTaskId: "child-rpd06", completionResultSummary: "Subtask finished despite overwrite failures", @@ -1058,12 +1079,12 @@ describe("History resume delegation - parent metadata transitions", () => { overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), }), taskHistoryStore, - } as any) + }) vi.mocked(readTaskMessages).mockResolvedValue([]) vi.mocked(readApiMessages).mockResolvedValue([]) - await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + await ClineProvider.prototype.reopenParentFromDelegation.call(provider, { parentTaskId: "p4", childTaskId: "c4", completionResultSummary: "S", @@ -1106,12 +1127,12 @@ describe("History resume delegation - parent metadata transitions", () => { removeClineFromStack, createTaskWithHistoryItem, taskHistoryStore, - } as any) + }) vi.mocked(readTaskMessages).mockResolvedValue([]) vi.mocked(readApiMessages).mockResolvedValue([]) - await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + await ClineProvider.prototype.reopenParentFromDelegation.call(provider, { parentTaskId: "parent-rpd02", childTaskId: "child-rpd02", completionResultSummary: "Child done without being current", @@ -1470,14 +1491,14 @@ describe("History resume delegation - parent metadata transitions", () => { removeClineFromStack: vi.fn().mockResolvedValue(undefined), createTaskWithHistoryItem, taskHistoryStore, - } as any) + }) vi.mocked(readTaskMessages).mockResolvedValue([]) vi.mocked(readApiMessages).mockResolvedValue([]) // Failure propagates — child is closed (step 4 already ran) but parent is NOT reopened await expect( - (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + ClineProvider.prototype.reopenParentFromDelegation.call(provider, { parentTaskId: "parent-rpd04", childTaskId: "child-rpd04", completionResultSummary: "Child completion with persistence failure", @@ -1518,13 +1539,13 @@ describe("History resume delegation - parent metadata transitions", () => { createTaskWithHistoryItem, taskHistoryStore, updateTaskHistory: vi.fn().mockRejectedValue(persistError), - } as any) + }) vi.mocked(readTaskMessages).mockResolvedValue([]) vi.mocked(readApiMessages).mockResolvedValue([]) await expect( - (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + ClineProvider.prototype.reopenParentFromDelegation.call(provider, { parentTaskId: "parent-rpd05", childTaskId: "child-rpd05", completionResultSummary: "Child completion", @@ -1561,14 +1582,14 @@ describe("History resume delegation - parent metadata transitions", () => { overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined), }), taskHistoryStore, - } as any) + }) // Mock read failures or empty returns vi.mocked(readTaskMessages).mockResolvedValue([]) vi.mocked(readApiMessages).mockResolvedValue([]) await expect( - (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + ClineProvider.prototype.reopenParentFromDelegation.call(provider, { parentTaskId: "p5", childTaskId: "c5", completionResultSummary: "Result", @@ -1614,7 +1635,7 @@ describe("History resume delegation - parent metadata transitions", () => { removeClineFromStack: vi.fn(), createTaskWithHistoryItem: vi.fn(), taskHistoryStore: { atomicUpdatePair, get: vi.fn() }, - } as any) + }) const providerActive = makeProvider({ id: "parent-guard", @@ -1622,7 +1643,7 @@ describe("History resume delegation - parent metadata transitions", () => { awaitingChildId: undefined, }) await expect( - (ClineProvider.prototype as any).reopenParentFromDelegation.call(providerActive, { + ClineProvider.prototype.reopenParentFromDelegation.call(providerActive, { parentTaskId: "parent-guard", childTaskId: "child-guard", completionResultSummary: "should be ignored", @@ -1655,10 +1676,10 @@ describe("History resume delegation - parent metadata transitions", () => { createTaskWithHistoryItem: vi.fn(), taskHistoryStore: { atomicUpdatePair, get: vi.fn() }, cancelledDelegationChildIds: new Set(["child-guard"]), - } as any) + }) await expect( - (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + ClineProvider.prototype.reopenParentFromDelegation.call(provider, { parentTaskId: "parent-guard", childTaskId: "child-guard", completionResultSummary: "should be ignored", @@ -1687,7 +1708,7 @@ describe("History resume delegation - parent metadata transitions", () => { removeClineFromStack: vi.fn(), createTaskWithHistoryItem: vi.fn(), taskHistoryStore: { atomicUpdatePair, get: vi.fn() }, - } as any) + }) const providerWrongChild = makeProvider({ id: "parent-guard", @@ -1695,7 +1716,7 @@ describe("History resume delegation - parent metadata transitions", () => { awaitingChildId: "other-child", }) await expect( - (ClineProvider.prototype as any).reopenParentFromDelegation.call(providerWrongChild, { + ClineProvider.prototype.reopenParentFromDelegation.call(providerWrongChild, { parentTaskId: "parent-guard", childTaskId: "child-guard", completionResultSummary: "should be ignored", @@ -1708,17 +1729,17 @@ describe("History resume delegation - parent metadata transitions", () => { }) it("serializes delegation transitions and continues after a rejected predecessor", async () => { - const provider = makeProviderStub({} as any) as any + const provider = makeProviderStub({}) const calls: string[] = [] let rejectFirst!: (error: Error) => void - const first = provider.runDelegationTransition("parent-lock", async () => { + const first = provider["runDelegationTransition"]("parent-lock", async () => { calls.push("first") await new Promise((_resolve, reject) => { rejectFirst = reject }) }) - const second = provider.runDelegationTransition("parent-lock", async () => { + const second = provider["runDelegationTransition"]("parent-lock", async () => { calls.push("second") return "done" }) @@ -1754,16 +1775,23 @@ describe("History resume delegation - parent metadata transitions", () => { awaitingChildId: undefined, completedByChildId: "c-webview", } - const itemMap = new Map([ - ["c-webview", updatedChild], - ["p-webview", updatedParent], + const itemMap = new Map>([ + ["c-webview", updatedChild as Partial], + ["p-webview", updatedParent as Partial], ]) const taskHistoryStore = { - atomicUpdatePair: vi.fn(async (_fId: string, _sId: string, fU: (h: any) => any, sU: (h: any) => any) => { - fU(childItem) - sU(parentItem) - return [] - }), + atomicUpdatePair: vi.fn( + async ( + _fId: string, + _sId: string, + fU: (h: HistoryItem) => HistoryItem, + sU: (h: HistoryItem) => HistoryItem, + ) => { + fU(childItem as unknown as HistoryItem) + sU(parentItem as unknown as HistoryItem) + return [] + }, + ), get: vi.fn((id: string) => itemMap.get(id)), } @@ -1783,12 +1811,12 @@ describe("History resume delegation - parent metadata transitions", () => { }), taskHistoryStore, postMessageToWebview, - } as any) + }) vi.mocked(readTaskMessages).mockResolvedValue([]) vi.mocked(readApiMessages).mockResolvedValue([]) - await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + await ClineProvider.prototype.reopenParentFromDelegation.call(provider, { parentTaskId: "p-webview", childTaskId: "c-webview", completionResultSummary: "Done", @@ -1843,12 +1871,12 @@ describe("History resume delegation - parent metadata transitions", () => { }), taskHistoryStore, postMessageToWebview, - } as any) + }) vi.mocked(readTaskMessages).mockResolvedValue([]) vi.mocked(readApiMessages).mockResolvedValue([]) - await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + await ClineProvider.prototype.reopenParentFromDelegation.call(provider, { parentTaskId: "p-noview", childTaskId: "c-noview", completionResultSummary: "Done", @@ -1901,12 +1929,12 @@ describe("History resume delegation - parent metadata transitions", () => { resumeAfterDelegation: vi.fn().mockResolvedValue(undefined), }), taskHistoryStore, - } as any) + }) vi.mocked(readTaskMessages).mockResolvedValue([]) vi.mocked(readApiMessages).mockResolvedValue([]) - await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, { + await ClineProvider.prototype.reopenParentFromDelegation.call(provider, { parentTaskId: "p-handoff", childTaskId: "c-handoff", completionResultSummary: "Done", @@ -1924,7 +1952,7 @@ describe("History resume delegation - parent metadata transitions", () => { it("reopens the parent after a subtask is cancelled mid-stream, resumed, and completes (interrupted → attempt_completion)", async () => { // Step 1: simulate cancelTask()'s persisted transition — child "active" → "interrupted", // parent stays "delegated" with awaitingChildId intact (ClineProvider.ts cancelTaskInternal). - const childItem: Record = { + const childItem = { id: "child-566", status: "interrupted", parentTaskId: "parent-566", @@ -1935,8 +1963,8 @@ describe("History resume delegation - parent metadata transitions", () => { totalCost: 0, mode: "code", workspace: "/tmp", - } - const parentItem: Record = { + } as HistoryItem + const parentItem = { id: "parent-566", status: "delegated", delegatedToId: "child-566", @@ -1949,7 +1977,7 @@ describe("History resume delegation - parent metadata transitions", () => { totalCost: 0, mode: "code", workspace: "/tmp", - } + } as HistoryItem // Step 2: user clicks "Resume" — the extension rehydrates the child from its persisted // history item (createTaskWithHistoryItem passes historyItem.status through as @@ -1970,7 +1998,7 @@ describe("History resume delegation - parent metadata transitions", () => { currentActiveId = undefined currentTask = undefined }) - const createTaskWithHistoryItem = vi.fn().mockImplementation(async (historyItem: any) => { + const createTaskWithHistoryItem = vi.fn().mockImplementation(async (historyItem: HistoryItem) => { currentActiveId = historyItem.id currentTask = { taskId: historyItem.id, @@ -1990,8 +2018,8 @@ describe("History resume delegation - parent metadata transitions", () => { async ( _firstId: string, _secondId: string, - firstUpdater: (h: any) => any, - secondUpdater: (h: any) => any, + firstUpdater: (h: HistoryItem) => HistoryItem, + secondUpdater: (h: HistoryItem) => HistoryItem, ) => { Object.assign(childItem, firstUpdater(childItem)) Object.assign(parentItem, secondUpdater(parentItem)) @@ -2011,10 +2039,12 @@ describe("History resume delegation - parent metadata transitions", () => { removeClineFromStack, createTaskWithHistoryItem, taskHistoryStore, - reopenParentFromDelegation: vi.fn(async (params: any) => { - // Intentional self-reference: the provider variable is initialized before this stub is invoked. - return await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, params) - }), + reopenParentFromDelegation: vi.fn( + async (params: Parameters[0]) => { + // Intentional self-reference: the provider variable is initialized before this stub is invoked. + return await ClineProvider.prototype.reopenParentFromDelegation.call(provider, params) + }, + ), } as unknown as ClineProvider) vi.mocked(readTaskMessages).mockResolvedValue([]) @@ -2041,13 +2071,13 @@ describe("History resume delegation - parent metadata transitions", () => { waitForCurrentAssistantMessagePersistence: vi.fn().mockResolvedValue(true), } as unknown as import("../core/task/Task").Task - const block = { + const block: ToolUse<"attempt_completion"> = { type: "tool_use", name: "attempt_completion", params: { result: "Child finished after resume" }, nativeArgs: { result: "Child finished after resume" }, partial: false, - } as any + } await attemptCompletionTool.handle(resumedChildTask, block, { askApproval: vi.fn(), @@ -2057,7 +2087,8 @@ describe("History resume delegation - parent metadata transitions", () => { pushToolResult: vi.fn(), askFinishSubTaskApproval: vi.fn(async () => true), toolDescription: () => "desc", - } as any) + // This callbacks double omits toolCallId because this path does not read it. + } as unknown as AttemptCompletionCallbacks) await vi.waitFor(() => expect(currentTask?.resumeAfterDelegation).toHaveBeenCalledTimes(1)) // The parent must regain control — this is the exact behavior issue #566 reported as broken. @@ -2066,7 +2097,7 @@ describe("History resume delegation - parent metadata transitions", () => { expect(parentItem.status).toBe("active") expect(parentItem.awaitingChildId).toBeUndefined() - const eventNames = emitSpy.mock.calls.map((c: any[]) => c[0]) + const eventNames = emitSpy.mock.calls.map((c) => c[0]) expect(eventNames).toContain(RooCodeEventName.TaskDelegationCompleted) expect(eventNames).toContain(RooCodeEventName.TaskDelegationResumed) }) diff --git a/src/__tests__/abandonSubtask.spec.ts b/src/__tests__/abandonSubtask.spec.ts index ded593c596..5a6e086e8d 100644 --- a/src/__tests__/abandonSubtask.spec.ts +++ b/src/__tests__/abandonSubtask.spec.ts @@ -10,10 +10,10 @@ import { makeProviderStub } from "./helpers/provider-stub" * Minimal taskHistoryStore stub whose atomicUpdatePair calls both updaters * against an in-memory item map and resolves, simulating the happy-path atomic write. */ -function makeTaskHistoryStoreStub(childItem: Record, parentItem: Record) { - const itemMap = new Map>([ - [childItem.id!, childItem], - [parentItem.id!, parentItem], +function makeTaskHistoryStoreStub(childItem: { id: string }, parentItem: { id: string }) { + const itemMap = new Map & { id: string }>([ + [childItem.id, childItem as Partial & { id: string }], + [parentItem.id, parentItem as Partial & { id: string }], ]) const atomicUpdatePair = vi.fn( @@ -31,7 +31,7 @@ function makeTaskHistoryStoreStub(childItem: Record, parentItem: Re return { atomicUpdatePair, - get: vi.fn((id: string) => itemMap.get(id)), + get: vi.fn<(id: string) => unknown>((id) => itemMap.get(id)), } } @@ -79,9 +79,9 @@ describe("ClineProvider.abandonSubtask()", () => { taskHistoryStore, isViewLaunched: true, postMessageToWebview: vi.fn().mockResolvedValue(undefined), - } as any) + }) - const result = await (ClineProvider.prototype as any).abandonSubtask.call(provider, "child-1") + const result = await ClineProvider.prototype.abandonSubtask.call(provider, "child-1") expect(result).toBe(true) expect(taskHistoryStore.atomicUpdatePair).toHaveBeenCalledTimes(1) @@ -110,7 +110,7 @@ describe("ClineProvider.abandonSubtask()", () => { ) // Guarded against a stale in-flight completion reattaching the child. - expect((provider as any).cancelledDelegationChildIds.has("child-1")).toBe(true) + expect(provider["cancelledDelegationChildIds"].has("child-1")).toBe(true) // Both updated items broadcast to the webview with the actual severed-link field values, // not just matching IDs — a stale/pre-abandon payload would still match an id-only assertion. @@ -178,9 +178,9 @@ describe("ClineProvider.abandonSubtask()", () => { removeClineFromStack, taskHistoryStore, isViewLaunched: false, - } as any) + }) - const result = await (ClineProvider.prototype as any).abandonSubtask.call(provider, "child-1") + const result = await ClineProvider.prototype.abandonSubtask.call(provider, "child-1") expect(result).toBe(true) expect(removeClineFromStack).toHaveBeenCalledWith() @@ -214,9 +214,9 @@ describe("ClineProvider.abandonSubtask()", () => { removeClineFromStack, taskHistoryStore, isViewLaunched: false, - } as any) + }) - const result = await (ClineProvider.prototype as any).abandonSubtask.call(provider, "child-1") + const result = await ClineProvider.prototype.abandonSubtask.call(provider, "child-1") expect(result).toBe(true) expect(removeClineFromStack).not.toHaveBeenCalled() @@ -238,9 +238,9 @@ describe("ClineProvider.abandonSubtask()", () => { }) const taskHistoryStore = makeTaskHistoryStoreStub(childHistoryItem, parentHistoryItem) - const provider = makeProviderStub({ getTaskWithId, taskHistoryStore } as any) + const provider = makeProviderStub({ getTaskWithId, taskHistoryStore }) - const result = await (ClineProvider.prototype as any).abandonSubtask.call(provider, "child-1") + const result = await ClineProvider.prototype.abandonSubtask.call(provider, "child-1") expect(result).toBe(false) expect(taskHistoryStore.atomicUpdatePair).not.toHaveBeenCalled() @@ -264,15 +264,15 @@ describe("ClineProvider.abandonSubtask()", () => { throw new Error("Task not found") }) - const taskHistoryStore: any = makeTaskHistoryStoreStub(childHistoryItem, parentHistoryItem) + const taskHistoryStore = makeTaskHistoryStoreStub(childHistoryItem, parentHistoryItem) // Simulate the concurrent completion landing right before the in-lock re-check runs. taskHistoryStore.get = vi.fn((id: string) => id === "child-1" ? { ...childHistoryItem, status: "completed" as const } : parentHistoryItem, ) - const provider = makeProviderStub({ getTaskWithId, taskHistoryStore } as any) + const provider = makeProviderStub({ getTaskWithId, taskHistoryStore }) - const result = await (ClineProvider.prototype as any).abandonSubtask.call(provider, "child-1") + const result = await ClineProvider.prototype.abandonSubtask.call(provider, "child-1") expect(result).toBe(false) expect(taskHistoryStore.atomicUpdatePair).not.toHaveBeenCalled() @@ -280,9 +280,9 @@ describe("ClineProvider.abandonSubtask()", () => { it("returns false and does not modify state when the child has no parentTaskId", async () => { const getTaskWithId = vi.fn().mockResolvedValue({ historyItem: { id: "standalone-1", status: "active" } }) - const provider = makeProviderStub({ getTaskWithId } as any) + const provider = makeProviderStub({ getTaskWithId }) - const result = await (ClineProvider.prototype as any).abandonSubtask.call(provider, "standalone-1") + const result = await ClineProvider.prototype.abandonSubtask.call(provider, "standalone-1") expect(result).toBe(false) }) @@ -298,9 +298,9 @@ describe("ClineProvider.abandonSubtask()", () => { }) const taskHistoryStore = makeTaskHistoryStoreStub(childHistoryItem, parentHistoryItem) - const provider = makeProviderStub({ getTaskWithId, taskHistoryStore } as any) + const provider = makeProviderStub({ getTaskWithId, taskHistoryStore }) - const result = await (ClineProvider.prototype as any).abandonSubtask.call(provider, "child-1") + const result = await ClineProvider.prototype.abandonSubtask.call(provider, "child-1") expect(result).toBe(false) expect(taskHistoryStore.atomicUpdatePair).not.toHaveBeenCalled() @@ -317,9 +317,9 @@ describe("ClineProvider.abandonSubtask()", () => { }) const taskHistoryStore = makeTaskHistoryStoreStub(childHistoryItem, parentHistoryItem) - const provider = makeProviderStub({ getTaskWithId, taskHistoryStore } as any) + const provider = makeProviderStub({ getTaskWithId, taskHistoryStore }) - const result = await (ClineProvider.prototype as any).abandonSubtask.call(provider, "child-1") + const result = await ClineProvider.prototype.abandonSubtask.call(provider, "child-1") expect(result).toBe(false) expect(taskHistoryStore.atomicUpdatePair).not.toHaveBeenCalled() diff --git a/src/__tests__/api-subtask.spec.ts b/src/__tests__/api-subtask.spec.ts index eb0c6177d4..05140e116c 100644 --- a/src/__tests__/api-subtask.spec.ts +++ b/src/__tests__/api-subtask.spec.ts @@ -2,6 +2,8 @@ import { describe, it, expect, vi, beforeEach } from "vitest" import { EventEmitter } from "events" +import type { OutputChannel } from "vscode" +import type { ClineProvider } from "../core/webview/ClineProvider" vi.mock("vscode", () => ({ workspace: { workspaceFolders: [] }, @@ -64,7 +66,7 @@ describe("API.clearCurrentTask()", () => { beforeEach(() => { vi.clearAllMocks() provider = makeProviderMock() - api = new API({} as any, provider as any, makeClineProviderFactory()) + api = new API({} as OutputChannel, provider as unknown as ClineProvider, makeClineProviderFactory()) }) it("calls evictCurrentTask then postStateToWebview on sidebarProvider", async () => { @@ -85,7 +87,7 @@ describe("API.abandonSubtask()", () => { beforeEach(() => { vi.clearAllMocks() provider = makeProviderMock() - api = new API({} as any, provider as any, makeClineProviderFactory()) + api = new API({} as OutputChannel, provider as unknown as ClineProvider, makeClineProviderFactory()) }) it("delegates to sidebarProvider.abandonSubtask and returns its result", async () => { diff --git a/src/__tests__/delegation-concurrent.spec.ts b/src/__tests__/delegation-concurrent.spec.ts index 40d9b49ee5..7da9bc14d0 100644 --- a/src/__tests__/delegation-concurrent.spec.ts +++ b/src/__tests__/delegation-concurrent.spec.ts @@ -54,7 +54,7 @@ describe("TaskHistoryStore.atomicReadAndUpdate", () => { it("serializes concurrent operations — second caller reads the state written by the first", async () => { // Seed the cache with an item that has no childIds yet. const item = makeItem("parent-task", { childIds: [] }) - ;(store as any).cache.set(item.id, item) + store["cache"].set(item.id, item) // Two concurrent delegations each append their child ID. // Because they are serialized by the lock, the second caller must @@ -72,14 +72,14 @@ describe("TaskHistoryStore.atomicReadAndUpdate", () => { await Promise.all([delegation1, delegation2]) // Both child IDs must be present: delegation2 saw delegation1's write. - const final = (store as any).cache.get("parent-task") as HistoryItem + const final = store["cache"].get("parent-task")! expect(final.childIds).toContain("child-A") expect(final.childIds).toContain("child-B") }) it("two concurrent delegations produce consistent HistoryItem state (full field set)", async () => { const item = makeItem("parent-task", { status: "active", childIds: [] }) - ;(store as any).cache.set(item.id, item) + store["cache"].set(item.id, item) // Each delegation sets awaitingChildId and appends to childIds. const delegation1 = store.atomicReadAndUpdate("parent-task", (historyItem) => { @@ -106,7 +106,7 @@ describe("TaskHistoryStore.atomicReadAndUpdate", () => { await Promise.all([delegation1, delegation2]) - const final = (store as any).cache.get("parent-task") as HistoryItem + const final = store["cache"].get("parent-task")! // Both child IDs present — neither write clobbered the other's childIds. expect(final.childIds).toContain("child-A") expect(final.childIds).toContain("child-B") @@ -119,7 +119,7 @@ describe("TaskHistoryStore.atomicReadAndUpdate", () => { it("completes without deadlock — updater is pure and does not re-acquire the lock", async () => { const item = makeItem("task-1", { childIds: [] }) - ;(store as any).cache.set(item.id, item) + store["cache"].set(item.id, item) // With the typed (taskId, updater) API, the updater is synchronous and // cannot call upsert/withLock — no re-entrancy, no deadlock. @@ -135,7 +135,7 @@ describe("TaskHistoryStore.atomicReadAndUpdate", () => { expect(result).toBe("completed") - const final = (store as any).cache.get("task-1") as HistoryItem + const final = store["cache"].get("task-1")! expect(final.childIds).toContain("child-1") }) diff --git a/src/__tests__/delegation-events.spec.ts b/src/__tests__/delegation-events.spec.ts index 15ea5b7c46..1e31e6c1f6 100644 --- a/src/__tests__/delegation-events.spec.ts +++ b/src/__tests__/delegation-events.spec.ts @@ -4,20 +4,16 @@ import { RooCodeEventName, rooCodeEventsSchema, taskEventSchema } from "@roo-cod describe("delegation event schemas", () => { test("rooCodeEventsSchema validates tuples", () => { - expect(() => (rooCodeEventsSchema.shape as any)[RooCodeEventName.TaskDelegated].parse(["p", "c"])).not.toThrow() + expect(() => rooCodeEventsSchema.shape[RooCodeEventName.TaskDelegated].parse(["p", "c"])).not.toThrow() expect(() => - (rooCodeEventsSchema.shape as any)[RooCodeEventName.TaskDelegationCompleted].parse(["p", "c", "s"]), - ).not.toThrow() - expect(() => - (rooCodeEventsSchema.shape as any)[RooCodeEventName.TaskDelegationResumed].parse(["p", "c"]), + rooCodeEventsSchema.shape[RooCodeEventName.TaskDelegationCompleted].parse(["p", "c", "s"]), ).not.toThrow() + expect(() => rooCodeEventsSchema.shape[RooCodeEventName.TaskDelegationResumed].parse(["p", "c"])).not.toThrow() // invalid shapes - expect(() => (rooCodeEventsSchema.shape as any)[RooCodeEventName.TaskDelegated].parse(["p"])).toThrow() - expect(() => - (rooCodeEventsSchema.shape as any)[RooCodeEventName.TaskDelegationCompleted].parse(["p", "c"]), - ).toThrow() - expect(() => (rooCodeEventsSchema.shape as any)[RooCodeEventName.TaskDelegationResumed].parse(["p"])).toThrow() + expect(() => rooCodeEventsSchema.shape[RooCodeEventName.TaskDelegated].parse(["p"])).toThrow() + expect(() => rooCodeEventsSchema.shape[RooCodeEventName.TaskDelegationCompleted].parse(["p", "c"])).toThrow() + expect(() => rooCodeEventsSchema.shape[RooCodeEventName.TaskDelegationResumed].parse(["p"])).toThrow() }) test("taskEventSchema discriminated union includes delegation events", () => { diff --git a/src/__tests__/migrateSettings.spec.ts b/src/__tests__/migrateSettings.spec.ts index 574b6032a6..e2cbb777db 100644 --- a/src/__tests__/migrateSettings.spec.ts +++ b/src/__tests__/migrateSettings.spec.ts @@ -51,7 +51,7 @@ describe("Settings Migration", () => { } as unknown as vscode.ExtensionContext // Set global outputChannel for all tests - ;(global as any).outputChannel = mockOutputChannel + Object.assign(global, { outputChannel: mockOutputChannel }) }) it("should migrate custom modes file if old file exists and new file doesn't", async () => { @@ -147,7 +147,7 @@ describe("Settings Migration", () => { const mockUnlink = vitest.mocked(fs.unlink).mockResolvedValue(undefined) // Mock file read to return JSON content - vitest.mocked(fs.readFile).mockImplementation(async (path: any) => { + vitest.mocked(fs.readFile).mockImplementation(async (path) => { if (path === legacyCustomModesJson) { return testJsonContent } @@ -185,7 +185,7 @@ describe("Settings Migration", () => { const mockUnlink = vitest.mocked(fs.unlink).mockResolvedValue(undefined) // Mock file read to return corrupt JSON - vitest.mocked(fs.readFile).mockImplementation(async (path: any) => { + vitest.mocked(fs.readFile).mockImplementation(async (path) => { if (path === legacyCustomModesJson) { return "{ invalid json content" // This will cause an error when parsed } @@ -222,7 +222,7 @@ describe("Settings Migration", () => { const mockUnlink = vitest.mocked(fs.unlink).mockResolvedValue(undefined) // Mock file read - vitest.mocked(fs.readFile).mockImplementation(async (path: any) => { + vitest.mocked(fs.readFile).mockImplementation(async (path) => { if (path === legacyCustomModesJson) { return JSON.stringify({ customModes: [] }) } diff --git a/src/__tests__/nested-delegation-resume.spec.ts b/src/__tests__/nested-delegation-resume.spec.ts index 7672174d7b..dd2410fa05 100644 --- a/src/__tests__/nested-delegation-resume.spec.ts +++ b/src/__tests__/nested-delegation-resume.spec.ts @@ -2,11 +2,12 @@ import { describe, it, expect, vi, beforeEach } from "vitest" import { RooCodeEventName } from "@roo-code/types" +import type { HistoryItem } from "@roo-code/types" import { makeProviderStub } from "./helpers/provider-stub" // Mock safe-stable-stringify to avoid runtime error vi.mock("safe-stable-stringify", () => ({ - default: (obj: any) => JSON.stringify(obj), + default: (obj: unknown) => JSON.stringify(obj), })) // Mock TelemetryService @@ -27,7 +28,7 @@ vi.mock("vscode", () => { } const workspace = { getConfiguration: vi.fn(() => ({ - get: vi.fn((_key: string, defaultValue: any) => defaultValue), + get: vi.fn((_key: string, defaultValue: unknown) => defaultValue), update: vi.fn(), })), workspaceFolders: [], @@ -52,6 +53,8 @@ vi.mock("../core/task-persistence", async (importOriginal) => ({ })) import { attemptCompletionTool } from "../core/tools/AttemptCompletionTool" +import type { AttemptCompletionCallbacks } from "../core/tools/AttemptCompletionTool" +import type { ToolUse } from "../shared/tools" import { ClineProvider } from "../core/webview/ClineProvider" import type { Task } from "../core/task/Task" import { readTaskMessages } from "../core/task-persistence/taskMessages" @@ -77,7 +80,7 @@ describe("Nested delegation resume (A → B → C)", () => { } // History index: A is parent of B, B is parent of C - const historyIndex: Record = { + const historyIndex: Record = { A: { id: "A", status: "delegated", @@ -120,9 +123,10 @@ describe("Nested delegation resume (A → B → C)", () => { mode: "code", workspace: "/tmp", }, - } + // The history fixtures provide only the persisted fields exercised by this flow. + } as unknown as Record - const emitSpy = vi.fn() + const emitSpy = vi.fn<(event: string | symbol, ...args: unknown[]) => boolean>() const removeClineFromStack = vi.fn().mockImplementation(async () => { // Simulate closing current child currentActiveId = undefined @@ -130,7 +134,7 @@ describe("Nested delegation resume (A → B → C)", () => { }) const createTaskWithHistoryItem = vi .fn() - .mockImplementation(async (historyItem: any, opts?: { startTask?: boolean }) => { + .mockImplementation(async (historyItem: HistoryItem, opts?: { startTask?: boolean }) => { // Assert startTask:false to avoid resume asks expect(opts).toEqual(expect.objectContaining({ startTask: false })) // Reopen the parent @@ -156,7 +160,7 @@ describe("Nested delegation resume (A → B → C)", () => { } }) - const updateTaskHistory = vi.fn(async (updated: any) => { + const updateTaskHistory = vi.fn(async (updated: HistoryItem) => { // Persist updated history back into index (simulate) historyIndex[updated.id] = updated return Object.values(historyIndex) @@ -167,8 +171,8 @@ describe("Nested delegation resume (A → B → C)", () => { async ( firstId: string, secondId: string, - firstUpdater: (h: any) => any, - secondUpdater: (h: any) => any, + firstUpdater: (h: HistoryItem) => HistoryItem, + secondUpdater: (h: HistoryItem) => HistoryItem, ) => { // Apply both updaters and persist to historyIndex atomically const updatedFirst = firstUpdater(historyIndex[firstId]) @@ -195,9 +199,11 @@ describe("Nested delegation resume (A → B → C)", () => { ClineProvider.prototype.emitDelegatedTaskCompleted.call(provider, taskId, tokenUsage, toolUsage) }), // Wire through provider method so attemptCompletionTool can call it - reopenParentFromDelegation: vi.fn(async (params: any) => { - return await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, params) - }), + reopenParentFromDelegation: vi.fn( + async (params: Parameters[0]) => { + return await ClineProvider.prototype.reopenParentFromDelegation.call(provider, params) + }, + ), } as unknown as ClineProvider) // Empty histories for simplicity @@ -223,13 +229,13 @@ describe("Nested delegation resume (A → B → C)", () => { waitForCurrentAssistantMessagePersistence: vi.fn().mockResolvedValue(true), } as unknown as Task - const blockC = { + const blockC: ToolUse<"attempt_completion"> = { type: "tool_use", name: "attempt_completion", params: { result: "C finished" }, nativeArgs: { result: "C finished" }, partial: false, - } as any + } const askFinishSubTaskApproval = vi.fn(async () => true) const handleError = vi.fn(async (_action: string, err: Error) => { @@ -243,14 +249,15 @@ describe("Nested delegation resume (A → B → C)", () => { pushToolResult: vi.fn(), askFinishSubTaskApproval, toolDescription: () => "desc", - } as any) + // The callbacks double omits toolCallId because this path does not read it. + } as unknown as AttemptCompletionCallbacks) await vi.waitFor(() => expect(currentTask?.resumeAfterDelegation).toHaveBeenCalledTimes(1)) // After C completes, B must be current expect(currentActiveId).toBe("B") // Events emitted: C -> B hop - const eventNamesAfterC = emitSpy.mock.calls.map((c: any[]) => c[0]) + const eventNamesAfterC = emitSpy.mock.calls.map((c) => c[0]) expect(eventNamesAfterC).toContain(RooCodeEventName.TaskDelegationCompleted) expect(eventNamesAfterC).toContain(RooCodeEventName.TaskDelegationResumed) @@ -273,13 +280,13 @@ describe("Nested delegation resume (A → B → C)", () => { waitForCurrentAssistantMessagePersistence: vi.fn().mockResolvedValue(true), } as unknown as Task - const blockB = { + const blockB: ToolUse<"attempt_completion"> = { type: "tool_use", name: "attempt_completion", params: { result: "B finished" }, nativeArgs: { result: "B finished" }, partial: false, - } as any + } await attemptCompletionTool.handle(clineB, blockB, { askApproval: vi.fn(), @@ -287,7 +294,8 @@ describe("Nested delegation resume (A → B → C)", () => { pushToolResult: vi.fn(), askFinishSubTaskApproval, toolDescription: () => "desc", - } as any) + // The callbacks double omits toolCallId because this path does not read it. + } as unknown as AttemptCompletionCallbacks) await vi.waitFor(() => expect(currentTask?.resumeAfterDelegation).toHaveBeenCalledTimes(1)) // After B completes, A should become current @@ -299,10 +307,8 @@ describe("Nested delegation resume (A → B → C)", () => { // (asserted in createTaskWithHistoryItem mock) // Provider emitted TaskDelegationCompleted/Resumed twice across both hops - const completedEvents = emitSpy.mock.calls.filter( - (c: any[]) => c[0] === RooCodeEventName.TaskDelegationCompleted, - ) - const resumedEvents = emitSpy.mock.calls.filter((c: any[]) => c[0] === RooCodeEventName.TaskDelegationResumed) + const completedEvents = emitSpy.mock.calls.filter((c) => c[0] === RooCodeEventName.TaskDelegationCompleted) + const resumedEvents = emitSpy.mock.calls.filter((c) => c[0] === RooCodeEventName.TaskDelegationResumed) const taskCompletedEvents = emitSpy.mock.calls.filter((call) => call[0] === RooCodeEventName.TaskCompleted) expect(completedEvents.length).toBeGreaterThanOrEqual(2) expect(resumedEvents.length).toBeGreaterThanOrEqual(2) @@ -310,7 +316,7 @@ describe("Nested delegation resume (A → B → C)", () => { // Verify second hop used parentId = A // Find a TaskDelegationCompleted matching A <- B - const hasAfromB = completedEvents.some(([, parentId, childId]: any[]) => parentId === "A" && childId === "B") + const hasAfromB = completedEvents.some(([, parentId, childId]) => parentId === "A" && childId === "B") expect(hasAfromB).toBe(true) }) }) diff --git a/src/__tests__/new-task-delegation.spec.ts b/src/__tests__/new-task-delegation.spec.ts index ce1702edb7..677f68f8a2 100644 --- a/src/__tests__/new-task-delegation.spec.ts +++ b/src/__tests__/new-task-delegation.spec.ts @@ -3,9 +3,11 @@ import { describe, it, expect, vi } from "vitest" import { RooCodeEventName } from "@roo-code/types" import { Task } from "../core/task/Task" +import type { ClineProvider } from "../core/webview/ClineProvider" describe("Task.startSubtask() metadata-driven delegation", () => { it("Routes to provider.delegateParentAndOpenChild without pausing parent", async () => { + // This double provides the state, delegation, task creation, and mode-switch members used here. const provider = { getState: vi.fn().mockResolvedValue({ experiments: {}, @@ -13,15 +15,18 @@ describe("Task.startSubtask() metadata-driven delegation", () => { delegateParentAndOpenChild: vi.fn().mockResolvedValue({ taskId: "child-1" }), createTask: vi.fn(), handleModeSwitch: vi.fn(), - } as any + } as unknown as ClineProvider // Create a minimal Task-like instance with only fields used by startSubtask + const emitMock = vi.fn() const parent = Object.create(Task.prototype) as Task - ;(parent as any).taskId = "parent-1" - ;(parent as any).providerRef = { deref: () => provider } - ;(parent as any).emit = vi.fn() + Object.assign(parent, { + taskId: "parent-1", + providerRef: { deref: () => provider }, + emit: emitMock, + }) - const child = await (Task.prototype as any).startSubtask.call(parent, "Do something", [], "code") + const child = await Task.prototype.startSubtask.call(parent, "Do something", [], "code") expect(provider.delegateParentAndOpenChild).toHaveBeenCalledWith({ parentTaskId: "parent-1", @@ -32,8 +37,8 @@ describe("Task.startSubtask() metadata-driven delegation", () => { expect(child.taskId).toBe("child-1") // No paused/unpaused events should be emitted - expect((parent as any).childTaskId).toBeUndefined() - const emittedEvents = (parent.emit as any).mock.calls.map((c: any[]) => c[0]) + expect(parent.childTaskId).toBeUndefined() + const emittedEvents = emitMock.mock.calls.map((c) => c[0]) expect(emittedEvents).not.toContain(RooCodeEventName.TaskPaused) expect(emittedEvents).not.toContain(RooCodeEventName.TaskUnpaused) diff --git a/src/activate/CodeActionProvider.ts b/src/activate/CodeActionProvider.ts index 31fa4e04ee..18962ede1a 100644 --- a/src/activate/CodeActionProvider.ts +++ b/src/activate/CodeActionProvider.ts @@ -17,7 +17,7 @@ export class CodeActionProvider implements vscode.CodeActionProvider { title: string, kind: vscode.CodeActionKind, command: CodeActionId, - args: any[], + args: unknown[], ): vscode.CodeAction { const action = new vscode.CodeAction(title, kind) action.command = { command: getCodeActionCommand(command), title, arguments: args } diff --git a/src/activate/__tests__/CodeActionProvider.spec.ts b/src/activate/__tests__/CodeActionProvider.spec.ts index facbc464d3..d8d90361d8 100644 --- a/src/activate/__tests__/CodeActionProvider.spec.ts +++ b/src/activate/__tests__/CodeActionProvider.spec.ts @@ -41,6 +41,9 @@ vi.mock("vscode", () => ({ Information: 2, Hint: 3, }, + CodeActionTriggerKind: { + Invoke: 0, + }, workspace: { getConfiguration: vi.fn().mockReturnValue({ get: vi.fn().mockReturnValue(true), @@ -59,9 +62,9 @@ vi.mock("../../integrations/editor/EditorUtils", () => ({ describe("CodeActionProvider", () => { let provider: CodeActionProvider - let mockDocument: any - let mockRange: any - let mockContext: any + let mockDocument: vscode.TextDocument + let mockRange: vscode.Range + let mockContext: vscode.CodeActionContext beforeEach(() => { provider = new CodeActionProvider() @@ -71,11 +74,11 @@ describe("CodeActionProvider", () => { lineAt: vi.fn(), lineCount: 10, uri: { fsPath: "/test/file.ts" }, - } + } as unknown as vscode.TextDocument mockRange = new vscode.Range(0, 0, 0, 10) - mockContext = { diagnostics: [] } + mockContext = { diagnostics: [], triggerKind: vscode.CodeActionTriggerKind.Invoke, only: undefined } ;(EditorUtils.getEffectiveRange as Mock).mockReturnValue({ range: mockRange, text: "test code", @@ -89,30 +92,31 @@ describe("CodeActionProvider", () => { describe("provideCodeActions", () => { it("should provide explain, improve, fix logic, and add to context actions by default", () => { - const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) as vscode.CodeAction[] expect(actions).toHaveLength(3) - expect((actions as any)[0].title).toBe("Add to Zoo Code") - expect((actions as any)[1].title).toBe("Explain with Zoo Code") - expect((actions as any)[2].title).toBe("Improve with Zoo Code") + expect(actions[0].title).toBe("Add to Zoo Code") + expect(actions[1].title).toBe("Explain with Zoo Code") + expect(actions[2].title).toBe("Improve with Zoo Code") }) it("should provide fix action instead of fix logic when diagnostics exist", () => { - mockContext.diagnostics = [ - { message: "test error", severity: vscode.DiagnosticSeverity.Error, range: mockRange }, - ] + mockContext = { + ...mockContext, + diagnostics: [{ message: "test error", severity: vscode.DiagnosticSeverity.Error, range: mockRange }], + } - const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) as vscode.CodeAction[] expect(actions).toHaveLength(2) - expect((actions as any).some((a: any) => a.title === "Fix with Zoo Code")).toBe(true) - expect((actions as any).some((a: any) => a.title === "Add to Zoo Code")).toBe(true) + expect(actions.some((a) => a.title === "Fix with Zoo Code")).toBe(true) + expect(actions.some((a) => a.title === "Add to Zoo Code")).toBe(true) }) it("should return empty array when no effective range", () => { ;(EditorUtils.getEffectiveRange as Mock).mockReturnValue(null) - const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) as vscode.CodeAction[] expect(actions).toEqual([]) }) @@ -125,7 +129,7 @@ describe("CodeActionProvider", () => { }) ;(vscode.workspace.getConfiguration as Mock).mockReturnValue(mockGetConfiguration()) - const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) as vscode.CodeAction[] expect(actions).toEqual([]) expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("zoo-code") @@ -145,7 +149,7 @@ describe("CodeActionProvider", () => { throw new Error("Test error") }) - const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) as vscode.CodeAction[] expect(actions).toEqual([]) expect(consoleErrorSpy).toHaveBeenCalledWith("Error providing code actions:", expect.any(Error)) diff --git a/src/activate/__tests__/handleUri.spec.ts b/src/activate/__tests__/handleUri.spec.ts index b7aef0762b..f02bc0e799 100644 --- a/src/activate/__tests__/handleUri.spec.ts +++ b/src/activate/__tests__/handleUri.spec.ts @@ -17,10 +17,10 @@ const { handleOpenRouterCallback: vi.fn(), handleRequestyCallback: vi.fn(), handleZooCodeCallback: vi.fn(), - } as any + } as unknown as ClineProvider return { - mockGetVisibleInstance: vi.fn(() => mockVisibleProvider), + mockGetVisibleInstance: vi.fn((): ClineProvider | null => mockVisibleProvider), mockGetAllInstances: vi.fn(() => [mockVisibleProvider]), mockHandleZooCodeAuthCallback: vi.fn(), mockSetZooCodeUserInfo: vi.fn(), @@ -41,6 +41,9 @@ vi.mock("../../services/zoo-code-auth", () => ({ })) import { handleUri } from "../handleUri" +import type { ClineProvider } from "../../core/webview/ClineProvider" + +const makeUri = (uri: Pick): vscode.Uri => uri as vscode.Uri describe("handleUri", () => { beforeEach(() => { @@ -50,10 +53,12 @@ describe("handleUri", () => { }) it("ignores legacy cloud auth callback", async () => { - await handleUri({ - path: "/auth/clerk/callback", - query: "code=test-code&state=test-state&organizationId=test-org", - } as any) + await handleUri( + makeUri({ + path: "/auth/clerk/callback", + query: "code=test-code&state=test-state&organizationId=test-org", + }), + ) expect(mockVisibleProvider.handleOpenRouterCallback).not.toHaveBeenCalled() expect(mockVisibleProvider.handleRequestyCallback).not.toHaveBeenCalled() @@ -67,10 +72,12 @@ describe("handleUri", () => { mockGetAllInstances.mockReturnValue([]) mockHandleZooCodeAuthCallback.mockResolvedValue(true) - await handleUri({ - path: "/auth-callback", - query: "token=zoo_ext_test_token&name=Jane%20Doe&email=jane%40example.com&image=https%3A%2F%2Fexample.com%2Favatar.png", - } as any) + await handleUri( + makeUri({ + path: "/auth-callback", + query: "token=zoo_ext_test_token&name=Jane%20Doe&email=jane%40example.com&image=https%3A%2F%2Fexample.com%2Favatar.png", + }), + ) expect(mockHandleZooCodeAuthCallback).toHaveBeenCalledWith("zoo_ext_test_token") expect(mockSetZooCodeUserInfo).toHaveBeenCalledWith({ @@ -85,10 +92,12 @@ describe("handleUri", () => { it("refreshes the visible provider after a successful auth callback", async () => { mockHandleZooCodeAuthCallback.mockResolvedValue(true) - await handleUri({ - path: "/auth-callback", - query: "token=zoo_ext_test_token", - } as any) + await handleUri( + makeUri({ + path: "/auth-callback", + query: "token=zoo_ext_test_token", + }), + ) // When no user info is provided, null values are passed to clear stale data expect(mockSetZooCodeUserInfo).toHaveBeenCalledWith({ @@ -103,10 +112,12 @@ describe("handleUri", () => { mockHandleZooCodeAuthCallback.mockResolvedValue(true) // Re-auth with only name - email and image should be cleared - await handleUri({ - path: "/auth-callback", - query: "token=zoo_ext_test_token&name=John%20Doe", - } as any) + await handleUri( + makeUri({ + path: "/auth-callback", + query: "token=zoo_ext_test_token&name=John%20Doe", + }), + ) expect(mockSetZooCodeUserInfo).toHaveBeenCalledWith({ name: "John Doe", @@ -118,10 +129,12 @@ describe("handleUri", () => { it("does not persist user info when auth callback validation fails", async () => { mockHandleZooCodeAuthCallback.mockResolvedValue(false) - await handleUri({ - path: "/auth-callback", - query: "token=zoo_ext_test_token&name=Jane%20Doe", - } as any) + await handleUri( + makeUri({ + path: "/auth-callback", + query: "token=zoo_ext_test_token&name=Jane%20Doe", + }), + ) expect(mockSetZooCodeUserInfo).not.toHaveBeenCalled() expect(mockVisibleProvider.handleZooCodeCallback).not.toHaveBeenCalled() @@ -133,14 +146,16 @@ describe("handleUri", () => { // stayed unauthenticated until reload. mockHandleZooCodeAuthCallback.mockResolvedValue(true) - const hiddenProvider = { handleZooCodeCallback: vi.fn() } as any - const secondHidden = { handleZooCodeCallback: vi.fn() } as any + const hiddenProvider = { handleZooCodeCallback: vi.fn() } as unknown as ClineProvider + const secondHidden = { handleZooCodeCallback: vi.fn() } as unknown as ClineProvider mockGetAllInstances.mockReturnValue([mockVisibleProvider, hiddenProvider, secondHidden]) - await handleUri({ - path: "/auth-callback", - query: "token=zoo_ext_test_token", - } as any) + await handleUri( + makeUri({ + path: "/auth-callback", + query: "token=zoo_ext_test_token", + }), + ) expect(mockHandleZooCodeAuthCallback).toHaveBeenCalledWith("zoo_ext_test_token") expect(mockSetZooCodeUserInfo).toHaveBeenCalled() @@ -164,16 +179,18 @@ describe("handleUri", () => { await new Promise((resolve) => setTimeout(resolve, 0)) order.push(`${name}:end`) }), - }) as any + }) as unknown as ClineProvider const a = makeProvider("a") const b = makeProvider("b") mockGetAllInstances.mockReturnValue([a, b]) - await handleUri({ - path: "/auth-callback", - query: "token=zoo_ext_test_token", - } as any) + await handleUri( + makeUri({ + path: "/auth-callback", + query: "token=zoo_ext_test_token", + }), + ) expect(order).toEqual(["a:start", "a:end", "b:start", "b:end"]) }) @@ -185,14 +202,16 @@ describe("handleUri", () => { handleZooCodeCallback: vi.fn(async () => { throw new Error("profile store unavailable") }), - } as any - const healthyProvider = { handleZooCodeCallback: vi.fn() } as any + } as unknown as ClineProvider + const healthyProvider = { handleZooCodeCallback: vi.fn() } as unknown as ClineProvider mockGetAllInstances.mockReturnValue([failingProvider, healthyProvider]) - await handleUri({ - path: "/auth-callback", - query: "token=zoo_ext_test_token", - } as any) + await handleUri( + makeUri({ + path: "/auth-callback", + query: "token=zoo_ext_test_token", + }), + ) expect(failingProvider.handleZooCodeCallback).toHaveBeenCalledWith("zoo_ext_test_token") expect(healthyProvider.handleZooCodeCallback).toHaveBeenCalledWith("zoo_ext_test_token") diff --git a/src/activate/__tests__/registerCommands.spec.ts b/src/activate/__tests__/registerCommands.spec.ts index 50aab25b48..324c011ae6 100644 --- a/src/activate/__tests__/registerCommands.spec.ts +++ b/src/activate/__tests__/registerCommands.spec.ts @@ -136,7 +136,12 @@ describe("getVisibleProviderOrLog", () => { describe("registerCommands handlers", () => { let mockOutputChannel: vscode.OutputChannel let mockContext: vscode.ExtensionContext - let mockVisibleProvider: { postMessageToWebview: Mock; retryWebviewRecovery: Mock<() => Promise> } + let mockVisibleProvider: { + postMessageToWebview: Mock + retryWebviewRecovery: Mock<() => Promise> + evictCurrentTask?: Mock + refreshWorkspace?: Mock + } let mockProvider: { postMessageToWebview: Mock webviewFocusTracker: WebviewFocusTracker @@ -426,8 +431,8 @@ describe("registerCommands handlers", () => { it("plusButtonClicked calls evictCurrentTask on the visible provider", async () => { const evictCurrentTask = vi.fn().mockResolvedValue(undefined) const refreshWorkspace = vi.fn().mockResolvedValue(undefined) - ;(mockVisibleProvider as any).evictCurrentTask = evictCurrentTask - ;(mockVisibleProvider as any).refreshWorkspace = refreshWorkspace + mockVisibleProvider.evictCurrentTask = evictCurrentTask + mockVisibleProvider.refreshWorkspace = refreshWorkspace await handlers["zoo-code.plusButtonClicked"]() diff --git a/src/activate/registerCodeActions.ts b/src/activate/registerCodeActions.ts index 66fc4c5582..85f4bea160 100644 --- a/src/activate/registerCodeActions.ts +++ b/src/activate/registerCodeActions.ts @@ -3,6 +3,7 @@ import * as vscode from "vscode" import { CodeActionId, CodeActionName } from "@roo-code/types" import { getCodeActionCommand } from "../utils/commands" +import type { DiagnosticData } from "../integrations/editor/EditorUtils" import { EditorUtils } from "../integrations/editor/EditorUtils" import type { WebviewFocusTracker } from "../core/webview/WebviewFocusTracker" import { resolveChatProvider } from "./resolveChatProvider" @@ -23,17 +24,23 @@ const registerCodeAction = ( let userInput: string | undefined context.subscriptions.push( - vscode.commands.registerCommand(getCodeActionCommand(command), async (...args: any[]) => { + vscode.commands.registerCommand(getCodeActionCommand(command), async (...args: unknown[]) => { // Handle both code action and direct command cases. let filePath: string let selectedText: string let startLine: number | undefined let endLine: number | undefined - let diagnostics: any[] | undefined + let diagnostics: DiagnosticData[] | undefined if (args.length > 1) { // Called from code action. - ;[filePath, selectedText, startLine, endLine, diagnostics] = args + ;[filePath, selectedText, startLine, endLine, diagnostics] = args as [ + string, + string, + number?, + number?, + DiagnosticData[]?, + ] } else { // Called directly from command palette. const context = EditorUtils.getEditorContext() diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index e4e0d855a2..e4d7615266 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -78,11 +78,10 @@ export const registerCommands = (options: RegisterCommandOptions) => { // // Callback shape mirrors VS Code's own `commands.registerCommand` signature // (`(...args: any[]) => any`), with the return narrowed to `unknown` so -// callers must inspect before using. `any[]` for args is unavoidable: the -// callbacks here are heterogeneous (`importSettings` takes an optional -// `filePath?: string`, others take none) and VS Code dispatches positional -// args dynamically. -type CommandCallback = (...args: any[]) => unknown +// callers must inspect before using. `never[]` permits the heterogeneous +// callbacks here (`importSettings` takes an optional `filePath?: string`, +// others take none) while VS Code dispatches positional args dynamically. +type CommandCallback = (...args: never[]) => unknown const getCommandsMap = ({ context, outputChannel, diff --git a/src/activate/registerTerminalActions.ts b/src/activate/registerTerminalActions.ts index 9773d01d38..41d20dbd61 100644 --- a/src/activate/registerTerminalActions.ts +++ b/src/activate/registerTerminalActions.ts @@ -19,7 +19,7 @@ const registerTerminalAction = ( promptType: TerminalActionPromptType, ) => { context.subscriptions.push( - vscode.commands.registerCommand(getTerminalCommand(command), async (args: any) => { + vscode.commands.registerCommand(getTerminalCommand(command), async (args?: { selection?: string }) => { let content = args?.selection if (!content || content === "") { diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index b2b4f7ba71..2a91fd0d79 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -1,89 +1,4 @@ { - "__mocks__/fs/promises.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 4 - } - }, - "__tests__/ClineProvider.delegation.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 9 - } - }, - "__tests__/ClineProvider.history-resume-delegation.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 70 - } - }, - "__tests__/abandonSubtask.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 20 - } - }, - "__tests__/api-subtask.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 4 - } - }, - "__tests__/delegation-concurrent.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 6 - } - }, - "__tests__/delegation-events.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 6 - } - }, - "__tests__/migrateSettings.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 4 - } - }, - "__tests__/nested-delegation-resume.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 19 - } - }, - "__tests__/new-task-delegation.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 8 - } - }, - "activate/CodeActionProvider.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "activate/__tests__/CodeActionProvider.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 10 - } - }, - "activate/__tests__/handleUri.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 14 - } - }, - "activate/__tests__/registerCommands.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 2 - } - }, - "activate/registerCodeActions.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 2 - } - }, - "activate/registerCommands.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "activate/registerTerminalActions.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, "api/providers/__tests__/anthropic-vertex.spec.ts": { "@typescript-eslint/no-explicit-any": { "count": 31 @@ -1114,31 +1029,6 @@ "count": 5 } }, - "extension/__tests__/api-delete-queued-message.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "extension/__tests__/api-send-message.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 7 - } - }, - "extension/api.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 9 - } - }, - "i18n/index.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "i18n/setup.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, "integrations/editor/DiffViewProvider.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 @@ -1568,60 +1458,5 @@ "@typescript-eslint/no-explicit-any": { "count": 2 } - }, - "shared/__tests__/api.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 8 - } - }, - "shared/__tests__/embeddingModels.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "shared/__tests__/modes-empty-prompt-component.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "shared/api.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 2 - } - }, - "shared/checkExistApiConfig.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "shared/cost.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "shared/parse-command.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "shared/support-prompt.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 5 - } - }, - "shared/tools.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } - }, - "shared/utils/__tests__/requesty.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 2 - } - }, - "vitest.setup.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - } } } diff --git a/src/extension/__tests__/api-delete-queued-message.spec.ts b/src/extension/__tests__/api-delete-queued-message.spec.ts index 91c945df39..7441be4bdd 100644 --- a/src/extension/__tests__/api-delete-queued-message.spec.ts +++ b/src/extension/__tests__/api-delete-queued-message.spec.ts @@ -38,7 +38,7 @@ describe("API - DeleteQueuedMessage Command", () => { mockLog = vi.fn() api = new API(mockOutputChannel, mockProvider, makeClineProviderFactory(), undefined, true) - ;(api as any).log = mockLog + Object.assign(api, { log: mockLog }) }) it("should remove a queued message by id", () => { diff --git a/src/extension/__tests__/api-send-message.spec.ts b/src/extension/__tests__/api-send-message.spec.ts index d78872f8d4..dc51d06445 100644 --- a/src/extension/__tests__/api-send-message.spec.ts +++ b/src/extension/__tests__/api-send-message.spec.ts @@ -12,8 +12,8 @@ describe("API - SendMessage Command", () => { let api: API let mockOutputChannel: vscode.OutputChannel let mockProvider: ClineProvider - let mockPostMessageToWebview: ReturnType any>> - let mockLog: ReturnType void>> + let mockPostMessageToWebview: ReturnType Promise>> + let mockLog: ReturnType void>> beforeEach(() => { // Setup mocks @@ -21,7 +21,7 @@ describe("API - SendMessage Command", () => { appendLine: vi.fn(), } as unknown as vscode.OutputChannel - mockPostMessageToWebview = vi.fn<(...args: any[]) => any>().mockResolvedValue(undefined) + mockPostMessageToWebview = vi.fn<(message: unknown) => Promise>().mockResolvedValue(undefined) mockProvider = { context: {} as vscode.ExtensionContext, @@ -32,12 +32,12 @@ describe("API - SendMessage Command", () => { viewLaunched: true, } as unknown as ClineProvider - mockLog = vi.fn<(...args: any[]) => void>() + mockLog = vi.fn<(...args: unknown[]) => void>() // Create API instance with logging enabled for testing api = new API(mockOutputChannel, mockProvider, makeClineProviderFactory(), undefined, true) // Override the log method to use our mock - ;(api as any).log = mockLog + Object.assign(api, { log: mockLog }) }) it("should handle SendMessage command with text only", async () => { diff --git a/src/extension/api.ts b/src/extension/api.ts index 3a33510346..da59c48b58 100644 --- a/src/extension/api.ts +++ b/src/extension/api.ts @@ -514,16 +514,16 @@ export class API extends EventEmitter implements RooCodeAPI { this.emit(RooCodeEventName.TaskSpawned, task.taskId, childTaskId) }) - task.on(RooCodeEventName.TaskDelegated as any, (childTaskId: string) => { - ;(this.emit as any)(RooCodeEventName.TaskDelegated, task.taskId, childTaskId) + task.on(RooCodeEventName.TaskDelegated, (childTaskId: string) => { + this.emit(RooCodeEventName.TaskDelegated, task.taskId, childTaskId) }) - task.on(RooCodeEventName.TaskDelegationCompleted as any, (childTaskId: string, summary: string) => { - ;(this.emit as any)(RooCodeEventName.TaskDelegationCompleted, task.taskId, childTaskId, summary) + task.on(RooCodeEventName.TaskDelegationCompleted, (childTaskId: string, summary: string) => { + this.emit(RooCodeEventName.TaskDelegationCompleted, task.taskId, childTaskId, summary) }) - task.on(RooCodeEventName.TaskDelegationResumed as any, (childTaskId: string) => { - ;(this.emit as any)(RooCodeEventName.TaskDelegationResumed, task.taskId, childTaskId) + task.on(RooCodeEventName.TaskDelegationResumed, (childTaskId: string) => { + this.emit(RooCodeEventName.TaskDelegationResumed, task.taskId, childTaskId) }) // Task Execution @@ -565,13 +565,13 @@ export class API extends EventEmitter implements RooCodeAPI { // Delegation events are emitted by the provider, not by individual task instances. provider.on(RooCodeEventName.TaskDelegated, (parentTaskId, childTaskId) => { - ;(this.emit as any)(RooCodeEventName.TaskDelegated, parentTaskId, childTaskId) + this.emit(RooCodeEventName.TaskDelegated, parentTaskId, childTaskId) }) provider.on(RooCodeEventName.TaskDelegationCompleted, (parentTaskId, childTaskId, summary) => { - ;(this.emit as any)(RooCodeEventName.TaskDelegationCompleted, parentTaskId, childTaskId, summary) + this.emit(RooCodeEventName.TaskDelegationCompleted, parentTaskId, childTaskId, summary) }) provider.on(RooCodeEventName.TaskDelegationResumed, (parentTaskId, childTaskId) => { - ;(this.emit as any)(RooCodeEventName.TaskDelegationResumed, parentTaskId, childTaskId) + this.emit(RooCodeEventName.TaskDelegationResumed, parentTaskId, childTaskId) }) } diff --git a/src/i18n/index.ts b/src/i18n/index.ts index a98fce9720..b4670b6d79 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -25,7 +25,7 @@ export function changeLanguage(language: string): void { * @param options Options for interpolation or pluralization * @returns The translated string */ -export function t(key: string, options?: Record): string { +export function t(key: string, options?: Record): string { return i18next.t(key, options) } diff --git a/src/i18n/setup.ts b/src/i18n/setup.ts index cf4c24446f..a4b44e934c 100644 --- a/src/i18n/setup.ts +++ b/src/i18n/setup.ts @@ -1,7 +1,7 @@ -import i18next from "i18next" +import i18next, { type Resource } from "i18next" // Build translations object -const translations: Record> = {} +const translations: Resource = {} // Determine if running in test environment const isTestEnv = process.env.NODE_ENV === "test" diff --git a/src/shared/__tests__/api.spec.ts b/src/shared/__tests__/api.spec.ts index b6babfca3a..d98750923d 100644 --- a/src/shared/__tests__/api.spec.ts +++ b/src/shared/__tests__/api.spec.ts @@ -725,9 +725,9 @@ describe("shouldUseReasoningEffort", () => { const model: ModelInfo = { contextWindow: 100_000, supportsPromptCache: true, - supportsReasoningEffort: ["disable", "low", "medium", "high"] as unknown as any, + supportsReasoningEffort: ["disable", "low", "medium", "high"], } - const settings: ProviderSettings = { enableReasoningEffort: true, reasoningEffort: "disable" as any } + const settings: ProviderSettings = { enableReasoningEffort: true, reasoningEffort: "disable" } expect(shouldUseReasoningEffort({ model, settings })).toBe(false) }) @@ -735,9 +735,9 @@ describe("shouldUseReasoningEffort", () => { const model: ModelInfo = { contextWindow: 100_000, supportsPromptCache: true, - supportsReasoningEffort: ["none", "minimal", "low", "medium", "high"] as unknown as any, + supportsReasoningEffort: ["none", "minimal", "low", "medium", "high"], } - const settings: ProviderSettings = { enableReasoningEffort: true, reasoningEffort: "none" as any } + const settings: ProviderSettings = { enableReasoningEffort: true, reasoningEffort: "none" } expect(shouldUseReasoningEffort({ model, settings })).toBe(true) }) @@ -745,9 +745,9 @@ describe("shouldUseReasoningEffort", () => { const model: ModelInfo = { contextWindow: 100_000, supportsPromptCache: true, - supportsReasoningEffort: ["none", "minimal", "low", "medium", "high"] as unknown as any, + supportsReasoningEffort: ["none", "minimal", "low", "medium", "high"], } - const settings: ProviderSettings = { enableReasoningEffort: true, reasoningEffort: "minimal" as any } + const settings: ProviderSettings = { enableReasoningEffort: true, reasoningEffort: "minimal" } expect(shouldUseReasoningEffort({ model, settings })).toBe(true) }) @@ -757,8 +757,8 @@ describe("shouldUseReasoningEffort", () => { supportsPromptCache: true, supportsReasoningEffort: true, } - expect(shouldUseReasoningEffort({ model, settings: { reasoningEffort: "none" as any } })).toBe(true) - expect(shouldUseReasoningEffort({ model, settings: { reasoningEffort: "minimal" as any } })).toBe(true) + expect(shouldUseReasoningEffort({ model, settings: { reasoningEffort: "none" } })).toBe(true) + expect(shouldUseReasoningEffort({ model, settings: { reasoningEffort: "minimal" } })).toBe(true) }) test("array capability with model default effort and no settings -> true when default is in the ladder", () => { diff --git a/src/shared/__tests__/embeddingModels.spec.ts b/src/shared/__tests__/embeddingModels.spec.ts index 16aa019c7f..144fa20f1f 100644 --- a/src/shared/__tests__/embeddingModels.spec.ts +++ b/src/shared/__tests__/embeddingModels.spec.ts @@ -1,4 +1,6 @@ import { describe, it, expect } from "vitest" +import type { EmbedderProvider } from "@roo-code/types" + import { getModelDimension, getModelScoreThreshold, @@ -48,7 +50,9 @@ describe("embeddingModels", () => { }) it("should return undefined for unknown provider", () => { - const dimension = getModelDimension("unknown-provider" as any, "some-model") + // Exercise the runtime fallback for an unrecognized provider value. + const unknownProvider = "unknown-provider" as unknown as EmbedderProvider + const dimension = getModelDimension(unknownProvider, "some-model") expect(dimension).toBeUndefined() }) diff --git a/src/shared/__tests__/modes-empty-prompt-component.spec.ts b/src/shared/__tests__/modes-empty-prompt-component.spec.ts index f3be0e9927..2fe0258b24 100644 --- a/src/shared/__tests__/modes-empty-prompt-component.spec.ts +++ b/src/shared/__tests__/modes-empty-prompt-component.spec.ts @@ -19,7 +19,9 @@ describe("getModeSelection with empty promptComponent", () => { const debugMode = modes.find((m) => m.slug === "debug")! // Test with null promptComponent - const result = getModeSelection("debug", null as any, []) + // Exercise the legacy null value, which the implementation treats like an empty component. + const nullPromptComponent = null as unknown as PromptComponent + const result = getModeSelection("debug", nullPromptComponent, []) // Should use built-in mode values expect(result.roleDefinition).toBe(debugMode.roleDefinition) diff --git a/src/shared/api.ts b/src/shared/api.ts index 065d32a7ea..d8cd547fdb 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -61,8 +61,7 @@ export const shouldUseReasoningEffort = ({ if (settings?.enableReasoningEffort === false) return false // Selected effort from settings or model default - const selectedEffort = (settings?.reasoningEffort ?? (model as any).reasoningEffort) as - "disable" | "none" | "minimal" | "low" | "medium" | "high" | undefined + const selectedEffort = settings?.reasoningEffort ?? model.reasoningEffort // "disable" explicitly omits reasoning if (selectedEffort === "disable") return false @@ -81,8 +80,7 @@ export const shouldUseReasoningEffort = ({ // Not explicitly supported: only allow when the model itself defines a default effort // Ignore settings-only selections when capability is absent/false - const modelDefaultEffort = (model as any).reasoningEffort as - "none" | "minimal" | "low" | "medium" | "high" | undefined + const modelDefaultEffort = model.reasoningEffort return !!modelDefaultEffort } diff --git a/src/shared/checkExistApiConfig.ts b/src/shared/checkExistApiConfig.ts index 3560199b91..89f9b40ac5 100644 --- a/src/shared/checkExistApiConfig.ts +++ b/src/shared/checkExistApiConfig.ts @@ -35,7 +35,9 @@ export function checkExistKey(config: ProviderSettings | undefined, zooCodeIsAut // Check all secret keys from the centralized SECRET_STATE_KEYS array. // Filter out keys that are not part of ProviderSettings (global secrets are stored separately) - const providerSecretKeys = SECRET_STATE_KEYS.filter((key) => !GLOBAL_SECRET_KEYS.includes(key as any)) + const providerSecretKeys = SECRET_STATE_KEYS.filter( + (key) => !(GLOBAL_SECRET_KEYS as readonly string[]).includes(key), + ) const hasSecretKey = providerSecretKeys.some((key) => config[key as keyof ProviderSettings] !== undefined) // Check additional non-secret configuration properties diff --git a/src/shared/cost.ts b/src/shared/cost.ts index 754a2d84df..7472d49375 100644 --- a/src/shared/cost.ts +++ b/src/shared/cost.ts @@ -114,4 +114,5 @@ export function calculateApiCostOpenAI( ) } -export const parseApiPrice = (price: any) => (price ? parseFloat(price) * 1_000_000 : undefined) +export const parseApiPrice = (price: string | number | null | undefined) => + price ? parseFloat(String(price)) * 1_000_000 : undefined diff --git a/src/shared/parse-command.ts b/src/shared/parse-command.ts index edaebcb81a..4195d99d4e 100644 --- a/src/shared/parse-command.ts +++ b/src/shared/parse-command.ts @@ -653,9 +653,9 @@ function parseCommandLine(command: string): string[] { let tokens: ShellToken[] try { tokens = parse(processedCommand) as ShellToken[] - } catch (error: any) { + } catch (error) { // If shell-quote fails to parse, fall back to simple splitting - console.warn("shell-quote parse error:", error.message, "for command:", processedCommand) + console.warn("shell-quote parse error:", (error as Error).message, "for command:", processedCommand) // Simple fallback: split by common operators const fallbackCommands = processedCommand diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index da14c4367f..6fe48fa364 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -1,7 +1,13 @@ // Support prompts -type PromptParams = Record +type PromptDiagnostic = { + source?: string + message: string + code?: string | number | { value: string | number; target: unknown } +} + +type PromptParams = { diagnostics?: PromptDiagnostic[] } & Record -const generateDiagnosticText = (diagnostics?: any[]) => { +const generateDiagnosticText = (diagnostics?: PromptDiagnostic[]) => { if (!diagnostics?.length) return "" return `\nCurrent problems detected:\n${diagnostics .map((d) => `- [${d.source || "Error"}] ${d.message}${d.code ? ` (${d.code})` : ""}`) @@ -11,7 +17,7 @@ const generateDiagnosticText = (diagnostics?: any[]) => { export const createPrompt = (template: string, params: PromptParams): string => { return template.replace(/\${(.*?)}/g, (_, key) => { if (key === "diagnosticText") { - return generateDiagnosticText(params["diagnostics"] as any[]) + return generateDiagnosticText(params["diagnostics"]) // eslint-disable-next-line no-prototype-builtins } else if (params.hasOwnProperty(key)) { // Ensure the value is treated as a string for replacement @@ -244,10 +250,14 @@ Please provide: export const supportPrompt = { default: Object.fromEntries(Object.entries(supportPromptConfigs).map(([key, config]) => [key, config.template])), - get: (customSupportPrompts: Record | undefined, type: SupportPromptType): string => { + get: (customSupportPrompts: Record | undefined, type: SupportPromptType): string => { return customSupportPrompts?.[type] ?? supportPromptConfigs[type].template }, - create: (type: SupportPromptType, params: PromptParams, customSupportPrompts?: Record): string => { + create: ( + type: SupportPromptType, + params: PromptParams, + customSupportPrompts?: Record, + ): string => { const template = supportPrompt.get(customSupportPrompts, type) return createPrompt(template, params) }, diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 4185380341..6d2e9422ee 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -278,5 +278,5 @@ export interface DiffStrategy { endLine?: number, ): Promise - getProgressStatus?(toolUse: ToolUse, result?: any): ToolProgressStatus + getProgressStatus?(toolUse: ToolUse, result?: DiffResult): ToolProgressStatus } diff --git a/src/shared/utils/__tests__/requesty.spec.ts b/src/shared/utils/__tests__/requesty.spec.ts index 0e8fa78b14..9104f5e769 100644 --- a/src/shared/utils/__tests__/requesty.spec.ts +++ b/src/shared/utils/__tests__/requesty.spec.ts @@ -99,12 +99,14 @@ describe("toRequestyServiceUrl", () => { }) it("should handle null baseUrl gracefully", () => { - const result = toRequestyServiceUrl(null as any) + const result = toRequestyServiceUrl(null) expect(result).toBe("https://router.requesty.ai/v1") }) it("should handle non-string baseUrl gracefully", () => { - const result = toRequestyServiceUrl(123 as any) + // Exercise the runtime fallback for a persisted value with an invalid type. + const invalidBaseUrl = 123 as unknown as string + const result = toRequestyServiceUrl(invalidBaseUrl) expect(result).toBe("https://router.requesty.ai/v1") }) }) diff --git a/src/vitest.setup.ts b/src/vitest.setup.ts index fd0bce1cf3..96c95d683f 100644 --- a/src/vitest.setup.ts +++ b/src/vitest.setup.ts @@ -14,4 +14,4 @@ export function allowNetConnect(host?: string | RegExp) { } // Global mocks that many tests expect. -global.structuredClone = global.structuredClone || ((obj: any) => JSON.parse(JSON.stringify(obj))) +global.structuredClone = global.structuredClone || ((obj: T): T => JSON.parse(JSON.stringify(obj)) as T) From fff23ed2a2bd4b34151d7a924de03ded4d7d8fe2 Mon Sep 17 00:00:00 2001 From: Elliott de Launay Date: Sun, 1 Nov 2026 16:23:20 +0000 Subject: [PATCH 2/2] test: expand URI callback coverage and address review feedback --- ...Provider.history-resume-delegation.spec.ts | 2 + src/__tests__/api-subtask.spec.ts | 2 + .../__tests__/CodeActionProvider.spec.ts | 1 + src/activate/__tests__/handleUri.spec.ts | 76 +++++++++++++++-- src/activate/registerCommands.ts | 8 +- .../__tests__/api-delegation-events.spec.ts | 83 +++++++++++++++++++ 6 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 src/extension/__tests__/api-delegation-events.spec.ts diff --git a/src/__tests__/ClineProvider.history-resume-delegation.spec.ts b/src/__tests__/ClineProvider.history-resume-delegation.spec.ts index 1283a2a59e..1e84c609e0 100644 --- a/src/__tests__/ClineProvider.history-resume-delegation.spec.ts +++ b/src/__tests__/ClineProvider.history-resume-delegation.spec.ts @@ -1787,7 +1787,9 @@ describe("History resume delegation - parent metadata transitions", () => { fU: (h: HistoryItem) => HistoryItem, sU: (h: HistoryItem) => HistoryItem, ) => { + // Child double provides id and status, the fields read by this updater. fU(childItem as unknown as HistoryItem) + // Parent double provides id, status, delegation links, timestamp, task, token counts, and cost. sU(parentItem as unknown as HistoryItem) return [] }, diff --git a/src/__tests__/api-subtask.spec.ts b/src/__tests__/api-subtask.spec.ts index 05140e116c..52dabdc7c8 100644 --- a/src/__tests__/api-subtask.spec.ts +++ b/src/__tests__/api-subtask.spec.ts @@ -66,6 +66,7 @@ describe("API.clearCurrentTask()", () => { beforeEach(() => { vi.clearAllMocks() provider = makeProviderMock() + // Double provides event methods, context, task access, eviction, abandonment, and webview posting. api = new API({} as OutputChannel, provider as unknown as ClineProvider, makeClineProviderFactory()) }) @@ -87,6 +88,7 @@ describe("API.abandonSubtask()", () => { beforeEach(() => { vi.clearAllMocks() provider = makeProviderMock() + // Double provides event methods, context, task access, eviction, abandonment, and webview posting. api = new API({} as OutputChannel, provider as unknown as ClineProvider, makeClineProviderFactory()) }) diff --git a/src/activate/__tests__/CodeActionProvider.spec.ts b/src/activate/__tests__/CodeActionProvider.spec.ts index d8d90361d8..d0e96bc559 100644 --- a/src/activate/__tests__/CodeActionProvider.spec.ts +++ b/src/activate/__tests__/CodeActionProvider.spec.ts @@ -69,6 +69,7 @@ describe("CodeActionProvider", () => { beforeEach(() => { provider = new CodeActionProvider() + // Document double provides getText, lineAt, lineCount, and uri.fsPath. mockDocument = { getText: vi.fn(), lineAt: vi.fn(), diff --git a/src/activate/__tests__/handleUri.spec.ts b/src/activate/__tests__/handleUri.spec.ts index f02bc0e799..c7f7236ebb 100644 --- a/src/activate/__tests__/handleUri.spec.ts +++ b/src/activate/__tests__/handleUri.spec.ts @@ -13,6 +13,7 @@ const { mockSetZooCodeUserInfo, mockVisibleProvider, } = vi.hoisted(() => { + // Visible provider double provides the OpenRouter, Requesty, and Zoo Code callbacks. const mockVisibleProvider = { handleOpenRouterCallback: vi.fn(), handleRequestyCallback: vi.fn(), @@ -45,6 +46,10 @@ import type { ClineProvider } from "../../core/webview/ClineProvider" const makeUri = (uri: Pick): vscode.Uri => uri as vscode.Uri +const makeCallbackProvider = (callback = vi.fn()): ClineProvider => + // Provider double provides handleZooCodeCallback only. + ({ handleZooCodeCallback: callback }) as unknown as ClineProvider + describe("handleUri", () => { beforeEach(() => { vi.clearAllMocks() @@ -52,6 +57,59 @@ describe("handleUri", () => { mockGetAllInstances.mockReturnValue([mockVisibleProvider]) }) + it.each(["/openrouter", "/requesty"])("dispatches %s codes without losing literal plus signs", async (path) => { + await handleUri(makeUri({ path, query: "code=a+b%2Bc&baseUrl=https%3A%2F%2Fexample.com" })) + + if (path === "/openrouter") { + expect(mockVisibleProvider.handleOpenRouterCallback).toHaveBeenCalledWith("a+b+c") + expect(mockVisibleProvider.handleRequestyCallback).not.toHaveBeenCalled() + } else { + expect(mockVisibleProvider.handleRequestyCallback).toHaveBeenCalledWith("a+b+c", "https://example.com") + expect(mockVisibleProvider.handleOpenRouterCallback).not.toHaveBeenCalled() + } + }) + + it("passes null when Requesty has no base URL", async () => { + await handleUri(makeUri({ path: "/requesty", query: "code=test-code" })) + expect(mockVisibleProvider.handleRequestyCallback).toHaveBeenCalledWith("test-code", null) + }) + + it.each(["/openrouter", "/requesty"])("ignores %s when no provider is visible", async (path) => { + mockGetVisibleInstance.mockReturnValue(null) + await handleUri(makeUri({ path, query: "code=test-code" })) + expect(mockVisibleProvider.handleOpenRouterCallback).not.toHaveBeenCalled() + expect(mockVisibleProvider.handleRequestyCallback).not.toHaveBeenCalled() + }) + + it.each(["/openrouter", "/requesty", "/auth-callback", "/unknown"])( + "ignores %s without callback credentials", + async (path) => { + await handleUri(makeUri({ path, query: "" })) + expect(mockVisibleProvider.handleOpenRouterCallback).not.toHaveBeenCalled() + expect(mockVisibleProvider.handleRequestyCallback).not.toHaveBeenCalled() + expect(mockHandleZooCodeAuthCallback).not.toHaveBeenCalled() + expect(mockSetZooCodeUserInfo).not.toHaveBeenCalled() + expect(mockVisibleProvider.handleZooCodeCallback).not.toHaveBeenCalled() + }, + ) + + it("logs non-Error callback failures and continues to the next provider", async () => { + mockHandleZooCodeAuthCallback.mockResolvedValue(true) + const log = vi.spyOn(console, "error").mockImplementation(() => {}) + const failingProvider = makeCallbackProvider(vi.fn().mockRejectedValue("storage unavailable")) + mockGetAllInstances.mockReturnValue([failingProvider, mockVisibleProvider]) + try { + await handleUri(makeUri({ path: "/auth-callback", query: "token=test-token" })) + expect(log).toHaveBeenCalledWith( + "Failed to persist Zoo Gateway token for a provider instance:", + "storage unavailable", + ) + expect(mockVisibleProvider.handleZooCodeCallback).toHaveBeenCalledWith("test-token") + } finally { + log.mockRestore() + } + }) + it("ignores legacy cloud auth callback", async () => { await handleUri( makeUri({ @@ -146,8 +204,8 @@ describe("handleUri", () => { // stayed unauthenticated until reload. mockHandleZooCodeAuthCallback.mockResolvedValue(true) - const hiddenProvider = { handleZooCodeCallback: vi.fn() } as unknown as ClineProvider - const secondHidden = { handleZooCodeCallback: vi.fn() } as unknown as ClineProvider + const hiddenProvider = makeCallbackProvider() + const secondHidden = makeCallbackProvider() mockGetAllInstances.mockReturnValue([mockVisibleProvider, hiddenProvider, secondHidden]) await handleUri( @@ -172,14 +230,14 @@ describe("handleUri", () => { const order: string[] = [] const makeProvider = (name: string) => - ({ - handleZooCodeCallback: vi.fn(async () => { + makeCallbackProvider( + vi.fn(async () => { order.push(`${name}:start`) // Yield to the event loop so a concurrent call would interleave. await new Promise((resolve) => setTimeout(resolve, 0)) order.push(`${name}:end`) }), - }) as unknown as ClineProvider + ) const a = makeProvider("a") const b = makeProvider("b") @@ -198,12 +256,12 @@ describe("handleUri", () => { it("continues fan-out when one instance fails to persist the callback token", async () => { mockHandleZooCodeAuthCallback.mockResolvedValue(true) - const failingProvider = { - handleZooCodeCallback: vi.fn(async () => { + const failingProvider = makeCallbackProvider( + vi.fn(async () => { throw new Error("profile store unavailable") }), - } as unknown as ClineProvider - const healthyProvider = { handleZooCodeCallback: vi.fn() } as unknown as ClineProvider + ) + const healthyProvider = makeCallbackProvider() mockGetAllInstances.mockReturnValue([failingProvider, healthyProvider]) await handleUri( diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index e4d7615266..c151235848 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -76,11 +76,9 @@ export const registerCommands = (options: RegisterCommandOptions) => { // lifecycle alongside the command registration, so it's intentionally // excluded from this map. // -// Callback shape mirrors VS Code's own `commands.registerCommand` signature -// (`(...args: any[]) => any`), with the return narrowed to `unknown` so -// callers must inspect before using. `never[]` permits the heterogeneous -// callbacks here (`importSettings` takes an optional `filePath?: string`, -// others take none) while VS Code dispatches positional args dynamically. +// `never[]` deliberately lets callbacks with different parameters share one map +// type while rejecting direct calls with arguments. VS Code dispatches positional +// arguments dynamically; `unknown` requires callers to inspect returned values. type CommandCallback = (...args: never[]) => unknown const getCommandsMap = ({ context, diff --git a/src/extension/__tests__/api-delegation-events.spec.ts b/src/extension/__tests__/api-delegation-events.spec.ts new file mode 100644 index 0000000000..dd27a90e03 --- /dev/null +++ b/src/extension/__tests__/api-delegation-events.spec.ts @@ -0,0 +1,83 @@ +import { EventEmitter } from "events" +import type { OutputChannel } from "vscode" +import { describe, expect, it, vi } from "vitest" + +import { RooCodeEventName, type TaskEvents, type TaskProviderEvents } from "@roo-code/types" + +import type { Task } from "../../core/task/Task" +import type { ClineProvider } from "../../core/webview/ClineProvider" +import { makeClineProviderFactory } from "../../test-utils/provider" +import { API } from "../api" + +vi.mock("vscode") +vi.mock("@roo-code/ipc", () => ({ IpcServer: vi.fn() })) + +function setup() { + const provider = new EventEmitter() + const output: OutputChannel = { + name: "test", + append: vi.fn(), + appendLine: vi.fn(), + replace: vi.fn(), + clear: vi.fn(), + show: vi.fn(), + hide: vi.fn(), + dispose: vi.fn(), + } + // The provider double supplies the typed event subscription API used by the constructor. + const api = new API(output, provider as unknown as ClineProvider, makeClineProviderFactory()) + return { api, provider } +} + +describe("API delegation event forwarding", () => { + it.each([ + RooCodeEventName.TaskDelegated, + RooCodeEventName.TaskDelegationCompleted, + RooCodeEventName.TaskDelegationResumed, + ])("forwards %s from tasks with the owning task ID", (event) => { + const { api, provider } = setup() + const listener = vi.fn() + api.on(event, listener) + const task = Object.assign(new EventEmitter(), { taskId: "parent-task" }) + // The task double supplies taskId and typed event subscriptions for TaskCreated wiring. + provider.emit(RooCodeEventName.TaskCreated, task as unknown as Task) + + if (event === RooCodeEventName.TaskDelegationCompleted) { + task.emit(event, "child-task", "Child finished successfully") + expect(listener).toHaveBeenCalledExactlyOnceWith("parent-task", "child-task", "Child finished successfully") + } else { + if (event === RooCodeEventName.TaskDelegated) { + task.emit(RooCodeEventName.TaskDelegated, "child-task") + } else { + task.emit(RooCodeEventName.TaskDelegationResumed, "child-task") + } + expect(listener).toHaveBeenCalledExactlyOnceWith("parent-task", "child-task") + } + }) + + it.each([ + RooCodeEventName.TaskDelegated, + RooCodeEventName.TaskDelegationCompleted, + RooCodeEventName.TaskDelegationResumed, + ])("forwards %s from providers with the supplied parent ID", (event) => { + const { api, provider } = setup() + const listener = vi.fn() + api.on(event, listener) + + if (event === RooCodeEventName.TaskDelegationCompleted) { + provider.emit(event, "other-parent", "child-task", "Child finished successfully") + expect(listener).toHaveBeenCalledExactlyOnceWith( + "other-parent", + "child-task", + "Child finished successfully", + ) + } else { + if (event === RooCodeEventName.TaskDelegated) { + provider.emit(RooCodeEventName.TaskDelegated, "other-parent", "child-task") + } else { + provider.emit(RooCodeEventName.TaskDelegationResumed, "other-parent", "child-task") + } + expect(listener).toHaveBeenCalledExactlyOnceWith("other-parent", "child-task") + } + }) +})