Rename Cline to Task (#3352)

This commit is contained in:
Chris Estreich 2025-05-08 12:47:50 -07:00 committed by hannesrudolph
parent 0069a2873d
commit 6af9fad180
26 changed files with 305 additions and 321 deletions

View file

@ -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`)
}

View file

@ -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) {

View file

@ -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<Cline>
let mockCline: Partial<Task>
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("<environment_details>")
expect(result).toContain("</environment_details>")
@ -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("<role>You are a code assistant</role>")
expect(result).toContain("<custom_instructions>Custom instructions</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("<environment_details>")
@ -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("<environment_details>")
@ -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()
})
})

View file

@ -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()

View file

@ -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<string, boolean>
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<ClineEvents> {
export class Task extends EventEmitter<ClineEvents> {
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<ClineEvents> {
parentTask,
taskNumber = -1,
onCreated,
}: ClineOptions) {
}: TaskOptions) {
super()
if (startTask && !task && !images && !historyItem) {
@ -264,8 +264,8 @@ export class Cline extends EventEmitter<ClineEvents> {
}
}
static create(options: ClineOptions): [Cline, Promise<void>] {
const instance = new Cline({ ...options, startTask: false })
static create(options: TaskOptions): [Task, Promise<void>] {
const instance = new Task({ ...options, startTask: false })
const { images, task, historyItem } = options
let promise

View file

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

View file

@ -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<Partial<Cline>> & { consecutiveMistakeCount: number; didRejectTool: boolean }
let mockCline: jest.Mocked<Partial<Task>> & { 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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<ClineProviderEvents> implements vscode.WebviewViewProvider {
@ -66,7 +66,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
private static activeInstances: Set<ClineProvider> = 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<ClineProviderEvents> 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<ClineProviderEvents> 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<ClineProviderEvents> 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<ClineProviderEvents> 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<ClineProviderEvents> 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<ClineProviderEvents> 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<ClineProviderEvents> 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,

View file

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