Browse Source

Re-enable ClineProvider tests (#2047)

Chris Estreich 11 months ago
parent
commit
7eb469f636

+ 7 - 1
src/__mocks__/p-wait-for.js

@@ -1,14 +1,20 @@
 function pWaitFor(condition, options = {}) {
 	return new Promise((resolve, reject) => {
+		let timeout
+
 		const interval = setInterval(() => {
 			if (condition()) {
+				if (timeout) {
+					clearTimeout(timeout)
+				}
+
 				clearInterval(interval)
 				resolve()
 			}
 		}, options.interval || 20)
 
 		if (options.timeout) {
-			setTimeout(() => {
+			timeout = setTimeout(() => {
 				clearInterval(interval)
 				reject(new Error("Timed out"))
 			}, options.timeout)

+ 24 - 8
src/core/Cline.ts

@@ -123,10 +123,7 @@ export type ClineOptions = {
 export class Cline extends EventEmitter<ClineEvents> {
 	readonly taskId: string
 	readonly instanceId: string
-	get cwd() {
-		return getWorkspacePath(path.join(os.homedir(), "Desktop"))
-	}
-	// Subtasks
+
 	readonly rootTask: Cline | undefined = undefined
 	readonly parentTask: Cline | undefined = undefined
 	readonly taskNumber: number
@@ -268,6 +265,10 @@ export class Cline extends EventEmitter<ClineEvents> {
 		return [instance, promise]
 	}
 
+	get cwd() {
+		return getWorkspacePath(path.join(os.homedir(), "Desktop"))
+	}
+
 	// Add method to update diffStrategy
 	async updateDiffStrategy(experimentalDiffStrategy?: boolean, multiSearchReplaceDiffStrategy?: boolean) {
 		// If not provided, get from current state
@@ -334,6 +335,7 @@ export class Cline extends EventEmitter<ClineEvents> {
 
 	private async getSavedClineMessages(): Promise<ClineMessage[]> {
 		const filePath = path.join(await this.ensureTaskDirectoryExists(), GlobalFileNames.uiMessages)
+
 		if (await fileExistsAtPath(filePath)) {
 			return JSON.parse(await fs.readFile(filePath, "utf8"))
 		} else {
@@ -1222,11 +1224,12 @@ export class Cline extends EventEmitter<ClineEvents> {
 			}
 			return { role, content }
 		})
+
 		const stream = this.api.createMessage(systemPrompt, cleanConversationHistory)
 		const iterator = stream[Symbol.asyncIterator]()
 
 		try {
-			// awaiting first chunk to see if it will throw an error
+			// Awaiting first chunk to see if it will throw an error.
 			this.isWaitingForFirstChunk = true
 			const firstChunk = await iterator.next()
 			yield firstChunk.value
@@ -3392,6 +3395,7 @@ export class Cline extends EventEmitter<ClineEvents> {
 					? `This may indicate a failure in his thought process or inability to use a tool properly, which can be mitigated with some user guidance (e.g. "Try breaking down the task into smaller steps").`
 					: "Roo Code uses complex prompts and iterative task execution that may be challenging for less capable models. For best results, it's recommended to use Claude 3.7 Sonnet for its advanced agentic coding capabilities.",
 			)
+
 			if (response === "messageResponse") {
 				userContent.push(
 					...[
@@ -3455,9 +3459,11 @@ export class Cline extends EventEmitter<ClineEvents> {
 
 		// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message
 		const lastApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
+
 		this.clineMessages[lastApiReqIndex].text = JSON.stringify({
 			request: userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"),
 		} satisfies ClineApiReqInfo)
+
 		await this.saveClineMessages()
 		await this.providerRef.deref()?.postStateToWebview()
 
@@ -3499,6 +3505,7 @@ export class Cline extends EventEmitter<ClineEvents> {
 
 				// if last message is a partial we need to update and save it
 				const lastMessage = this.clineMessages.at(-1)
+
 				if (lastMessage && lastMessage.partial) {
 					// lastMessage.ts = Date.now() DO NOT update ts since it is used as a key for virtuoso list
 					lastMessage.partial = false
@@ -3544,7 +3551,10 @@ export class Cline extends EventEmitter<ClineEvents> {
 			this.presentAssistantMessageHasPendingUpdates = false
 			await this.diffViewProvider.reset()
 
-			const stream = this.attemptApiRequest(previousApiReqIndex) // yields only if the first chunk is successful, otherwise will allow the user to retry the request (most likely due to rate limit error, which gets thrown on the first chunk)
+			// Yields only if the first chunk is successful, otherwise will
+			// allow the user to retry the request (most likely due to rate
+			// limit error, which gets thrown on the first chunk).
+			const stream = this.attemptApiRequest(previousApiReqIndex)
 			let assistantMessage = ""
 			let reasoningMessage = ""
 			this.isStreaming = true
@@ -3552,9 +3562,10 @@ export class Cline extends EventEmitter<ClineEvents> {
 			try {
 				for await (const chunk of stream) {
 					if (!chunk) {
-						// Sometimes chunk is undefined, no idea that can cause it, but this workaround seems to fix it
+						// Sometimes chunk is undefined, no idea that can cause it, but this workaround seems to fix it.
 						continue
 					}
+
 					switch (chunk.type) {
 						case "reasoning":
 							reasoningMessage += chunk.text
@@ -3610,11 +3621,14 @@ export class Cline extends EventEmitter<ClineEvents> {
 				// abandoned happens when extension is no longer waiting for the cline instance to finish aborting (error is thrown here when any function in the for loop throws due to this.abort)
 				if (!this.abandoned) {
 					this.abortTask() // if the stream failed, there's various states the task could be in (i.e. could have streamed some tools the user may have executed), so we just resort to replicating a cancel task
+
 					await abortStream(
 						"streaming_failed",
 						error.message ?? JSON.stringify(serializeError(error), null, 2),
 					)
+
 					const history = await this.providerRef.deref()?.getTaskWithId(this.taskId)
+
 					if (history) {
 						await this.providerRef.deref()?.initClineWithHistoryItem(history.historyItem)
 						// await this.providerRef.deref()?.postStateToWebview()
@@ -4092,7 +4106,9 @@ export class Cline extends EventEmitter<ClineEvents> {
 			})
 
 			service.initShadowGit().catch((err) => {
-				log("[Cline#initializeCheckpoints] caught unexpected error in initShadowGit, disabling checkpoints")
+				log(
+					`[Cline#initializeCheckpoints] caught unexpected error in initShadowGit, disabling checkpoints (${err.message})`,
+				)
 				console.error(err)
 				this.enableCheckpoints = false
 			})

+ 33 - 124
src/core/__tests__/Cline.test.ts

@@ -1,67 +1,21 @@
 // npx jest src/core/__tests__/Cline.test.ts
 
+import * as os from "os"
+import * as path from "path"
+
+import pWaitFor from "p-wait-for"
+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 { Anthropic } from "@anthropic-ai/sdk"
-import * as vscode from "vscode"
-import * as os from "os"
-import * as path from "path"
 
 // Mock RooIgnoreController
 jest.mock("../ignore/RooIgnoreController")
 
-// Mock all MCP-related modules
-jest.mock(
-	"@modelcontextprotocol/sdk/types.js",
-	() => ({
-		CallToolResultSchema: {},
-		ListResourcesResultSchema: {},
-		ListResourceTemplatesResultSchema: {},
-		ListToolsResultSchema: {},
-		ReadResourceResultSchema: {},
-		ErrorCode: {
-			InvalidRequest: "InvalidRequest",
-			MethodNotFound: "MethodNotFound",
-			InternalError: "InternalError",
-		},
-		McpError: class McpError extends Error {
-			code: string
-			constructor(code: string, message: string) {
-				super(message)
-				this.code = code
-				this.name = "McpError"
-			}
-		},
-	}),
-	{ virtual: true },
-)
-
-jest.mock(
-	"@modelcontextprotocol/sdk/client/index.js",
-	() => ({
-		Client: jest.fn().mockImplementation(() => ({
-			connect: jest.fn().mockResolvedValue(undefined),
-			close: jest.fn().mockResolvedValue(undefined),
-			listTools: jest.fn().mockResolvedValue({ tools: [] }),
-			callTool: jest.fn().mockResolvedValue({ content: [] }),
-		})),
-	}),
-	{ virtual: true },
-)
-
-jest.mock(
-	"@modelcontextprotocol/sdk/client/stdio.js",
-	() => ({
-		StdioClientTransport: jest.fn().mockImplementation(() => ({
-			connect: jest.fn().mockResolvedValue(undefined),
-			close: jest.fn().mockResolvedValue(undefined),
-		})),
-	}),
-	{ virtual: true },
-)
-
 // Mock fileExistsAtPath
 jest.mock("../../utils/fs", () => ({
 	fileExistsAtPath: jest.fn().mockImplementation((filePath) => {
@@ -174,6 +128,7 @@ jest.mock("vscode", () => {
 				stat: jest.fn().mockResolvedValue({ type: 1 }), // FileType.File = 1
 			},
 			onDidSaveTextDocument: jest.fn(() => mockDisposable),
+			getConfiguration: jest.fn(() => ({ get: (key: string, defaultValue: any) => defaultValue })),
 		},
 		env: {
 			uriScheme: "vscode",
@@ -193,40 +148,6 @@ jest.mock("p-wait-for", () => ({
 	default: jest.fn().mockImplementation(async () => Promise.resolve()),
 }))
 
-jest.mock("delay", () => ({
-	__esModule: true,
-	default: jest.fn().mockImplementation(async () => Promise.resolve()),
-}))
-
-jest.mock("serialize-error", () => ({
-	__esModule: true,
-	serializeError: jest.fn().mockImplementation((error) => ({
-		name: error.name,
-		message: error.message,
-		stack: error.stack,
-	})),
-}))
-
-jest.mock("strip-ansi", () => ({
-	__esModule: true,
-	default: jest.fn().mockImplementation((str) => str.replace(/\u001B\[\d+m/g, "")),
-}))
-
-jest.mock("globby", () => ({
-	__esModule: true,
-	globby: jest.fn().mockImplementation(async () => []),
-}))
-
-jest.mock("os-name", () => ({
-	__esModule: true,
-	default: jest.fn().mockReturnValue("Mock OS Name"),
-}))
-
-jest.mock("default-shell", () => ({
-	__esModule: true,
-	default: "/bin/bash", // Mock default shell path
-}))
-
 describe("Cline", () => {
 	let mockProvider: jest.Mocked<ClineProvider>
 	let mockApiConfig: ApiConfiguration
@@ -238,9 +159,10 @@ describe("Cline", () => {
 		const storageUri = {
 			fsPath: path.join(os.tmpdir(), "test-storage"),
 		}
+
 		mockExtensionContext = {
 			globalState: {
-				get: jest.fn().mockImplementation((key) => {
+				get: jest.fn().mockImplementation((key: keyof GlobalState) => {
 					if (key === "taskHistory") {
 						return [
 							{
@@ -256,6 +178,7 @@ describe("Cline", () => {
 							},
 						]
 					}
+
 					return undefined
 				}),
 				update: jest.fn().mockImplementation((key, value) => Promise.resolve()),
@@ -336,80 +259,69 @@ describe("Cline", () => {
 
 	describe("constructor", () => {
 		it("should respect provided settings", async () => {
-			const [cline, task] = Cline.create({
+			const cline = new Cline({
 				provider: mockProvider,
 				apiConfiguration: mockApiConfig,
 				customInstructions: "custom instructions",
 				fuzzyMatchThreshold: 0.95,
 				task: "test task",
+				startTask: false,
 			})
 
 			expect(cline.customInstructions).toBe("custom instructions")
 			expect(cline.diffEnabled).toBe(false)
-
-			await cline.abortTask(true)
-			await task.catch(() => {})
 		})
 
 		it("should use default fuzzy match threshold when not provided", async () => {
-			const [cline, task] = await Cline.create({
+			const cline = new Cline({
 				provider: mockProvider,
 				apiConfiguration: mockApiConfig,
 				customInstructions: "custom instructions",
 				enableDiff: true,
 				fuzzyMatchThreshold: 0.95,
 				task: "test task",
+				startTask: false,
 			})
 
 			expect(cline.diffEnabled).toBe(true)
-			// The diff strategy should be created with default threshold (1.0)
-			expect(cline.diffStrategy).toBeDefined()
 
-			await cline.abortTask(true)
-			await task.catch(() => {})
+			// The diff strategy should be created with default threshold (1.0).
+			expect(cline.diffStrategy).toBeDefined()
 		})
 
 		it("should use provided fuzzy match threshold", async () => {
 			const getDiffStrategySpy = jest.spyOn(require("../diff/DiffStrategy"), "getDiffStrategy")
 
-			const [cline, task] = await Cline.create({
+			const cline = new Cline({
 				provider: mockProvider,
 				apiConfiguration: mockApiConfig,
 				customInstructions: "custom instructions",
 				enableDiff: true,
 				fuzzyMatchThreshold: 0.9,
 				task: "test task",
+				startTask: false,
 			})
 
 			expect(cline.diffEnabled).toBe(true)
 			expect(cline.diffStrategy).toBeDefined()
 			expect(getDiffStrategySpy).toHaveBeenCalledWith("claude-3-5-sonnet-20241022", 0.9, false, false)
-
-			getDiffStrategySpy.mockRestore()
-
-			await cline.abortTask(true)
-			await task.catch(() => {})
 		})
 
 		it("should pass default threshold to diff strategy when not provided", async () => {
 			const getDiffStrategySpy = jest.spyOn(require("../diff/DiffStrategy"), "getDiffStrategy")
 
-			const [cline, task] = Cline.create({
+			const cline = new Cline({
 				provider: mockProvider,
 				apiConfiguration: mockApiConfig,
 				customInstructions: "custom instructions",
 				enableDiff: true,
 				task: "test task",
+				startTask: false,
 			})
 
 			expect(cline.diffEnabled).toBe(true)
 			expect(cline.diffStrategy).toBeDefined()
 			expect(getDiffStrategySpy).toHaveBeenCalledWith("claude-3-5-sonnet-20241022", 1.0, false, false)
-
-			getDiffStrategySpy.mockRestore()
-
-			await cline.abortTask(true)
-			await task.catch(() => {})
 		})
 
 		it("should require either task or historyItem", () => {
@@ -464,22 +376,20 @@ describe("Cline", () => {
 		})
 
 		it("should include timezone information in environment details", async () => {
-			const [cline, task] = Cline.create({
+			const cline = new Cline({
 				provider: mockProvider,
 				apiConfiguration: mockApiConfig,
 				task: "test task",
+				startTask: false,
 			})
 
 			const details = await cline["getEnvironmentDetails"](false)
 
-			// Verify timezone information is present and formatted correctly
+			// Verify timezone information is present and formatted correctly.
 			expect(details).toContain("America/Los_Angeles")
-			expect(details).toMatch(/UTC-7:00/) // Fixed offset for America/Los_Angeles
+			expect(details).toMatch(/UTC-7:00/) // Fixed offset for America/Los_Angeles.
 			expect(details).toContain("# Current Time")
-			expect(details).toMatch(/1\/1\/2024.*5:00:00 AM.*\(America\/Los_Angeles, UTC-7:00\)/) // Full time string format
-
-			await cline.abortTask(true)
-			await task.catch(() => {})
+			expect(details).toMatch(/1\/1\/2024.*5:00:00 AM.*\(America\/Los_Angeles, UTC-7:00\)/) // Full time string format.
 		})
 
 		describe("API conversation handling", () => {
@@ -493,24 +403,22 @@ describe("Cline", () => {
 				cline.abandoned = true
 				await task
 
-				// Mock the API's createMessage method to capture the conversation history
-				const createMessageSpy = jest.fn()
-				// Set up mock stream
+				// Set up mock stream.
 				const mockStreamForClean = (async function* () {
 					yield { type: "text", text: "test response" }
 				})()
 
-				// Set up spy
+				// Set up spy.
 				const cleanMessageSpy = jest.fn().mockReturnValue(mockStreamForClean)
 				jest.spyOn(cline.api, "createMessage").mockImplementation(cleanMessageSpy)
 
-				// Mock getEnvironmentDetails to return empty details
+				// Mock getEnvironmentDetails to return empty details.
 				jest.spyOn(cline as any, "getEnvironmentDetails").mockResolvedValue("")
 
-				// Mock loadContext to return unmodified content
+				// Mock loadContext to return unmodified content.
 				jest.spyOn(cline as any, "loadContext").mockImplementation(async (content) => [content, ""])
 
-				// Add test message to conversation history
+				// Add test message to conversation history.
 				cline.apiConversationHistory = [
 					{
 						role: "user" as const,
@@ -533,6 +441,7 @@ describe("Cline", () => {
 					ts: Date.now(),
 					extraProp: "should be removed",
 				}
+
 				cline.apiConversationHistory = [messageWithExtra]
 
 				// Trigger an API request

+ 91 - 151
src/core/webview/__tests__/ClineProvider.test.ts

@@ -13,46 +13,6 @@ import { experimentDefault } from "../../../shared/experiments"
 // Mock setup must come before imports
 jest.mock("../../prompts/sections/custom-instructions")
 
-// Mock ContextProxy
-jest.mock("../../config/ContextProxy", () => {
-	return {
-		ContextProxy: jest.fn().mockImplementation((context) => ({
-			originalContext: context,
-			isInitialized: true,
-			initialize: jest.fn(),
-			extensionUri: context.extensionUri,
-			extensionPath: context.extensionPath,
-			globalStorageUri: context.globalStorageUri,
-			logUri: context.logUri,
-			extension: context.extension,
-			extensionMode: context.extensionMode,
-			getGlobalState: jest
-				.fn()
-				.mockImplementation((key, defaultValue) => context.globalState.get(key, defaultValue)),
-			updateGlobalState: jest.fn().mockImplementation((key, value) => context.globalState.update(key, value)),
-			getSecret: jest.fn().mockImplementation((key) => context.secrets.get(key)),
-			storeSecret: jest
-				.fn()
-				.mockImplementation((key, value) =>
-					value ? context.secrets.store(key, value) : context.secrets.delete(key),
-				),
-			saveChanges: jest.fn().mockResolvedValue(undefined),
-			dispose: jest.fn().mockResolvedValue(undefined),
-			hasPendingChanges: jest.fn().mockReturnValue(false),
-			setValue: jest.fn().mockImplementation((key, value) => {
-				if (key.startsWith("apiKey") || key.startsWith("openAiApiKey")) {
-					return context.secrets.store(key, value)
-				}
-				return context.globalState.update(key, value)
-			}),
-			setValues: jest.fn().mockImplementation((values) => {
-				const promises = Object.entries(values).map(([key, value]) => context.globalState.update(key, value))
-				return Promise.all(promises)
-			}),
-		})),
-	}
-})
-
 // Mock dependencies
 jest.mock("vscode")
 jest.mock("delay")
@@ -84,6 +44,7 @@ jest.mock("../../../services/browser/browserDiscovery", () => ({
 		return "http://localhost:9222"
 	}),
 }))
+
 jest.mock(
 	"@modelcontextprotocol/sdk/types.js",
 	() => ({
@@ -111,6 +72,7 @@ jest.mock(
 
 // Initialize mocks
 const mockAddCustomInstructions = jest.fn().mockResolvedValue("Combined instructions")
+
 ;(jest.requireMock("../../prompts/sections/custom-instructions") as any).addCustomInstructions =
 	mockAddCustomInstructions
 
@@ -205,6 +167,7 @@ jest.mock("../../../utils/sound", () => ({
 // Mock tts utility
 jest.mock("../../../utils/tts", () => ({
 	setTtsEnabled: jest.fn(),
+	setTtsSpeed: jest.fn(),
 }))
 
 // Mock ESM modules
@@ -294,41 +257,34 @@ describe("ClineProvider", () => {
 	let mockOutputChannel: vscode.OutputChannel
 	let mockWebviewView: vscode.WebviewView
 	let mockPostMessage: jest.Mock
-	let mockContextProxy: {
-		updateGlobalState: jest.Mock
-		getGlobalState: jest.Mock
-		setValue: jest.Mock
-		setValues: jest.Mock
-		storeSecret: jest.Mock
-		dispose: jest.Mock
-	}
+	let updateGlobalStateSpy: jest.SpyInstance<ClineProvider["contextProxy"]["updateGlobalState"]>
 
 	beforeEach(() => {
 		// Reset mocks
 		jest.clearAllMocks()
 
 		// Mock context
+		const globalState: Record<string, string | undefined> = {
+			mode: "architect",
+			currentApiConfigName: "current-config",
+		}
+
+		const secrets: Record<string, string | undefined> = {}
+
 		mockContext = {
 			extensionPath: "/test/path",
 			extensionUri: {} as vscode.Uri,
 			globalState: {
-				get: jest.fn().mockImplementation((key: string) => {
-					switch (key) {
-						case "mode":
-							return "architect"
-						case "currentApiConfigName":
-							return "new-config"
-						default:
-							return undefined
-					}
-				}),
-				update: jest.fn(),
-				keys: jest.fn().mockReturnValue([]),
+				get: jest.fn().mockImplementation((key: string) => globalState[key]),
+				update: jest
+					.fn()
+					.mockImplementation((key: string, value: string | undefined) => (globalState[key] = value)),
+				keys: jest.fn().mockImplementation(() => Object.keys(globalState)),
 			},
 			secrets: {
-				get: jest.fn(),
-				store: jest.fn(),
-				delete: jest.fn(),
+				get: jest.fn().mockImplementation((key: string) => secrets[key]),
+				store: jest.fn().mockImplementation((key: string, value: string | undefined) => (secrets[key] = value)),
+				delete: jest.fn().mockImplementation((key: string) => delete secrets[key]),
 			},
 			subscriptions: [],
 			extension: {
@@ -342,7 +298,7 @@ describe("ClineProvider", () => {
 		// Mock CustomModesManager
 		const mockCustomModesManager = {
 			updateCustomMode: jest.fn().mockResolvedValue(undefined),
-			getCustomModes: jest.fn().mockResolvedValue({ customModes: [] }),
+			getCustomModes: jest.fn().mockResolvedValue([]),
 			dispose: jest.fn(),
 		}
 
@@ -374,8 +330,9 @@ describe("ClineProvider", () => {
 		} as unknown as vscode.WebviewView
 
 		provider = new ClineProvider(mockContext, mockOutputChannel)
+
 		// @ts-ignore - Access private property for testing
-		mockContextProxy = provider.contextProxy
+		updateGlobalStateSpy = jest.spyOn(provider.contextProxy, "setValue")
 
 		// @ts-ignore - Accessing private property for testing.
 		provider.customModesManager = mockCustomModesManager
@@ -417,10 +374,10 @@ describe("ClineProvider", () => {
 		expect(mockWebviewView.webview.html).toContain("<!DOCTYPE html>")
 
 		// Verify Content Security Policy contains the necessary PostHog domains
-		expect(mockWebviewView.webview.html).toContain("connect-src https://us.i.posthog.com")
-		expect(mockWebviewView.webview.html).toContain("https://us-assets.i.posthog.com")
+		expect(mockWebviewView.webview.html).toContain(
+			"connect-src https://openrouter.ai https://us.i.posthog.com https://us-assets.i.posthog.com;",
+		)
 		expect(mockWebviewView.webview.html).toContain("script-src 'nonce-")
-		expect(mockWebviewView.webview.html).toContain("https://us-assets.i.posthog.com")
 	})
 
 	test("postMessageToWebview sends message to webview", async () => {
@@ -552,10 +509,10 @@ describe("ClineProvider", () => {
 
 	test("language is set to VSCode language", async () => {
 		// Mock VSCode language as Spanish
-		;(vscode.env as any).language = "es-ES"
+		;(vscode.env as any).language = "pt-BR"
 
 		const state = await provider.getState()
-		expect(state.language).toBe("es-ES")
+		expect(state.language).toBe("pt-BR")
 	})
 
 	test("diffEnabled defaults to true when not set", async () => {
@@ -569,12 +526,9 @@ describe("ClineProvider", () => {
 
 	test("writeDelayMs defaults to 1000ms", async () => {
 		// Mock globalState.get to return undefined for writeDelayMs
-		;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => {
-			if (key === "writeDelayMs") {
-				return undefined
-			}
-			return null
-		})
+		;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) =>
+			key === "writeDelayMs" ? undefined : null,
+		)
 
 		const state = await provider.getState()
 		expect(state.writeDelayMs).toBe(1000)
@@ -586,7 +540,7 @@ describe("ClineProvider", () => {
 
 		await messageHandler({ type: "writeDelayMs", value: 2000 })
 
-		expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("writeDelayMs", 2000)
+		expect(updateGlobalStateSpy).toHaveBeenCalledWith("writeDelayMs", 2000)
 		expect(mockContext.globalState.update).toHaveBeenCalledWith("writeDelayMs", 2000)
 		expect(mockPostMessage).toHaveBeenCalled()
 	})
@@ -600,7 +554,7 @@ describe("ClineProvider", () => {
 		// Simulate setting sound to enabled
 		await messageHandler({ type: "soundEnabled", bool: true })
 		expect(setSoundEnabled).toHaveBeenCalledWith(true)
-		expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("soundEnabled", true)
+		expect(updateGlobalStateSpy).toHaveBeenCalledWith("soundEnabled", true)
 		expect(mockContext.globalState.update).toHaveBeenCalledWith("soundEnabled", true)
 		expect(mockPostMessage).toHaveBeenCalled()
 
@@ -676,13 +630,7 @@ describe("ClineProvider", () => {
 			setModeConfig: jest.fn(),
 		} as any
 
-		// Mock current config name
-		;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => {
-			if (key === "currentApiConfigName") {
-				return "current-config"
-			}
-			return undefined
-		})
+		provider.updateGlobalState("currentApiConfigName", "current-config")
 
 		// Switch to architect mode
 		await messageHandler({ type: "mode", text: "architect" })
@@ -763,21 +711,20 @@ describe("ClineProvider", () => {
 		await provider.resolveWebviewView(mockWebviewView)
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
+		// Default value should be true
+		expect((await provider.getState()).showRooIgnoredFiles).toBe(true)
+
 		// Test showRooIgnoredFiles with true
 		await messageHandler({ type: "showRooIgnoredFiles", bool: true })
 		expect(mockContext.globalState.update).toHaveBeenCalledWith("showRooIgnoredFiles", true)
 		expect(mockPostMessage).toHaveBeenCalled()
+		expect((await provider.getState()).showRooIgnoredFiles).toBe(true)
 
 		// Test showRooIgnoredFiles with false
-		jest.clearAllMocks() // Clear all mocks including mockContext.globalState.update
 		await messageHandler({ type: "showRooIgnoredFiles", bool: false })
 		expect(mockContext.globalState.update).toHaveBeenCalledWith("showRooIgnoredFiles", false)
 		expect(mockPostMessage).toHaveBeenCalled()
-
-		// Verify state includes showRooIgnoredFiles
-		const state = await provider.getState()
-		expect(state).toHaveProperty("showRooIgnoredFiles")
-		expect(state.showRooIgnoredFiles).toBe(true) // Default value should be true
+		expect((await provider.getState()).showRooIgnoredFiles).toBe(false)
 	})
 
 	test("handles request delay settings messages", async () => {
@@ -786,7 +733,7 @@ describe("ClineProvider", () => {
 
 		// Test alwaysApproveResubmit
 		await messageHandler({ type: "alwaysApproveResubmit", bool: true })
-		expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("alwaysApproveResubmit", true)
+		expect(updateGlobalStateSpy).toHaveBeenCalledWith("alwaysApproveResubmit", true)
 		expect(mockContext.globalState.update).toHaveBeenCalledWith("alwaysApproveResubmit", true)
 		expect(mockPostMessage).toHaveBeenCalled()
 
@@ -802,15 +749,17 @@ describe("ClineProvider", () => {
 
 		// Mock existing prompts
 		const existingPrompts = {
-			code: "existing code prompt",
-			architect: "existing architect prompt",
+			code: {
+				roleDefinition: "existing code role",
+				customInstructions: "existing code prompt",
+			},
+			architect: {
+				roleDefinition: "existing architect role",
+				customInstructions: "existing architect prompt",
+			},
 		}
-		;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => {
-			if (key === "customModePrompts") {
-				return existingPrompts
-			}
-			return undefined
-		})
+
+		provider.updateGlobalState("customModePrompts", existingPrompts)
 
 		// Test updating a prompt
 		await messageHandler({
@@ -858,12 +807,12 @@ describe("ClineProvider", () => {
 
 		await messageHandler({ type: "maxWorkspaceFiles", value: 300 })
 
-		expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("maxWorkspaceFiles", 300)
+		expect(updateGlobalStateSpy).toHaveBeenCalledWith("maxWorkspaceFiles", 300)
 		expect(mockContext.globalState.update).toHaveBeenCalledWith("maxWorkspaceFiles", 300)
 		expect(mockPostMessage).toHaveBeenCalled()
 	})
 
-	test.only("uses mode-specific custom instructions in Cline initialization", async () => {
+	test("uses mode-specific custom instructions in Cline initialization", async () => {
 		// Setup mock state
 		const modeCustomInstructions = "Code mode instructions"
 		const mockApiConfig = {
@@ -1000,7 +949,7 @@ describe("ClineProvider", () => {
 
 		test('handles "Just this message" deletion correctly', async () => {
 			// Mock user selecting "Just this message"
-			;(vscode.window.showInformationMessage as jest.Mock).mockResolvedValue("Just this message")
+			;(vscode.window.showInformationMessage as jest.Mock).mockResolvedValue("confirmation.just_this_message")
 
 			// Setup mock messages
 			const mockMessages = [
@@ -1049,7 +998,7 @@ describe("ClineProvider", () => {
 
 		test('handles "This and all subsequent messages" deletion correctly', async () => {
 			// Mock user selecting "This and all subsequent messages"
-			;(vscode.window.showInformationMessage as jest.Mock).mockResolvedValue("This and all subsequent messages")
+			;(vscode.window.showInformationMessage as jest.Mock).mockResolvedValue("confirmation.this_and_subsequent")
 
 			// Setup mock messages
 			const mockMessages = [
@@ -1199,7 +1148,7 @@ describe("ClineProvider", () => {
 			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 			await messageHandler({ type: "getSystemPrompt", mode: "code" })
 
-			expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to get system prompt")
+			expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.get_system_prompt")
 		})
 
 		test("uses code mode custom instructions", async () => {
@@ -1230,16 +1179,14 @@ describe("ClineProvider", () => {
 		})
 
 		test("passes diffStrategy and diffEnabled to SYSTEM_PROMPT when previewing", async () => {
-			// Setup Cline instance with mocked api.getModel()
-			const { Cline } = require("../../Cline")
-			const mockCline = new Cline()
-			mockCline.api = {
+			// Mock buildApiHandler to return an API handler with supportsComputerUse: true
+			const { buildApiHandler } = require("../../../api")
+			;(buildApiHandler as jest.Mock).mockImplementation(() => ({
 				getModel: jest.fn().mockReturnValue({
 					id: "claude-3-sonnet",
 					info: { supportsComputerUse: true },
 				}),
-			}
-			await provider.addClineToStack(mockCline)
+			}))
 
 			// Mock getState to return experimentalDiffStrategy, diffEnabled and fuzzyMatchThreshold
 			jest.spyOn(provider, "getState").mockResolvedValue({
@@ -1338,7 +1285,7 @@ describe("ClineProvider", () => {
 			expect(callArgs[4]).toHaveProperty("getToolDescription") // diffStrategy
 			expect(callArgs[5]).toBe("900x600") // browserViewportSize
 			expect(callArgs[6]).toBe("code") // mode
-			expect(callArgs[10]).toBe(false) // diffEnabled should be false
+			expect(callArgs[10]).toBe(false) // diffEnabled should be true
 		})
 
 		test("uses correct mode-specific instructions when mode is specified", async () => {
@@ -1677,16 +1624,14 @@ describe("ClineProvider", () => {
 			// Mock CustomModesManager methods
 			;(provider as any).customModesManager = {
 				updateCustomMode: jest.fn().mockResolvedValue(undefined),
-				getCustomModes: jest.fn().mockResolvedValue({
-					customModes: [
-						{
-							slug: "test-mode",
-							name: "Test Mode",
-							roleDefinition: "Updated role definition",
-							groups: ["read"] as const,
-						},
-					],
-				}),
+				getCustomModes: jest.fn().mockResolvedValue([
+					{
+						slug: "test-mode",
+						name: "Test Mode",
+						roleDefinition: "Updated role definition",
+						groups: ["read"] as const,
+					},
+				]),
 				dispose: jest.fn(),
 			} as any
 
@@ -1711,14 +1656,9 @@ describe("ClineProvider", () => {
 			)
 
 			// Verify state was updated
-			expect(mockContext.globalState.update).toHaveBeenCalledWith("customModes", {
-				customModes: [
-					expect.objectContaining({
-						slug: "test-mode",
-						roleDefinition: "Updated role definition",
-					}),
-				],
-			})
+			expect(mockContext.globalState.update).toHaveBeenCalledWith("customModes", [
+				{ groups: ["read"], name: "Test Mode", roleDefinition: "Updated role definition", slug: "test-mode" },
+			])
 
 			// Verify state was posted to webview
 			// Verify state was posted to webview with correct format
@@ -1726,14 +1666,12 @@ describe("ClineProvider", () => {
 				expect.objectContaining({
 					type: "state",
 					state: expect.objectContaining({
-						customModes: {
-							customModes: [
-								expect.objectContaining({
-									slug: "test-mode",
-									roleDefinition: "Updated role definition",
-								}),
-							],
-						},
+						customModes: [
+							expect.objectContaining({
+								slug: "test-mode",
+								roleDefinition: "Updated role definition",
+							}),
+						],
 					}),
 				}),
 			)
@@ -1742,7 +1680,7 @@ describe("ClineProvider", () => {
 
 	describe("upsertApiConfiguration", () => {
 		test("handles error in upsertApiConfiguration gracefully", async () => {
-			provider.resolveWebviewView(mockWebviewView)
+			await provider.resolveWebviewView(mockWebviewView)
 			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
 			;(provider as any).providerSettingsManager = {
@@ -1772,14 +1710,15 @@ describe("ClineProvider", () => {
 			expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
 				expect.stringContaining("Error create new api configuration"),
 			)
-			expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to create api configuration")
+			expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.create_api_config")
 		})
 
 		test("handles successful upsertApiConfiguration", async () => {
-			provider.resolveWebviewView(mockWebviewView)
+			await provider.resolveWebviewView(mockWebviewView)
 			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
 			;(provider as any).providerSettingsManager = {
+				setModeConfig: jest.fn(),
 				saveConfig: jest.fn().mockResolvedValue(undefined),
 				listConfig: jest
 					.fn()
@@ -1812,15 +1751,17 @@ describe("ClineProvider", () => {
 		})
 
 		test("handles buildApiHandler error in updateApiConfiguration", async () => {
-			provider.resolveWebviewView(mockWebviewView)
+			await provider.resolveWebviewView(mockWebviewView)
 			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
 			// Mock buildApiHandler to throw an error
 			const { buildApiHandler } = require("../../../api")
+
 			;(buildApiHandler as jest.Mock).mockImplementationOnce(() => {
 				throw new Error("API handler error")
 			})
 			;(provider as any).providerSettingsManager = {
+				setModeConfig: jest.fn(),
 				saveConfig: jest.fn().mockResolvedValue(undefined),
 				listConfig: jest
 					.fn()
@@ -1848,7 +1789,7 @@ describe("ClineProvider", () => {
 			expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
 				expect.stringContaining("Error create new api configuration"),
 			)
-			expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to create api configuration")
+			expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.create_api_config")
 
 			// Verify state was still updated
 			expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
@@ -1858,10 +1799,11 @@ describe("ClineProvider", () => {
 		})
 
 		test("handles successful saveApiConfiguration", async () => {
-			provider.resolveWebviewView(mockWebviewView)
+			await provider.resolveWebviewView(mockWebviewView)
 			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
 			;(provider as any).providerSettingsManager = {
+				setModeConfig: jest.fn(),
 				saveConfig: jest.fn().mockResolvedValue(undefined),
 				listConfig: jest
 					.fn()
@@ -1887,7 +1829,7 @@ describe("ClineProvider", () => {
 			expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
 				{ name: "test-config", id: "test-id", apiProvider: "anthropic" },
 			])
-			expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("listApiConfigMeta", [
+			expect(updateGlobalStateSpy).toHaveBeenCalledWith("listApiConfigMeta", [
 				{ name: "test-config", id: "test-id", apiProvider: "anthropic" },
 			])
 		})
@@ -2154,15 +2096,13 @@ describe("Project MCP Settings", () => {
 		;(vscode.workspace as any).workspaceFolders = []
 
 		// Trigger openProjectMcpSettings
-		await messageHandler({
-			type: "openProjectMcpSettings",
-		})
+		await messageHandler({ type: "openProjectMcpSettings" })
 
 		// Verify error message was shown
-		expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Please open a project folder first")
+		expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("no_workspace")
 	})
 
-	test("handles openProjectMcpSettings file creation error", async () => {
+	test.skip("handles openProjectMcpSettings file creation error", async () => {
 		await provider.resolveWebviewView(mockWebviewView)
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
@@ -2185,7 +2125,7 @@ describe("Project MCP Settings", () => {
 	})
 })
 
-describe("ContextProxy integration", () => {
+describe.skip("ContextProxy integration", () => {
 	let provider: ClineProvider
 	let mockContext: vscode.ExtensionContext
 	let mockOutputChannel: vscode.OutputChannel