ClineProvider.test.ts 81 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423
  1. // npx jest core/webview/__tests__/ClineProvider.test.ts
  2. import Anthropic from "@anthropic-ai/sdk"
  3. import * as vscode from "vscode"
  4. import axios from "axios"
  5. import { type ProviderSettingsEntry, type ClineMessage, ORGANIZATION_ALLOW_ALL } from "@roo-code/types"
  6. import { TelemetryService } from "@roo-code/telemetry"
  7. import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage"
  8. import { defaultModeSlug } from "../../../shared/modes"
  9. import { experimentDefault } from "../../../shared/experiments"
  10. import { setTtsEnabled } from "../../../utils/tts"
  11. import { ContextProxy } from "../../config/ContextProxy"
  12. import { Task, TaskOptions } from "../../task/Task"
  13. import { ClineProvider } from "../ClineProvider"
  14. // Mock setup must come before imports
  15. jest.mock("../../prompts/sections/custom-instructions")
  16. jest.mock("vscode")
  17. jest.mock("delay")
  18. jest.mock("p-wait-for", () => ({
  19. __esModule: true,
  20. default: jest.fn().mockResolvedValue(undefined),
  21. }))
  22. jest.mock("fs/promises", () => ({
  23. mkdir: jest.fn(),
  24. writeFile: jest.fn(),
  25. readFile: jest.fn(),
  26. unlink: jest.fn(),
  27. rmdir: jest.fn(),
  28. }))
  29. jest.mock("axios", () => ({
  30. get: jest.fn().mockResolvedValue({ data: { data: [] } }),
  31. post: jest.fn(),
  32. }))
  33. jest.mock(
  34. "@modelcontextprotocol/sdk/types.js",
  35. () => ({
  36. CallToolResultSchema: {},
  37. ListResourcesResultSchema: {},
  38. ListResourceTemplatesResultSchema: {},
  39. ListToolsResultSchema: {},
  40. ReadResourceResultSchema: {},
  41. ErrorCode: {
  42. InvalidRequest: "InvalidRequest",
  43. MethodNotFound: "MethodNotFound",
  44. InternalError: "InternalError",
  45. },
  46. McpError: class McpError extends Error {
  47. code: string
  48. constructor(code: string, message: string) {
  49. super(message)
  50. this.code = code
  51. this.name = "McpError"
  52. }
  53. },
  54. }),
  55. { virtual: true },
  56. )
  57. jest.mock("../../../services/browser/BrowserSession", () => ({
  58. BrowserSession: jest.fn().mockImplementation(() => ({
  59. testConnection: jest.fn().mockImplementation(async (url) => {
  60. if (url === "http://localhost:9222") {
  61. return {
  62. success: true,
  63. message: "Successfully connected to Chrome",
  64. endpoint: "ws://localhost:9222/devtools/browser/123",
  65. }
  66. } else {
  67. return {
  68. success: false,
  69. message: "Failed to connect to Chrome",
  70. endpoint: undefined,
  71. }
  72. }
  73. }),
  74. })),
  75. }))
  76. jest.mock("../../../services/browser/browserDiscovery", () => ({
  77. discoverChromeHostUrl: jest.fn().mockImplementation(async () => {
  78. return "http://localhost:9222"
  79. }),
  80. tryChromeHostUrl: jest.fn().mockImplementation(async (url) => {
  81. return url === "http://localhost:9222"
  82. }),
  83. }))
  84. const mockAddCustomInstructions = jest.fn().mockResolvedValue("Combined instructions")
  85. ;(jest.requireMock("../../prompts/sections/custom-instructions") as any).addCustomInstructions =
  86. mockAddCustomInstructions
  87. jest.mock("delay", () => {
  88. const delayFn = (_ms: number) => Promise.resolve()
  89. delayFn.createDelay = () => delayFn
  90. delayFn.reject = () => Promise.reject(new Error("Delay rejected"))
  91. delayFn.range = () => Promise.resolve()
  92. return delayFn
  93. })
  94. // MCP-related modules are mocked once above (lines 87-109).
  95. jest.mock(
  96. "@modelcontextprotocol/sdk/client/index.js",
  97. () => ({
  98. Client: jest.fn().mockImplementation(() => ({
  99. connect: jest.fn().mockResolvedValue(undefined),
  100. close: jest.fn().mockResolvedValue(undefined),
  101. listTools: jest.fn().mockResolvedValue({ tools: [] }),
  102. callTool: jest.fn().mockResolvedValue({ content: [] }),
  103. })),
  104. }),
  105. { virtual: true },
  106. )
  107. jest.mock(
  108. "@modelcontextprotocol/sdk/client/stdio.js",
  109. () => ({
  110. StdioClientTransport: jest.fn().mockImplementation(() => ({
  111. connect: jest.fn().mockResolvedValue(undefined),
  112. close: jest.fn().mockResolvedValue(undefined),
  113. })),
  114. }),
  115. { virtual: true },
  116. )
  117. jest.mock("vscode", () => ({
  118. ExtensionContext: jest.fn(),
  119. OutputChannel: jest.fn(),
  120. WebviewView: jest.fn(),
  121. Uri: {
  122. joinPath: jest.fn(),
  123. file: jest.fn(),
  124. },
  125. CodeActionKind: {
  126. QuickFix: { value: "quickfix" },
  127. RefactorRewrite: { value: "refactor.rewrite" },
  128. },
  129. window: {
  130. showInformationMessage: jest.fn(),
  131. showErrorMessage: jest.fn(),
  132. },
  133. workspace: {
  134. getConfiguration: jest.fn().mockReturnValue({
  135. get: jest.fn().mockReturnValue([]),
  136. update: jest.fn(),
  137. }),
  138. onDidChangeConfiguration: jest.fn().mockImplementation(() => ({
  139. dispose: jest.fn(),
  140. })),
  141. onDidSaveTextDocument: jest.fn(() => ({ dispose: jest.fn() })),
  142. onDidChangeTextDocument: jest.fn(() => ({ dispose: jest.fn() })),
  143. onDidOpenTextDocument: jest.fn(() => ({ dispose: jest.fn() })),
  144. onDidCloseTextDocument: jest.fn(() => ({ dispose: jest.fn() })),
  145. },
  146. env: {
  147. uriScheme: "vscode",
  148. language: "en",
  149. },
  150. ExtensionMode: {
  151. Production: 1,
  152. Development: 2,
  153. Test: 3,
  154. },
  155. }))
  156. jest.mock("../../../utils/tts", () => ({
  157. setTtsEnabled: jest.fn(),
  158. setTtsSpeed: jest.fn(),
  159. }))
  160. jest.mock("../../../api", () => ({
  161. buildApiHandler: jest.fn(),
  162. }))
  163. jest.mock("../../prompts/system", () => ({
  164. SYSTEM_PROMPT: jest.fn().mockImplementation(async () => "mocked system prompt"),
  165. codeMode: "code",
  166. }))
  167. jest.mock("../../../integrations/workspace/WorkspaceTracker", () => {
  168. return jest.fn().mockImplementation(() => ({
  169. initializeFilePaths: jest.fn(),
  170. dispose: jest.fn(),
  171. }))
  172. })
  173. jest.mock("../../task/Task", () => ({
  174. Task: jest
  175. .fn()
  176. .mockImplementation(
  177. (_provider, _apiConfiguration, _customInstructions, _diffEnabled, _fuzzyMatchThreshold, _task, taskId) => ({
  178. api: undefined,
  179. abortTask: jest.fn(),
  180. handleWebviewAskResponse: jest.fn(),
  181. clineMessages: [],
  182. apiConversationHistory: [],
  183. overwriteClineMessages: jest.fn(),
  184. overwriteApiConversationHistory: jest.fn(),
  185. getTaskNumber: jest.fn().mockReturnValue(0),
  186. setTaskNumber: jest.fn(),
  187. setParentTask: jest.fn(),
  188. setRootTask: jest.fn(),
  189. taskId: taskId || "test-task-id",
  190. }),
  191. ),
  192. }))
  193. jest.mock("../../../integrations/misc/extract-text", () => ({
  194. extractTextFromFile: jest.fn().mockImplementation(async (_filePath: string) => {
  195. const content = "const x = 1;\nconst y = 2;\nconst z = 3;"
  196. const lines = content.split("\n")
  197. return lines.map((line, index) => `${index + 1} | ${line}`).join("\n")
  198. }),
  199. }))
  200. afterAll(() => {
  201. jest.restoreAllMocks()
  202. })
  203. describe("ClineProvider", () => {
  204. let defaultTaskOptions: TaskOptions
  205. let provider: ClineProvider
  206. let mockContext: vscode.ExtensionContext
  207. let mockOutputChannel: vscode.OutputChannel
  208. let mockWebviewView: vscode.WebviewView
  209. let mockPostMessage: jest.Mock
  210. let updateGlobalStateSpy: jest.SpyInstance<ClineProvider["contextProxy"]["updateGlobalState"]>
  211. beforeEach(() => {
  212. jest.clearAllMocks()
  213. if (!TelemetryService.hasInstance()) {
  214. TelemetryService.createInstance([])
  215. }
  216. const globalState: Record<string, string | undefined> = {
  217. mode: "architect",
  218. currentApiConfigName: "current-config",
  219. }
  220. const secrets: Record<string, string | undefined> = {}
  221. mockContext = {
  222. extensionPath: "/test/path",
  223. extensionUri: {} as vscode.Uri,
  224. globalState: {
  225. get: jest.fn().mockImplementation((key: string) => globalState[key]),
  226. update: jest
  227. .fn()
  228. .mockImplementation((key: string, value: string | undefined) => (globalState[key] = value)),
  229. keys: jest.fn().mockImplementation(() => Object.keys(globalState)),
  230. },
  231. secrets: {
  232. get: jest.fn().mockImplementation((key: string) => secrets[key]),
  233. store: jest.fn().mockImplementation((key: string, value: string | undefined) => (secrets[key] = value)),
  234. delete: jest.fn().mockImplementation((key: string) => delete secrets[key]),
  235. },
  236. subscriptions: [],
  237. extension: {
  238. packageJSON: { version: "1.0.0" },
  239. },
  240. globalStorageUri: {
  241. fsPath: "/test/storage/path",
  242. },
  243. } as unknown as vscode.ExtensionContext
  244. // Mock CustomModesManager
  245. const mockCustomModesManager = {
  246. updateCustomMode: jest.fn().mockResolvedValue(undefined),
  247. getCustomModes: jest.fn().mockResolvedValue([]),
  248. dispose: jest.fn(),
  249. }
  250. // Mock output channel
  251. mockOutputChannel = {
  252. appendLine: jest.fn(),
  253. clear: jest.fn(),
  254. dispose: jest.fn(),
  255. } as unknown as vscode.OutputChannel
  256. // Mock webview
  257. mockPostMessage = jest.fn()
  258. mockWebviewView = {
  259. webview: {
  260. postMessage: mockPostMessage,
  261. html: "",
  262. options: {},
  263. onDidReceiveMessage: jest.fn(),
  264. asWebviewUri: jest.fn(),
  265. },
  266. visible: true,
  267. onDidDispose: jest.fn().mockImplementation((callback) => {
  268. callback()
  269. return { dispose: jest.fn() }
  270. }),
  271. onDidChangeVisibility: jest.fn().mockImplementation(() => ({ dispose: jest.fn() })),
  272. } as unknown as vscode.WebviewView
  273. provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
  274. defaultTaskOptions = {
  275. provider,
  276. apiConfiguration: {
  277. apiProvider: "openrouter",
  278. },
  279. }
  280. // @ts-ignore - Access private property for testing
  281. updateGlobalStateSpy = jest.spyOn(provider.contextProxy, "setValue")
  282. // @ts-ignore - Accessing private property for testing.
  283. provider.customModesManager = mockCustomModesManager
  284. })
  285. test("constructor initializes correctly", () => {
  286. expect(provider).toBeInstanceOf(ClineProvider)
  287. // Since getVisibleInstance returns the last instance where view.visible is true
  288. // @ts-ignore - accessing private property for testing
  289. provider.view = mockWebviewView
  290. expect(ClineProvider.getVisibleInstance()).toBe(provider)
  291. })
  292. test("resolveWebviewView sets up webview correctly", async () => {
  293. await provider.resolveWebviewView(mockWebviewView)
  294. expect(mockWebviewView.webview.options).toEqual({
  295. enableScripts: true,
  296. localResourceRoots: [mockContext.extensionUri],
  297. })
  298. expect(mockWebviewView.webview.html).toContain("<!DOCTYPE html>")
  299. })
  300. test("resolveWebviewView sets up webview correctly in development mode even if local server is not running", async () => {
  301. provider = new ClineProvider(
  302. { ...mockContext, extensionMode: vscode.ExtensionMode.Development },
  303. mockOutputChannel,
  304. "sidebar",
  305. new ContextProxy(mockContext),
  306. )
  307. ;(axios.get as jest.Mock).mockRejectedValueOnce(new Error("Network error"))
  308. await provider.resolveWebviewView(mockWebviewView)
  309. expect(mockWebviewView.webview.options).toEqual({
  310. enableScripts: true,
  311. localResourceRoots: [mockContext.extensionUri],
  312. })
  313. expect(mockWebviewView.webview.html).toContain("<!DOCTYPE html>")
  314. // Verify Content Security Policy contains the necessary PostHog domains
  315. expect(mockWebviewView.webview.html).toContain(
  316. "connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com",
  317. )
  318. // Extract the script-src directive section and verify required security elements
  319. const html = mockWebviewView.webview.html
  320. const scriptSrcMatch = html.match(/script-src[^;]*;/)
  321. expect(scriptSrcMatch).not.toBeNull()
  322. expect(scriptSrcMatch![0]).toContain("'nonce-")
  323. // Verify wasm-unsafe-eval is present for Shiki syntax highlighting
  324. expect(scriptSrcMatch![0]).toContain("'wasm-unsafe-eval'")
  325. })
  326. test("postMessageToWebview sends message to webview", async () => {
  327. await provider.resolveWebviewView(mockWebviewView)
  328. const mockState: ExtensionState = {
  329. version: "1.0.0",
  330. clineMessages: [],
  331. taskHistory: [],
  332. shouldShowAnnouncement: false,
  333. apiConfiguration: {
  334. apiProvider: "openrouter",
  335. },
  336. customInstructions: undefined,
  337. alwaysAllowReadOnly: false,
  338. alwaysAllowReadOnlyOutsideWorkspace: false,
  339. alwaysAllowWrite: false,
  340. codebaseIndexConfig: {
  341. codebaseIndexEnabled: false,
  342. codebaseIndexQdrantUrl: "",
  343. codebaseIndexEmbedderProvider: "openai",
  344. codebaseIndexEmbedderBaseUrl: "",
  345. codebaseIndexEmbedderModelId: "",
  346. },
  347. alwaysAllowWriteOutsideWorkspace: false,
  348. alwaysAllowExecute: false,
  349. alwaysAllowBrowser: false,
  350. alwaysAllowMcp: false,
  351. uriScheme: "vscode",
  352. soundEnabled: false,
  353. ttsEnabled: false,
  354. diffEnabled: false,
  355. enableCheckpoints: false,
  356. writeDelayMs: 1000,
  357. browserViewportSize: "900x600",
  358. fuzzyMatchThreshold: 1.0,
  359. mcpEnabled: true,
  360. enableMcpServerCreation: false,
  361. requestDelaySeconds: 5,
  362. mode: defaultModeSlug,
  363. customModes: [],
  364. experiments: experimentDefault,
  365. maxOpenTabsContext: 20,
  366. maxWorkspaceFiles: 200,
  367. browserToolEnabled: true,
  368. telemetrySetting: "unset",
  369. showRooIgnoredFiles: true,
  370. renderContext: "sidebar",
  371. maxReadFileLine: 500,
  372. cloudUserInfo: null,
  373. organizationAllowList: ORGANIZATION_ALLOW_ALL,
  374. autoCondenseContext: true,
  375. autoCondenseContextPercent: 100,
  376. cloudIsAuthenticated: false,
  377. }
  378. const message: ExtensionMessage = {
  379. type: "state",
  380. state: mockState,
  381. }
  382. await provider.postMessageToWebview(message)
  383. expect(mockPostMessage).toHaveBeenCalledWith(message)
  384. })
  385. test("handles webviewDidLaunch message", async () => {
  386. await provider.resolveWebviewView(mockWebviewView)
  387. // Get the message handler from onDidReceiveMessage
  388. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  389. // Simulate webviewDidLaunch message
  390. await messageHandler({ type: "webviewDidLaunch" })
  391. // Should post state and theme to webview
  392. expect(mockPostMessage).toHaveBeenCalled()
  393. })
  394. test("clearTask aborts current task", async () => {
  395. // Setup Cline instance with auto-mock from the top of the file
  396. const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
  397. // add the mock object to the stack
  398. await provider.addClineToStack(mockCline)
  399. // get the stack size before the abort call
  400. const stackSizeBeforeAbort = provider.getClineStackSize()
  401. // call the removeClineFromStack method so it will call the current cline abort and remove it from the stack
  402. await provider.removeClineFromStack()
  403. // get the stack size after the abort call
  404. const stackSizeAfterAbort = provider.getClineStackSize()
  405. // check if the abort method was called
  406. expect(mockCline.abortTask).toHaveBeenCalled()
  407. // check if the stack size was decreased
  408. expect(stackSizeBeforeAbort - stackSizeAfterAbort).toBe(1)
  409. })
  410. test("addClineToStack adds multiple Cline instances to the stack", async () => {
  411. // Setup Cline instance with auto-mock from the top of the file
  412. const mockCline1 = new Task(defaultTaskOptions) // Create a new mocked instance
  413. const mockCline2 = new Task(defaultTaskOptions) // Create a new mocked instance
  414. Object.defineProperty(mockCline1, "taskId", { value: "test-task-id-1", writable: true })
  415. Object.defineProperty(mockCline2, "taskId", { value: "test-task-id-2", writable: true })
  416. // add Cline instances to the stack
  417. await provider.addClineToStack(mockCline1)
  418. await provider.addClineToStack(mockCline2)
  419. // verify cline instances were added to the stack
  420. expect(provider.getClineStackSize()).toBe(2)
  421. // verify current cline instance is the last one added
  422. expect(provider.getCurrentCline()).toBe(mockCline2)
  423. })
  424. test("getState returns correct initial state", async () => {
  425. const state = await provider.getState()
  426. expect(state).toHaveProperty("apiConfiguration")
  427. expect(state.apiConfiguration).toHaveProperty("apiProvider")
  428. expect(state).toHaveProperty("customInstructions")
  429. expect(state).toHaveProperty("alwaysAllowReadOnly")
  430. expect(state).toHaveProperty("alwaysAllowWrite")
  431. expect(state).toHaveProperty("alwaysAllowExecute")
  432. expect(state).toHaveProperty("alwaysAllowBrowser")
  433. expect(state).toHaveProperty("taskHistory")
  434. expect(state).toHaveProperty("soundEnabled")
  435. expect(state).toHaveProperty("ttsEnabled")
  436. expect(state).toHaveProperty("diffEnabled")
  437. expect(state).toHaveProperty("writeDelayMs")
  438. })
  439. test("language is set to VSCode language", async () => {
  440. // Mock VSCode language as Spanish
  441. ;(vscode.env as any).language = "pt-BR"
  442. const state = await provider.getState()
  443. expect(state.language).toBe("pt-BR")
  444. })
  445. test("diffEnabled defaults to true when not set", async () => {
  446. // Mock globalState.get to return undefined for diffEnabled
  447. ;(mockContext.globalState.get as jest.Mock).mockReturnValue(undefined)
  448. const state = await provider.getState()
  449. expect(state.diffEnabled).toBe(true)
  450. })
  451. test("writeDelayMs defaults to 1000ms", async () => {
  452. // Mock globalState.get to return undefined for writeDelayMs
  453. ;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) =>
  454. key === "writeDelayMs" ? undefined : null,
  455. )
  456. const state = await provider.getState()
  457. expect(state.writeDelayMs).toBe(1000)
  458. })
  459. test("handles writeDelayMs message", async () => {
  460. await provider.resolveWebviewView(mockWebviewView)
  461. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  462. await messageHandler({ type: "writeDelayMs", value: 2000 })
  463. expect(updateGlobalStateSpy).toHaveBeenCalledWith("writeDelayMs", 2000)
  464. expect(mockContext.globalState.update).toHaveBeenCalledWith("writeDelayMs", 2000)
  465. expect(mockPostMessage).toHaveBeenCalled()
  466. })
  467. test("updates sound utility when sound setting changes", async () => {
  468. await provider.resolveWebviewView(mockWebviewView)
  469. // Get the message handler from onDidReceiveMessage
  470. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  471. // Simulate setting sound to enabled
  472. await messageHandler({ type: "soundEnabled", bool: true })
  473. expect(updateGlobalStateSpy).toHaveBeenCalledWith("soundEnabled", true)
  474. expect(mockContext.globalState.update).toHaveBeenCalledWith("soundEnabled", true)
  475. expect(mockPostMessage).toHaveBeenCalled()
  476. // Simulate setting sound to disabled
  477. await messageHandler({ type: "soundEnabled", bool: false })
  478. expect(mockContext.globalState.update).toHaveBeenCalledWith("soundEnabled", false)
  479. expect(mockPostMessage).toHaveBeenCalled()
  480. // Simulate setting tts to enabled
  481. await messageHandler({ type: "ttsEnabled", bool: true })
  482. expect(setTtsEnabled).toHaveBeenCalledWith(true)
  483. expect(mockContext.globalState.update).toHaveBeenCalledWith("ttsEnabled", true)
  484. expect(mockPostMessage).toHaveBeenCalled()
  485. // Simulate setting tts to disabled
  486. await messageHandler({ type: "ttsEnabled", bool: false })
  487. expect(setTtsEnabled).toHaveBeenCalledWith(false)
  488. expect(mockContext.globalState.update).toHaveBeenCalledWith("ttsEnabled", false)
  489. expect(mockPostMessage).toHaveBeenCalled()
  490. })
  491. test("requestDelaySeconds defaults to 10 seconds", async () => {
  492. // Mock globalState.get to return undefined for requestDelaySeconds
  493. ;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => {
  494. if (key === "requestDelaySeconds") {
  495. return undefined
  496. }
  497. return null
  498. })
  499. const state = await provider.getState()
  500. expect(state.requestDelaySeconds).toBe(10)
  501. })
  502. test("alwaysApproveResubmit defaults to false", async () => {
  503. // Mock globalState.get to return undefined for alwaysApproveResubmit
  504. ;(mockContext.globalState.get as jest.Mock).mockReturnValue(undefined)
  505. const state = await provider.getState()
  506. expect(state.alwaysApproveResubmit).toBe(false)
  507. })
  508. test("autoCondenseContext defaults to true", async () => {
  509. // Mock globalState.get to return undefined for autoCondenseContext
  510. ;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) =>
  511. key === "autoCondenseContext" ? undefined : null,
  512. )
  513. const state = await provider.getState()
  514. expect(state.autoCondenseContext).toBe(true)
  515. })
  516. test("handles autoCondenseContext message", async () => {
  517. await provider.resolveWebviewView(mockWebviewView)
  518. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  519. await messageHandler({ type: "autoCondenseContext", bool: false })
  520. expect(updateGlobalStateSpy).toHaveBeenCalledWith("autoCondenseContext", false)
  521. expect(mockContext.globalState.update).toHaveBeenCalledWith("autoCondenseContext", false)
  522. expect(mockPostMessage).toHaveBeenCalled()
  523. })
  524. test("autoCondenseContextPercent defaults to 100", async () => {
  525. // Mock globalState.get to return undefined for autoCondenseContextPercent
  526. ;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) =>
  527. key === "autoCondenseContextPercent" ? undefined : null,
  528. )
  529. const state = await provider.getState()
  530. expect(state.autoCondenseContextPercent).toBe(100)
  531. })
  532. test("handles autoCondenseContextPercent message", async () => {
  533. await provider.resolveWebviewView(mockWebviewView)
  534. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  535. await messageHandler({ type: "autoCondenseContextPercent", value: 75 })
  536. expect(updateGlobalStateSpy).toHaveBeenCalledWith("autoCondenseContextPercent", 75)
  537. expect(mockContext.globalState.update).toHaveBeenCalledWith("autoCondenseContextPercent", 75)
  538. expect(mockPostMessage).toHaveBeenCalled()
  539. })
  540. it("loads saved API config when switching modes", async () => {
  541. await provider.resolveWebviewView(mockWebviewView)
  542. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  543. const profile: ProviderSettingsEntry = { name: "test-config", id: "test-id", apiProvider: "anthropic" }
  544. ;(provider as any).providerSettingsManager = {
  545. getModeConfigId: jest.fn().mockResolvedValue("test-id"),
  546. listConfig: jest.fn().mockResolvedValue([profile]),
  547. activateProfile: jest.fn().mockResolvedValue(profile),
  548. setModeConfig: jest.fn(),
  549. } as any
  550. // Switch to architect mode
  551. await messageHandler({ type: "mode", text: "architect" })
  552. // Should load the saved config for architect mode
  553. expect(provider.providerSettingsManager.getModeConfigId).toHaveBeenCalledWith("architect")
  554. expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ name: "test-config" })
  555. expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
  556. })
  557. it("saves current config when switching to mode without config", async () => {
  558. await provider.resolveWebviewView(mockWebviewView)
  559. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  560. ;(provider as any).providerSettingsManager = {
  561. getModeConfigId: jest.fn().mockResolvedValue(undefined),
  562. listConfig: jest
  563. .fn()
  564. .mockResolvedValue([{ name: "current-config", id: "current-id", apiProvider: "anthropic" }]),
  565. setModeConfig: jest.fn(),
  566. } as any
  567. provider.setValue("currentApiConfigName", "current-config")
  568. // Switch to architect mode
  569. await messageHandler({ type: "mode", text: "architect" })
  570. // Should save current config as default for architect mode
  571. expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "current-id")
  572. })
  573. it("saves config as default for current mode when loading config", async () => {
  574. await provider.resolveWebviewView(mockWebviewView)
  575. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  576. const profile: ProviderSettingsEntry = { apiProvider: "anthropic", id: "new-id", name: "new-config" }
  577. ;(provider as any).providerSettingsManager = {
  578. activateProfile: jest.fn().mockResolvedValue(profile),
  579. listConfig: jest.fn().mockResolvedValue([profile]),
  580. setModeConfig: jest.fn(),
  581. getModeConfigId: jest.fn().mockResolvedValue(undefined),
  582. } as any
  583. // First set the mode
  584. await messageHandler({ type: "mode", text: "architect" })
  585. // Then load the config
  586. await messageHandler({ type: "loadApiConfiguration", text: "new-config" })
  587. // Should save new config as default for architect mode
  588. expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "new-id")
  589. })
  590. it("load API configuration by ID works and updates mode config", async () => {
  591. await provider.resolveWebviewView(mockWebviewView)
  592. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  593. const profile: ProviderSettingsEntry = {
  594. name: "config-by-id",
  595. id: "config-id-123",
  596. apiProvider: "anthropic",
  597. }
  598. ;(provider as any).providerSettingsManager = {
  599. activateProfile: jest.fn().mockResolvedValue(profile),
  600. listConfig: jest.fn().mockResolvedValue([profile]),
  601. setModeConfig: jest.fn(),
  602. getModeConfigId: jest.fn().mockResolvedValue(undefined),
  603. } as any
  604. // First set the mode
  605. await messageHandler({ type: "mode", text: "architect" })
  606. // Then load the config by ID
  607. await messageHandler({ type: "loadApiConfigurationById", text: "config-id-123" })
  608. // Should save new config as default for architect mode
  609. expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "config-id-123")
  610. // Ensure the `activateProfile` method was called with the correct ID
  611. expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ id: "config-id-123" })
  612. })
  613. test("handles browserToolEnabled setting", async () => {
  614. await provider.resolveWebviewView(mockWebviewView)
  615. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  616. // Test browserToolEnabled
  617. await messageHandler({ type: "browserToolEnabled", bool: true })
  618. expect(mockContext.globalState.update).toHaveBeenCalledWith("browserToolEnabled", true)
  619. expect(mockPostMessage).toHaveBeenCalled()
  620. // Verify state includes browserToolEnabled
  621. const state = await provider.getState()
  622. expect(state).toHaveProperty("browserToolEnabled")
  623. expect(state.browserToolEnabled).toBe(true) // Default value should be true
  624. })
  625. test("handles showRooIgnoredFiles setting", async () => {
  626. await provider.resolveWebviewView(mockWebviewView)
  627. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  628. // Default value should be true
  629. expect((await provider.getState()).showRooIgnoredFiles).toBe(true)
  630. // Test showRooIgnoredFiles with true
  631. await messageHandler({ type: "showRooIgnoredFiles", bool: true })
  632. expect(mockContext.globalState.update).toHaveBeenCalledWith("showRooIgnoredFiles", true)
  633. expect(mockPostMessage).toHaveBeenCalled()
  634. expect((await provider.getState()).showRooIgnoredFiles).toBe(true)
  635. // Test showRooIgnoredFiles with false
  636. await messageHandler({ type: "showRooIgnoredFiles", bool: false })
  637. expect(mockContext.globalState.update).toHaveBeenCalledWith("showRooIgnoredFiles", false)
  638. expect(mockPostMessage).toHaveBeenCalled()
  639. expect((await provider.getState()).showRooIgnoredFiles).toBe(false)
  640. })
  641. test("handles request delay settings messages", async () => {
  642. await provider.resolveWebviewView(mockWebviewView)
  643. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  644. // Test alwaysApproveResubmit
  645. await messageHandler({ type: "alwaysApproveResubmit", bool: true })
  646. expect(updateGlobalStateSpy).toHaveBeenCalledWith("alwaysApproveResubmit", true)
  647. expect(mockContext.globalState.update).toHaveBeenCalledWith("alwaysApproveResubmit", true)
  648. expect(mockPostMessage).toHaveBeenCalled()
  649. // Test requestDelaySeconds
  650. await messageHandler({ type: "requestDelaySeconds", value: 10 })
  651. expect(mockContext.globalState.update).toHaveBeenCalledWith("requestDelaySeconds", 10)
  652. expect(mockPostMessage).toHaveBeenCalled()
  653. })
  654. test("handles updatePrompt message correctly", async () => {
  655. await provider.resolveWebviewView(mockWebviewView)
  656. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  657. // Mock existing prompts
  658. const existingPrompts = {
  659. code: {
  660. roleDefinition: "existing code role",
  661. customInstructions: "existing code prompt",
  662. },
  663. architect: {
  664. roleDefinition: "existing architect role",
  665. customInstructions: "existing architect prompt",
  666. },
  667. }
  668. provider.setValue("customModePrompts", existingPrompts)
  669. // Test updating a prompt
  670. await messageHandler({
  671. type: "updatePrompt",
  672. promptMode: "code",
  673. customPrompt: "new code prompt",
  674. })
  675. // Verify state was updated correctly
  676. expect(mockContext.globalState.update).toHaveBeenCalledWith("customModePrompts", {
  677. ...existingPrompts,
  678. code: "new code prompt",
  679. })
  680. // Verify state was posted to webview
  681. expect(mockPostMessage).toHaveBeenCalledWith(
  682. expect.objectContaining({
  683. type: "state",
  684. state: expect.objectContaining({
  685. customModePrompts: {
  686. ...existingPrompts,
  687. code: "new code prompt",
  688. },
  689. }),
  690. }),
  691. )
  692. })
  693. test("customModePrompts defaults to empty object", async () => {
  694. // Mock globalState.get to return undefined for customModePrompts
  695. ;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => {
  696. if (key === "customModePrompts") {
  697. return undefined
  698. }
  699. return null
  700. })
  701. const state = await provider.getState()
  702. expect(state.customModePrompts).toEqual({})
  703. })
  704. test("handles maxWorkspaceFiles message", async () => {
  705. await provider.resolveWebviewView(mockWebviewView)
  706. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  707. await messageHandler({ type: "maxWorkspaceFiles", value: 300 })
  708. expect(updateGlobalStateSpy).toHaveBeenCalledWith("maxWorkspaceFiles", 300)
  709. expect(mockContext.globalState.update).toHaveBeenCalledWith("maxWorkspaceFiles", 300)
  710. expect(mockPostMessage).toHaveBeenCalled()
  711. })
  712. test("handles mode-specific custom instructions updates", async () => {
  713. await provider.resolveWebviewView(mockWebviewView)
  714. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  715. // Mock existing prompts
  716. const existingPrompts = {
  717. code: {
  718. roleDefinition: "Code role",
  719. customInstructions: "Old instructions",
  720. },
  721. }
  722. mockContext.globalState.get = jest.fn((key: string) => {
  723. if (key === "customModePrompts") {
  724. return existingPrompts
  725. }
  726. return undefined
  727. })
  728. // Update custom instructions for code mode
  729. await messageHandler({
  730. type: "updatePrompt",
  731. promptMode: "code",
  732. customPrompt: {
  733. roleDefinition: "Code role",
  734. customInstructions: "New instructions",
  735. },
  736. })
  737. // Verify state was updated correctly
  738. expect(mockContext.globalState.update).toHaveBeenCalledWith("customModePrompts", {
  739. code: {
  740. roleDefinition: "Code role",
  741. customInstructions: "New instructions",
  742. },
  743. })
  744. })
  745. it("saves mode config when updating API configuration", async () => {
  746. // Setup mock context with mode and config name
  747. mockContext = {
  748. ...mockContext,
  749. globalState: {
  750. ...mockContext.globalState,
  751. get: jest.fn((key: string) => {
  752. if (key === "mode") {
  753. return "code"
  754. } else if (key === "currentApiConfigName") {
  755. return "test-config"
  756. }
  757. return undefined
  758. }),
  759. update: jest.fn(),
  760. keys: jest.fn().mockReturnValue([]),
  761. },
  762. } as unknown as vscode.ExtensionContext
  763. // Create new provider with updated mock context
  764. provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
  765. await provider.resolveWebviewView(mockWebviewView)
  766. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  767. ;(provider as any).providerSettingsManager = {
  768. listConfig: jest.fn().mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
  769. saveConfig: jest.fn().mockResolvedValue("test-id"),
  770. setModeConfig: jest.fn(),
  771. } as any
  772. // Update API configuration
  773. await messageHandler({
  774. type: "upsertApiConfiguration",
  775. text: "test-config",
  776. apiConfiguration: { apiProvider: "anthropic" },
  777. })
  778. // Should save config as default for current mode
  779. expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("code", "test-id")
  780. })
  781. test("file content includes line numbers", async () => {
  782. const { extractTextFromFile } = require("../../../integrations/misc/extract-text")
  783. const result = await extractTextFromFile("test.js")
  784. expect(result).toBe("1 | const x = 1;\n2 | const y = 2;\n3 | const z = 3;")
  785. })
  786. describe("deleteMessage", () => {
  787. beforeEach(async () => {
  788. // Mock window.showInformationMessage
  789. ;(vscode.window.showInformationMessage as jest.Mock) = jest.fn()
  790. await provider.resolveWebviewView(mockWebviewView)
  791. })
  792. test('handles "Just this message" deletion correctly', async () => {
  793. // Mock user selecting "Just this message"
  794. ;(vscode.window.showInformationMessage as jest.Mock).mockResolvedValue("confirmation.just_this_message")
  795. // Setup mock messages
  796. const mockMessages = [
  797. { ts: 1000, type: "say", say: "user_feedback" }, // User message 1
  798. { ts: 2000, type: "say", say: "tool" }, // Tool message
  799. { ts: 3000, type: "say", say: "text", value: 4000 }, // Message to delete
  800. { ts: 4000, type: "say", say: "browser_action" }, // Response to delete
  801. { ts: 5000, type: "say", say: "user_feedback" }, // Next user message
  802. { ts: 6000, type: "say", say: "user_feedback" }, // Final message
  803. ] as ClineMessage[]
  804. const mockApiHistory = [
  805. { ts: 1000 },
  806. { ts: 2000 },
  807. { ts: 3000 },
  808. { ts: 4000 },
  809. { ts: 5000 },
  810. { ts: 6000 },
  811. ] as (Anthropic.MessageParam & { ts?: number })[]
  812. // Setup Task instance with auto-mock from the top of the file
  813. const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
  814. mockCline.clineMessages = mockMessages // Set test-specific messages
  815. mockCline.apiConversationHistory = mockApiHistory // Set API history
  816. await provider.addClineToStack(mockCline) // Add the mocked instance to the stack
  817. // Mock getTaskWithId
  818. ;(provider as any).getTaskWithId = jest.fn().mockResolvedValue({
  819. historyItem: { id: "test-task-id" },
  820. })
  821. // Trigger message deletion
  822. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  823. await messageHandler({ type: "deleteMessage", value: 4000 })
  824. // Verify correct messages were kept
  825. expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([
  826. mockMessages[0],
  827. mockMessages[1],
  828. mockMessages[4],
  829. mockMessages[5],
  830. ])
  831. // Verify correct API messages were kept
  832. expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
  833. mockApiHistory[0],
  834. mockApiHistory[1],
  835. mockApiHistory[4],
  836. mockApiHistory[5],
  837. ])
  838. })
  839. test('handles "This and all subsequent messages" deletion correctly', async () => {
  840. // Mock user selecting "This and all subsequent messages"
  841. ;(vscode.window.showInformationMessage as jest.Mock).mockResolvedValue("confirmation.this_and_subsequent")
  842. // Setup mock messages
  843. const mockMessages = [
  844. { ts: 1000, type: "say", say: "user_feedback" },
  845. { ts: 2000, type: "say", say: "text", value: 3000 }, // Message to delete
  846. { ts: 3000, type: "say", say: "user_feedback" },
  847. { ts: 4000, type: "say", say: "user_feedback" },
  848. ] as ClineMessage[]
  849. const mockApiHistory = [
  850. { ts: 1000 },
  851. { ts: 2000 },
  852. { ts: 3000 },
  853. { ts: 4000 },
  854. ] as (Anthropic.MessageParam & {
  855. ts?: number
  856. })[]
  857. // Setup Cline instance with auto-mock from the top of the file
  858. const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
  859. mockCline.clineMessages = mockMessages
  860. mockCline.apiConversationHistory = mockApiHistory
  861. await provider.addClineToStack(mockCline)
  862. // Mock getTaskWithId
  863. ;(provider as any).getTaskWithId = jest.fn().mockResolvedValue({
  864. historyItem: { id: "test-task-id" },
  865. })
  866. // Trigger message deletion
  867. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  868. await messageHandler({ type: "deleteMessage", value: 3000 })
  869. // Verify only messages before the deleted message were kept
  870. expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]])
  871. // Verify only API messages before the deleted message were kept
  872. expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([mockApiHistory[0]])
  873. })
  874. test("handles Cancel correctly", async () => {
  875. // Mock user selecting "Cancel"
  876. ;(vscode.window.showInformationMessage as jest.Mock).mockResolvedValue("Cancel")
  877. // Setup Cline instance with auto-mock from the top of the file
  878. const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
  879. mockCline.clineMessages = [{ ts: 1000 }, { ts: 2000 }] as ClineMessage[]
  880. mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as (Anthropic.MessageParam & {
  881. ts?: number
  882. })[]
  883. await provider.addClineToStack(mockCline)
  884. // Trigger message deletion
  885. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  886. await messageHandler({ type: "deleteMessage", value: 2000 })
  887. // Verify no messages were deleted
  888. expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled()
  889. expect(mockCline.overwriteApiConversationHistory).not.toHaveBeenCalled()
  890. })
  891. })
  892. describe("getSystemPrompt", () => {
  893. beforeEach(async () => {
  894. mockPostMessage.mockClear()
  895. await provider.resolveWebviewView(mockWebviewView)
  896. // Reset and setup mock
  897. mockAddCustomInstructions.mockClear()
  898. mockAddCustomInstructions.mockImplementation(
  899. (modeInstructions: string, globalInstructions: string, _cwd: string) => {
  900. return Promise.resolve(modeInstructions || globalInstructions || "")
  901. },
  902. )
  903. })
  904. const getMessageHandler = () => {
  905. const mockCalls = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls
  906. expect(mockCalls.length).toBeGreaterThan(0)
  907. return mockCalls[0][0]
  908. }
  909. test("handles mcpEnabled setting correctly", async () => {
  910. // Mock getState to return mcpEnabled: true
  911. jest.spyOn(provider, "getState").mockResolvedValue({
  912. apiConfiguration: {
  913. apiProvider: "openrouter" as const,
  914. },
  915. mcpEnabled: true,
  916. enableMcpServerCreation: false,
  917. mode: "code" as const,
  918. experiments: experimentDefault,
  919. } as any)
  920. const handler1 = getMessageHandler()
  921. expect(typeof handler1).toBe("function")
  922. await handler1({ type: "getSystemPrompt", mode: "code" })
  923. // Verify mcpHub is passed when mcpEnabled is true
  924. expect(mockPostMessage).toHaveBeenCalledWith(
  925. expect.objectContaining({
  926. type: "systemPrompt",
  927. text: expect.any(String),
  928. }),
  929. )
  930. // Mock getState to return mcpEnabled: false
  931. jest.spyOn(provider, "getState").mockResolvedValue({
  932. apiConfiguration: {
  933. apiProvider: "openrouter" as const,
  934. },
  935. mcpEnabled: false,
  936. enableMcpServerCreation: false,
  937. mode: "code" as const,
  938. experiments: experimentDefault,
  939. } as any)
  940. const handler2 = getMessageHandler()
  941. await handler2({ type: "getSystemPrompt", mode: "code" })
  942. // Verify mcpHub is not passed when mcpEnabled is false
  943. expect(mockPostMessage).toHaveBeenCalledWith(
  944. expect.objectContaining({
  945. type: "systemPrompt",
  946. text: expect.any(String),
  947. }),
  948. )
  949. })
  950. test("handles errors gracefully", async () => {
  951. // Mock SYSTEM_PROMPT to throw an error
  952. const systemPrompt = require("../../prompts/system")
  953. jest.spyOn(systemPrompt, "SYSTEM_PROMPT").mockRejectedValueOnce(new Error("Test error"))
  954. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  955. await messageHandler({ type: "getSystemPrompt", mode: "code" })
  956. expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.get_system_prompt")
  957. })
  958. test("uses code mode custom instructions", async () => {
  959. // Get the mock function
  960. const mockAddCustomInstructions = (jest.requireMock("../../prompts/sections/custom-instructions") as any)
  961. .addCustomInstructions
  962. // Clear any previous calls
  963. mockAddCustomInstructions.mockClear()
  964. // Mock SYSTEM_PROMPT
  965. const systemPromptModule = require("../../prompts/system")
  966. jest.spyOn(systemPromptModule, "SYSTEM_PROMPT").mockImplementation(async () => {
  967. await mockAddCustomInstructions("Code mode specific instructions", "", "/mock/path")
  968. return "mocked system prompt"
  969. })
  970. // Trigger getSystemPrompt
  971. const promptHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  972. await promptHandler({ type: "getSystemPrompt" })
  973. // Verify mock was called with code mode instructions
  974. expect(mockAddCustomInstructions).toHaveBeenCalledWith(
  975. "Code mode specific instructions",
  976. "",
  977. expect.any(String),
  978. )
  979. })
  980. test("passes diffStrategy and diffEnabled to SYSTEM_PROMPT when previewing", async () => {
  981. // Mock buildApiHandler to return an API handler with supportsComputerUse: true
  982. const { buildApiHandler } = require("../../../api")
  983. ;(buildApiHandler as jest.Mock).mockImplementation(() => ({
  984. getModel: jest.fn().mockReturnValue({
  985. id: "claude-3-sonnet",
  986. info: { supportsComputerUse: true },
  987. }),
  988. }))
  989. // Mock getState to return diffEnabled and fuzzyMatchThreshold
  990. jest.spyOn(provider, "getState").mockResolvedValue({
  991. apiConfiguration: {
  992. apiProvider: "openrouter",
  993. apiModelId: "test-model",
  994. },
  995. customModePrompts: {},
  996. mode: "code",
  997. enableMcpServerCreation: true,
  998. mcpEnabled: false,
  999. browserViewportSize: "900x600",
  1000. diffEnabled: true,
  1001. fuzzyMatchThreshold: 0.8,
  1002. experiments: experimentDefault,
  1003. browserToolEnabled: true,
  1004. } as any)
  1005. // Mock SYSTEM_PROMPT to verify diffStrategy and diffEnabled are passed
  1006. const systemPromptModule = require("../../prompts/system")
  1007. const systemPromptSpy = jest.spyOn(systemPromptModule, "SYSTEM_PROMPT")
  1008. // Trigger getSystemPrompt
  1009. const handler = getMessageHandler()
  1010. await handler({ type: "getSystemPrompt", mode: "code" })
  1011. // Verify SYSTEM_PROMPT was called
  1012. expect(systemPromptSpy).toHaveBeenCalled()
  1013. // Get the actual arguments passed to SYSTEM_PROMPT
  1014. const callArgs = systemPromptSpy.mock.calls[0]
  1015. // Verify key parameters
  1016. expect(callArgs[2]).toBe(true) // supportsComputerUse
  1017. expect(callArgs[3]).toBeUndefined() // mcpHub (disabled)
  1018. expect(callArgs[4]).toHaveProperty("getToolDescription") // diffStrategy
  1019. expect(callArgs[5]).toBe("900x600") // browserViewportSize
  1020. expect(callArgs[6]).toBe("code") // mode
  1021. expect(callArgs[10]).toBe(true) // diffEnabled
  1022. // Run the test again to verify it's consistent
  1023. await handler({ type: "getSystemPrompt", mode: "code" })
  1024. expect(systemPromptSpy).toHaveBeenCalledTimes(2)
  1025. })
  1026. test("passes diffEnabled: false to SYSTEM_PROMPT when diff is disabled", async () => {
  1027. // Setup Task instance with mocked api.getModel()
  1028. const mockCline = new Task(defaultTaskOptions)
  1029. mockCline.api = {
  1030. getModel: jest.fn().mockReturnValue({
  1031. id: "claude-3-sonnet",
  1032. info: { supportsComputerUse: true },
  1033. }),
  1034. } as any
  1035. await provider.addClineToStack(mockCline)
  1036. // Mock getState to return diffEnabled: false
  1037. jest.spyOn(provider, "getState").mockResolvedValue({
  1038. apiConfiguration: {
  1039. apiProvider: "openrouter",
  1040. apiModelId: "test-model",
  1041. },
  1042. customModePrompts: {},
  1043. mode: "code",
  1044. mcpEnabled: false,
  1045. browserViewportSize: "900x600",
  1046. diffEnabled: false,
  1047. fuzzyMatchThreshold: 0.8,
  1048. experiments: experimentDefault,
  1049. enableMcpServerCreation: true,
  1050. browserToolEnabled: true,
  1051. } as any)
  1052. // Mock SYSTEM_PROMPT to verify diffEnabled is passed as false
  1053. const systemPromptModule = require("../../prompts/system")
  1054. const systemPromptSpy = jest.spyOn(systemPromptModule, "SYSTEM_PROMPT")
  1055. // Trigger getSystemPrompt
  1056. const handler = getMessageHandler()
  1057. await handler({ type: "getSystemPrompt", mode: "code" })
  1058. // Verify SYSTEM_PROMPT was called
  1059. expect(systemPromptSpy).toHaveBeenCalled()
  1060. // Get the actual arguments passed to SYSTEM_PROMPT
  1061. const callArgs = systemPromptSpy.mock.calls[0]
  1062. // Verify key parameters
  1063. expect(callArgs[2]).toBe(true) // supportsComputerUse
  1064. expect(callArgs[3]).toBeUndefined() // mcpHub (disabled)
  1065. expect(callArgs[4]).toHaveProperty("getToolDescription") // diffStrategy
  1066. expect(callArgs[5]).toBe("900x600") // browserViewportSize
  1067. expect(callArgs[6]).toBe("code") // mode
  1068. expect(callArgs[10]).toBe(false) // diffEnabled should be true
  1069. })
  1070. test("uses correct mode-specific instructions when mode is specified", async () => {
  1071. // Mock getState to return architect mode instructions
  1072. jest.spyOn(provider, "getState").mockResolvedValue({
  1073. apiConfiguration: {
  1074. apiProvider: "openrouter",
  1075. },
  1076. customModePrompts: {
  1077. architect: { customInstructions: "Architect mode instructions" },
  1078. },
  1079. mode: "architect",
  1080. enableMcpServerCreation: false,
  1081. mcpEnabled: false,
  1082. browserViewportSize: "900x600",
  1083. experiments: experimentDefault,
  1084. } as any)
  1085. // Mock SYSTEM_PROMPT to call addCustomInstructions
  1086. const systemPromptModule = require("../../prompts/system")
  1087. jest.spyOn(systemPromptModule, "SYSTEM_PROMPT").mockImplementation(async () => {
  1088. await mockAddCustomInstructions("Architect mode instructions", "", "/mock/path")
  1089. return "mocked system prompt"
  1090. })
  1091. // Resolve webview and trigger getSystemPrompt
  1092. await provider.resolveWebviewView(mockWebviewView)
  1093. const architectHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  1094. await architectHandler({ type: "getSystemPrompt" })
  1095. // Verify architect mode instructions were used
  1096. expect(mockAddCustomInstructions).toHaveBeenCalledWith(
  1097. "Architect mode instructions",
  1098. "",
  1099. expect.any(String),
  1100. )
  1101. })
  1102. // Tests for browser tool support
  1103. test("correctly determines model support for computer use without Cline instance", async () => {
  1104. // Mock buildApiHandler to return an API handler with supportsComputerUse: true
  1105. const { buildApiHandler } = require("../../../api")
  1106. ;(buildApiHandler as jest.Mock).mockImplementation(() => ({
  1107. getModel: jest.fn().mockReturnValue({
  1108. id: "claude-3-sonnet",
  1109. info: { supportsComputerUse: true },
  1110. }),
  1111. }))
  1112. // Mock SYSTEM_PROMPT to verify supportsComputerUse is passed correctly
  1113. const systemPromptModule = require("../../prompts/system")
  1114. const systemPromptSpy = jest.spyOn(systemPromptModule, "SYSTEM_PROMPT")
  1115. // Mock getState to return browserToolEnabled: true and a mode that supports browser
  1116. jest.spyOn(provider, "getState").mockResolvedValue({
  1117. apiConfiguration: {
  1118. apiProvider: "openrouter",
  1119. },
  1120. browserToolEnabled: true,
  1121. mode: "code", // code mode includes browser tool group
  1122. experiments: experimentDefault,
  1123. } as any)
  1124. // Trigger getSystemPrompt
  1125. const handler = getMessageHandler()
  1126. await handler({ type: "getSystemPrompt", mode: "code" })
  1127. // Verify SYSTEM_PROMPT was called
  1128. expect(systemPromptSpy).toHaveBeenCalled()
  1129. // Get the actual arguments passed to SYSTEM_PROMPT
  1130. const callArgs = systemPromptSpy.mock.calls[0]
  1131. // Verify the supportsComputerUse parameter (3rd parameter, index 2)
  1132. expect(callArgs[2]).toBe(true)
  1133. })
  1134. test("correctly handles when model doesn't support computer use", async () => {
  1135. // Mock buildApiHandler to return an API handler with supportsComputerUse: false
  1136. const { buildApiHandler } = require("../../../api")
  1137. ;(buildApiHandler as jest.Mock).mockImplementation(() => ({
  1138. getModel: jest.fn().mockReturnValue({
  1139. id: "non-computer-use-model",
  1140. info: { supportsComputerUse: false },
  1141. }),
  1142. }))
  1143. // Mock SYSTEM_PROMPT to verify supportsComputerUse is passed correctly
  1144. const systemPromptModule = require("../../prompts/system")
  1145. const systemPromptSpy = jest.spyOn(systemPromptModule, "SYSTEM_PROMPT")
  1146. // Mock getState to return browserToolEnabled: true
  1147. jest.spyOn(provider, "getState").mockResolvedValue({
  1148. apiConfiguration: {
  1149. apiProvider: "openrouter",
  1150. },
  1151. browserToolEnabled: true,
  1152. mode: "code",
  1153. experiments: experimentDefault,
  1154. } as any)
  1155. // Trigger getSystemPrompt
  1156. const handler = getMessageHandler()
  1157. await handler({ type: "getSystemPrompt", mode: "code" })
  1158. // Verify SYSTEM_PROMPT was called
  1159. expect(systemPromptSpy).toHaveBeenCalled()
  1160. // Get the actual arguments passed to SYSTEM_PROMPT
  1161. const callArgs = systemPromptSpy.mock.calls[0]
  1162. // Verify the supportsComputerUse parameter (3rd parameter, index 2)
  1163. // Even though browserToolEnabled is true, the model doesn't support it
  1164. expect(callArgs[2]).toBe(false)
  1165. })
  1166. test("correctly handles when browserToolEnabled is false", async () => {
  1167. // Mock buildApiHandler to return an API handler with supportsComputerUse: true
  1168. const { buildApiHandler } = require("../../../api")
  1169. ;(buildApiHandler as jest.Mock).mockImplementation(() => ({
  1170. getModel: jest.fn().mockReturnValue({
  1171. id: "claude-3-sonnet",
  1172. info: { supportsComputerUse: true },
  1173. }),
  1174. }))
  1175. // Mock SYSTEM_PROMPT to verify supportsComputerUse is passed correctly
  1176. const systemPromptModule = require("../../prompts/system")
  1177. const systemPromptSpy = jest.spyOn(systemPromptModule, "SYSTEM_PROMPT")
  1178. // Mock getState to return browserToolEnabled: false
  1179. jest.spyOn(provider, "getState").mockResolvedValue({
  1180. apiConfiguration: {
  1181. apiProvider: "openrouter",
  1182. },
  1183. browserToolEnabled: false,
  1184. mode: "code",
  1185. experiments: experimentDefault,
  1186. } as any)
  1187. // Trigger getSystemPrompt
  1188. const handler = getMessageHandler()
  1189. await handler({ type: "getSystemPrompt", mode: "code" })
  1190. // Verify SYSTEM_PROMPT was called
  1191. expect(systemPromptSpy).toHaveBeenCalled()
  1192. // Get the actual arguments passed to SYSTEM_PROMPT
  1193. const callArgs = systemPromptSpy.mock.calls[0]
  1194. // Verify the supportsComputerUse parameter (3rd parameter, index 2)
  1195. // Even though model supports it, browserToolEnabled is false
  1196. expect(callArgs[2]).toBe(false)
  1197. })
  1198. test("correctly handles when mode doesn't include browser tool group", async () => {
  1199. // Mock buildApiHandler to return an API handler with supportsComputerUse: true
  1200. const { buildApiHandler } = require("../../../api")
  1201. ;(buildApiHandler as jest.Mock).mockImplementation(() => ({
  1202. getModel: jest.fn().mockReturnValue({
  1203. id: "claude-3-sonnet",
  1204. info: { supportsComputerUse: true },
  1205. }),
  1206. }))
  1207. // Mock SYSTEM_PROMPT to verify supportsComputerUse is passed correctly
  1208. const systemPromptModule = require("../../prompts/system")
  1209. const systemPromptSpy = jest.spyOn(systemPromptModule, "SYSTEM_PROMPT")
  1210. // Mock getState to return a mode that doesn't include browser tool group
  1211. jest.spyOn(provider, "getState").mockResolvedValue({
  1212. apiConfiguration: {
  1213. apiProvider: "openrouter",
  1214. },
  1215. browserToolEnabled: true,
  1216. mode: "custom-mode-without-browser", // Custom mode without browser tool group
  1217. experiments: experimentDefault,
  1218. } as any)
  1219. // Mock getModeBySlug to return a mode without browser tool group
  1220. const modesModule = require("../../../shared/modes")
  1221. jest.spyOn(modesModule, "getModeBySlug").mockReturnValue({
  1222. slug: "custom-mode-without-browser",
  1223. name: "Custom Mode",
  1224. roleDefinition: "Custom role",
  1225. groups: ["read", "edit"], // No browser group
  1226. })
  1227. // Trigger getSystemPrompt
  1228. const handler = getMessageHandler()
  1229. await handler({ type: "getSystemPrompt", mode: "custom-mode-without-browser" })
  1230. // Verify SYSTEM_PROMPT was called
  1231. expect(systemPromptSpy).toHaveBeenCalled()
  1232. // Get the actual arguments passed to SYSTEM_PROMPT
  1233. const callArgs = systemPromptSpy.mock.calls[0]
  1234. // Verify the supportsComputerUse parameter (3rd parameter, index 2)
  1235. // Even though model supports it and browserToolEnabled is true, the mode doesn't include browser tool group
  1236. expect(callArgs[2]).toBe(false)
  1237. })
  1238. test("correctly calculates canUseBrowserTool based on all three conditions", async () => {
  1239. // Mock buildApiHandler
  1240. const { buildApiHandler } = require("../../../api")
  1241. // Mock SYSTEM_PROMPT
  1242. const systemPromptModule = require("../../prompts/system")
  1243. const systemPromptSpy = jest.spyOn(systemPromptModule, "SYSTEM_PROMPT")
  1244. // Mock getModeBySlug
  1245. const modesModule = require("../../../shared/modes")
  1246. // Test all combinations of model support, mode support, and browserToolEnabled
  1247. const testCases = [
  1248. { modelSupports: true, modeSupports: true, settingEnabled: true, expected: true },
  1249. { modelSupports: true, modeSupports: true, settingEnabled: false, expected: false },
  1250. { modelSupports: true, modeSupports: false, settingEnabled: true, expected: false },
  1251. { modelSupports: false, modeSupports: true, settingEnabled: true, expected: false },
  1252. { modelSupports: false, modeSupports: false, settingEnabled: false, expected: false },
  1253. ]
  1254. for (const testCase of testCases) {
  1255. // Reset mocks
  1256. systemPromptSpy.mockClear()
  1257. // Mock buildApiHandler to return appropriate model support
  1258. ;(buildApiHandler as jest.Mock).mockImplementation(() => ({
  1259. getModel: jest.fn().mockReturnValue({
  1260. id: "test-model",
  1261. info: { supportsComputerUse: testCase.modelSupports },
  1262. }),
  1263. }))
  1264. // Mock getModeBySlug to return appropriate mode support
  1265. jest.spyOn(modesModule, "getModeBySlug").mockReturnValue({
  1266. slug: "test-mode",
  1267. name: "Test Mode",
  1268. roleDefinition: "Test role",
  1269. groups: testCase.modeSupports ? ["read", "browser"] : ["read"],
  1270. })
  1271. // Mock getState
  1272. jest.spyOn(provider, "getState").mockResolvedValue({
  1273. apiConfiguration: {
  1274. apiProvider: "openrouter",
  1275. },
  1276. browserToolEnabled: testCase.settingEnabled,
  1277. mode: "test-mode",
  1278. experiments: experimentDefault,
  1279. } as any)
  1280. // Trigger getSystemPrompt
  1281. const handler = getMessageHandler()
  1282. await handler({ type: "getSystemPrompt", mode: "test-mode" })
  1283. // Verify SYSTEM_PROMPT was called
  1284. expect(systemPromptSpy).toHaveBeenCalled()
  1285. // Get the actual arguments passed to SYSTEM_PROMPT
  1286. const callArgs = systemPromptSpy.mock.calls[0]
  1287. // Verify the supportsComputerUse parameter (3rd parameter, index 2)
  1288. expect(callArgs[2]).toBe(testCase.expected)
  1289. }
  1290. })
  1291. })
  1292. describe("handleModeSwitch", () => {
  1293. beforeEach(async () => {
  1294. // Set up webview for each test
  1295. await provider.resolveWebviewView(mockWebviewView)
  1296. })
  1297. it("loads saved API config when switching modes", async () => {
  1298. const profile: ProviderSettingsEntry = {
  1299. name: "saved-config",
  1300. id: "saved-config-id",
  1301. apiProvider: "anthropic",
  1302. }
  1303. ;(provider as any).providerSettingsManager = {
  1304. getModeConfigId: jest.fn().mockResolvedValue("saved-config-id"),
  1305. listConfig: jest.fn().mockResolvedValue([profile]),
  1306. activateProfile: jest.fn().mockResolvedValue(profile),
  1307. setModeConfig: jest.fn(),
  1308. } as any
  1309. // Switch to architect mode
  1310. await provider.handleModeSwitch("architect")
  1311. // Verify mode was updated
  1312. expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "architect")
  1313. // Verify saved config was loaded
  1314. expect(provider.providerSettingsManager.getModeConfigId).toHaveBeenCalledWith("architect")
  1315. expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ name: "saved-config" })
  1316. expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "saved-config")
  1317. // Verify state was posted to webview
  1318. expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
  1319. })
  1320. test("saves current config when switching to mode without config", async () => {
  1321. ;(provider as any).providerSettingsManager = {
  1322. getModeConfigId: jest.fn().mockResolvedValue(undefined),
  1323. listConfig: jest
  1324. .fn()
  1325. .mockResolvedValue([{ name: "current-config", id: "current-id", apiProvider: "anthropic" }]),
  1326. setModeConfig: jest.fn(),
  1327. } as any
  1328. // Mock the ContextProxy's getValue method to return the current config name
  1329. const contextProxy = (provider as any).contextProxy
  1330. const getValueSpy = jest.spyOn(contextProxy, "getValue")
  1331. getValueSpy.mockImplementation((key: any) => {
  1332. if (key === "currentApiConfigName") return "current-config"
  1333. return undefined
  1334. })
  1335. // Switch to architect mode
  1336. await provider.handleModeSwitch("architect")
  1337. // Verify mode was updated
  1338. expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "architect")
  1339. // Verify current config was saved as default for new mode
  1340. expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "current-id")
  1341. // Verify state was posted to webview
  1342. expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
  1343. })
  1344. })
  1345. describe("updateCustomMode", () => {
  1346. test("updates both file and state when updating custom mode", async () => {
  1347. await provider.resolveWebviewView(mockWebviewView)
  1348. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  1349. // Mock CustomModesManager methods
  1350. ;(provider as any).customModesManager = {
  1351. updateCustomMode: jest.fn().mockResolvedValue(undefined),
  1352. getCustomModes: jest.fn().mockResolvedValue([
  1353. {
  1354. slug: "test-mode",
  1355. name: "Test Mode",
  1356. roleDefinition: "Updated role definition",
  1357. groups: ["read"] as const,
  1358. },
  1359. ]),
  1360. dispose: jest.fn(),
  1361. } as any
  1362. // Test updating a custom mode
  1363. await messageHandler({
  1364. type: "updateCustomMode",
  1365. modeConfig: {
  1366. slug: "test-mode",
  1367. name: "Test Mode",
  1368. roleDefinition: "Updated role definition",
  1369. groups: ["read"] as const,
  1370. },
  1371. })
  1372. // Verify CustomModesManager.updateCustomMode was called
  1373. expect(provider.customModesManager.updateCustomMode).toHaveBeenCalledWith(
  1374. "test-mode",
  1375. expect.objectContaining({
  1376. slug: "test-mode",
  1377. roleDefinition: "Updated role definition",
  1378. }),
  1379. )
  1380. // Verify state was updated
  1381. expect(mockContext.globalState.update).toHaveBeenCalledWith("customModes", [
  1382. { groups: ["read"], name: "Test Mode", roleDefinition: "Updated role definition", slug: "test-mode" },
  1383. ])
  1384. // Verify state was posted to webview
  1385. // Verify state was posted to webview with correct format
  1386. expect(mockPostMessage).toHaveBeenCalledWith(
  1387. expect.objectContaining({
  1388. type: "state",
  1389. state: expect.objectContaining({
  1390. customModes: [
  1391. expect.objectContaining({
  1392. slug: "test-mode",
  1393. roleDefinition: "Updated role definition",
  1394. }),
  1395. ],
  1396. }),
  1397. }),
  1398. )
  1399. })
  1400. })
  1401. describe("upsertApiConfiguration", () => {
  1402. test("handles error in upsertApiConfiguration gracefully", async () => {
  1403. await provider.resolveWebviewView(mockWebviewView)
  1404. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  1405. ;(provider as any).providerSettingsManager = {
  1406. setModeConfig: jest.fn().mockRejectedValue(new Error("Failed to update mode config")),
  1407. listConfig: jest
  1408. .fn()
  1409. .mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
  1410. } as any
  1411. // Mock getState to provide necessary data
  1412. jest.spyOn(provider, "getState").mockResolvedValue({
  1413. mode: "code",
  1414. currentApiConfigName: "test-config",
  1415. } as any)
  1416. // Trigger upsertApiConfiguration
  1417. await messageHandler({
  1418. type: "upsertApiConfiguration",
  1419. text: "test-config",
  1420. apiConfiguration: { apiProvider: "anthropic", apiKey: "test-key" },
  1421. })
  1422. // Verify error was logged and user was notified
  1423. expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
  1424. expect.stringContaining("Error create new api configuration"),
  1425. )
  1426. expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.create_api_config")
  1427. })
  1428. test("handles successful upsertApiConfiguration", async () => {
  1429. await provider.resolveWebviewView(mockWebviewView)
  1430. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  1431. ;(provider as any).providerSettingsManager = {
  1432. setModeConfig: jest.fn(),
  1433. saveConfig: jest.fn().mockResolvedValue(undefined),
  1434. listConfig: jest
  1435. .fn()
  1436. .mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
  1437. } as any
  1438. const testApiConfig = {
  1439. apiProvider: "anthropic" as const,
  1440. apiKey: "test-key",
  1441. }
  1442. // Trigger upsertApiConfiguration
  1443. await messageHandler({
  1444. type: "upsertApiConfiguration",
  1445. text: "test-config",
  1446. apiConfiguration: testApiConfig,
  1447. })
  1448. // Verify config was saved
  1449. expect(provider.providerSettingsManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
  1450. // Verify state updates
  1451. expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
  1452. { name: "test-config", id: "test-id", apiProvider: "anthropic" },
  1453. ])
  1454. expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
  1455. // Verify state was posted to webview
  1456. expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
  1457. })
  1458. test("handles buildApiHandler error in updateApiConfiguration", async () => {
  1459. await provider.resolveWebviewView(mockWebviewView)
  1460. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  1461. // Mock buildApiHandler to throw an error
  1462. const { buildApiHandler } = require("../../../api")
  1463. ;(buildApiHandler as jest.Mock).mockImplementationOnce(() => {
  1464. throw new Error("API handler error")
  1465. })
  1466. ;(provider as any).providerSettingsManager = {
  1467. setModeConfig: jest.fn(),
  1468. saveConfig: jest.fn().mockResolvedValue(undefined),
  1469. listConfig: jest
  1470. .fn()
  1471. .mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
  1472. } as any
  1473. // Setup Task instance with auto-mock from the top of the file
  1474. const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
  1475. await provider.addClineToStack(mockCline)
  1476. const testApiConfig = {
  1477. apiProvider: "anthropic" as const,
  1478. apiKey: "test-key",
  1479. }
  1480. // Trigger upsertApiConfiguration
  1481. await messageHandler({
  1482. type: "upsertApiConfiguration",
  1483. text: "test-config",
  1484. apiConfiguration: testApiConfig,
  1485. })
  1486. // Verify error handling
  1487. expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
  1488. expect.stringContaining("Error create new api configuration"),
  1489. )
  1490. expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.create_api_config")
  1491. // Verify state was still updated
  1492. expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
  1493. { name: "test-config", id: "test-id", apiProvider: "anthropic" },
  1494. ])
  1495. expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
  1496. })
  1497. test("handles successful saveApiConfiguration", async () => {
  1498. await provider.resolveWebviewView(mockWebviewView)
  1499. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  1500. ;(provider as any).providerSettingsManager = {
  1501. setModeConfig: jest.fn(),
  1502. saveConfig: jest.fn().mockResolvedValue(undefined),
  1503. listConfig: jest
  1504. .fn()
  1505. .mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
  1506. } as any
  1507. const testApiConfig = {
  1508. apiProvider: "anthropic" as const,
  1509. apiKey: "test-key",
  1510. }
  1511. // Trigger upsertApiConfiguration
  1512. await messageHandler({
  1513. type: "saveApiConfiguration",
  1514. text: "test-config",
  1515. apiConfiguration: testApiConfig,
  1516. })
  1517. // Verify config was saved
  1518. expect(provider.providerSettingsManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
  1519. // Verify state updates
  1520. expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
  1521. { name: "test-config", id: "test-id", apiProvider: "anthropic" },
  1522. ])
  1523. expect(updateGlobalStateSpy).toHaveBeenCalledWith("listApiConfigMeta", [
  1524. { name: "test-config", id: "test-id", apiProvider: "anthropic" },
  1525. ])
  1526. })
  1527. })
  1528. describe("browser connection features", () => {
  1529. beforeEach(async () => {
  1530. // Reset mocks
  1531. jest.clearAllMocks()
  1532. await provider.resolveWebviewView(mockWebviewView)
  1533. })
  1534. // Mock BrowserSession and discoverChromeInstances
  1535. jest.mock("../../../services/browser/BrowserSession", () => ({
  1536. BrowserSession: jest.fn().mockImplementation(() => ({
  1537. testConnection: jest.fn().mockImplementation(async (url) => {
  1538. if (url === "http://localhost:9222") {
  1539. return {
  1540. success: true,
  1541. message: "Successfully connected to Chrome",
  1542. endpoint: "ws://localhost:9222/devtools/browser/123",
  1543. }
  1544. } else {
  1545. return {
  1546. success: false,
  1547. message: "Failed to connect to Chrome",
  1548. endpoint: undefined,
  1549. }
  1550. }
  1551. }),
  1552. })),
  1553. }))
  1554. jest.mock("../../../services/browser/browserDiscovery", () => ({
  1555. discoverChromeInstances: jest.fn().mockImplementation(async () => {
  1556. return "http://localhost:9222"
  1557. }),
  1558. }))
  1559. test("handles testBrowserConnection with provided URL", async () => {
  1560. // Get the message handler
  1561. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  1562. // Test with valid URL
  1563. await messageHandler({
  1564. type: "testBrowserConnection",
  1565. text: "http://localhost:9222",
  1566. })
  1567. // Verify postMessage was called with success result
  1568. expect(mockPostMessage).toHaveBeenCalledWith(
  1569. expect.objectContaining({
  1570. type: "browserConnectionResult",
  1571. success: true,
  1572. text: expect.stringContaining("Successfully connected to Chrome"),
  1573. }),
  1574. )
  1575. // Reset mock
  1576. mockPostMessage.mockClear()
  1577. // Test with invalid URL
  1578. await messageHandler({
  1579. type: "testBrowserConnection",
  1580. text: "http://inlocalhost:9222",
  1581. })
  1582. // Verify postMessage was called with failure result
  1583. expect(mockPostMessage).toHaveBeenCalledWith(
  1584. expect.objectContaining({
  1585. type: "browserConnectionResult",
  1586. success: false,
  1587. text: expect.stringContaining("Failed to connect to Chrome"),
  1588. }),
  1589. )
  1590. })
  1591. test("handles testBrowserConnection with auto-discovery", async () => {
  1592. // Get the message handler
  1593. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  1594. // Test auto-discovery (no URL provided)
  1595. await messageHandler({
  1596. type: "testBrowserConnection",
  1597. })
  1598. // Verify discoverChromeHostUrl was called
  1599. const { discoverChromeHostUrl } = require("../../../services/browser/browserDiscovery")
  1600. expect(discoverChromeHostUrl).toHaveBeenCalled()
  1601. // Verify postMessage was called with success result
  1602. expect(mockPostMessage).toHaveBeenCalledWith(
  1603. expect.objectContaining({
  1604. type: "browserConnectionResult",
  1605. success: true,
  1606. text: expect.stringContaining("Auto-discovered and tested connection to Chrome"),
  1607. }),
  1608. )
  1609. })
  1610. })
  1611. })
  1612. describe("Project MCP Settings", () => {
  1613. let provider: ClineProvider
  1614. let mockContext: vscode.ExtensionContext
  1615. let mockOutputChannel: vscode.OutputChannel
  1616. let mockWebviewView: vscode.WebviewView
  1617. let mockPostMessage: jest.Mock
  1618. beforeEach(() => {
  1619. jest.clearAllMocks()
  1620. mockContext = {
  1621. extensionPath: "/test/path",
  1622. extensionUri: {} as vscode.Uri,
  1623. globalState: {
  1624. get: jest.fn(),
  1625. update: jest.fn(),
  1626. keys: jest.fn().mockReturnValue([]),
  1627. },
  1628. secrets: {
  1629. get: jest.fn(),
  1630. store: jest.fn(),
  1631. delete: jest.fn(),
  1632. },
  1633. subscriptions: [],
  1634. extension: {
  1635. packageJSON: { version: "1.0.0" },
  1636. },
  1637. globalStorageUri: {
  1638. fsPath: "/test/storage/path",
  1639. },
  1640. } as unknown as vscode.ExtensionContext
  1641. mockOutputChannel = {
  1642. appendLine: jest.fn(),
  1643. clear: jest.fn(),
  1644. dispose: jest.fn(),
  1645. } as unknown as vscode.OutputChannel
  1646. mockPostMessage = jest.fn()
  1647. mockWebviewView = {
  1648. webview: {
  1649. postMessage: mockPostMessage,
  1650. html: "",
  1651. options: {},
  1652. onDidReceiveMessage: jest.fn(),
  1653. asWebviewUri: jest.fn(),
  1654. },
  1655. visible: true,
  1656. onDidDispose: jest.fn(),
  1657. onDidChangeVisibility: jest.fn(),
  1658. } as unknown as vscode.WebviewView
  1659. provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
  1660. })
  1661. test("handles openProjectMcpSettings message", async () => {
  1662. await provider.resolveWebviewView(mockWebviewView)
  1663. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  1664. // Mock workspace folders
  1665. ;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
  1666. // Mock fs functions
  1667. const fs = require("fs/promises")
  1668. fs.mkdir.mockResolvedValue(undefined)
  1669. fs.writeFile.mockResolvedValue(undefined)
  1670. // Trigger openProjectMcpSettings
  1671. await messageHandler({
  1672. type: "openProjectMcpSettings",
  1673. })
  1674. // Verify directory was created
  1675. expect(fs.mkdir).toHaveBeenCalledWith(
  1676. expect.stringContaining(".roo"),
  1677. expect.objectContaining({ recursive: true }),
  1678. )
  1679. // Verify file was created with default content
  1680. expect(fs.writeFile).toHaveBeenCalledWith(
  1681. expect.stringContaining("mcp.json"),
  1682. JSON.stringify({ mcpServers: {} }, null, 2),
  1683. )
  1684. })
  1685. test("handles openProjectMcpSettings when workspace is not open", async () => {
  1686. await provider.resolveWebviewView(mockWebviewView)
  1687. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  1688. // Mock no workspace folders
  1689. ;(vscode.workspace as any).workspaceFolders = []
  1690. // Trigger openProjectMcpSettings
  1691. await messageHandler({ type: "openProjectMcpSettings" })
  1692. // Verify error message was shown
  1693. expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.no_workspace")
  1694. })
  1695. test.skip("handles openProjectMcpSettings file creation error", async () => {
  1696. await provider.resolveWebviewView(mockWebviewView)
  1697. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  1698. // Mock workspace folders
  1699. ;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
  1700. // Mock fs functions to fail
  1701. const fs = require("fs/promises")
  1702. fs.mkdir.mockRejectedValue(new Error("Failed to create directory"))
  1703. // Trigger openProjectMcpSettings
  1704. await messageHandler({
  1705. type: "openProjectMcpSettings",
  1706. })
  1707. // Verify error message was shown
  1708. expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
  1709. expect.stringContaining("Failed to create or open .roo/mcp.json"),
  1710. )
  1711. })
  1712. })
  1713. describe.skip("ContextProxy integration", () => {
  1714. let provider: ClineProvider
  1715. let mockContext: vscode.ExtensionContext
  1716. let mockOutputChannel: vscode.OutputChannel
  1717. let mockContextProxy: any
  1718. beforeEach(() => {
  1719. // Reset mocks
  1720. jest.clearAllMocks()
  1721. // Setup basic mocks
  1722. mockContext = {
  1723. globalState: {
  1724. get: jest.fn(),
  1725. update: jest.fn(),
  1726. keys: jest.fn().mockReturnValue([]),
  1727. },
  1728. secrets: { get: jest.fn(), store: jest.fn(), delete: jest.fn() },
  1729. extensionUri: {} as vscode.Uri,
  1730. globalStorageUri: { fsPath: "/test/path" },
  1731. extension: { packageJSON: { version: "1.0.0" } },
  1732. } as unknown as vscode.ExtensionContext
  1733. mockOutputChannel = { appendLine: jest.fn() } as unknown as vscode.OutputChannel
  1734. mockContextProxy = new ContextProxy(mockContext)
  1735. provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", mockContextProxy)
  1736. })
  1737. test("updateGlobalState uses contextProxy", async () => {
  1738. await provider.setValue("currentApiConfigName", "testValue")
  1739. expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("currentApiConfigName", "testValue")
  1740. })
  1741. test("getGlobalState uses contextProxy", async () => {
  1742. mockContextProxy.getGlobalState.mockResolvedValueOnce("testValue")
  1743. const result = await provider.getValue("currentApiConfigName")
  1744. expect(mockContextProxy.getGlobalState).toHaveBeenCalledWith("currentApiConfigName")
  1745. expect(result).toBe("testValue")
  1746. })
  1747. test("storeSecret uses contextProxy", async () => {
  1748. await provider.setValue("apiKey", "test-secret")
  1749. expect(mockContextProxy.storeSecret).toHaveBeenCalledWith("apiKey", "test-secret")
  1750. })
  1751. test("contextProxy methods are available", () => {
  1752. // Verify the contextProxy has all the required methods
  1753. expect(mockContextProxy.getGlobalState).toBeDefined()
  1754. expect(mockContextProxy.updateGlobalState).toBeDefined()
  1755. expect(mockContextProxy.storeSecret).toBeDefined()
  1756. expect(mockContextProxy.setValue).toBeDefined()
  1757. expect(mockContextProxy.setValues).toBeDefined()
  1758. })
  1759. })
  1760. describe("getTelemetryProperties", () => {
  1761. let defaultTaskOptions: TaskOptions
  1762. let provider: ClineProvider
  1763. let mockContext: vscode.ExtensionContext
  1764. let mockOutputChannel: vscode.OutputChannel
  1765. let mockCline: any
  1766. beforeEach(() => {
  1767. // Reset mocks
  1768. jest.clearAllMocks()
  1769. // Setup basic mocks
  1770. mockContext = {
  1771. globalState: {
  1772. get: jest.fn().mockImplementation((key: string) => {
  1773. if (key === "mode") return "code"
  1774. if (key === "apiProvider") return "anthropic"
  1775. return undefined
  1776. }),
  1777. update: jest.fn(),
  1778. keys: jest.fn().mockReturnValue([]),
  1779. },
  1780. secrets: { get: jest.fn(), store: jest.fn(), delete: jest.fn() },
  1781. extensionUri: {} as vscode.Uri,
  1782. globalStorageUri: { fsPath: "/test/path" },
  1783. extension: { packageJSON: { version: "1.0.0" } },
  1784. } as unknown as vscode.ExtensionContext
  1785. mockOutputChannel = { appendLine: jest.fn() } as unknown as vscode.OutputChannel
  1786. provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
  1787. defaultTaskOptions = {
  1788. provider,
  1789. apiConfiguration: {
  1790. apiProvider: "openrouter",
  1791. },
  1792. }
  1793. // Setup Task instance with mocked getModel method
  1794. mockCline = new Task(defaultTaskOptions)
  1795. mockCline.api = {
  1796. getModel: jest.fn().mockReturnValue({
  1797. id: "claude-sonnet-4-20250514",
  1798. info: { contextWindow: 200000 },
  1799. }),
  1800. }
  1801. })
  1802. test("includes basic properties in telemetry", async () => {
  1803. const properties = await provider.getTelemetryProperties()
  1804. expect(properties).toHaveProperty("vscodeVersion")
  1805. expect(properties).toHaveProperty("platform")
  1806. expect(properties).toHaveProperty("appVersion", "1.0.0")
  1807. })
  1808. test("includes model ID from current Cline instance if available", async () => {
  1809. // Add mock Cline to stack
  1810. await provider.addClineToStack(mockCline)
  1811. const properties = await provider.getTelemetryProperties()
  1812. expect(properties).toHaveProperty("modelId", "claude-sonnet-4-20250514")
  1813. })
  1814. })
  1815. // Mock getModels for router model tests
  1816. jest.mock("../../../api/providers/fetchers/modelCache", () => ({
  1817. getModels: jest.fn(),
  1818. flushModels: jest.fn(),
  1819. }))
  1820. describe("ClineProvider - Router Models", () => {
  1821. let provider: ClineProvider
  1822. let mockContext: vscode.ExtensionContext
  1823. let mockOutputChannel: vscode.OutputChannel
  1824. let mockWebviewView: vscode.WebviewView
  1825. let mockPostMessage: jest.Mock
  1826. beforeEach(() => {
  1827. jest.clearAllMocks()
  1828. const globalState: Record<string, string | undefined> = {}
  1829. const secrets: Record<string, string | undefined> = {}
  1830. mockContext = {
  1831. extensionPath: "/test/path",
  1832. extensionUri: {} as vscode.Uri,
  1833. globalState: {
  1834. get: jest.fn().mockImplementation((key: string) => globalState[key]),
  1835. update: jest
  1836. .fn()
  1837. .mockImplementation((key: string, value: string | undefined) => (globalState[key] = value)),
  1838. keys: jest.fn().mockImplementation(() => Object.keys(globalState)),
  1839. },
  1840. secrets: {
  1841. get: jest.fn().mockImplementation((key: string) => secrets[key]),
  1842. store: jest.fn().mockImplementation((key: string, value: string | undefined) => (secrets[key] = value)),
  1843. delete: jest.fn().mockImplementation((key: string) => delete secrets[key]),
  1844. },
  1845. subscriptions: [],
  1846. extension: {
  1847. packageJSON: { version: "1.0.0" },
  1848. },
  1849. globalStorageUri: {
  1850. fsPath: "/test/storage/path",
  1851. },
  1852. } as unknown as vscode.ExtensionContext
  1853. mockOutputChannel = {
  1854. appendLine: jest.fn(),
  1855. clear: jest.fn(),
  1856. dispose: jest.fn(),
  1857. } as unknown as vscode.OutputChannel
  1858. mockPostMessage = jest.fn()
  1859. mockWebviewView = {
  1860. webview: {
  1861. postMessage: mockPostMessage,
  1862. html: "",
  1863. options: {},
  1864. onDidReceiveMessage: jest.fn(),
  1865. asWebviewUri: jest.fn(),
  1866. },
  1867. visible: true,
  1868. onDidDispose: jest.fn().mockImplementation((callback) => {
  1869. callback()
  1870. return { dispose: jest.fn() }
  1871. }),
  1872. onDidChangeVisibility: jest.fn().mockImplementation(() => ({ dispose: jest.fn() })),
  1873. } as unknown as vscode.WebviewView
  1874. provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
  1875. })
  1876. test("handles requestRouterModels with successful responses", async () => {
  1877. await provider.resolveWebviewView(mockWebviewView)
  1878. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  1879. // Mock getState to return API configuration
  1880. jest.spyOn(provider, "getState").mockResolvedValue({
  1881. apiConfiguration: {
  1882. openRouterApiKey: "openrouter-key",
  1883. requestyApiKey: "requesty-key",
  1884. glamaApiKey: "glama-key",
  1885. unboundApiKey: "unbound-key",
  1886. litellmApiKey: "litellm-key",
  1887. litellmBaseUrl: "http://localhost:4000",
  1888. },
  1889. } as any)
  1890. const mockModels = {
  1891. "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model 1" },
  1892. "model-2": { maxTokens: 8192, contextWindow: 16384, description: "Test model 2" },
  1893. }
  1894. const { getModels } = require("../../../api/providers/fetchers/modelCache")
  1895. getModels.mockResolvedValue(mockModels)
  1896. await messageHandler({ type: "requestRouterModels" })
  1897. // Verify getModels was called for each provider with correct options
  1898. expect(getModels).toHaveBeenCalledWith({ provider: "openrouter" })
  1899. expect(getModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" })
  1900. expect(getModels).toHaveBeenCalledWith({ provider: "glama" })
  1901. expect(getModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" })
  1902. expect(getModels).toHaveBeenCalledWith({
  1903. provider: "litellm",
  1904. apiKey: "litellm-key",
  1905. baseUrl: "http://localhost:4000",
  1906. })
  1907. // Verify response was sent
  1908. expect(mockPostMessage).toHaveBeenCalledWith({
  1909. type: "routerModels",
  1910. routerModels: {
  1911. openrouter: mockModels,
  1912. requesty: mockModels,
  1913. glama: mockModels,
  1914. unbound: mockModels,
  1915. litellm: mockModels,
  1916. },
  1917. })
  1918. })
  1919. test("handles requestRouterModels with individual provider failures", async () => {
  1920. await provider.resolveWebviewView(mockWebviewView)
  1921. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  1922. jest.spyOn(provider, "getState").mockResolvedValue({
  1923. apiConfiguration: {
  1924. openRouterApiKey: "openrouter-key",
  1925. requestyApiKey: "requesty-key",
  1926. glamaApiKey: "glama-key",
  1927. unboundApiKey: "unbound-key",
  1928. litellmApiKey: "litellm-key",
  1929. litellmBaseUrl: "http://localhost:4000",
  1930. },
  1931. } as any)
  1932. const mockModels = { "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model" } }
  1933. const { getModels } = require("../../../api/providers/fetchers/modelCache")
  1934. // Mock some providers to succeed and others to fail
  1935. getModels
  1936. .mockResolvedValueOnce(mockModels) // openrouter success
  1937. .mockRejectedValueOnce(new Error("Requesty API error")) // requesty fail
  1938. .mockResolvedValueOnce(mockModels) // glama success
  1939. .mockRejectedValueOnce(new Error("Unbound API error")) // unbound fail
  1940. .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail
  1941. await messageHandler({ type: "requestRouterModels" })
  1942. // Verify main response includes successful providers and empty objects for failed ones
  1943. expect(mockPostMessage).toHaveBeenCalledWith({
  1944. type: "routerModels",
  1945. routerModels: {
  1946. openrouter: mockModels,
  1947. requesty: {},
  1948. glama: mockModels,
  1949. unbound: {},
  1950. litellm: {},
  1951. },
  1952. })
  1953. // Verify error messages were sent for failed providers
  1954. expect(mockPostMessage).toHaveBeenCalledWith({
  1955. type: "singleRouterModelFetchResponse",
  1956. success: false,
  1957. error: "Requesty API error",
  1958. values: { provider: "requesty" },
  1959. })
  1960. expect(mockPostMessage).toHaveBeenCalledWith({
  1961. type: "singleRouterModelFetchResponse",
  1962. success: false,
  1963. error: "Unbound API error",
  1964. values: { provider: "unbound" },
  1965. })
  1966. expect(mockPostMessage).toHaveBeenCalledWith({
  1967. type: "singleRouterModelFetchResponse",
  1968. success: false,
  1969. error: "LiteLLM connection failed",
  1970. values: { provider: "litellm" },
  1971. })
  1972. })
  1973. test("handles requestRouterModels with LiteLLM values from message", async () => {
  1974. await provider.resolveWebviewView(mockWebviewView)
  1975. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  1976. // Mock state without LiteLLM config
  1977. jest.spyOn(provider, "getState").mockResolvedValue({
  1978. apiConfiguration: {
  1979. openRouterApiKey: "openrouter-key",
  1980. requestyApiKey: "requesty-key",
  1981. glamaApiKey: "glama-key",
  1982. unboundApiKey: "unbound-key",
  1983. // No litellm config
  1984. },
  1985. } as any)
  1986. const mockModels = { "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model" } }
  1987. const { getModels } = require("../../../api/providers/fetchers/modelCache")
  1988. getModels.mockResolvedValue(mockModels)
  1989. await messageHandler({
  1990. type: "requestRouterModels",
  1991. values: {
  1992. litellmApiKey: "message-litellm-key",
  1993. litellmBaseUrl: "http://message-url:4000",
  1994. },
  1995. })
  1996. // Verify LiteLLM was called with values from message
  1997. expect(getModels).toHaveBeenCalledWith({
  1998. provider: "litellm",
  1999. apiKey: "message-litellm-key",
  2000. baseUrl: "http://message-url:4000",
  2001. })
  2002. })
  2003. test("skips LiteLLM when neither config nor message values are provided", async () => {
  2004. await provider.resolveWebviewView(mockWebviewView)
  2005. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
  2006. jest.spyOn(provider, "getState").mockResolvedValue({
  2007. apiConfiguration: {
  2008. openRouterApiKey: "openrouter-key",
  2009. requestyApiKey: "requesty-key",
  2010. glamaApiKey: "glama-key",
  2011. unboundApiKey: "unbound-key",
  2012. // No litellm config
  2013. },
  2014. } as any)
  2015. const mockModels = { "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model" } }
  2016. const { getModels } = require("../../../api/providers/fetchers/modelCache")
  2017. getModels.mockResolvedValue(mockModels)
  2018. await messageHandler({ type: "requestRouterModels" })
  2019. // Verify LiteLLM was NOT called
  2020. expect(getModels).not.toHaveBeenCalledWith(
  2021. expect.objectContaining({
  2022. provider: "litellm",
  2023. }),
  2024. )
  2025. // Verify response includes empty object for LiteLLM
  2026. expect(mockPostMessage).toHaveBeenCalledWith({
  2027. type: "routerModels",
  2028. routerModels: {
  2029. openrouter: mockModels,
  2030. requesty: mockModels,
  2031. glama: mockModels,
  2032. unbound: mockModels,
  2033. litellm: {},
  2034. },
  2035. })
  2036. })
  2037. })