From 6af9fad180a702e32729c179b92c104e3a833d59 Mon Sep 17 00:00:00 2001 From: Chris Estreich Date: Thu, 8 May 2025 12:47:50 -0700 Subject: [PATCH] Rename `Cline` to `Task` (#3352) --- .../presentAssistantMessage.ts | 4 +- src/core/checkpoints/index.ts | 15 +- .../__tests__/getEnvironmentDetails.test.ts | 30 +-- src/core/environment/getEnvironmentDetails.ts | 4 +- src/core/{Cline.ts => task/Task.ts} | 96 ++++----- .../__tests__/Task.test.ts} | 169 +++++++-------- .../__tests__/executeCommandTool.test.ts | 16 +- src/core/tools/accessMcpResourceTool.ts | 4 +- src/core/tools/applyDiffTool.ts | 4 +- src/core/tools/askFollowupQuestionTool.ts | 4 +- src/core/tools/attemptCompletionTool.ts | 4 +- src/core/tools/browserActionTool.ts | 4 +- src/core/tools/executeCommandTool.ts | 6 +- src/core/tools/fetchInstructionsTool.ts | 4 +- src/core/tools/insertContentTool.ts | 4 +- src/core/tools/listCodeDefinitionNamesTool.ts | 4 +- src/core/tools/listFilesTool.ts | 4 +- src/core/tools/newTaskTool.ts | 4 +- src/core/tools/readFileTool.ts | 4 +- src/core/tools/searchAndReplaceTool.ts | 6 +- src/core/tools/searchFilesTool.ts | 4 +- src/core/tools/switchModeTool.ts | 4 +- src/core/tools/useMcpToolTool.ts | 4 +- src/core/tools/writeToFileTool.ts | 4 +- src/core/webview/ClineProvider.ts | 22 +- .../webview/__tests__/ClineProvider.test.ts | 198 ++++++++++-------- 26 files changed, 305 insertions(+), 321 deletions(-) rename src/core/{Cline.ts => task/Task.ts} (95%) rename src/core/{__tests__/Cline.test.ts => task/__tests__/Task.test.ts} (93%) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 7fc53c4126..ef2bb04963 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -31,7 +31,7 @@ import { checkpointSave } from "../checkpoints" import { formatResponse } from "../prompts/responses" import { validateToolUse } from "../tools/validateToolUse" -import { Cline } from "../Cline" +import { Task } from "../task/Task" /** * Processes and presents assistant message content to the user interface. @@ -50,7 +50,7 @@ import { Cline } from "../Cline" * as it becomes available. */ -export async function presentAssistantMessage(cline: Cline) { +export async function presentAssistantMessage(cline: Task) { if (cline.abort) { throw new Error(`[Cline#presentAssistantMessage] task ${cline.taskId}.${cline.instanceId} aborted`) } diff --git a/src/core/checkpoints/index.ts b/src/core/checkpoints/index.ts index dab71594ad..8c5dd5203f 100644 --- a/src/core/checkpoints/index.ts +++ b/src/core/checkpoints/index.ts @@ -1,7 +1,7 @@ import pWaitFor from "p-wait-for" import * as vscode from "vscode" -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { getWorkspacePath } from "../../utils/path" @@ -13,7 +13,7 @@ import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider import { telemetryService } from "../../services/telemetry/TelemetryService" import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../../services/checkpoints" -export function getCheckpointService(cline: Cline) { +export function getCheckpointService(cline: Task) { if (!cline.enableCheckpoints) { return undefined } @@ -124,7 +124,7 @@ export function getCheckpointService(cline: Cline) { } async function getInitializedCheckpointService( - cline: Cline, + cline: Task, { interval = 250, timeout = 15_000 }: { interval?: number; timeout?: number } = {}, ) { const service = getCheckpointService(cline) @@ -148,7 +148,7 @@ async function getInitializedCheckpointService( } } -export async function checkpointSave(cline: Cline) { +export async function checkpointSave(cline: Task) { const service = getCheckpointService(cline) if (!service) { @@ -177,7 +177,7 @@ export type CheckpointRestoreOptions = { mode: "preview" | "restore" } -export async function checkpointRestore(cline: Cline, { ts, commitHash, mode }: CheckpointRestoreOptions) { +export async function checkpointRestore(cline: Task, { ts, commitHash, mode }: CheckpointRestoreOptions) { const service = await getInitializedCheckpointService(cline) if (!service) { @@ -245,10 +245,7 @@ export type CheckpointDiffOptions = { mode: "full" | "checkpoint" } -export async function checkpointDiff( - cline: Cline, - { ts, previousCommitHash, commitHash, mode }: CheckpointDiffOptions, -) { +export async function checkpointDiff(cline: Task, { ts, previousCommitHash, commitHash, mode }: CheckpointDiffOptions) { const service = await getInitializedCheckpointService(cline) if (!service) { diff --git a/src/core/environment/__tests__/getEnvironmentDetails.test.ts b/src/core/environment/__tests__/getEnvironmentDetails.test.ts index 730055c3c1..c375375b73 100644 --- a/src/core/environment/__tests__/getEnvironmentDetails.test.ts +++ b/src/core/environment/__tests__/getEnvironmentDetails.test.ts @@ -16,7 +16,7 @@ import { ApiHandler } from "../../../api/index" import { ClineProvider } from "../../webview/ClineProvider" import { RooIgnoreController } from "../../ignore/RooIgnoreController" import { formatResponse } from "../../prompts/responses" -import { Cline } from "../../Cline" +import { Task } from "../../task/Task" jest.mock("vscode", () => ({ window: { @@ -56,7 +56,7 @@ describe("getEnvironmentDetails", () => { cleanCompletedProcessQueue?: jest.Mock } - let mockCline: Partial + let mockCline: Partial let mockProvider: any let mockState: any @@ -134,7 +134,7 @@ describe("getEnvironmentDetails", () => { }) it("should return basic environment details", async () => { - const result = await getEnvironmentDetails(mockCline as Cline) + const result = await getEnvironmentDetails(mockCline as Task) expect(result).toContain("") expect(result).toContain("") @@ -157,7 +157,7 @@ describe("getEnvironmentDetails", () => { }) it("should include file details when includeFileDetails is true", async () => { - const result = await getEnvironmentDetails(mockCline as Cline, true) + const result = await getEnvironmentDetails(mockCline as Task, true) expect(result).toContain("# Current Workspace Directory") expect(result).toContain("Files") @@ -173,14 +173,14 @@ describe("getEnvironmentDetails", () => { }) it("should not include file details when includeFileDetails is false", async () => { - await getEnvironmentDetails(mockCline as Cline, false) + await getEnvironmentDetails(mockCline as Task, false) expect(listFiles).not.toHaveBeenCalled() expect(formatResponse.formatFilesList).not.toHaveBeenCalled() }) it("should handle desktop directory specially", async () => { ;(arePathsEqual as jest.Mock).mockReturnValue(true) - const result = await getEnvironmentDetails(mockCline as Cline, true) + const result = await getEnvironmentDetails(mockCline as Task, true) expect(result).toContain("Desktop files not shown automatically") expect(listFiles).not.toHaveBeenCalled() }) @@ -191,7 +191,7 @@ describe("getEnvironmentDetails", () => { "modified2.ts", ]) - const result = await getEnvironmentDetails(mockCline as Cline) + const result = await getEnvironmentDetails(mockCline as Task) expect(result).toContain("# Recently Modified Files") expect(result).toContain("modified1.ts") @@ -208,14 +208,14 @@ describe("getEnvironmentDetails", () => { ;(TerminalRegistry.getTerminals as jest.Mock).mockReturnValue([mockActiveTerminal]) ;(TerminalRegistry.getUnretrievedOutput as jest.Mock).mockReturnValue("Test output") - const result = await getEnvironmentDetails(mockCline as Cline) + const result = await getEnvironmentDetails(mockCline as Task) expect(result).toContain("# Actively Running Terminals") expect(result).toContain("Original command: `npm test`") expect(result).toContain("Test output") mockCline.didEditFile = true - await getEnvironmentDetails(mockCline as Cline) + await getEnvironmentDetails(mockCline as Task) expect(delay).toHaveBeenCalledWith(300) expect(pWaitFor).toHaveBeenCalled() @@ -237,7 +237,7 @@ describe("getEnvironmentDetails", () => { active ? [] : [mockInactiveTerminal], ) - const result = await getEnvironmentDetails(mockCline as Cline) + const result = await getEnvironmentDetails(mockCline as Task) expect(result).toContain("# Inactive Terminals with Completed Process Output") expect(result).toContain("Terminal terminal-2") @@ -261,7 +261,7 @@ describe("getEnvironmentDetails", () => { return null }) - const result = await getEnvironmentDetails(mockCline as Cline) + const result = await getEnvironmentDetails(mockCline as Task) expect(result).toContain("NOTE: You are currently in '💻 Code' mode, which does not allow write operations") }) @@ -270,7 +270,7 @@ describe("getEnvironmentDetails", () => { mockState.experiments = { [EXPERIMENT_IDS.POWER_STEERING]: true } ;(experiments.isEnabled as jest.Mock).mockReturnValue(true) - const result = await getEnvironmentDetails(mockCline as Cline) + const result = await getEnvironmentDetails(mockCline as Task) expect(result).toContain("You are a code assistant") expect(result).toContain("Custom instructions") @@ -280,7 +280,7 @@ describe("getEnvironmentDetails", () => { // Mock provider to return null. mockCline.providerRef!.deref = jest.fn().mockReturnValue(null) - const result = await getEnvironmentDetails(mockCline as Cline) + const result = await getEnvironmentDetails(mockCline as Task) // Verify the function still returns a result. expect(result).toContain("") @@ -291,7 +291,7 @@ describe("getEnvironmentDetails", () => { getState: jest.fn().mockResolvedValue(null), }) - const result2 = await getEnvironmentDetails(mockCline as Cline) + const result2 = await getEnvironmentDetails(mockCline as Task) // Verify the function still returns a result. expect(result2).toContain("") @@ -311,6 +311,6 @@ describe("getEnvironmentDetails", () => { ;(TerminalRegistry.getBackgroundTerminals as jest.Mock).mockReturnValue([]) ;(mockCline.fileContextTracker!.getAndClearRecentlyModifiedFiles as jest.Mock).mockReturnValue([]) - await expect(getEnvironmentDetails(mockCline as Cline)).resolves.not.toThrow() + await expect(getEnvironmentDetails(mockCline as Task)).resolves.not.toThrow() }) }) diff --git a/src/core/environment/getEnvironmentDetails.ts b/src/core/environment/getEnvironmentDetails.ts index bb662cf9e3..dc43f2e347 100644 --- a/src/core/environment/getEnvironmentDetails.ts +++ b/src/core/environment/getEnvironmentDetails.ts @@ -15,9 +15,9 @@ import { Terminal } from "../../integrations/terminal/Terminal" import { arePathsEqual } from "../../utils/path" import { formatResponse } from "../prompts/responses" -import { Cline } from "../Cline" +import { Task } from "../task/Task" -export async function getEnvironmentDetails(cline: Cline, includeFileDetails: boolean = false) { +export async function getEnvironmentDetails(cline: Task, includeFileDetails: boolean = false) { let details = "" const clineProvider = cline.providerRef.deref() diff --git a/src/core/Cline.ts b/src/core/task/Task.ts similarity index 95% rename from src/core/Cline.ts rename to src/core/task/Task.ts index 032ee0d32f..f90d692617 100644 --- a/src/core/Cline.ts +++ b/src/core/task/Task.ts @@ -9,17 +9,17 @@ import pWaitFor from "p-wait-for" import { serializeError } from "serialize-error" // schemas -import { TokenUsage, ToolUsage, ToolName } from "../schemas" +import { TokenUsage, ToolUsage, ToolName } from "../../schemas" // api -import { ApiHandler, buildApiHandler } from "../api" -import { ApiStream } from "../api/transform/stream" +import { ApiHandler, buildApiHandler } from "../../api" +import { ApiStream } from "../../api/transform/stream" // shared -import { ApiConfiguration } from "../shared/api" -import { findLastIndex } from "../shared/array" -import { combineApiRequests } from "../shared/combineApiRequests" -import { combineCommandSequences } from "../shared/combineCommandSequences" +import { ApiConfiguration } from "../../shared/api" +import { findLastIndex } from "../../shared/array" +import { combineApiRequests } from "../../shared/combineApiRequests" +import { combineCommandSequences } from "../../shared/combineCommandSequences" import { ClineApiReqCancelReason, ClineApiReqInfo, @@ -27,45 +27,45 @@ import { ClineMessage, ClineSay, ToolProgressStatus, -} from "../shared/ExtensionMessage" -import { getApiMetrics } from "../shared/getApiMetrics" -import { HistoryItem } from "../shared/HistoryItem" -import { ClineAskResponse } from "../shared/WebviewMessage" -import { defaultModeSlug } from "../shared/modes" -import { DiffStrategy } from "../shared/tools" +} from "../../shared/ExtensionMessage" +import { getApiMetrics } from "../../shared/getApiMetrics" +import { HistoryItem } from "../../shared/HistoryItem" +import { ClineAskResponse } from "../../shared/WebviewMessage" +import { defaultModeSlug } from "../../shared/modes" +import { DiffStrategy } from "../../shared/tools" // services -import { UrlContentFetcher } from "../services/browser/UrlContentFetcher" -import { BrowserSession } from "../services/browser/BrowserSession" -import { McpHub } from "../services/mcp/McpHub" -import { McpServerManager } from "../services/mcp/McpServerManager" -import { telemetryService } from "../services/telemetry/TelemetryService" -import { RepoPerTaskCheckpointService } from "../services/checkpoints" +import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher" +import { BrowserSession } from "../../services/browser/BrowserSession" +import { McpHub } from "../../services/mcp/McpHub" +import { McpServerManager } from "../../services/mcp/McpServerManager" +import { telemetryService } from "../../services/telemetry/TelemetryService" +import { RepoPerTaskCheckpointService } from "../../services/checkpoints" // integrations -import { DiffViewProvider } from "../integrations/editor/DiffViewProvider" -import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown" -import { RooTerminalProcess } from "../integrations/terminal/types" -import { TerminalRegistry } from "../integrations/terminal/TerminalRegistry" +import { DiffViewProvider } from "../../integrations/editor/DiffViewProvider" +import { findToolName, formatContentBlockToMarkdown } from "../../integrations/misc/export-markdown" +import { RooTerminalProcess } from "../../integrations/terminal/types" +import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry" // utils -import { calculateApiCostAnthropic } from "../utils/cost" -import { getWorkspacePath } from "../utils/path" +import { calculateApiCostAnthropic } from "../../utils/cost" +import { getWorkspacePath } from "../../utils/path" // prompts -import { formatResponse } from "./prompts/responses" -import { SYSTEM_PROMPT } from "./prompts/system" +import { formatResponse } from "../prompts/responses" +import { SYSTEM_PROMPT } from "../prompts/system" // core modules -import { ToolRepetitionDetector } from "./tools/ToolRepetitionDetector" -import { FileContextTracker } from "./context-tracking/FileContextTracker" -import { RooIgnoreController } from "./ignore/RooIgnoreController" -import { type AssistantMessageContent, parseAssistantMessage, presentAssistantMessage } from "./assistant-message" -import { truncateConversationIfNeeded } from "./sliding-window" -import { ClineProvider } from "./webview/ClineProvider" -import { MultiSearchReplaceDiffStrategy } from "./diff/strategies/multi-search-replace" -import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "./task-persistence" -import { getEnvironmentDetails } from "./environment/getEnvironmentDetails" +import { ToolRepetitionDetector } from "../tools/ToolRepetitionDetector" +import { FileContextTracker } from "../context-tracking/FileContextTracker" +import { RooIgnoreController } from "../ignore/RooIgnoreController" +import { type AssistantMessageContent, parseAssistantMessage, presentAssistantMessage } from "../assistant-message" +import { truncateConversationIfNeeded } from "../sliding-window" +import { ClineProvider } from "../webview/ClineProvider" +import { MultiSearchReplaceDiffStrategy } from "../diff/strategies/multi-search-replace" +import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "../task-persistence" +import { getEnvironmentDetails } from "../environment/getEnvironmentDetails" import { type CheckpointDiffOptions, type CheckpointRestoreOptions, @@ -73,8 +73,8 @@ import { checkpointSave, checkpointRestore, checkpointDiff, -} from "./checkpoints" -import { processUserContentMentions } from "./mentions/processUserContentMentions" +} from "../checkpoints" +import { processUserContentMentions } from "../mentions/processUserContentMentions" export type ClineEvents = { message: [{ action: "created" | "updated"; message: ClineMessage }] @@ -90,7 +90,7 @@ export type ClineEvents = { taskToolFailed: [taskId: string, tool: ToolName, error: string] } -export type ClineOptions = { +export type TaskOptions = { provider: ClineProvider apiConfiguration: ApiConfiguration customInstructions?: string @@ -103,18 +103,18 @@ export type ClineOptions = { historyItem?: HistoryItem experiments?: Record startTask?: boolean - rootTask?: Cline - parentTask?: Cline + rootTask?: Task + parentTask?: Task taskNumber?: number - onCreated?: (cline: Cline) => void + onCreated?: (cline: Task) => void } -export class Cline extends EventEmitter { +export class Task extends EventEmitter { readonly taskId: string readonly instanceId: string - readonly rootTask: Cline | undefined = undefined - readonly parentTask: Cline | undefined = undefined + readonly rootTask: Task | undefined = undefined + readonly parentTask: Task | undefined = undefined readonly taskNumber: number readonly workspacePath: string @@ -201,7 +201,7 @@ export class Cline extends EventEmitter { parentTask, taskNumber = -1, onCreated, - }: ClineOptions) { + }: TaskOptions) { super() if (startTask && !task && !images && !historyItem) { @@ -264,8 +264,8 @@ export class Cline extends EventEmitter { } } - static create(options: ClineOptions): [Cline, Promise] { - const instance = new Cline({ ...options, startTask: false }) + static create(options: TaskOptions): [Task, Promise] { + const instance = new Task({ ...options, startTask: false }) const { images, task, historyItem } = options let promise diff --git a/src/core/__tests__/Cline.test.ts b/src/core/task/__tests__/Task.test.ts similarity index 93% rename from src/core/__tests__/Cline.test.ts rename to src/core/task/__tests__/Task.test.ts index fd55f995be..f70edfdefa 100644 --- a/src/core/__tests__/Cline.test.ts +++ b/src/core/task/__tests__/Task.test.ts @@ -1,4 +1,4 @@ -// npx jest src/core/__tests__/Cline.test.ts +// npx jest src/core/task/__tests__/Task.test.ts import * as os from "os" import * as path from "path" @@ -6,64 +6,18 @@ import * as path from "path" import * as vscode from "vscode" import { Anthropic } from "@anthropic-ai/sdk" -import { GlobalState } from "../../schemas" -import { Cline } from "../Cline" -import { ClineProvider } from "../webview/ClineProvider" -import { ApiConfiguration, ModelInfo } from "../../shared/api" -import { ApiStreamChunk } from "../../api/transform/stream" -import { ContextProxy } from "../config/ContextProxy" -import { processUserContentMentions } from "../mentions/processUserContentMentions" - -jest.mock("../mentions", () => ({ - parseMentions: jest.fn().mockImplementation((text) => { - return Promise.resolve(`processed: ${text}`) - }), - openMention: jest.fn(), - getLatestTerminalOutput: jest.fn(), -})) - -jest.mock("../../integrations/misc/extract-text", () => ({ - extractTextFromFile: jest.fn().mockResolvedValue("Mock file content"), -})) - -jest.mock("../environment/getEnvironmentDetails", () => ({ - getEnvironmentDetails: jest.fn().mockResolvedValue(""), -})) +import { GlobalState } from "../../../schemas" +import { Task } from "../Task" +import { ClineProvider } from "../../webview/ClineProvider" +import { ApiConfiguration, ModelInfo } from "../../../shared/api" +import { ApiStreamChunk } from "../../../api/transform/stream" +import { ContextProxy } from "../../config/ContextProxy" +import { processUserContentMentions } from "../../mentions/processUserContentMentions" jest.mock("execa", () => ({ execa: jest.fn(), })) -// Mock RooIgnoreController -jest.mock("../ignore/RooIgnoreController") - -// Mock storagePathManager to prevent dynamic import issues -jest.mock("../../shared/storagePathManager", () => ({ - getTaskDirectoryPath: jest - .fn() - .mockImplementation((globalStoragePath, taskId) => Promise.resolve(`${globalStoragePath}/tasks/${taskId}`)), - getSettingsDirectoryPath: jest - .fn() - .mockImplementation((globalStoragePath) => Promise.resolve(`${globalStoragePath}/settings`)), -})) - -// Mock fileExistsAtPath -jest.mock("../../utils/fs", () => ({ - fileExistsAtPath: jest.fn().mockImplementation((filePath) => { - return filePath.includes("ui_messages.json") || filePath.includes("api_conversation_history.json") - }), -})) - -// Mock fs/promises -const mockMessages = [ - { - ts: Date.now(), - type: "say", - say: "text", - text: "historical task", - }, -] - jest.mock("fs/promises", () => ({ mkdir: jest.fn().mockResolvedValue(undefined), writeFile: jest.fn().mockResolvedValue(undefined), @@ -93,35 +47,18 @@ jest.mock("fs/promises", () => ({ rmdir: jest.fn().mockResolvedValue(undefined), })) -// Mock dependencies +jest.mock("p-wait-for", () => ({ + __esModule: true, + default: jest.fn().mockImplementation(async () => Promise.resolve()), +})) + jest.mock("vscode", () => { const mockDisposable = { dispose: jest.fn() } - const mockEventEmitter = { - event: jest.fn(), - fire: jest.fn(), - } - - const mockTextDocument = { - uri: { - fsPath: "/mock/workspace/path/file.ts", - }, - } - - const mockTextEditor = { - document: mockTextDocument, - } - - const mockTab = { - input: { - uri: { - fsPath: "/mock/workspace/path/file.ts", - }, - }, - } - - const mockTabGroup = { - tabs: [mockTab], - } + const mockEventEmitter = { event: jest.fn(), fire: jest.fn() } + const mockTextDocument = { uri: { fsPath: "/mock/workspace/path/file.ts" } } + const mockTextEditor = { document: mockTextDocument } + const mockTab = { input: { uri: { fsPath: "/mock/workspace/path/file.ts" } } } + const mockTabGroup = { tabs: [mockTab] } return { CodeActionKind: { @@ -142,9 +79,7 @@ jest.mock("vscode", () => { workspace: { workspaceFolders: [ { - uri: { - fsPath: "/mock/workspace/path", - }, + uri: { fsPath: "/mock/workspace/path" }, name: "mock-workspace", index: 0, }, @@ -173,12 +108,52 @@ jest.mock("vscode", () => { } }) -// Mock p-wait-for to resolve immediately -jest.mock("p-wait-for", () => ({ - __esModule: true, - default: jest.fn().mockImplementation(async () => Promise.resolve()), +jest.mock("../../mentions", () => ({ + parseMentions: jest.fn().mockImplementation((text) => { + return Promise.resolve(`processed: ${text}`) + }), + openMention: jest.fn(), + getLatestTerminalOutput: jest.fn(), })) +jest.mock("../../../integrations/misc/extract-text", () => ({ + extractTextFromFile: jest.fn().mockResolvedValue("Mock file content"), +})) + +jest.mock("../../environment/getEnvironmentDetails", () => ({ + getEnvironmentDetails: jest.fn().mockResolvedValue(""), +})) + +// Mock RooIgnoreController +jest.mock("../../ignore/RooIgnoreController") + +// Mock storagePathManager to prevent dynamic import issues +jest.mock("../../../shared/storagePathManager", () => ({ + getTaskDirectoryPath: jest + .fn() + .mockImplementation((globalStoragePath, taskId) => Promise.resolve(`${globalStoragePath}/tasks/${taskId}`)), + getSettingsDirectoryPath: jest + .fn() + .mockImplementation((globalStoragePath) => Promise.resolve(`${globalStoragePath}/settings`)), +})) + +// Mock fileExistsAtPath +jest.mock("../../../utils/fs", () => ({ + fileExistsAtPath: jest.fn().mockImplementation((filePath) => { + return filePath.includes("ui_messages.json") || filePath.includes("api_conversation_history.json") + }), +})) + +// Mock fs/promises +const mockMessages = [ + { + ts: Date.now(), + type: "say", + say: "text", + text: "historical task", + }, +] + describe("Cline", () => { let mockProvider: jest.Mocked let mockApiConfig: ApiConfiguration @@ -295,7 +270,7 @@ describe("Cline", () => { describe("constructor", () => { it("should respect provided settings", async () => { - const cline = new Cline({ + const cline = new Task({ provider: mockProvider, apiConfiguration: mockApiConfig, customInstructions: "custom instructions", @@ -309,7 +284,7 @@ describe("Cline", () => { }) it("should use default fuzzy match threshold when not provided", async () => { - const cline = new Cline({ + const cline = new Task({ provider: mockProvider, apiConfiguration: mockApiConfig, customInstructions: "custom instructions", @@ -327,7 +302,7 @@ describe("Cline", () => { it("should require either task or historyItem", () => { expect(() => { - new Cline({ provider: mockProvider, apiConfiguration: mockApiConfig }) + new Task({ provider: mockProvider, apiConfiguration: mockApiConfig }) }).toThrow("Either historyItem or task/images must be provided") }) }) @@ -336,7 +311,7 @@ describe("Cline", () => { describe("API conversation handling", () => { it("should clean conversation history before sending to API", async () => { // Cline.create will now use our mocked getEnvironmentDetails - const [cline, task] = Cline.create({ + const [cline, task] = Task.create({ provider: mockProvider, apiConfiguration: mockApiConfig, task: "test task", @@ -444,7 +419,7 @@ describe("Cline", () => { ] // Test with model that supports images - const [clineWithImages, taskWithImages] = Cline.create({ + const [clineWithImages, taskWithImages] = Task.create({ provider: mockProvider, apiConfiguration: configWithImages, task: "test task", @@ -467,7 +442,7 @@ describe("Cline", () => { clineWithImages.apiConversationHistory = conversationHistory // Test with model that doesn't support images - const [clineWithoutImages, taskWithoutImages] = Cline.create({ + const [clineWithoutImages, taskWithoutImages] = Task.create({ provider: mockProvider, apiConfiguration: configWithoutImages, task: "test task", @@ -558,7 +533,7 @@ describe("Cline", () => { }) it.skip("should handle API retry with countdown", async () => { - const [cline, task] = Cline.create({ + const [cline, task] = Task.create({ provider: mockProvider, apiConfiguration: mockApiConfig, task: "test task", @@ -682,7 +657,7 @@ describe("Cline", () => { }) it.skip("should not apply retry delay twice", async () => { - const [cline, task] = Cline.create({ + const [cline, task] = Task.create({ provider: mockProvider, apiConfiguration: mockApiConfig, task: "test task", @@ -806,7 +781,7 @@ describe("Cline", () => { describe("processUserContentMentions", () => { it("should process mentions in task and feedback tags", async () => { - const [cline, task] = Cline.create({ + const [cline, task] = Task.create({ provider: mockProvider, apiConfiguration: mockApiConfig, task: "test task", diff --git a/src/core/tools/__tests__/executeCommandTool.test.ts b/src/core/tools/__tests__/executeCommandTool.test.ts index ee70ae5c09..615d72042d 100644 --- a/src/core/tools/__tests__/executeCommandTool.test.ts +++ b/src/core/tools/__tests__/executeCommandTool.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, jest, beforeEach } from "@jest/globals" -import { Cline } from "../../Cline" +import { Task } from "../../task/Task" import { formatResponse } from "../../prompts/responses" import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../../shared/tools" import { ToolUsage } from "../../../schemas" @@ -13,7 +13,7 @@ jest.mock("execa", () => ({ execa: jest.fn(), })) -jest.mock("../../Cline") +jest.mock("../../task/Task") jest.mock("../../prompts/responses") // Create a mock for the executeCommand function @@ -74,7 +74,7 @@ beforeEach(() => { describe("executeCommandTool", () => { // Setup common test variables - let mockCline: jest.Mocked> & { consecutiveMistakeCount: number; didRejectTool: boolean } + let mockCline: jest.Mocked> & { consecutiveMistakeCount: number; didRejectTool: boolean } let mockAskApproval: jest.Mock let mockHandleError: jest.Mock let mockPushToolResult: jest.Mock @@ -160,7 +160,7 @@ describe("executeCommandTool", () => { // Execute await executeCommandTool( - mockCline as unknown as Cline, + mockCline as unknown as Task, mockToolUse, mockAskApproval as unknown as AskApproval, mockHandleError as unknown as HandleError, @@ -181,7 +181,7 @@ describe("executeCommandTool", () => { // Execute await executeCommandTool( - mockCline as unknown as Cline, + mockCline as unknown as Task, mockToolUse, mockAskApproval as unknown as AskApproval, mockHandleError as unknown as HandleError, @@ -204,7 +204,7 @@ describe("executeCommandTool", () => { // Execute await executeCommandTool( - mockCline as unknown as Cline, + mockCline as unknown as Task, mockToolUse, mockAskApproval as unknown as AskApproval, mockHandleError as unknown as HandleError, @@ -228,7 +228,7 @@ describe("executeCommandTool", () => { // Execute await executeCommandTool( - mockCline as unknown as Cline, + mockCline as unknown as Task, mockToolUse, mockAskApproval as unknown as AskApproval, mockHandleError as unknown as HandleError, @@ -258,7 +258,7 @@ describe("executeCommandTool", () => { // Execute await executeCommandTool( - mockCline as unknown as Cline, + mockCline as unknown as Task, mockToolUse, mockAskApproval as unknown as AskApproval, mockHandleError as unknown as HandleError, diff --git a/src/core/tools/accessMcpResourceTool.ts b/src/core/tools/accessMcpResourceTool.ts index 3161a3f8d5..22b1aba909 100644 --- a/src/core/tools/accessMcpResourceTool.ts +++ b/src/core/tools/accessMcpResourceTool.ts @@ -1,10 +1,10 @@ import { ClineAskUseMcpServer } from "../../shared/ExtensionMessage" import { ToolUse, RemoveClosingTag, AskApproval, HandleError, PushToolResult } from "../../shared/tools" -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { formatResponse } from "../prompts/responses" export async function accessMcpResourceTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, diff --git a/src/core/tools/applyDiffTool.ts b/src/core/tools/applyDiffTool.ts index 590040f2ba..19cb874be8 100644 --- a/src/core/tools/applyDiffTool.ts +++ b/src/core/tools/applyDiffTool.ts @@ -3,7 +3,7 @@ import fs from "fs/promises" import { ClineSayTool } from "../../shared/ExtensionMessage" import { getReadablePath } from "../../utils/path" -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { ToolUse, RemoveClosingTag } from "../../shared/tools" import { formatResponse } from "../prompts/responses" import { AskApproval, HandleError, PushToolResult } from "../../shared/tools" @@ -14,7 +14,7 @@ import { telemetryService } from "../../services/telemetry/TelemetryService" import { unescapeHtmlEntities } from "../../utils/text-normalization" export async function applyDiffTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, diff --git a/src/core/tools/askFollowupQuestionTool.ts b/src/core/tools/askFollowupQuestionTool.ts index 46ce2e4e07..d5adf6d4cd 100644 --- a/src/core/tools/askFollowupQuestionTool.ts +++ b/src/core/tools/askFollowupQuestionTool.ts @@ -1,10 +1,10 @@ -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { formatResponse } from "../prompts/responses" import { parseXml } from "../../utils/xml" export async function askFollowupQuestionTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, diff --git a/src/core/tools/attemptCompletionTool.ts b/src/core/tools/attemptCompletionTool.ts index a911da98ec..998101fc4a 100644 --- a/src/core/tools/attemptCompletionTool.ts +++ b/src/core/tools/attemptCompletionTool.ts @@ -1,6 +1,6 @@ import Anthropic from "@anthropic-ai/sdk" -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { ToolResponse, ToolUse, @@ -16,7 +16,7 @@ import { telemetryService } from "../../services/telemetry/TelemetryService" import { type ExecuteCommandOptions, executeCommand } from "./executeCommandTool" export async function attemptCompletionTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, diff --git a/src/core/tools/browserActionTool.ts b/src/core/tools/browserActionTool.ts index 093a89a7d5..13cb9b0ec2 100644 --- a/src/core/tools/browserActionTool.ts +++ b/src/core/tools/browserActionTool.ts @@ -1,4 +1,4 @@ -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { BrowserAction, @@ -9,7 +9,7 @@ import { import { formatResponse } from "../prompts/responses" export async function browserActionTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, diff --git a/src/core/tools/executeCommandTool.ts b/src/core/tools/executeCommandTool.ts index 97ec0e6ffc..9fe9e9d535 100644 --- a/src/core/tools/executeCommandTool.ts +++ b/src/core/tools/executeCommandTool.ts @@ -3,7 +3,7 @@ import * as path from "path" import delay from "delay" -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { CommandExecutionStatus } from "../../schemas" import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag, ToolResponse } from "../../shared/tools" import { formatResponse } from "../prompts/responses" @@ -16,7 +16,7 @@ import { Terminal } from "../../integrations/terminal/Terminal" class ShellIntegrationError extends Error {} export async function executeCommandTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, @@ -114,7 +114,7 @@ export type ExecuteCommandOptions = { } export async function executeCommand( - cline: Cline, + cline: Task, { executionId, command, diff --git a/src/core/tools/fetchInstructionsTool.ts b/src/core/tools/fetchInstructionsTool.ts index d72c19ce90..5325f98fbf 100644 --- a/src/core/tools/fetchInstructionsTool.ts +++ b/src/core/tools/fetchInstructionsTool.ts @@ -1,11 +1,11 @@ -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { fetchInstructions } from "../prompts/instructions/instructions" import { ClineSayTool } from "../../shared/ExtensionMessage" import { formatResponse } from "../prompts/responses" import { ToolUse, AskApproval, HandleError, PushToolResult } from "../../shared/tools" export async function fetchInstructionsTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, diff --git a/src/core/tools/insertContentTool.ts b/src/core/tools/insertContentTool.ts index 8e6c5fc89e..33c3f88675 100644 --- a/src/core/tools/insertContentTool.ts +++ b/src/core/tools/insertContentTool.ts @@ -3,7 +3,7 @@ import fs from "fs/promises" import path from "path" import { getReadablePath } from "../../utils/path" -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { formatResponse } from "../prompts/responses" import { ClineSayTool } from "../../shared/ExtensionMessage" @@ -12,7 +12,7 @@ import { fileExistsAtPath } from "../../utils/fs" import { insertGroups } from "../diff/insert-groups" export async function insertContentTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, diff --git a/src/core/tools/listCodeDefinitionNamesTool.ts b/src/core/tools/listCodeDefinitionNamesTool.ts index 5f1e5ad883..8c9ae74fcf 100644 --- a/src/core/tools/listCodeDefinitionNamesTool.ts +++ b/src/core/tools/listCodeDefinitionNamesTool.ts @@ -2,14 +2,14 @@ import path from "path" import fs from "fs/promises" import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { ClineSayTool } from "../../shared/ExtensionMessage" import { getReadablePath } from "../../utils/path" import { parseSourceCodeForDefinitionsTopLevel, parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter" import { RecordSource } from "../context-tracking/FileContextTrackerTypes" export async function listCodeDefinitionNamesTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, diff --git a/src/core/tools/listFilesTool.ts b/src/core/tools/listFilesTool.ts index 7c785526e8..6d40de711c 100644 --- a/src/core/tools/listFilesTool.ts +++ b/src/core/tools/listFilesTool.ts @@ -1,6 +1,6 @@ import * as path from "path" -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { ClineSayTool } from "../../shared/ExtensionMessage" import { formatResponse } from "../prompts/responses" import { listFiles } from "../../services/glob/list-files" @@ -23,7 +23,7 @@ import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } f */ export async function listFilesTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, diff --git a/src/core/tools/newTaskTool.ts b/src/core/tools/newTaskTool.ts index dc45c73d3a..38b4cbf302 100644 --- a/src/core/tools/newTaskTool.ts +++ b/src/core/tools/newTaskTool.ts @@ -1,12 +1,12 @@ import delay from "delay" import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { defaultModeSlug, getModeBySlug } from "../../shared/modes" import { formatResponse } from "../prompts/responses" export async function newTaskTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index e982420bf1..f615ba17c4 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -1,7 +1,7 @@ import path from "path" import { isBinaryFile } from "isbinaryfile" -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { ClineSayTool } from "../../shared/ExtensionMessage" import { formatResponse } from "../prompts/responses" import { t } from "../../i18n" @@ -15,7 +15,7 @@ import { extractTextFromFile, addLineNumbers } from "../../integrations/misc/ext import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter" export async function readFileTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, diff --git a/src/core/tools/searchAndReplaceTool.ts b/src/core/tools/searchAndReplaceTool.ts index 7a503b5f1e..4871d246d7 100644 --- a/src/core/tools/searchAndReplaceTool.ts +++ b/src/core/tools/searchAndReplaceTool.ts @@ -4,7 +4,7 @@ import fs from "fs/promises" import delay from "delay" // Internal imports -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { AskApproval, HandleError, PushToolResult, RemoveClosingTag, ToolUse } from "../../shared/tools" import { formatResponse } from "../prompts/responses" import { ClineSayTool } from "../../shared/ExtensionMessage" @@ -21,7 +21,7 @@ import { RecordSource } from "../context-tracking/FileContextTrackerTypes" * Validates required parameters for search and replace operation */ async function validateParams( - cline: Cline, + cline: Task, relPath: string | undefined, search: string | undefined, replace: string | undefined, @@ -61,7 +61,7 @@ async function validateParams( * @param removeClosingTag - Function to remove closing tags */ export async function searchAndReplaceTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, diff --git a/src/core/tools/searchFilesTool.ts b/src/core/tools/searchFilesTool.ts index 33a8b8b3cc..6528f20d54 100644 --- a/src/core/tools/searchFilesTool.ts +++ b/src/core/tools/searchFilesTool.ts @@ -1,13 +1,13 @@ import path from "path" -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { ClineSayTool } from "../../shared/ExtensionMessage" import { getReadablePath } from "../../utils/path" import { regexSearchFiles } from "../../services/ripgrep" export async function searchFilesTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, diff --git a/src/core/tools/switchModeTool.ts b/src/core/tools/switchModeTool.ts index 28f719ff2d..8ce906b41f 100644 --- a/src/core/tools/switchModeTool.ts +++ b/src/core/tools/switchModeTool.ts @@ -1,12 +1,12 @@ import delay from "delay" -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { formatResponse } from "../prompts/responses" import { defaultModeSlug, getModeBySlug } from "../../shared/modes" export async function switchModeTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, diff --git a/src/core/tools/useMcpToolTool.ts b/src/core/tools/useMcpToolTool.ts index 882f214a6f..b8339bc8d0 100644 --- a/src/core/tools/useMcpToolTool.ts +++ b/src/core/tools/useMcpToolTool.ts @@ -1,10 +1,10 @@ -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" import { formatResponse } from "../prompts/responses" import { ClineAskUseMcpServer } from "../../shared/ExtensionMessage" export async function useMcpToolTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, diff --git a/src/core/tools/writeToFileTool.ts b/src/core/tools/writeToFileTool.ts index a23aea9714..78f72b50f8 100644 --- a/src/core/tools/writeToFileTool.ts +++ b/src/core/tools/writeToFileTool.ts @@ -2,7 +2,7 @@ import path from "path" import delay from "delay" import * as vscode from "vscode" -import { Cline } from "../Cline" +import { Task } from "../task/Task" import { ClineSayTool } from "../../shared/ExtensionMessage" import { formatResponse } from "../prompts/responses" import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools" @@ -15,7 +15,7 @@ import { detectCodeOmission } from "../../integrations/editor/detect-omission" import { unescapeHtmlEntities } from "../../utils/text-normalization" export async function writeToFileTool( - cline: Cline, + cline: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 5cd4b3341e..87598bd45d 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -42,7 +42,7 @@ import { ProviderSettingsManager } from "../config/ProviderSettingsManager" import { CustomModesManager } from "../config/CustomModesManager" import { buildApiHandler } from "../../api" import { CodeActionName } from "../../activate/CodeActionProvider" -import { Cline, ClineOptions } from "../Cline" +import { Task, TaskOptions } from "../task/Task" import { getNonce } from "./getNonce" import { getUri } from "./getUri" import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt" @@ -57,7 +57,7 @@ import { WebviewMessage } from "../../shared/WebviewMessage" */ export type ClineProviderEvents = { - clineCreated: [cline: Cline] + clineCreated: [cline: Task] } export class ClineProvider extends EventEmitter implements vscode.WebviewViewProvider { @@ -66,7 +66,7 @@ export class ClineProvider extends EventEmitter implements private static activeInstances: Set = new Set() private disposables: vscode.Disposable[] = [] private view?: vscode.WebviewView | vscode.WebviewPanel - private clineStack: Cline[] = [] + private clineStack: Task[] = [] private _workspaceTracker?: WorkspaceTracker // workSpaceTracker read-only for access outside this class public get workspaceTracker(): WorkspaceTracker | undefined { return this._workspaceTracker @@ -116,7 +116,7 @@ export class ClineProvider extends EventEmitter implements // Adds a new Cline instance to clineStack, marking the start of a new task. // The instance is pushed to the top of the stack (LIFO order). // When the task is completed, the top instance is removed, reactivating the previous task. - async addClineToStack(cline: Cline) { + async addClineToStack(cline: Task) { console.log(`[subtasks] adding task ${cline.taskId}.${cline.instanceId} to stack`) // Add this cline instance into the stack that represents the order of all the called tasks. @@ -161,7 +161,7 @@ export class ClineProvider extends EventEmitter implements // returns the current cline object in the stack (the top one) // if the stack is empty, returns undefined - getCurrentCline(): Cline | undefined { + getCurrentCline(): Task | undefined { if (this.clineStack.length === 0) { return undefined } @@ -438,7 +438,7 @@ export class ClineProvider extends EventEmitter implements this.log("Webview view resolved") } - public async initClineWithSubTask(parent: Cline, task?: string, images?: string[]) { + public async initClineWithSubTask(parent: Task, task?: string, images?: string[]) { return this.initClineWithTask(task, images, parent) } @@ -451,10 +451,10 @@ export class ClineProvider extends EventEmitter implements public async initClineWithTask( task?: string, images?: string[], - parentTask?: Cline, + parentTask?: Task, options: Partial< Pick< - ClineOptions, + TaskOptions, | "customInstructions" | "enableDiff" | "enableCheckpoints" @@ -478,7 +478,7 @@ export class ClineProvider extends EventEmitter implements const modePrompt = customModePrompts?.[mode] as PromptComponent const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") - const cline = new Cline({ + const cline = new Task({ provider: this, apiConfiguration, customInstructions: effectiveInstructions, @@ -504,7 +504,7 @@ export class ClineProvider extends EventEmitter implements return cline } - public async initClineWithHistoryItem(historyItem: HistoryItem & { rootTask?: Cline; parentTask?: Cline }) { + public async initClineWithHistoryItem(historyItem: HistoryItem & { rootTask?: Task; parentTask?: Task }) { await this.removeClineFromStack() const { @@ -521,7 +521,7 @@ export class ClineProvider extends EventEmitter implements const modePrompt = customModePrompts?.[mode] as PromptComponent const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") - const cline = new Cline({ + const cline = new Task({ provider: this, apiConfiguration, customInstructions: effectiveInstructions, diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index 2942bc43b1..1897a7e7e8 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -1,52 +1,41 @@ // npx jest src/core/webview/__tests__/ClineProvider.test.ts +import Anthropic from "@anthropic-ai/sdk" import * as vscode from "vscode" import axios from "axios" import { ClineProvider } from "../ClineProvider" -import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage" +import { ClineMessage, ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage" import { setSoundEnabled } from "../../../utils/sound" import { setTtsEnabled } from "../../../utils/tts" import { defaultModeSlug } from "../../../shared/modes" import { experimentDefault } from "../../../shared/experiments" import { ContextProxy } from "../../config/ContextProxy" +import { Task, TaskOptions } from "../../task/Task" // Mock setup must come before imports jest.mock("../../prompts/sections/custom-instructions") -// Mock dependencies jest.mock("vscode") + jest.mock("delay") -// Mock BrowserSession -jest.mock("../../../services/browser/BrowserSession", () => ({ - BrowserSession: jest.fn().mockImplementation(() => ({ - testConnection: jest.fn().mockImplementation(async (url) => { - if (url === "http://localhost:9222") { - return { - success: true, - message: "Successfully connected to Chrome", - endpoint: "ws://localhost:9222/devtools/browser/123", - } - } else { - return { - success: false, - message: "Failed to connect to Chrome", - endpoint: undefined, - } - } - }), - })), +jest.mock("p-wait-for", () => ({ + __esModule: true, + default: jest.fn().mockResolvedValue(undefined), })) -// Mock browserDiscovery -jest.mock("../../../services/browser/browserDiscovery", () => ({ - discoverChromeHostUrl: jest.fn().mockImplementation(async () => { - return "http://localhost:9222" - }), - tryChromeHostUrl: jest.fn().mockImplementation(async (url) => { - return url === "http://localhost:9222" - }), +jest.mock("fs/promises", () => ({ + mkdir: jest.fn(), + writeFile: jest.fn(), + readFile: jest.fn(), + unlink: jest.fn(), + rmdir: jest.fn(), +})) + +jest.mock("axios", () => ({ + get: jest.fn().mockResolvedValue({ data: { data: [] } }), + post: jest.fn(), })) jest.mock( @@ -74,6 +63,35 @@ jest.mock( { virtual: true }, ) +jest.mock("../../../services/browser/BrowserSession", () => ({ + BrowserSession: jest.fn().mockImplementation(() => ({ + testConnection: jest.fn().mockImplementation(async (url) => { + if (url === "http://localhost:9222") { + return { + success: true, + message: "Successfully connected to Chrome", + endpoint: "ws://localhost:9222/devtools/browser/123", + } + } else { + return { + success: false, + message: "Failed to connect to Chrome", + endpoint: undefined, + } + } + }), + })), +})) + +jest.mock("../../../services/browser/browserDiscovery", () => ({ + discoverChromeHostUrl: jest.fn().mockImplementation(async () => { + return "http://localhost:9222" + }), + tryChromeHostUrl: jest.fn().mockImplementation(async (url) => { + return url === "http://localhost:9222" + }), +})) + // Initialize mocks const mockAddCustomInstructions = jest.fn().mockResolvedValue("Combined instructions") @@ -115,7 +133,6 @@ jest.mock( { virtual: true }, ) -// Mock dependencies jest.mock("vscode", () => ({ ExtensionContext: jest.fn(), OutputChannel: jest.fn(), @@ -156,50 +173,24 @@ jest.mock("vscode", () => ({ }, })) -// Mock sound utility jest.mock("../../../utils/sound", () => ({ setSoundEnabled: jest.fn(), })) -// Mock tts utility jest.mock("../../../utils/tts", () => ({ setTtsEnabled: jest.fn(), setTtsSpeed: jest.fn(), })) -// Mock ESM modules -jest.mock("p-wait-for", () => ({ - __esModule: true, - default: jest.fn().mockResolvedValue(undefined), -})) - -// Mock fs/promises -jest.mock("fs/promises", () => ({ - mkdir: jest.fn(), - writeFile: jest.fn(), - readFile: jest.fn(), - unlink: jest.fn(), - rmdir: jest.fn(), -})) - -// Mock axios -jest.mock("axios", () => ({ - get: jest.fn().mockResolvedValue({ data: { data: [] } }), - post: jest.fn(), -})) - -// Mock buildApiHandler jest.mock("../../../api", () => ({ buildApiHandler: jest.fn(), })) -// Mock system prompt jest.mock("../../prompts/system", () => ({ SYSTEM_PROMPT: jest.fn().mockImplementation(async () => "mocked system prompt"), codeMode: "code", })) -// Mock WorkspaceTracker jest.mock("../../../integrations/workspace/WorkspaceTracker", () => { return jest.fn().mockImplementation(() => ({ initializeFilePaths: jest.fn(), @@ -207,9 +198,8 @@ jest.mock("../../../integrations/workspace/WorkspaceTracker", () => { })) }) -// Mock Cline -jest.mock("../../Cline", () => ({ - Cline: jest +jest.mock("../../task/Task", () => ({ + Task: jest .fn() .mockImplementation( (_provider, _apiConfiguration, _customInstructions, _diffEnabled, _fuzzyMatchThreshold, _task, taskId) => ({ @@ -229,7 +219,6 @@ jest.mock("../../Cline", () => ({ ), })) -// Mock extract-text jest.mock("../../../integrations/misc/extract-text", () => ({ extractTextFromFile: jest.fn().mockImplementation(async (_filePath: string) => { const content = "const x = 1;\nconst y = 2;\nconst z = 3;" @@ -249,6 +238,8 @@ afterAll(() => { }) describe("ClineProvider", () => { + let defaultTaskOptions: TaskOptions + let provider: ClineProvider let mockContext: vscode.ExtensionContext let mockOutputChannel: vscode.OutputChannel @@ -327,6 +318,13 @@ describe("ClineProvider", () => { provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext)) + defaultTaskOptions = { + provider, + apiConfiguration: { + apiProvider: "openrouter", + }, + } + // @ts-ignore - Access private property for testing updateGlobalStateSpy = jest.spyOn(provider.contextProxy, "setValue") @@ -451,8 +449,7 @@ describe("ClineProvider", () => { test("clearTask aborts current task", async () => { // Setup Cline instance with auto-mock from the top of the file - const { Cline } = require("../../Cline") // Get the mocked class - const mockCline = new Cline() // Create a new mocked instance + const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance // add the mock object to the stack await provider.addClineToStack(mockCline) @@ -475,9 +472,8 @@ describe("ClineProvider", () => { test("addClineToStack adds multiple Cline instances to the stack", async () => { // Setup Cline instance with auto-mock from the top of the file - const { Cline } = require("../../Cline") // Get the mocked class - const mockCline1 = new Cline() // Create a new mocked instance - const mockCline2 = new Cline() // Create a new mocked instance + const mockCline1 = new Task(defaultTaskOptions) // Create a new mocked instance + const mockCline2 = new Task(defaultTaskOptions) // Create a new mocked instance Object.defineProperty(mockCline1, "taskId", { value: "test-task-id-1", writable: true }) Object.defineProperty(mockCline2, "taskId", { value: "test-task-id-2", writable: true }) @@ -833,15 +829,11 @@ describe("ClineProvider", () => { experiments: experimentDefault, } as any) - // Reset Cline mock - const { Cline } = require("../../Cline") - ;(Cline as jest.Mock).mockClear() - // Initialize Cline with a task await provider.initClineWithTask("Test task") // Verify Cline was initialized with mode-specific instructions - expect(Cline).toHaveBeenCalledWith({ + expect(Task).toHaveBeenCalledWith({ provider, apiConfiguration: mockApiConfig, customInstructions: modeCustomInstructions, @@ -958,13 +950,19 @@ describe("ClineProvider", () => { { ts: 4000, type: "say", say: "browser_action" }, // Response to delete { ts: 5000, type: "say", say: "user_feedback" }, // Next user message { ts: 6000, type: "say", say: "user_feedback" }, // Final message - ] + ] as ClineMessage[] - const mockApiHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }, { ts: 4000 }, { ts: 5000 }, { ts: 6000 }] + const mockApiHistory = [ + { ts: 1000 }, + { ts: 2000 }, + { ts: 3000 }, + { ts: 4000 }, + { ts: 5000 }, + { ts: 6000 }, + ] as (Anthropic.MessageParam & { ts?: number })[] - // Setup Cline instance with auto-mock from the top of the file - const { Cline } = require("../../Cline") // Get the mocked class - const mockCline = new Cline() // Create a new mocked instance + // Setup Task instance with auto-mock from the top of the file + const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance mockCline.clineMessages = mockMessages // Set test-specific messages mockCline.apiConversationHistory = mockApiHistory // Set API history await provider.addClineToStack(mockCline) // Add the mocked instance to the stack @@ -1005,13 +1003,19 @@ describe("ClineProvider", () => { { ts: 2000, type: "say", say: "text", value: 3000 }, // Message to delete { ts: 3000, type: "say", say: "user_feedback" }, { ts: 4000, type: "say", say: "user_feedback" }, - ] + ] as ClineMessage[] - const mockApiHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }, { ts: 4000 }] + const mockApiHistory = [ + { ts: 1000 }, + { ts: 2000 }, + { ts: 3000 }, + { ts: 4000 }, + ] as (Anthropic.MessageParam & { + ts?: number + })[] // Setup Cline instance with auto-mock from the top of the file - const { Cline } = require("../../Cline") // Get the mocked class - const mockCline = new Cline() // Create a new mocked instance + const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance mockCline.clineMessages = mockMessages mockCline.apiConversationHistory = mockApiHistory await provider.addClineToStack(mockCline) @@ -1037,10 +1041,11 @@ describe("ClineProvider", () => { ;(vscode.window.showInformationMessage as jest.Mock).mockResolvedValue("Cancel") // Setup Cline instance with auto-mock from the top of the file - const { Cline } = require("../../Cline") // Get the mocked class - const mockCline = new Cline() // Create a new mocked instance - mockCline.clineMessages = [{ ts: 1000 }, { ts: 2000 }] - mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] + const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance + mockCline.clineMessages = [{ ts: 1000 }, { ts: 2000 }] as ClineMessage[] + mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as (Anthropic.MessageParam & { + ts?: number + })[] await provider.addClineToStack(mockCline) // Trigger message deletion @@ -1212,15 +1217,16 @@ describe("ClineProvider", () => { }) test("passes diffEnabled: false to SYSTEM_PROMPT when diff is disabled", async () => { - // Setup Cline instance with mocked api.getModel() - const { Cline } = require("../../Cline") - const mockCline = new Cline() + // Setup Task instance with mocked api.getModel() + const mockCline = new Task(defaultTaskOptions) + mockCline.api = { getModel: jest.fn().mockReturnValue({ id: "claude-3-sonnet", info: { supportsComputerUse: true }, }), - } + } as any + await provider.addClineToStack(mockCline) // Mock getState to return diffEnabled: false @@ -1742,9 +1748,8 @@ describe("ClineProvider", () => { .mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]), } as any - // Setup Cline instance with auto-mock from the top of the file - const { Cline } = require("../../Cline") // Get the mocked class - const mockCline = new Cline() // Create a new mocked instance + // Setup Task instance with auto-mock from the top of the file + const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance await provider.addClineToStack(mockCline) const testApiConfig = { @@ -2084,6 +2089,7 @@ describe.skip("ContextProxy integration", () => { }) describe("getTelemetryProperties", () => { + let defaultTaskOptions: TaskOptions let provider: ClineProvider let mockContext: vscode.ExtensionContext let mockOutputChannel: vscode.OutputChannel @@ -2113,9 +2119,15 @@ describe("getTelemetryProperties", () => { mockOutputChannel = { appendLine: jest.fn() } as unknown as vscode.OutputChannel provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext)) - // Setup Cline instance with mocked getModel method - const { Cline } = require("../../Cline") - mockCline = new Cline() + defaultTaskOptions = { + provider, + apiConfiguration: { + apiProvider: "openrouter", + }, + } + + // Setup Task instance with mocked getModel method + mockCline = new Task(defaultTaskOptions) mockCline.api = { getModel: jest.fn().mockReturnValue({ id: "claude-3-7-sonnet-20250219",