From 2ae31c29d1a2b82d1dc5814aba0bd28957f13d62 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Sat, 25 Jul 2026 23:04:30 +0300 Subject: [PATCH 1/6] refactor(core): use canonical provider identifiers --- src/core/task/Task.ts | 3 +- src/core/task/__tests__/Task.spec.ts | 48 ++++++++++++++++++- src/core/webview/ClineProvider.ts | 9 ++-- .../webview/__tests__/ClineProvider.spec.ts | 28 +++++++++++ .../__tests__/webviewMessageHandler.spec.ts | 36 +++++++++++++- src/core/webview/webviewMessageHandler.ts | 5 +- 6 files changed, 120 insertions(+), 9 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 7bee3c5e14..da593f0553 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -54,6 +54,7 @@ import { ConsecutiveMistakeError, MAX_MCP_TOOLS_THRESHOLD, countEnabledMcpTools, + providerIdentifiers, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { CloudService } from "@roo-code/cloud" @@ -4220,7 +4221,7 @@ export class Task extends EventEmitter implements TaskLike { // but uses allowedFunctionNames to restrict which tools can be called. // Other providers (Anthropic, OpenAI, etc.) don't support this feature yet, // so they continue to receive only the filtered tools for the current mode. - const supportsAllowedFunctionNames = apiConfiguration?.apiProvider === "gemini" + const supportsAllowedFunctionNames = apiConfiguration?.apiProvider === providerIdentifiers.gemini { const provider = this.providerRef.deref() diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index 0c784821c1..e69cb21b1b 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -6,7 +6,7 @@ import * as path from "path" import * as vscode from "vscode" import { Anthropic } from "@anthropic-ai/sdk" -import type { GlobalState, ProviderSettings, ModelInfo } from "@roo-code/types" +import { providerIdentifiers, type GlobalState, type ProviderSettings, type ModelInfo } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { Task } from "../Task" @@ -1902,6 +1902,52 @@ describe("Cline", () => { expect(metadata!.abortSignal).toBe(task.currentRequestAbortController!.signal) }) + it("enables allowed function names through the canonical Gemini provider identifier", async () => { + const identifiers = providerIdentifiers as Record + const originalIdentifier = identifiers.gemini + + try { + identifiers.gemini = "canonical-gemini" + const apiConfiguration = { + ...mockApiConfig, + apiProvider: identifiers.gemini, + } as ProviderSettings + const task = new Task({ + provider: mockProvider, + apiConfiguration, + task: "test task", + startTask: false, + }) + + vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(task.api, "getModel").mockReturnValue({ + id: mockApiConfig.apiModelId!, + info: { contextWindow: 200000, maxTokens: 4096 } as ModelInfo, + }) + const providerState = await mockProvider.getState() + vi.spyOn(mockProvider, "getState").mockResolvedValue({ + ...providerState, + apiConfiguration, + autoApprovalEnabled: true, + requestDelaySeconds: 0, + }) + const mockStream = (async function* () { + yield { type: "text", text: "response" } as ApiStreamChunk + })() + const createMessageSpy = vi.spyOn(task.api, "createMessage").mockReturnValue(mockStream) + task.apiConversationHistory = [ + { role: "user", content: [{ type: "text", text: "test message" }], ts: Date.now() }, + ] as any + + await task.attemptApiRequest(0).next() + + const [, , metadata] = createMessageSpy.mock.calls[0]! + expect(metadata?.allowedFunctionNames).toEqual(expect.any(Array)) + } finally { + identifiers.gemini = originalIdentifier + } + }) + it("should invoke abort on currentRequestAbortController during first-chunk wait", async () => { const task = new Task({ provider: mockProvider, diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 643c4f5a22..458120d9a4 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -50,6 +50,7 @@ import { DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, getModelId, isRetiredProvider, + providerIdentifiers, } from "@roo-code/types" import { RateLimitClock, createRateLimitClock } from "../task/RateLimitClock" import { TaskRegistry } from "../task/TaskRegistry" @@ -479,7 +480,7 @@ export class ClineProvider async performPreparationTasks(cline: Task) { // LMStudio: We need to force model loading in order to read its context // size; we do it now since we're starting a task with that model selected. - if (cline.apiConfiguration && cline.apiConfiguration.apiProvider === "lmstudio") { + if (cline.apiConfiguration && cline.apiConfiguration.apiProvider === providerIdentifiers.lmstudio) { try { if (!hasLoadedFullDetails(cline.apiConfiguration.lmStudioModelId!)) { await forceFullModelDetailsLoad( @@ -1007,7 +1008,7 @@ export class ClineProvider // Using .find() would miss stale tokens in duplicate/renamed profiles since handleZooCodeCallback // uses .filter() and updates all of them — the early-return guard must match. const allProfiles = await this.providerSettingsManager.listConfig() - const zooGatewayProfiles = allProfiles.filter((p) => p.apiProvider === "zoo-gateway") + const zooGatewayProfiles = allProfiles.filter((p) => p.apiProvider === providerIdentifiers.zooGateway) if (zooGatewayProfiles.length === 0) { this.log("[ensureZooGatewayProfileSeeded] No zoo-gateway profile found, creating one") @@ -1868,14 +1869,14 @@ export class ClineProvider // Check if Zoo Gateway is the currently active profile by apiProvider identity, // not by profile name (profile names are user-renameable). - const isZooGatewayActive = currentSettings.apiProvider === "zoo-gateway" + const isZooGatewayActive = currentSettings.apiProvider === providerIdentifiers.zooGateway // Always scan ALL profiles and update every zoo-gateway profile with the new token. // This ensures renamed profiles, duplicate profiles, and inactive profiles all stay // in sync. The model lookup in requestRouterModels uses .find() which returns the // first zoo-gateway profile it finds — if that profile has a stale token, requests fail. const allProfiles = await this.providerSettingsManager.listConfig() - const zooProfiles = allProfiles.filter((p) => p.apiProvider === "zoo-gateway") + const zooProfiles = allProfiles.filter((p) => p.apiProvider === providerIdentifiers.zooGateway) if (zooProfiles.length === 0) { // No existing zoo-gateway profile — create the canonical default. diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 7e111442a9..697336dc7a 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -16,6 +16,7 @@ import { DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, DEFAULT_DIFF_FUZZY_THRESHOLD, DEFAULT_WRITE_DELAY_MS, + providerIdentifiers, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" @@ -29,6 +30,7 @@ import { safeWriteJson } from "../../../utils/safeWriteJson" import { ClineProvider } from "../ClineProvider" import { Terminal } from "../../../integrations/terminal/Terminal" import { MessageManager } from "../../message-manager" +import { forceFullModelDetailsLoad } from "../../../api/providers/fetchers/lmstudio" // Mock setup must come before imports. vi.mock("../../prompts/sections/custom-instructions") @@ -258,6 +260,11 @@ vi.mock("../../../api/providers/fetchers/modelCache", () => ({ getModelsFromCache: vi.fn().mockReturnValue(undefined), })) +vi.mock("../../../api/providers/fetchers/lmstudio", () => ({ + hasLoadedFullDetails: vi.fn().mockReturnValue(false), + forceFullModelDetailsLoad: vi.fn().mockResolvedValue(undefined), +})) + vi.mock("../../../services/zoo-code-auth", () => ({ getZooCodeBaseUrl: vi.fn(() => "https://www.zoocode.dev"), getCachedZooCodeToken: vi.fn(), @@ -521,6 +528,27 @@ describe("ClineProvider", () => { expect(ClineProvider.getVisibleInstance()).toBe(provider) }) + test("prepares LM Studio tasks through the canonical provider identifier", async () => { + const identifiers = providerIdentifiers as Record + const originalIdentifier = identifiers.lmstudio + + try { + identifiers.lmstudio = "canonical-lmstudio" + + await provider.performPreparationTasks({ + apiConfiguration: { + apiProvider: identifiers.lmstudio, + lmStudioBaseUrl: "http://localhost:1234", + lmStudioModelId: "test-model", + }, + } as Task) + + expect(forceFullModelDetailsLoad).toHaveBeenCalledWith("http://localhost:1234", "test-model") + } finally { + identifiers.lmstudio = originalIdentifier + } + }) + test("resolveWebviewView hydrates the saved terminalProfile into the process-wide Terminal state", async () => { const setTerminalProfileSpy = vi.spyOn(Terminal, "setTerminalProfile").mockImplementation(() => {}) // Seed the persisted setting so the real getState() returns it during hydration. diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index d843a3a6d9..e7ede5d44d 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -51,7 +51,7 @@ vi.mock("../rulesMessageHandler", () => ({ handleOpenRulesDirectory: vi.fn(), })) -import type { ModelRecord } from "@roo-code/types" +import { providerIdentifiers, type ModelRecord } from "@roo-code/types" import { webviewMessageHandler } from "../webviewMessageHandler" import type { ClineProvider } from "../ClineProvider" @@ -1540,6 +1540,40 @@ describe("zooCodeSignOut", () => { expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() }) + it("clears tokens using the canonical Zoo Gateway provider identifier", async () => { + const identifiers = providerIdentifiers as Record + const originalIdentifier = identifiers.zooGateway + const upsertProviderProfile = vi.fn().mockResolvedValue(undefined) + + try { + identifiers.zooGateway = "canonical-zoo-gateway" + ;(mockClineProvider as any).contextProxy = { + ...mockClineProvider.contextProxy, + getProviderSettings: vi.fn().mockReturnValue({ apiProvider: identifiers.zooGateway }), + getValues: vi.fn().mockReturnValue({ currentApiConfigName: "Zoo Gateway" }), + } + ;(mockClineProvider as any).providerSettingsManager = { + listConfig: vi.fn().mockResolvedValue([{ name: "Zoo Gateway", apiProvider: identifiers.zooGateway }]), + getProfile: vi.fn().mockResolvedValue({ + apiProvider: identifiers.zooGateway, + zooSessionToken: "token-active", + }), + saveConfig: vi.fn(), + } + ;(mockClineProvider as any).upsertProviderProfile = upsertProviderProfile + + await webviewMessageHandler(mockClineProvider, { type: "zooCodeSignOut" }) + + expect(upsertProviderProfile).toHaveBeenCalledWith( + "Zoo Gateway", + expect.not.objectContaining({ zooSessionToken: expect.anything() }), + true, + ) + } finally { + identifiers.zooGateway = originalIdentifier + } + }) + it("still clears the in-memory handler when the active profile token is already empty on disk", async () => { const upsertProviderProfile = vi.fn().mockResolvedValue(undefined) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index a72c4955a3..3126d4ebf5 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -22,6 +22,7 @@ import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, getCompletionCheckpoint, + providerIdentifiers, } from "@roo-code/types" import { customToolRegistry } from "@roo-code/core" import { CloudService } from "@roo-code/cloud" @@ -2773,11 +2774,11 @@ export const webviewMessageHandler = async ( const allProfiles = await provider.providerSettingsManager.listConfig() // Check if Zoo Gateway is the currently active profile by apiProvider identity const currentSettings = provider.contextProxy.getProviderSettings() - const isZooGatewayActive = currentSettings.apiProvider === "zoo-gateway" + const isZooGatewayActive = currentSettings.apiProvider === providerIdentifiers.zooGateway const currentApiConfigName = provider.contextProxy.getValues().currentApiConfigName for (const entry of allProfiles) { - if (entry.apiProvider !== "zoo-gateway") { + if (entry.apiProvider !== providerIdentifiers.zooGateway) { continue } From 53a2fdd8fabd59d1122dc631efe2d02b5c7bd894 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Sat, 25 Jul 2026 23:13:23 +0300 Subject: [PATCH 2/6] test(core): tighten canonical provider coverage --- src/core/task/__tests__/Task.spec.ts | 9 ++- .../__tests__/webviewMessageHandler.spec.ts | 75 ++++++------------- 2 files changed, 31 insertions(+), 53 deletions(-) diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index e69cb21b1b..cb2638bf06 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -1902,7 +1902,7 @@ describe("Cline", () => { expect(metadata!.abortSignal).toBe(task.currentRequestAbortController!.signal) }) - it("enables allowed function names through the canonical Gemini provider identifier", async () => { + it("uses the canonical Gemini identifier when configuring tool restrictions", async () => { const identifiers = providerIdentifiers as Record const originalIdentifier = identifiers.gemini @@ -1942,7 +1942,12 @@ describe("Cline", () => { await task.attemptApiRequest(0).next() const [, , metadata] = createMessageSpy.mock.calls[0]! - expect(metadata?.allowedFunctionNames).toEqual(expect.any(Array)) + expect(metadata).toEqual( + expect.objectContaining({ + tools: expect.any(Array), + allowedFunctionNames: expect.any(Array), + }), + ) } finally { identifiers.gemini = originalIdentifier } diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index e7ede5d44d..040fc53d3c 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -1495,55 +1495,12 @@ describe("zooCodeSignOut", () => { vi.clearAllMocks() }) - it("disconnects Zoo Code and clears tokens from all zoo-gateway profiles", async () => { + it("disconnects Zoo Code and clears tokens from all profiles using the canonical Zoo Gateway identifier", async () => { const { disconnectZooCode } = await import("../../../services/zoo-code-auth") - const upsertProviderProfile = vi.fn().mockResolvedValue(undefined) - const saveConfig = vi.fn().mockResolvedValue(undefined) - - ;(mockClineProvider as any).contextProxy = { - ...mockClineProvider.contextProxy, - getProviderSettings: vi.fn().mockReturnValue({ apiProvider: "zoo-gateway" }), - getValues: vi.fn().mockReturnValue({ currentApiConfigName: "Zoo Gateway" }), - } - ;(mockClineProvider as any).providerSettingsManager = { - listConfig: vi.fn().mockResolvedValue([ - { name: "Zoo Gateway", apiProvider: "zoo-gateway" }, - { name: "Backup Zoo", apiProvider: "zoo-gateway" }, - ]), - getProfile: vi - .fn() - .mockResolvedValueOnce({ - apiProvider: "zoo-gateway", - zooSessionToken: "token-active", - zooGatewayModelId: "anthropic/claude-sonnet-4", - }) - .mockResolvedValueOnce({ - apiProvider: "zoo-gateway", - zooSessionToken: "token-backup", - }), - saveConfig, - } - ;(mockClineProvider as any).upsertProviderProfile = upsertProviderProfile - - await webviewMessageHandler(mockClineProvider, { type: "zooCodeSignOut" }) - - expect(disconnectZooCode).toHaveBeenCalled() - expect(upsertProviderProfile).toHaveBeenCalledWith( - "Zoo Gateway", - expect.not.objectContaining({ zooSessionToken: expect.anything() }), - true, - ) - expect(saveConfig).toHaveBeenCalledWith( - "Backup Zoo", - expect.not.objectContaining({ zooSessionToken: expect.anything() }), - ) - expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() - }) - - it("clears tokens using the canonical Zoo Gateway provider identifier", async () => { const identifiers = providerIdentifiers as Record const originalIdentifier = identifiers.zooGateway const upsertProviderProfile = vi.fn().mockResolvedValue(undefined) + const saveConfig = vi.fn().mockResolvedValue(undefined) try { identifiers.zooGateway = "canonical-zoo-gateway" @@ -1553,22 +1510,38 @@ describe("zooCodeSignOut", () => { getValues: vi.fn().mockReturnValue({ currentApiConfigName: "Zoo Gateway" }), } ;(mockClineProvider as any).providerSettingsManager = { - listConfig: vi.fn().mockResolvedValue([{ name: "Zoo Gateway", apiProvider: identifiers.zooGateway }]), - getProfile: vi.fn().mockResolvedValue({ - apiProvider: identifiers.zooGateway, - zooSessionToken: "token-active", - }), - saveConfig: vi.fn(), + listConfig: vi.fn().mockResolvedValue([ + { name: "Zoo Gateway", apiProvider: identifiers.zooGateway }, + { name: "Backup Zoo", apiProvider: identifiers.zooGateway }, + ]), + getProfile: vi + .fn() + .mockResolvedValueOnce({ + apiProvider: identifiers.zooGateway, + zooSessionToken: "token-active", + zooGatewayModelId: "anthropic/claude-sonnet-4", + }) + .mockResolvedValueOnce({ + apiProvider: identifiers.zooGateway, + zooSessionToken: "token-backup", + }), + saveConfig, } ;(mockClineProvider as any).upsertProviderProfile = upsertProviderProfile await webviewMessageHandler(mockClineProvider, { type: "zooCodeSignOut" }) + expect(disconnectZooCode).toHaveBeenCalled() expect(upsertProviderProfile).toHaveBeenCalledWith( "Zoo Gateway", expect.not.objectContaining({ zooSessionToken: expect.anything() }), true, ) + expect(saveConfig).toHaveBeenCalledWith( + "Backup Zoo", + expect.not.objectContaining({ zooSessionToken: expect.anything() }), + ) + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() } finally { identifiers.zooGateway = originalIdentifier } From 0e23a5a7da7406d695f7b7a4904eb823d3d969ac Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Sun, 26 Jul 2026 00:14:52 +0300 Subject: [PATCH 3/6] test(core): avoid mutating provider identifiers --- src/core/task/__tests__/Task.spec.ts | 92 +++++++++---------- .../webview/__tests__/ClineProvider.spec.ts | 27 ++---- .../__tests__/webviewMessageHandler.spec.ts | 85 ++++++++--------- 3 files changed, 90 insertions(+), 114 deletions(-) diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index cb2638bf06..e17e9cccfa 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -6,7 +6,7 @@ import * as path from "path" import * as vscode from "vscode" import { Anthropic } from "@anthropic-ai/sdk" -import { providerIdentifiers, type GlobalState, type ProviderSettings, type ModelInfo } from "@roo-code/types" +import type { GlobalState, ProviderSettings, ModelInfo } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { Task } from "../Task" @@ -1902,55 +1902,47 @@ describe("Cline", () => { expect(metadata!.abortSignal).toBe(task.currentRequestAbortController!.signal) }) - it("uses the canonical Gemini identifier when configuring tool restrictions", async () => { - const identifiers = providerIdentifiers as Record - const originalIdentifier = identifiers.gemini - - try { - identifiers.gemini = "canonical-gemini" - const apiConfiguration = { - ...mockApiConfig, - apiProvider: identifiers.gemini, - } as ProviderSettings - const task = new Task({ - provider: mockProvider, - apiConfiguration, - task: "test task", - startTask: false, - }) - - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") - vi.spyOn(task.api, "getModel").mockReturnValue({ - id: mockApiConfig.apiModelId!, - info: { contextWindow: 200000, maxTokens: 4096 } as ModelInfo, - }) - const providerState = await mockProvider.getState() - vi.spyOn(mockProvider, "getState").mockResolvedValue({ - ...providerState, - apiConfiguration, - autoApprovalEnabled: true, - requestDelaySeconds: 0, - }) - const mockStream = (async function* () { - yield { type: "text", text: "response" } as ApiStreamChunk - })() - const createMessageSpy = vi.spyOn(task.api, "createMessage").mockReturnValue(mockStream) - task.apiConversationHistory = [ - { role: "user", content: [{ type: "text", text: "test message" }], ts: Date.now() }, - ] as any - - await task.attemptApiRequest(0).next() - - const [, , metadata] = createMessageSpy.mock.calls[0]! - expect(metadata).toEqual( - expect.objectContaining({ - tools: expect.any(Array), - allowedFunctionNames: expect.any(Array), - }), - ) - } finally { - identifiers.gemini = originalIdentifier - } + it("configures tool restrictions for Gemini requests", async () => { + const apiConfiguration = { + ...mockApiConfig, + apiProvider: "gemini", + } as ProviderSettings + const task = new Task({ + provider: mockProvider, + apiConfiguration, + task: "test task", + startTask: false, + }) + + vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(task.api, "getModel").mockReturnValue({ + id: mockApiConfig.apiModelId!, + info: { contextWindow: 200000, maxTokens: 4096 } as ModelInfo, + }) + const providerState = await mockProvider.getState() + vi.spyOn(mockProvider, "getState").mockResolvedValue({ + ...providerState, + apiConfiguration, + autoApprovalEnabled: true, + requestDelaySeconds: 0, + }) + const mockStream = (async function* () { + yield { type: "text", text: "response" } as ApiStreamChunk + })() + const createMessageSpy = vi.spyOn(task.api, "createMessage").mockReturnValue(mockStream) + task.apiConversationHistory = [ + { role: "user", content: [{ type: "text", text: "test message" }], ts: Date.now() }, + ] as any + + await task.attemptApiRequest(0).next() + + const [, , metadata] = createMessageSpy.mock.calls[0]! + expect(metadata).toEqual( + expect.objectContaining({ + tools: expect.any(Array), + allowedFunctionNames: expect.any(Array), + }), + ) }) it("should invoke abort on currentRequestAbortController during first-chunk wait", async () => { diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 697336dc7a..32cd798dff 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -528,25 +528,16 @@ describe("ClineProvider", () => { expect(ClineProvider.getVisibleInstance()).toBe(provider) }) - test("prepares LM Studio tasks through the canonical provider identifier", async () => { - const identifiers = providerIdentifiers as Record - const originalIdentifier = identifiers.lmstudio - - try { - identifiers.lmstudio = "canonical-lmstudio" - - await provider.performPreparationTasks({ - apiConfiguration: { - apiProvider: identifiers.lmstudio, - lmStudioBaseUrl: "http://localhost:1234", - lmStudioModelId: "test-model", - }, - } as Task) + test("loads full model details when preparing an LM Studio task", async () => { + await provider.performPreparationTasks({ + apiConfiguration: { + apiProvider: "lmstudio", + lmStudioBaseUrl: "http://localhost:1234", + lmStudioModelId: "test-model", + }, + } as Task) - expect(forceFullModelDetailsLoad).toHaveBeenCalledWith("http://localhost:1234", "test-model") - } finally { - identifiers.lmstudio = originalIdentifier - } + expect(forceFullModelDetailsLoad).toHaveBeenCalledWith("http://localhost:1234", "test-model") }) test("resolveWebviewView hydrates the saved terminalProfile into the process-wide Terminal state", async () => { diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 040fc53d3c..64ad6804df 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -51,7 +51,7 @@ vi.mock("../rulesMessageHandler", () => ({ handleOpenRulesDirectory: vi.fn(), })) -import { providerIdentifiers, type ModelRecord } from "@roo-code/types" +import type { ModelRecord } from "@roo-code/types" import { webviewMessageHandler } from "../webviewMessageHandler" import type { ClineProvider } from "../ClineProvider" @@ -1495,56 +1495,49 @@ describe("zooCodeSignOut", () => { vi.clearAllMocks() }) - it("disconnects Zoo Code and clears tokens from all profiles using the canonical Zoo Gateway identifier", async () => { + it("disconnects Zoo Code and clears tokens from all Zoo Gateway profiles", async () => { const { disconnectZooCode } = await import("../../../services/zoo-code-auth") - const identifiers = providerIdentifiers as Record - const originalIdentifier = identifiers.zooGateway const upsertProviderProfile = vi.fn().mockResolvedValue(undefined) const saveConfig = vi.fn().mockResolvedValue(undefined) - try { - identifiers.zooGateway = "canonical-zoo-gateway" - ;(mockClineProvider as any).contextProxy = { - ...mockClineProvider.contextProxy, - getProviderSettings: vi.fn().mockReturnValue({ apiProvider: identifiers.zooGateway }), - getValues: vi.fn().mockReturnValue({ currentApiConfigName: "Zoo Gateway" }), - } - ;(mockClineProvider as any).providerSettingsManager = { - listConfig: vi.fn().mockResolvedValue([ - { name: "Zoo Gateway", apiProvider: identifiers.zooGateway }, - { name: "Backup Zoo", apiProvider: identifiers.zooGateway }, - ]), - getProfile: vi - .fn() - .mockResolvedValueOnce({ - apiProvider: identifiers.zooGateway, - zooSessionToken: "token-active", - zooGatewayModelId: "anthropic/claude-sonnet-4", - }) - .mockResolvedValueOnce({ - apiProvider: identifiers.zooGateway, - zooSessionToken: "token-backup", - }), - saveConfig, - } - ;(mockClineProvider as any).upsertProviderProfile = upsertProviderProfile - - await webviewMessageHandler(mockClineProvider, { type: "zooCodeSignOut" }) - - expect(disconnectZooCode).toHaveBeenCalled() - expect(upsertProviderProfile).toHaveBeenCalledWith( - "Zoo Gateway", - expect.not.objectContaining({ zooSessionToken: expect.anything() }), - true, - ) - expect(saveConfig).toHaveBeenCalledWith( - "Backup Zoo", - expect.not.objectContaining({ zooSessionToken: expect.anything() }), - ) - expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() - } finally { - identifiers.zooGateway = originalIdentifier + ;(mockClineProvider as any).contextProxy = { + ...mockClineProvider.contextProxy, + getProviderSettings: vi.fn().mockReturnValue({ apiProvider: "zoo-gateway" }), + getValues: vi.fn().mockReturnValue({ currentApiConfigName: "Zoo Gateway" }), + } + ;(mockClineProvider as any).providerSettingsManager = { + listConfig: vi.fn().mockResolvedValue([ + { name: "Zoo Gateway", apiProvider: "zoo-gateway" }, + { name: "Backup Zoo", apiProvider: "zoo-gateway" }, + ]), + getProfile: vi + .fn() + .mockResolvedValueOnce({ + apiProvider: "zoo-gateway", + zooSessionToken: "token-active", + zooGatewayModelId: "anthropic/claude-sonnet-4", + }) + .mockResolvedValueOnce({ + apiProvider: "zoo-gateway", + zooSessionToken: "token-backup", + }), + saveConfig, } + ;(mockClineProvider as any).upsertProviderProfile = upsertProviderProfile + + await webviewMessageHandler(mockClineProvider, { type: "zooCodeSignOut" }) + + expect(disconnectZooCode).toHaveBeenCalled() + expect(upsertProviderProfile).toHaveBeenCalledWith( + "Zoo Gateway", + expect.not.objectContaining({ zooSessionToken: expect.anything() }), + true, + ) + expect(saveConfig).toHaveBeenCalledWith( + "Backup Zoo", + expect.not.objectContaining({ zooSessionToken: expect.anything() }), + ) + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() }) it("still clears the in-memory handler when the active profile token is already empty on disk", async () => { From 1038c389883fc2b1b37c6c55110a34eb1694bb23 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Sun, 26 Jul 2026 20:53:26 +0300 Subject: [PATCH 4/6] test(core): address canonical identifier review feedback --- src/core/task/__tests__/Task.spec.ts | 290 +++++++++++------- .../webview/__tests__/ClineProvider.spec.ts | 20 +- src/eslint-suppressions.json | 2 +- 3 files changed, 191 insertions(+), 121 deletions(-) diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index e17e9cccfa..e014e7af9e 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -6,7 +6,13 @@ import * as path from "path" import * as vscode from "vscode" import { Anthropic } from "@anthropic-ai/sdk" -import type { GlobalState, ProviderSettings, ModelInfo } from "@roo-code/types" +import { + providerIdentifiers, + type GlobalState, + type ProviderSettings, + type ModelInfo, + type TaskLike, +} from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { Task } from "../Task" @@ -17,6 +23,27 @@ import { ApiStreamChunk } from "../../../api/transform/stream" import { ContextProxy } from "../../config/ContextProxy" import { processUserContentMentions } from "../../mentions/processUserContentMentions" import { MultiSearchReplaceDiffStrategy } from "../../diff/strategies/multi-search-replace" +import type { ApiMessage } from "../../task-persistence" + +type TaskTestAccess = { + getSystemPrompt: () => Promise + startTask: (task?: string, images?: string[]) => Promise + resumeTaskFromHistory: () => Promise + presentAssistantMessageSafe: () => void + updateClineMessage: (message: import("@roo-code/types").ClineMessage) => Promise + saveClineMessages: () => Promise +} + +function getTaskTestAccess(task: Task): TaskTestAccess { + return task as unknown as TaskTestAccess +} + +function requireDefined(value: T | null | undefined): T { + if (value == null) { + throw new Error("Expected test value to be defined") + } + return value +} // Mock delay before any imports that might use it vi.mock("delay", () => ({ @@ -161,7 +188,7 @@ vi.mock("../../environment/getEnvironmentDetails", () => ({ vi.mock("../../ignore/RooIgnoreController") vi.mock("../../condense", async (importOriginal) => { - const actual = (await importOriginal()) as any + const actual = await importOriginal() return { ...actual, summarizeConversation: vi.fn().mockResolvedValue({ @@ -274,11 +301,11 @@ describe("Cline", () => { mockOutputChannel, "sidebar", new ContextProxy(mockExtensionContext), - ) as any + ) // Setup mock API configuration mockApiConfig = { - apiProvider: "anthropic", + apiProvider: providerIdentifiers.anthropic, apiModelId: "claude-3-5-sonnet-20241022", apiKey: "test-api-key", // Add API key to mock config } @@ -409,7 +436,7 @@ describe("Cline", () => { task: "test task", startTask: false, }) - vi.spyOn(cline as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(cline), "getSystemPrompt").mockResolvedValue("mock system prompt") const mockStream = { async *[Symbol.asyncIterator]() { @@ -437,7 +464,7 @@ describe("Cline", () => { ts: Date.now(), extraProp: "should be removed", }, - ] as any + ] as Array const iterator = cline.attemptApiRequest(0) await iterator.next() @@ -450,7 +477,7 @@ describe("Cline", () => { content: [{ type: "text", text: "test message" }], }, ]) - expect(Object.keys(cleanConversationHistory[0]!)).toEqual(["role", "content"]) + expect(Object.keys(requireDefined(cleanConversationHistory[0]))).toEqual(["role", "content"]) }) it("should shape image blocks for API compatibility before request construction", async () => { @@ -480,7 +507,7 @@ describe("Cline", () => { task: "test task", startTask: false, }) - vi.spyOn(withImages as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(withImages), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(withImages.api, "getModel").mockReturnValue({ id: "claude-3-sonnet", @@ -503,7 +530,7 @@ describe("Cline", () => { task: "test task", startTask: false, }) - vi.spyOn(withoutImages as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(withoutImages), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(withoutImages.api, "getModel").mockReturnValue({ id: "gpt-3.5-turbo", @@ -537,8 +564,8 @@ describe("Cline", () => { const withImagesSpy = vi.spyOn(withImages.api, "createMessage").mockReturnValue(mockStream) const withoutImagesSpy = vi.spyOn(withoutImages.api, "createMessage").mockReturnValue(mockStream) - withImages.apiConversationHistory = conversationHistory as any - withoutImages.apiConversationHistory = conversationHistory as any + withImages.apiConversationHistory = conversationHistory as ApiMessage[] + withoutImages.apiConversationHistory = conversationHistory as ApiMessage[] const withImagesIterator = withImages.attemptApiRequest(0) await withImagesIterator.next() @@ -582,7 +609,7 @@ describe("Cline", () => { task: "test task", startTask: false, }) - vi.spyOn(cline as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(cline), "getSystemPrompt").mockResolvedValue("mock system prompt") // Mock delay to track countdown timing const mockDelay = vi.fn().mockResolvedValue(undefined) @@ -676,7 +703,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: clock, }) - vi.spyOn(cline as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(cline), "getSystemPrompt").mockResolvedValue("mock system prompt") const mockDelay = vi.fn().mockResolvedValue(undefined) vi.spyOn(await import("delay"), "default").mockImplementation(mockDelay) @@ -750,7 +777,7 @@ describe("Cline", () => { task: "test task", startTask: false, }) - vi.spyOn(cline as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(cline), "getSystemPrompt").mockResolvedValue("mock system prompt") // Mock delay to track countdown timing const mockDelay = vi.fn().mockResolvedValue(undefined) @@ -919,7 +946,7 @@ describe("Cline", () => { beforeEach(() => { vi.clearAllMocks() mockApiConfig = { - apiProvider: "anthropic", + apiProvider: providerIdentifiers.anthropic, apiKey: "test-key", rateLimitSeconds: 5, } @@ -966,7 +993,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(parent as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(parent), "getSystemPrompt").mockResolvedValue("mock system prompt") // Mock the API stream response const mockStream = { @@ -1004,7 +1031,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(child as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(child), "getSystemPrompt").mockResolvedValue("mock system prompt") // Spy on child.say to verify the emitted message type const saySpy = vi.spyOn(child, "say") @@ -1059,7 +1086,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(parent as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(parent), "getSystemPrompt").mockResolvedValue("mock system prompt") // Mock the API stream response const mockStream = { @@ -1099,7 +1126,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(child as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(child), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(child.api, "createMessage").mockReturnValue(mockStream) @@ -1125,7 +1152,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(parent as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(parent), "getSystemPrompt").mockResolvedValue("mock system prompt") // Mock the API stream response const mockStream = { @@ -1160,7 +1187,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(child1 as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(child1), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(child1.api, "createMessage").mockReturnValue(mockStream) @@ -1185,7 +1212,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(child2 as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(child2), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(child2.api, "createMessage").mockReturnValue(mockStream) @@ -1215,7 +1242,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(parent as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(parent), "getSystemPrompt").mockResolvedValue("mock system prompt") // Mock the API stream response const mockStream = { @@ -1250,7 +1277,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: sharedClock, }) - vi.spyOn(child as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(child), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(child.api, "createMessage").mockReturnValue(mockStream) @@ -1273,7 +1300,7 @@ describe("Cline", () => { startTask: false, rateLimitClock: clock, }) - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") // Mock the API stream response const mockStream = { @@ -1312,7 +1339,7 @@ describe("Cline", () => { vi.clearAllMocks() mockApiConfig = { - apiProvider: "anthropic", + apiProvider: providerIdentifiers.anthropic, apiKey: "test-key", } @@ -1366,7 +1393,7 @@ describe("Cline", () => { // Test with Anthropic provider const anthropicConfig = { ...mockApiConfig, - apiProvider: "anthropic" as const, + apiProvider: providerIdentifiers.anthropic, apiModelId: "gpt-4", } const anthropicTask = new Task({ @@ -1380,7 +1407,7 @@ describe("Cline", () => { // Test with OpenRouter provider and Claude model const openrouterClaudeConfig = { - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterModelId: "anthropic/claude-3-opus", } const openrouterClaudeTask = new Task({ @@ -1393,7 +1420,7 @@ describe("Cline", () => { // Test with OpenRouter provider and non-Claude model const openrouterGptConfig = { - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterModelId: "openai/gpt-4", } const openrouterGptTask = new Task({ @@ -1415,7 +1442,7 @@ describe("Cline", () => { for (const modelId of claudeModelFormats) { const config = { - apiProvider: "openai" as const, + apiProvider: providerIdentifiers.openai, openAiModelId: modelId, } const task = new Task({ @@ -1444,7 +1471,7 @@ describe("Cline", () => { // Test with no model ID const noModelConfig = { - apiProvider: "openai" as const, + apiProvider: providerIdentifiers.openai, } const noModelTask = new Task({ provider: mockProvider, @@ -1629,7 +1656,7 @@ describe("Cline", () => { }) // Cast to TaskLike to ensure interface compliance - const taskLike = task as any // TaskLike interface from types package + const taskLike: TaskLike = task // Verify abortTask method exists and is callable expect(typeof taskLike.abortTask).toBe("function") @@ -1784,11 +1811,9 @@ describe("Cline", () => { const cancelSpy = vi.spyOn(task, "cancelCurrentRequest") // Mock other dispose operations - vi.spyOn(task.messageQueueService, "removeListener").mockImplementation( - () => task.messageQueueService as any, - ) + vi.spyOn(task.messageQueueService, "removeListener").mockImplementation(() => task.messageQueueService) vi.spyOn(task.messageQueueService, "dispose").mockImplementation(() => {}) - vi.spyOn(task, "removeAllListeners").mockImplementation(() => task as any) + vi.spyOn(task, "removeAllListeners").mockImplementation(() => task) // Call dispose task.dispose() @@ -1806,13 +1831,15 @@ describe("Cline", () => { }) task.currentRequestAbortController = new AbortController() - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") await task.condenseContext() expect(summarizeConversation).toHaveBeenCalled() const [options] = vi.mocked(summarizeConversation).mock.calls.at(-1)! - expect(options.metadata?.abortSignal).toBe(task.currentRequestAbortController!.signal) + expect(options.metadata?.abortSignal).toBe( + requireDefined(task.currentRequestAbortController).signal, + ) }) it("should omit abortSignal from condenseContext metadata when no current request exists", async () => { @@ -1823,7 +1850,7 @@ describe("Cline", () => { startTask: false, }) - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") await task.condenseContext() @@ -1842,10 +1869,10 @@ describe("Cline", () => { }) // Mock required methods for attemptApiRequest to work without hanging - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ - id: mockApiConfig.apiModelId!, + id: requireDefined(mockApiConfig.apiModelId), info: { supportsImages: false, supportsPromptCache: true, @@ -1889,7 +1916,7 @@ describe("Cline", () => { content: [{ type: "text" as const, text: "test message" }], ts: Date.now(), }, - ] as any + ] const iterator = task.attemptApiRequest(0) await iterator.next() @@ -1899,13 +1926,15 @@ describe("Cline", () => { const [, , metadata] = createMessageSpy.mock.calls[0]! expect(metadata).toBeDefined() - expect(metadata!.abortSignal).toBe(task.currentRequestAbortController!.signal) + expect(requireDefined(metadata).abortSignal).toBe( + requireDefined(task.currentRequestAbortController).signal, + ) }) it("configures tool restrictions for Gemini requests", async () => { const apiConfiguration = { ...mockApiConfig, - apiProvider: "gemini", + apiProvider: providerIdentifiers.gemini, } as ProviderSettings const task = new Task({ provider: mockProvider, @@ -1914,9 +1943,9 @@ describe("Cline", () => { startTask: false, }) - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ - id: mockApiConfig.apiModelId!, + id: requireDefined(mockApiConfig.apiModelId), info: { contextWindow: 200000, maxTokens: 4096 } as ModelInfo, }) const providerState = await mockProvider.getState() @@ -1932,17 +1961,23 @@ describe("Cline", () => { const createMessageSpy = vi.spyOn(task.api, "createMessage").mockReturnValue(mockStream) task.apiConversationHistory = [ { role: "user", content: [{ type: "text", text: "test message" }], ts: Date.now() }, - ] as any + ] await task.attemptApiRequest(0).next() - const [, , metadata] = createMessageSpy.mock.calls[0]! - expect(metadata).toEqual( - expect.objectContaining({ - tools: expect.any(Array), - allowedFunctionNames: expect.any(Array), - }), - ) + const [, , metadata] = requireDefined(createMessageSpy.mock.calls[0]) + const tools = requireDefined(metadata?.tools) + const allowedFunctionNames = requireDefined(metadata?.allowedFunctionNames) + const toolNames = tools.map((tool) => { + if (tool.type !== "function") { + throw new Error(`Unexpected tool type: ${tool.type}`) + } + return tool.function.name + }) + + expect(tools.length).toBeGreaterThan(0) + expect(allowedFunctionNames.length).toBeGreaterThan(0) + expect(allowedFunctionNames.every((name) => toolNames.includes(name))).toBe(true) }) it("should invoke abort on currentRequestAbortController during first-chunk wait", async () => { @@ -1973,9 +2008,9 @@ describe("Cline", () => { startTask: false, }) - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ - id: mockApiConfig.apiModelId!, + id: requireDefined(mockApiConfig.apiModelId), info: { supportsImages: false, supportsPromptCache: true, @@ -2034,7 +2069,7 @@ describe("Cline", () => { content: [{ type: "text" as const, text: "test message" }], ts: Date.now(), }, - ] as any + ] const streamIterator = task.attemptApiRequest(0) await expect(streamIterator.next()).resolves.toMatchObject({ @@ -2057,10 +2092,10 @@ describe("Cline", () => { }) // Mock required methods for attemptApiRequest to work without hanging - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ - id: mockApiConfig.apiModelId!, + id: requireDefined(mockApiConfig.apiModelId), info: { supportsImages: false, supportsPromptCache: true, @@ -2104,17 +2139,17 @@ describe("Cline", () => { content: [{ type: "text" as const, text: "test message" }], ts: Date.now(), }, - ] as any + ] const iterator = task.attemptApiRequest(0) await iterator.next() // Get the signal from metadata const [, , metadata] = createMessageSpy.mock.calls[0]! - const metadataSignal = metadata!.abortSignal + const metadataSignal = requireDefined(metadata).abortSignal // The signal in metadata should be the same as the one from currentRequestAbortController - expect(metadataSignal).toBe(task.currentRequestAbortController!.signal) + expect(metadataSignal).toBe(requireDefined(task.currentRequestAbortController).signal) }) it("should omit createMessage abortSignal metadata when no current request exists before condense metadata checks", async () => { @@ -2125,9 +2160,9 @@ describe("Cline", () => { startTask: false, }) - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ - id: mockApiConfig.apiModelId!, + id: requireDefined(mockApiConfig.apiModelId), info: { supportsImages: false, supportsPromptCache: true, @@ -2169,7 +2204,7 @@ describe("Cline", () => { content: [{ type: "text" as const, text: "test message" }], ts: Date.now(), }, - ] as any + ] expect(task.currentRequestAbortController).toBeUndefined() @@ -2178,8 +2213,10 @@ describe("Cline", () => { const [, , metadata] = createMessageSpy.mock.calls[0]! expect(metadata).toBeDefined() - expect("abortSignal" in metadata!).toBe(true) - expect(metadata!.abortSignal).toBe(task.currentRequestAbortController!.signal) + expect("abortSignal" in requireDefined(metadata)).toBe(true) + expect(requireDefined(metadata).abortSignal).toBe( + requireDefined(task.currentRequestAbortController).signal, + ) }) it("should keep createMessage abortSignal metadata unaborted before cancellation", async () => { @@ -2190,9 +2227,9 @@ describe("Cline", () => { startTask: false, }) - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ - id: mockApiConfig.apiModelId!, + id: requireDefined(mockApiConfig.apiModelId), info: { supportsImages: false, supportsPromptCache: true, @@ -2234,13 +2271,13 @@ describe("Cline", () => { content: [{ type: "text" as const, text: "test message" }], ts: Date.now(), }, - ] as any + ] const iterator = task.attemptApiRequest(0) await iterator.next() const [, , metadata] = createMessageSpy.mock.calls[0]! - expect(metadata?.abortSignal).toBe(task.currentRequestAbortController!.signal) + expect(metadata?.abortSignal).toBe(requireDefined(task.currentRequestAbortController).signal) expect(metadata?.abortSignal?.aborted).toBe(false) }) }) @@ -2253,9 +2290,9 @@ describe("Cline", () => { startTask: false, }) - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ - id: mockApiConfig.apiModelId!, + id: requireDefined(mockApiConfig.apiModelId), info: { supportsImages: false, supportsPromptCache: true, @@ -2289,7 +2326,7 @@ describe("Cline", () => { content: [{ type: "text" as const, text: "test message" }], ts: Date.now(), }, - ] as any + ] // First request const iterator1 = task.attemptApiRequest(0) @@ -2297,9 +2334,9 @@ describe("Cline", () => { expect(createMessageSpy).toHaveBeenCalledTimes(1) const [, , metadata1] = createMessageSpy.mock.calls[0]! - const signal1 = metadata1!.abortSignal + const signal1 = requireDefined(metadata1).abortSignal expect(signal1).toBeDefined() - expect(signal1!.aborted).toBe(false) + expect(requireDefined(signal1).aborted).toBe(false) // Simulate request completion and cancellation to clear the controller task.cancelCurrentRequest() @@ -2311,12 +2348,12 @@ describe("Cline", () => { expect(createMessageSpy).toHaveBeenCalledTimes(2) const [, , metadata2] = createMessageSpy.mock.calls[1]! - const signal2 = metadata2!.abortSignal + const signal2 = requireDefined(metadata2).abortSignal // Signals should be different instances (fresh controller per request) expect(signal2).not.toBe(signal1) - expect(signal2).toBe(task.currentRequestAbortController!.signal) - expect(signal2!.aborted).toBe(false) + expect(signal2).toBe(requireDefined(task.currentRequestAbortController).signal) + expect(requireDefined(signal2).aborted).toBe(false) }) it("should propagate AbortController signal through attemptApiRequest context-window retry path", async () => { @@ -2327,7 +2364,7 @@ describe("Cline", () => { startTask: false, }) - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("mock system prompt") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task, "getTokenUsage").mockReturnValue({ totalCost: 0, totalTokensIn: 0, @@ -2335,7 +2372,7 @@ describe("Cline", () => { contextTokens: 120000, }) vi.spyOn(task.api, "getModel").mockReturnValue({ - id: mockApiConfig.apiModelId!, + id: requireDefined(mockApiConfig.apiModelId), info: { supportsImages: false, supportsPromptCache: true, @@ -2368,7 +2405,7 @@ describe("Cline", () => { content: [{ type: "text" as const, text: "test message" }], ts: Date.now(), }, - ] as any + ] let firstCall = true const retryStream = { @@ -2436,7 +2473,7 @@ describe("Cline", () => { }) // Manually trigger start - const startTaskSpy = vi.spyOn(task as any, "startTask").mockImplementation(async () => {}) + const startTaskSpy = vi.spyOn(getTaskTestAccess(task), "startTask").mockImplementation(async () => {}) task.start() expect(startTaskSpy).toHaveBeenCalledTimes(1) @@ -2449,7 +2486,9 @@ describe("Cline", () => { it("should not call startTask if already started via constructor", () => { // Create a task that starts immediately (startTask defaults to true) // but mock startTask to prevent actual execution - const startTaskSpy = vi.spyOn(Task.prototype as any, "startTask").mockImplementation(async () => {}) + const startTaskSpy = vi + .spyOn(getTaskTestAccess(Task.prototype), "startTask") + .mockImplementation(async () => {}) const task = new Task({ provider: mockProvider, @@ -2490,9 +2529,11 @@ describe("Cline", () => { it("logs (instead of crashing) when startTask rejects from the constructor", async () => { const boom = new Error("startTask boom") - const startTaskSpy = vi.spyOn(Task.prototype as any, "startTask").mockImplementation(async () => { - throw boom - }) + const startTaskSpy = vi + .spyOn(getTaskTestAccess(Task.prototype), "startTask") + .mockImplementation(async () => { + throw boom + }) new Task({ provider: mockProvider, @@ -2510,9 +2551,11 @@ describe("Cline", () => { it("logs (instead of crashing) when resumeTaskFromHistory rejects from the constructor", async () => { const boom = new Error("resume boom") - const resumeSpy = vi.spyOn(Task.prototype as any, "resumeTaskFromHistory").mockImplementation(async () => { - throw boom - }) + const resumeSpy = vi + .spyOn(getTaskTestAccess(Task.prototype), "resumeTaskFromHistory") + .mockImplementation(async () => { + throw boom + }) new Task({ provider: mockProvider, @@ -2569,7 +2612,7 @@ describe("Cline", () => { startTask: false, }) - vi.spyOn(task as any, "startTask").mockImplementation(async () => { + vi.spyOn(getTaskTestAccess(task), "startTask").mockImplementation(async () => { throw boom }) @@ -2599,7 +2642,7 @@ describe("Cline", () => { consoleErrorSpy.mockClear() task.abort = true - ;(task as any).presentAssistantMessageSafe() + getTaskTestAccess(task).presentAssistantMessageSafe() await flushMicrotasks() expect(presentSpy).toHaveBeenCalledTimes(1) @@ -2622,7 +2665,7 @@ describe("Cline", () => { }) expect(task.abort).toBeFalsy() - ;(task as any).presentAssistantMessageSafe() + getTaskTestAccess(task).presentAssistantMessageSafe() await flushMicrotasks() expect(presentSpy).toHaveBeenCalledTimes(1) @@ -2653,7 +2696,7 @@ describe("Cline", () => { // Simulate the TOCTOU race: abort flips between throw and catch. task.abort = true - ;(task as any).presentAssistantMessageSafe() + getTaskTestAccess(task).presentAssistantMessageSafe() await flushMicrotasks() expect(presentSpy).toHaveBeenCalledTimes(1) @@ -2683,7 +2726,7 @@ describe("Cline", () => { consoleErrorSpy.mockClear() expect(task.abort).toBeFalsy() - ;(task as any).presentAssistantMessageSafe() + getTaskTestAccess(task).presentAssistantMessageSafe() await flushMicrotasks() expect(presentSpy).toHaveBeenCalledTimes(1) @@ -2700,9 +2743,11 @@ describe("Cline", () => { // consumer-attached listener — that path must surface as a log, // not an unhandled rejection. const boom = new Error("updateClineMessage boom") - const updateSpy = vi.spyOn(Task.prototype as any, "updateClineMessage").mockImplementation(async () => { - throw boom - }) + const updateSpy = vi + .spyOn(getTaskTestAccess(Task.prototype), "updateClineMessage") + .mockImplementation(async () => { + throw boom + }) const task = new Task({ provider: mockProvider, @@ -2732,10 +2777,12 @@ describe("Cline", () => { // Pins the symmetric .catch arm on the fire-and-forget // updateClineMessage call in ask() when finalizing a partial. const boom = new Error("updateClineMessage boom") - const updateSpy = vi.spyOn(Task.prototype as any, "updateClineMessage").mockImplementation(async () => { - throw boom - }) - const saveSpy = vi.spyOn(Task.prototype as any, "saveClineMessages").mockResolvedValue(true) + const updateSpy = vi + .spyOn(getTaskTestAccess(Task.prototype), "updateClineMessage") + .mockImplementation(async () => { + throw boom + }) + const saveSpy = vi.spyOn(getTaskTestAccess(Task.prototype), "saveClineMessages").mockResolvedValue(true) const task = new Task({ provider: mockProvider, @@ -2771,9 +2818,11 @@ describe("Cline", () => { // in ask() when a new partial ask arrives while the previous partial // is still pending (AskIgnoredError path). const boom = new Error("updateClineMessage boom") - const updateSpy = vi.spyOn(Task.prototype as any, "updateClineMessage").mockImplementation(async () => { - throw boom - }) + const updateSpy = vi + .spyOn(getTaskTestAccess(Task.prototype), "updateClineMessage") + .mockImplementation(async () => { + throw boom + }) const task = new Task({ provider: mockProvider, @@ -2804,10 +2853,12 @@ describe("Cline", () => { // Pins the .catch arm on the fire-and-forget updateClineMessage call // in handleWebviewAskResponse when marking a tool ask as answered. const boom = new Error("updateClineMessage boom") - const updateSpy = vi.spyOn(Task.prototype as any, "updateClineMessage").mockImplementation(async () => { - throw boom - }) - vi.spyOn(Task.prototype as any, "saveClineMessages").mockResolvedValue(undefined) + const updateSpy = vi + .spyOn(getTaskTestAccess(Task.prototype), "updateClineMessage") + .mockImplementation(async () => { + throw boom + }) + vi.spyOn(getTaskTestAccess(Task.prototype), "saveClineMessages").mockResolvedValue(false) const task = new Task({ provider: mockProvider, @@ -2870,7 +2921,12 @@ describe("Queued message processing after condense", () => { dispose: vi.fn(), } - const provider = new ClineProvider(ctx, output as any, "sidebar", new ContextProxy(ctx)) as any + const provider = new ClineProvider( + ctx, + output as unknown as vscode.OutputChannel, + "sidebar", + new ContextProxy(ctx), + ) provider.postMessageToWebview = vi.fn().mockResolvedValue(undefined) provider.postStateToWebview = vi.fn().mockResolvedValue(undefined) provider.postStateToWebviewWithoutTaskHistory = vi.fn().mockResolvedValue(undefined) @@ -2879,10 +2935,10 @@ describe("Queued message processing after condense", () => { } const apiConfig: ProviderSettings = { - apiProvider: "anthropic", + apiProvider: providerIdentifiers.anthropic, apiModelId: "claude-3-5-sonnet-20241022", apiKey: "test-api-key", - } as any + } it("processes queued message after condense completes", async () => { const provider = createProvider() @@ -2894,7 +2950,7 @@ describe("Queued message processing after condense", () => { }) // Make condense fast + deterministic - vi.spyOn(task as any, "getSystemPrompt").mockResolvedValue("system") + vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("system") const submitSpy = vi.spyOn(task, "submitUserMessage").mockResolvedValue(undefined) // Queue a message during condensing @@ -2929,8 +2985,8 @@ describe("Queued message processing after condense", () => { startTask: false, }) - vi.spyOn(taskA as any, "getSystemPrompt").mockResolvedValue("system") - vi.spyOn(taskB as any, "getSystemPrompt").mockResolvedValue("system") + vi.spyOn(getTaskTestAccess(taskA), "getSystemPrompt").mockResolvedValue("system") + vi.spyOn(getTaskTestAccess(taskB), "getSystemPrompt").mockResolvedValue("system") const spyA = vi.spyOn(taskA, "submitUserMessage").mockResolvedValue(undefined) const spyB = vi.spyOn(taskB, "submitUserMessage").mockResolvedValue(undefined) @@ -2965,7 +3021,7 @@ describe("pushToolResultToUserContent", () => { beforeEach(() => { mockApiConfig = { - apiProvider: "anthropic", + apiProvider: providerIdentifiers.anthropic, apiModelId: "claude-3-5-sonnet-20241022", apiKey: "test-api-key", } @@ -3008,7 +3064,7 @@ describe("pushToolResultToUserContent", () => { mockOutputChannel, "sidebar", new ContextProxy(mockExtensionContext), - ) as any + ) mockProvider.postMessageToWebview = vi.fn().mockResolvedValue(undefined) mockProvider.postStateToWebview = vi.fn().mockResolvedValue(undefined) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 32cd798dff..93f4f2afaa 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -30,7 +30,7 @@ import { safeWriteJson } from "../../../utils/safeWriteJson" import { ClineProvider } from "../ClineProvider" import { Terminal } from "../../../integrations/terminal/Terminal" import { MessageManager } from "../../message-manager" -import { forceFullModelDetailsLoad } from "../../../api/providers/fetchers/lmstudio" +import { forceFullModelDetailsLoad, hasLoadedFullDetails } from "../../../api/providers/fetchers/lmstudio" // Mock setup must come before imports. vi.mock("../../prompts/sections/custom-instructions") @@ -531,7 +531,7 @@ describe("ClineProvider", () => { test("loads full model details when preparing an LM Studio task", async () => { await provider.performPreparationTasks({ apiConfiguration: { - apiProvider: "lmstudio", + apiProvider: providerIdentifiers.lmstudio, lmStudioBaseUrl: "http://localhost:1234", lmStudioModelId: "test-model", }, @@ -540,6 +540,20 @@ describe("ClineProvider", () => { expect(forceFullModelDetailsLoad).toHaveBeenCalledWith("http://localhost:1234", "test-model") }) + test("does not reload full model details when the LM Studio model is already loaded", async () => { + vi.mocked(hasLoadedFullDetails).mockReturnValue(true) + + await provider.performPreparationTasks({ + apiConfiguration: { + apiProvider: providerIdentifiers.lmstudio, + lmStudioBaseUrl: "http://localhost:1234", + lmStudioModelId: "test-model", + }, + } as Task) + + expect(forceFullModelDetailsLoad).not.toHaveBeenCalled() + }) + test("resolveWebviewView hydrates the saved terminalProfile into the process-wide Terminal state", async () => { const setTerminalProfileSpy = vi.spyOn(Terminal, "setTerminalProfile").mockImplementation(() => {}) // Seed the persisted setting so the real getState() returns it during hydration. @@ -1477,7 +1491,7 @@ describe("ClineProvider", () => { test("handles case when no current task exists", async () => { // Clear the cline stack - ;(provider as any).taskRegistry = new TaskRegistry() + Object.assign(provider, { taskRegistry: new TaskRegistry() }) // Trigger message deletion const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index a0f00c04e3..2c632fc30c 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -881,7 +881,7 @@ }, "core/task/__tests__/Task.spec.ts": { "@typescript-eslint/no-explicit-any": { - "count": 93 + "count": 31 } }, "core/task/__tests__/Task.sticky-profile-race.spec.ts": { From d2f09314d0f61b27d5c49e9a833e46228b403d12 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Sun, 26 Jul 2026 20:59:27 +0300 Subject: [PATCH 5/6] chore(lint): update test suppression counts --- src/eslint-suppressions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index 2c632fc30c..2c3ff403cd 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -1111,7 +1111,7 @@ }, "core/webview/__tests__/ClineProvider.spec.ts": { "@typescript-eslint/no-explicit-any": { - "count": 199 + "count": 198 } }, "core/webview/__tests__/ClineProvider.sticky-mode.spec.ts": { From 14a89749ad4f23b68799e91ee685bc8c2b9a7bc5 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Sun, 26 Jul 2026 21:22:01 +0300 Subject: [PATCH 6/6] test(core): keep existing non-null assertions --- src/core/task/__tests__/Task.spec.ts | 46 ++++++++++++---------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index e014e7af9e..0b4201d135 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -477,7 +477,7 @@ describe("Cline", () => { content: [{ type: "text", text: "test message" }], }, ]) - expect(Object.keys(requireDefined(cleanConversationHistory[0]))).toEqual(["role", "content"]) + expect(Object.keys(cleanConversationHistory[0]!)).toEqual(["role", "content"]) }) it("should shape image blocks for API compatibility before request construction", async () => { @@ -1837,9 +1837,7 @@ describe("Cline", () => { expect(summarizeConversation).toHaveBeenCalled() const [options] = vi.mocked(summarizeConversation).mock.calls.at(-1)! - expect(options.metadata?.abortSignal).toBe( - requireDefined(task.currentRequestAbortController).signal, - ) + expect(options.metadata?.abortSignal).toBe(task.currentRequestAbortController!.signal) }) it("should omit abortSignal from condenseContext metadata when no current request exists", async () => { @@ -1872,7 +1870,7 @@ describe("Cline", () => { vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ - id: requireDefined(mockApiConfig.apiModelId), + id: mockApiConfig.apiModelId!, info: { supportsImages: false, supportsPromptCache: true, @@ -1926,9 +1924,7 @@ describe("Cline", () => { const [, , metadata] = createMessageSpy.mock.calls[0]! expect(metadata).toBeDefined() - expect(requireDefined(metadata).abortSignal).toBe( - requireDefined(task.currentRequestAbortController).signal, - ) + expect(metadata!.abortSignal).toBe(task.currentRequestAbortController!.signal) }) it("configures tool restrictions for Gemini requests", async () => { @@ -2010,7 +2006,7 @@ describe("Cline", () => { vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ - id: requireDefined(mockApiConfig.apiModelId), + id: mockApiConfig.apiModelId!, info: { supportsImages: false, supportsPromptCache: true, @@ -2095,7 +2091,7 @@ describe("Cline", () => { vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ - id: requireDefined(mockApiConfig.apiModelId), + id: mockApiConfig.apiModelId!, info: { supportsImages: false, supportsPromptCache: true, @@ -2146,10 +2142,10 @@ describe("Cline", () => { // Get the signal from metadata const [, , metadata] = createMessageSpy.mock.calls[0]! - const metadataSignal = requireDefined(metadata).abortSignal + const metadataSignal = metadata!.abortSignal // The signal in metadata should be the same as the one from currentRequestAbortController - expect(metadataSignal).toBe(requireDefined(task.currentRequestAbortController).signal) + expect(metadataSignal).toBe(task.currentRequestAbortController!.signal) }) it("should omit createMessage abortSignal metadata when no current request exists before condense metadata checks", async () => { @@ -2162,7 +2158,7 @@ describe("Cline", () => { vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ - id: requireDefined(mockApiConfig.apiModelId), + id: mockApiConfig.apiModelId!, info: { supportsImages: false, supportsPromptCache: true, @@ -2213,10 +2209,8 @@ describe("Cline", () => { const [, , metadata] = createMessageSpy.mock.calls[0]! expect(metadata).toBeDefined() - expect("abortSignal" in requireDefined(metadata)).toBe(true) - expect(requireDefined(metadata).abortSignal).toBe( - requireDefined(task.currentRequestAbortController).signal, - ) + expect("abortSignal" in metadata!).toBe(true) + expect(metadata!.abortSignal).toBe(task.currentRequestAbortController!.signal) }) it("should keep createMessage abortSignal metadata unaborted before cancellation", async () => { @@ -2229,7 +2223,7 @@ describe("Cline", () => { vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ - id: requireDefined(mockApiConfig.apiModelId), + id: mockApiConfig.apiModelId!, info: { supportsImages: false, supportsPromptCache: true, @@ -2277,7 +2271,7 @@ describe("Cline", () => { await iterator.next() const [, , metadata] = createMessageSpy.mock.calls[0]! - expect(metadata?.abortSignal).toBe(requireDefined(task.currentRequestAbortController).signal) + expect(metadata?.abortSignal).toBe(task.currentRequestAbortController!.signal) expect(metadata?.abortSignal?.aborted).toBe(false) }) }) @@ -2292,7 +2286,7 @@ describe("Cline", () => { vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt") vi.spyOn(task.api, "getModel").mockReturnValue({ - id: requireDefined(mockApiConfig.apiModelId), + id: mockApiConfig.apiModelId!, info: { supportsImages: false, supportsPromptCache: true, @@ -2334,9 +2328,9 @@ describe("Cline", () => { expect(createMessageSpy).toHaveBeenCalledTimes(1) const [, , metadata1] = createMessageSpy.mock.calls[0]! - const signal1 = requireDefined(metadata1).abortSignal + const signal1 = metadata1!.abortSignal expect(signal1).toBeDefined() - expect(requireDefined(signal1).aborted).toBe(false) + expect(signal1!.aborted).toBe(false) // Simulate request completion and cancellation to clear the controller task.cancelCurrentRequest() @@ -2348,12 +2342,12 @@ describe("Cline", () => { expect(createMessageSpy).toHaveBeenCalledTimes(2) const [, , metadata2] = createMessageSpy.mock.calls[1]! - const signal2 = requireDefined(metadata2).abortSignal + const signal2 = metadata2!.abortSignal // Signals should be different instances (fresh controller per request) expect(signal2).not.toBe(signal1) - expect(signal2).toBe(requireDefined(task.currentRequestAbortController).signal) - expect(requireDefined(signal2).aborted).toBe(false) + expect(signal2).toBe(task.currentRequestAbortController!.signal) + expect(signal2!.aborted).toBe(false) }) it("should propagate AbortController signal through attemptApiRequest context-window retry path", async () => { @@ -2372,7 +2366,7 @@ describe("Cline", () => { contextTokens: 120000, }) vi.spyOn(task.api, "getModel").mockReturnValue({ - id: requireDefined(mockApiConfig.apiModelId), + id: mockApiConfig.apiModelId!, info: { supportsImages: false, supportsPromptCache: true,