ClineProvider.spec.ts 126 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852
  1. // pnpm --filter roo-cline test core/webview/__tests__/ClineProvider.spec.ts
  2. import Anthropic from "@anthropic-ai/sdk"
  3. import * as vscode from "vscode"
  4. import axios from "axios"
  5. import {
  6. type ProviderSettingsEntry,
  7. type ClineMessage,
  8. type ExtensionMessage,
  9. type ExtensionState,
  10. ORGANIZATION_ALLOW_ALL,
  11. DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
  12. } from "@roo-code/types"
  13. import { TelemetryService } from "@roo-code/telemetry"
  14. import { defaultModeSlug } from "../../../shared/modes"
  15. import { experimentDefault } from "../../../shared/experiments"
  16. import { setTtsEnabled } from "../../../utils/tts"
  17. import { ContextProxy } from "../../config/ContextProxy"
  18. import { Task, TaskOptions } from "../../task/Task"
  19. import { safeWriteJson } from "../../../utils/safeWriteJson"
  20. import { ClineProvider } from "../ClineProvider"
  21. import { MessageManager } from "../../message-manager"
  22. // Mock setup must come before imports.
  23. vi.mock("../../prompts/sections/custom-instructions")
  24. vi.mock("p-wait-for", () => ({
  25. __esModule: true,
  26. default: vi.fn().mockResolvedValue(undefined),
  27. }))
  28. vi.mock("fs/promises", () => ({
  29. mkdir: vi.fn().mockResolvedValue(undefined),
  30. writeFile: vi.fn().mockResolvedValue(undefined),
  31. readFile: vi.fn().mockResolvedValue(""),
  32. unlink: vi.fn().mockResolvedValue(undefined),
  33. rmdir: vi.fn().mockResolvedValue(undefined),
  34. }))
  35. vi.mock("axios", () => ({
  36. default: {
  37. get: vi.fn().mockResolvedValue({ data: { data: [] } }),
  38. post: vi.fn(),
  39. },
  40. get: vi.fn().mockResolvedValue({ data: { data: [] } }),
  41. post: vi.fn(),
  42. }))
  43. vi.mock("../../../utils/safeWriteJson")
  44. vi.mock("../../../utils/storage", () => ({
  45. getSettingsDirectoryPath: vi.fn().mockResolvedValue("/test/settings/path"),
  46. getTaskDirectoryPath: vi.fn().mockResolvedValue("/test/task/path"),
  47. getGlobalStoragePath: vi.fn().mockResolvedValue("/test/storage/path"),
  48. }))
  49. vi.mock("@modelcontextprotocol/sdk/types.js", () => ({
  50. CallToolResultSchema: {},
  51. ListResourcesResultSchema: {},
  52. ListResourceTemplatesResultSchema: {},
  53. ListToolsResultSchema: {},
  54. ReadResourceResultSchema: {},
  55. ErrorCode: {
  56. InvalidRequest: "InvalidRequest",
  57. MethodNotFound: "MethodNotFound",
  58. InternalError: "InternalError",
  59. },
  60. McpError: class McpError extends Error {
  61. code: string
  62. constructor(code: string, message: string) {
  63. super(message)
  64. this.code = code
  65. this.name = "McpError"
  66. }
  67. },
  68. }))
  69. vi.mock("../../../services/browser/BrowserSession", () => ({
  70. BrowserSession: vi.fn().mockImplementation(() => ({
  71. testConnection: vi.fn().mockImplementation(async (url) => {
  72. if (url === "http://localhost:9222") {
  73. return {
  74. success: true,
  75. message: "Successfully connected to Chrome",
  76. endpoint: "ws://localhost:9222/devtools/browser/123",
  77. }
  78. } else {
  79. return {
  80. success: false,
  81. message: "Failed to connect to Chrome",
  82. endpoint: undefined,
  83. }
  84. }
  85. }),
  86. })),
  87. }))
  88. vi.mock("../../../services/browser/browserDiscovery", () => ({
  89. discoverChromeHostUrl: vi.fn().mockResolvedValue("http://localhost:9222"),
  90. tryChromeHostUrl: vi.fn().mockImplementation(async (url) => {
  91. return url === "http://localhost:9222"
  92. }),
  93. testBrowserConnection: vi.fn(),
  94. }))
  95. // Remove duplicate mock - it's already defined below.
  96. const mockAddCustomInstructions = vi.fn().mockResolvedValue("Combined instructions")
  97. ;(vi.mocked(await import("../../prompts/sections/custom-instructions")) as any).addCustomInstructions =
  98. mockAddCustomInstructions
  99. vi.mock("delay", () => {
  100. const delayFn = (_ms: number) => Promise.resolve()
  101. delayFn.createDelay = () => delayFn
  102. delayFn.reject = () => Promise.reject(new Error("Delay rejected"))
  103. delayFn.range = () => Promise.resolve()
  104. return { default: delayFn }
  105. })
  106. // MCP-related modules are mocked once above (lines 87-109).
  107. vi.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
  108. Client: vi.fn().mockImplementation(() => ({
  109. connect: vi.fn().mockResolvedValue(undefined),
  110. close: vi.fn().mockResolvedValue(undefined),
  111. listTools: vi.fn().mockResolvedValue({ tools: [] }),
  112. callTool: vi.fn().mockResolvedValue({ content: [] }),
  113. })),
  114. }))
  115. vi.mock("@modelcontextprotocol/sdk/client/stdio.js", () => ({
  116. StdioClientTransport: vi.fn().mockImplementation(() => ({
  117. connect: vi.fn().mockResolvedValue(undefined),
  118. close: vi.fn().mockResolvedValue(undefined),
  119. })),
  120. }))
  121. vi.mock("vscode", () => ({
  122. ExtensionContext: vi.fn(),
  123. OutputChannel: vi.fn(),
  124. WebviewView: vi.fn(),
  125. Uri: {
  126. joinPath: vi.fn(),
  127. file: vi.fn(),
  128. },
  129. CodeActionKind: {
  130. QuickFix: { value: "quickfix" },
  131. RefactorRewrite: { value: "refactor.rewrite" },
  132. },
  133. commands: {
  134. executeCommand: vi.fn().mockResolvedValue(undefined),
  135. },
  136. window: {
  137. showInformationMessage: vi.fn(),
  138. showWarningMessage: vi.fn(),
  139. showErrorMessage: vi.fn(),
  140. onDidChangeActiveTextEditor: vi.fn(() => ({ dispose: vi.fn() })),
  141. },
  142. workspace: {
  143. getConfiguration: vi.fn().mockReturnValue({
  144. get: vi.fn().mockReturnValue([]),
  145. update: vi.fn(),
  146. }),
  147. onDidChangeConfiguration: vi.fn().mockImplementation(() => ({
  148. dispose: vi.fn(),
  149. })),
  150. onDidSaveTextDocument: vi.fn(() => ({ dispose: vi.fn() })),
  151. onDidChangeTextDocument: vi.fn(() => ({ dispose: vi.fn() })),
  152. onDidOpenTextDocument: vi.fn(() => ({ dispose: vi.fn() })),
  153. onDidCloseTextDocument: vi.fn(() => ({ dispose: vi.fn() })),
  154. },
  155. env: {
  156. uriScheme: "vscode",
  157. language: "en",
  158. appName: "Visual Studio Code",
  159. },
  160. ExtensionMode: {
  161. Production: 1,
  162. Development: 2,
  163. Test: 3,
  164. },
  165. version: "1.85.0",
  166. }))
  167. vi.mock("../../../utils/tts", () => ({
  168. setTtsEnabled: vi.fn(),
  169. setTtsSpeed: vi.fn(),
  170. }))
  171. vi.mock("../../../api", () => ({
  172. buildApiHandler: vi.fn(),
  173. }))
  174. vi.mock("../../prompts/system", () => ({
  175. SYSTEM_PROMPT: vi.fn().mockImplementation(async () => "mocked system prompt"),
  176. codeMode: "code",
  177. }))
  178. vi.mock("../../../integrations/workspace/WorkspaceTracker", () => {
  179. return {
  180. default: vi.fn().mockImplementation(() => ({
  181. initializeFilePaths: vi.fn(),
  182. dispose: vi.fn(),
  183. })),
  184. }
  185. })
  186. vi.mock("../../task/Task", () => ({
  187. Task: vi.fn().mockImplementation((options: any) => ({
  188. api: undefined,
  189. abortTask: vi.fn(),
  190. handleWebviewAskResponse: vi.fn(),
  191. clineMessages: [],
  192. apiConversationHistory: [],
  193. overwriteClineMessages: vi.fn(),
  194. overwriteApiConversationHistory: vi.fn(),
  195. getTaskNumber: vi.fn().mockReturnValue(0),
  196. setTaskNumber: vi.fn(),
  197. setParentTask: vi.fn(),
  198. setRootTask: vi.fn(),
  199. taskId: options?.historyItem?.id || "test-task-id",
  200. emit: vi.fn(),
  201. })),
  202. }))
  203. vi.mock("../../../integrations/misc/extract-text", () => ({
  204. extractTextFromFile: vi.fn().mockImplementation(async (_filePath: string) => {
  205. const content = "const x = 1;\nconst y = 2;\nconst z = 3;"
  206. const lines = content.split("\n")
  207. return lines.map((line, index) => `${index + 1} | ${line}`).join("\n")
  208. }),
  209. }))
  210. vi.mock("../../../api/providers/fetchers/modelCache", () => ({
  211. getModels: vi.fn().mockResolvedValue({}),
  212. flushModels: vi.fn(),
  213. getModelsFromCache: vi.fn().mockReturnValue(undefined),
  214. }))
  215. vi.mock("../../../shared/modes", () => ({
  216. modes: [
  217. {
  218. slug: "code",
  219. name: "Code Mode",
  220. roleDefinition: "You are a code assistant",
  221. groups: ["read", "edit", "browser"],
  222. },
  223. {
  224. slug: "architect",
  225. name: "Architect Mode",
  226. roleDefinition: "You are an architect",
  227. groups: ["read", "edit"],
  228. },
  229. {
  230. slug: "ask",
  231. name: "Ask Mode",
  232. roleDefinition: "You are a helpful assistant",
  233. groups: ["read"],
  234. },
  235. ],
  236. getModeBySlug: vi.fn().mockReturnValue({
  237. slug: "code",
  238. name: "Code Mode",
  239. roleDefinition: "You are a code assistant",
  240. groups: ["read", "edit", "browser"],
  241. }),
  242. getGroupName: vi.fn().mockImplementation((group: string) => {
  243. // Return appropriate group names for different tool groups
  244. switch (group) {
  245. case "read":
  246. return "Read Tools"
  247. case "edit":
  248. return "Edit Tools"
  249. case "browser":
  250. return "Browser Tools"
  251. case "mcp":
  252. return "MCP Tools"
  253. default:
  254. return "General Tools"
  255. }
  256. }),
  257. defaultModeSlug: "code",
  258. }))
  259. vi.mock("../../prompts/system", () => ({
  260. SYSTEM_PROMPT: vi.fn().mockResolvedValue("mocked system prompt"),
  261. codeMode: "code",
  262. }))
  263. vi.mock("../../../api", () => ({
  264. buildApiHandler: vi.fn().mockReturnValue({
  265. getModel: vi.fn().mockReturnValue({
  266. id: "claude-3-sonnet",
  267. }),
  268. }),
  269. }))
  270. vi.mock("../../../integrations/misc/extract-text", () => ({
  271. extractTextFromFile: vi.fn().mockImplementation(async (_filePath: string) => {
  272. const content = "const x = 1;\nconst y = 2;\nconst z = 3;"
  273. const lines = content.split("\n")
  274. return lines.map((line, index) => `${index + 1} | ${line}`).join("\n")
  275. }),
  276. }))
  277. vi.mock("../../../api/providers/fetchers/modelCache", () => ({
  278. getModels: vi.fn().mockResolvedValue({}),
  279. flushModels: vi.fn(),
  280. getModelsFromCache: vi.fn().mockReturnValue(undefined),
  281. }))
  282. vi.mock("../diff/strategies/multi-search-replace", () => ({
  283. MultiSearchReplaceDiffStrategy: vi.fn().mockImplementation(() => ({
  284. getToolDescription: () => "test",
  285. getName: () => "test-strategy",
  286. applyDiff: vi.fn(),
  287. })),
  288. }))
  289. vi.mock("@roo-code/cloud", () => ({
  290. CloudService: {
  291. hasInstance: vi.fn().mockReturnValue(true),
  292. get instance() {
  293. return {
  294. isAuthenticated: vi.fn().mockReturnValue(false),
  295. }
  296. },
  297. },
  298. BridgeOrchestrator: {
  299. isEnabled: vi.fn().mockReturnValue(false),
  300. },
  301. getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"),
  302. }))
  303. afterAll(() => {
  304. vi.restoreAllMocks()
  305. })
  306. describe("ClineProvider", () => {
  307. beforeAll(() => {
  308. vi.mocked(Task).mockImplementation((options: any) => {
  309. const task: any = {
  310. api: undefined,
  311. abortTask: vi.fn(),
  312. handleWebviewAskResponse: vi.fn(),
  313. clineMessages: [],
  314. apiConversationHistory: [],
  315. overwriteClineMessages: vi.fn(),
  316. overwriteApiConversationHistory: vi.fn(),
  317. getTaskNumber: vi.fn().mockReturnValue(0),
  318. setTaskNumber: vi.fn(),
  319. setParentTask: vi.fn(),
  320. setRootTask: vi.fn(),
  321. taskId: options?.historyItem?.id || "test-task-id",
  322. emit: vi.fn(),
  323. }
  324. Object.defineProperty(task, "messageManager", {
  325. get: () => new MessageManager(task),
  326. })
  327. return task
  328. })
  329. })
  330. let defaultTaskOptions: TaskOptions
  331. let provider: ClineProvider
  332. let mockContext: vscode.ExtensionContext
  333. let mockOutputChannel: vscode.OutputChannel
  334. let mockWebviewView: vscode.WebviewView
  335. let mockPostMessage: any
  336. let updateGlobalStateSpy: any
  337. beforeEach(() => {
  338. vi.clearAllMocks()
  339. if (!TelemetryService.hasInstance()) {
  340. TelemetryService.createInstance([])
  341. }
  342. const globalState: Record<string, string | undefined> = {
  343. mode: "architect",
  344. currentApiConfigName: "current-config",
  345. }
  346. const secrets: Record<string, string | undefined> = {}
  347. mockContext = {
  348. extensionPath: "/test/path",
  349. extensionUri: {} as vscode.Uri,
  350. globalState: {
  351. get: vi.fn().mockImplementation((key: string) => globalState[key]),
  352. update: vi
  353. .fn()
  354. .mockImplementation((key: string, value: string | undefined) => (globalState[key] = value)),
  355. keys: vi.fn().mockImplementation(() => Object.keys(globalState)),
  356. },
  357. secrets: {
  358. get: vi.fn().mockImplementation((key: string) => secrets[key]),
  359. store: vi.fn().mockImplementation((key: string, value: string | undefined) => (secrets[key] = value)),
  360. delete: vi.fn().mockImplementation((key: string) => delete secrets[key]),
  361. },
  362. workspaceState: {
  363. get: vi.fn().mockReturnValue(undefined),
  364. update: vi.fn().mockResolvedValue(undefined),
  365. keys: vi.fn().mockReturnValue([]),
  366. },
  367. subscriptions: [],
  368. extension: {
  369. packageJSON: { version: "1.0.0" },
  370. },
  371. globalStorageUri: {
  372. fsPath: "/test/storage/path",
  373. },
  374. } as unknown as vscode.ExtensionContext
  375. // Mock CustomModesManager
  376. const mockCustomModesManager = {
  377. updateCustomMode: vi.fn().mockResolvedValue(undefined),
  378. getCustomModes: vi.fn().mockResolvedValue([]),
  379. dispose: vi.fn(),
  380. }
  381. // Mock output channel
  382. mockOutputChannel = {
  383. appendLine: vi.fn(),
  384. clear: vi.fn(),
  385. dispose: vi.fn(),
  386. } as unknown as vscode.OutputChannel
  387. // Mock webview
  388. mockPostMessage = vi.fn()
  389. mockWebviewView = {
  390. webview: {
  391. postMessage: mockPostMessage,
  392. html: "",
  393. options: {},
  394. onDidReceiveMessage: vi.fn(),
  395. asWebviewUri: vi.fn(),
  396. cspSource: "vscode-webview://test-csp-source",
  397. },
  398. visible: true,
  399. onDidDispose: vi.fn().mockImplementation((callback) => {
  400. callback()
  401. return { dispose: vi.fn() }
  402. }),
  403. onDidChangeVisibility: vi.fn().mockImplementation(() => ({ dispose: vi.fn() })),
  404. } as unknown as vscode.WebviewView
  405. provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
  406. defaultTaskOptions = {
  407. provider,
  408. apiConfiguration: {
  409. apiProvider: "openrouter",
  410. },
  411. }
  412. // @ts-ignore - Access private property for testing
  413. updateGlobalStateSpy = vi.spyOn(provider.contextProxy, "setValue")
  414. // @ts-ignore - Accessing private property for testing.
  415. provider.customModesManager = mockCustomModesManager
  416. // Mock getMcpHub method for generateSystemPrompt
  417. provider.getMcpHub = vi.fn().mockReturnValue({
  418. listTools: vi.fn().mockResolvedValue([]),
  419. callTool: vi.fn().mockResolvedValue({ content: [] }),
  420. listResources: vi.fn().mockResolvedValue([]),
  421. readResource: vi.fn().mockResolvedValue({ contents: [] }),
  422. getAllServers: vi.fn().mockReturnValue([]),
  423. })
  424. })
  425. test("constructor initializes correctly", () => {
  426. expect(provider).toBeInstanceOf(ClineProvider)
  427. // Since getVisibleInstance returns the last instance where view.visible is true
  428. // @ts-ignore - accessing private property for testing
  429. provider.view = mockWebviewView
  430. expect(ClineProvider.getVisibleInstance()).toBe(provider)
  431. })
  432. test("resolveWebviewView sets up webview correctly", async () => {
  433. await provider.resolveWebviewView(mockWebviewView)
  434. expect(mockWebviewView.webview.options).toEqual({
  435. enableScripts: true,
  436. localResourceRoots: [mockContext.extensionUri],
  437. })
  438. expect(mockWebviewView.webview.html).toContain("<!DOCTYPE html>")
  439. })
  440. test("resolveWebviewView sets up webview correctly in development mode even if local server is not running", async () => {
  441. provider = new ClineProvider(
  442. { ...mockContext, extensionMode: vscode.ExtensionMode.Development },
  443. mockOutputChannel,
  444. "sidebar",
  445. new ContextProxy(mockContext),
  446. )
  447. ;(axios.get as any).mockRejectedValueOnce(new Error("Network error"))
  448. await provider.resolveWebviewView(mockWebviewView)
  449. expect(mockWebviewView.webview.options).toEqual({
  450. enableScripts: true,
  451. localResourceRoots: [mockContext.extensionUri],
  452. })
  453. expect(mockWebviewView.webview.html).toContain("<!DOCTYPE html>")
  454. // Verify Content Security Policy contains the necessary PostHog domains
  455. expect(mockWebviewView.webview.html).toContain(
  456. "connect-src vscode-webview://test-csp-source https://openrouter.ai https://api.requesty.ai https://ph.roocode.com",
  457. )
  458. // Extract the script-src directive section and verify required security elements
  459. const html = mockWebviewView.webview.html
  460. const scriptSrcMatch = html.match(/script-src[^;]*;/)
  461. expect(scriptSrcMatch).not.toBeNull()
  462. expect(scriptSrcMatch![0]).toContain("'nonce-")
  463. // Verify wasm-unsafe-eval is present for Shiki syntax highlighting
  464. expect(scriptSrcMatch![0]).toContain("'wasm-unsafe-eval'")
  465. })
  466. test("postMessageToWebview sends message to webview", async () => {
  467. await provider.resolveWebviewView(mockWebviewView)
  468. const mockState: ExtensionState = {
  469. version: "1.0.0",
  470. isBrowserSessionActive: false,
  471. clineMessages: [],
  472. taskHistory: [],
  473. shouldShowAnnouncement: false,
  474. apiConfiguration: {
  475. apiProvider: "openrouter",
  476. },
  477. customInstructions: undefined,
  478. alwaysAllowReadOnly: false,
  479. alwaysAllowReadOnlyOutsideWorkspace: false,
  480. alwaysAllowWrite: false,
  481. codebaseIndexConfig: {
  482. codebaseIndexEnabled: true,
  483. codebaseIndexQdrantUrl: "",
  484. codebaseIndexEmbedderProvider: "openai",
  485. codebaseIndexEmbedderBaseUrl: "",
  486. codebaseIndexEmbedderModelId: "",
  487. },
  488. alwaysAllowWriteOutsideWorkspace: false,
  489. alwaysAllowExecute: false,
  490. alwaysAllowBrowser: false,
  491. alwaysAllowMcp: false,
  492. uriScheme: "vscode",
  493. soundEnabled: false,
  494. ttsEnabled: false,
  495. enableCheckpoints: false,
  496. writeDelayMs: 1000,
  497. browserViewportSize: "900x600",
  498. mcpEnabled: true,
  499. mode: defaultModeSlug,
  500. customModes: [],
  501. experiments: experimentDefault,
  502. maxOpenTabsContext: 20,
  503. maxWorkspaceFiles: 200,
  504. browserToolEnabled: true,
  505. telemetrySetting: "unset",
  506. showRooIgnoredFiles: false,
  507. enableSubfolderRules: false,
  508. renderContext: "sidebar",
  509. maxImageFileSize: 5,
  510. maxTotalImageSize: 20,
  511. cloudUserInfo: null,
  512. organizationAllowList: ORGANIZATION_ALLOW_ALL,
  513. autoCondenseContext: true,
  514. autoCondenseContextPercent: 100,
  515. cloudIsAuthenticated: false,
  516. sharingEnabled: false,
  517. publicSharingEnabled: false,
  518. profileThresholds: {},
  519. hasOpenedModeSelector: false,
  520. diagnosticsEnabled: true,
  521. openRouterImageApiKey: undefined,
  522. openRouterImageGenerationSelectedModel: undefined,
  523. remoteControlEnabled: false,
  524. taskSyncEnabled: false,
  525. featureRoomoteControlEnabled: false,
  526. checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
  527. }
  528. const message: ExtensionMessage = {
  529. type: "state",
  530. state: mockState,
  531. }
  532. await provider.postMessageToWebview(message)
  533. expect(mockPostMessage).toHaveBeenCalledWith(message)
  534. })
  535. test("handles webviewDidLaunch message", async () => {
  536. await provider.resolveWebviewView(mockWebviewView)
  537. // Get the message handler from onDidReceiveMessage
  538. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  539. // Simulate webviewDidLaunch message
  540. await messageHandler({ type: "webviewDidLaunch" })
  541. // Should post state and theme to webview
  542. expect(mockPostMessage).toHaveBeenCalled()
  543. })
  544. test("clearTask aborts current task", async () => {
  545. // Setup Cline instance with auto-mock from the top of the file
  546. const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
  547. // add the mock object to the stack
  548. await provider.addClineToStack(mockCline)
  549. // get the stack size before the abort call
  550. const stackSizeBeforeAbort = provider.getTaskStackSize()
  551. // call the removeClineFromStack method so it will call the current cline abort and remove it from the stack
  552. await provider.removeClineFromStack()
  553. // get the stack size after the abort call
  554. const stackSizeAfterAbort = provider.getTaskStackSize()
  555. // check if the abort method was called
  556. expect(mockCline.abortTask).toHaveBeenCalled()
  557. // check if the stack size was decreased
  558. expect(stackSizeBeforeAbort - stackSizeAfterAbort).toBe(1)
  559. })
  560. describe("clearTask message handler", () => {
  561. beforeEach(async () => {
  562. await provider.resolveWebviewView(mockWebviewView)
  563. })
  564. test("calls clearTask (delegation handled via metadata)", async () => {
  565. // Setup a single task without parent
  566. const mockCline = new Task(defaultTaskOptions)
  567. // Mock the provider methods
  568. const clearTaskSpy = vi.spyOn(provider, "clearTask").mockResolvedValue(undefined)
  569. const postStateToWebviewSpy = vi.spyOn(provider, "postStateToWebview").mockResolvedValue(undefined)
  570. // Add task to stack
  571. await provider.addClineToStack(mockCline)
  572. // Get the message handler
  573. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  574. // Trigger clearTask message
  575. await messageHandler({ type: "clearTask" })
  576. // Verify clearTask was called
  577. expect(clearTaskSpy).toHaveBeenCalled()
  578. expect(postStateToWebviewSpy).toHaveBeenCalled()
  579. })
  580. test("calls clearTask even with parent task (delegation via metadata)", async () => {
  581. // Setup parent and child tasks
  582. const parentTask = new Task(defaultTaskOptions)
  583. const childTask = new Task(defaultTaskOptions)
  584. // Set up parent-child relationship
  585. ;(childTask as any).parentTask = parentTask
  586. ;(childTask as any).rootTask = parentTask
  587. // Mock the provider methods
  588. const clearTaskSpy = vi.spyOn(provider, "clearTask").mockResolvedValue(undefined)
  589. const postStateToWebviewSpy = vi.spyOn(provider, "postStateToWebview").mockResolvedValue(undefined)
  590. // Add both tasks to stack (parent first, then child)
  591. await provider.addClineToStack(parentTask)
  592. await provider.addClineToStack(childTask)
  593. // Get the message handler
  594. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  595. // Trigger clearTask message
  596. await messageHandler({ type: "clearTask" })
  597. // Verify clearTask was called (delegation happens via metadata, not finishSubTask)
  598. expect(clearTaskSpy).toHaveBeenCalled()
  599. expect(postStateToWebviewSpy).toHaveBeenCalled()
  600. })
  601. test("handles case when no current task exists", async () => {
  602. // Don't add any tasks to the stack
  603. // Mock the provider methods
  604. const clearTaskSpy = vi.spyOn(provider, "clearTask").mockResolvedValue(undefined)
  605. const postStateToWebviewSpy = vi.spyOn(provider, "postStateToWebview").mockResolvedValue(undefined)
  606. // Get the message handler
  607. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  608. // Trigger clearTask message
  609. await messageHandler({ type: "clearTask" })
  610. // When there's no current task, clearTask is still called (it handles the no-task case internally)
  611. expect(clearTaskSpy).toHaveBeenCalled()
  612. expect(postStateToWebviewSpy).toHaveBeenCalled()
  613. })
  614. test("correctly identifies task scenario for issue #4602", async () => {
  615. // This test validates the fix for issue #4602
  616. // where canceling during API retry correctly uses clearTask
  617. const mockCline = new Task(defaultTaskOptions)
  618. // Mock the provider methods
  619. const clearTaskSpy = vi.spyOn(provider, "clearTask").mockResolvedValue(undefined)
  620. // Add only one task to stack
  621. await provider.addClineToStack(mockCline)
  622. // Verify stack size is 1
  623. expect(provider.getTaskStackSize()).toBe(1)
  624. // Get the message handler
  625. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  626. // Trigger clearTask message (simulating cancel during API retry)
  627. await messageHandler({ type: "clearTask" })
  628. // clearTask should be called (delegation handled via metadata)
  629. expect(clearTaskSpy).toHaveBeenCalled()
  630. })
  631. })
  632. test("addClineToStack adds multiple Cline instances to the stack", async () => {
  633. // Setup Cline instance with auto-mock from the top of the file
  634. const mockCline1 = new Task(defaultTaskOptions) // Create a new mocked instance
  635. const mockCline2 = new Task(defaultTaskOptions) // Create a new mocked instance
  636. Object.defineProperty(mockCline1, "taskId", { value: "test-task-id-1", writable: true })
  637. Object.defineProperty(mockCline2, "taskId", { value: "test-task-id-2", writable: true })
  638. // add Cline instances to the stack
  639. await provider.addClineToStack(mockCline1)
  640. await provider.addClineToStack(mockCline2)
  641. // verify cline instances were added to the stack
  642. expect(provider.getTaskStackSize()).toBe(2)
  643. // verify current cline instance is the last one added
  644. expect(provider.getCurrentTask()).toBe(mockCline2)
  645. })
  646. test("getState returns correct initial state", async () => {
  647. const state = await provider.getState()
  648. expect(state).toHaveProperty("apiConfiguration")
  649. expect(state.apiConfiguration).toHaveProperty("apiProvider")
  650. expect(state).toHaveProperty("customInstructions")
  651. expect(state).toHaveProperty("alwaysAllowReadOnly")
  652. expect(state).toHaveProperty("alwaysAllowWrite")
  653. expect(state).toHaveProperty("alwaysAllowExecute")
  654. expect(state).toHaveProperty("alwaysAllowBrowser")
  655. expect(state).toHaveProperty("taskHistory")
  656. expect(state).toHaveProperty("soundEnabled")
  657. expect(state).toHaveProperty("ttsEnabled")
  658. expect(state).toHaveProperty("writeDelayMs")
  659. })
  660. test("language is set to VSCode language", async () => {
  661. // Mock VSCode language as Spanish
  662. ;(vscode.env as any).language = "pt-BR"
  663. const state = await provider.getState()
  664. expect(state.language).toBe("pt-BR")
  665. })
  666. test("writeDelayMs defaults to 1000ms", async () => {
  667. // Mock globalState.get to return undefined for writeDelayMs
  668. ;(mockContext.globalState.get as any).mockImplementation((key: string) =>
  669. key === "writeDelayMs" ? undefined : null,
  670. )
  671. const state = await provider.getState()
  672. expect(state.writeDelayMs).toBe(1000)
  673. })
  674. test("handles writeDelayMs message", async () => {
  675. await provider.resolveWebviewView(mockWebviewView)
  676. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  677. await messageHandler({ type: "updateSettings", updatedSettings: { writeDelayMs: 2000 } })
  678. expect(updateGlobalStateSpy).toHaveBeenCalledWith("writeDelayMs", 2000)
  679. expect(mockContext.globalState.update).toHaveBeenCalledWith("writeDelayMs", 2000)
  680. expect(mockPostMessage).toHaveBeenCalled()
  681. })
  682. test("updates sound utility when sound setting changes", async () => {
  683. await provider.resolveWebviewView(mockWebviewView)
  684. // Get the message handler from onDidReceiveMessage
  685. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  686. // Simulate setting sound to enabled
  687. await messageHandler({ type: "updateSettings", updatedSettings: { soundEnabled: true } })
  688. expect(updateGlobalStateSpy).toHaveBeenCalledWith("soundEnabled", true)
  689. expect(mockContext.globalState.update).toHaveBeenCalledWith("soundEnabled", true)
  690. expect(mockPostMessage).toHaveBeenCalled()
  691. // Simulate setting sound to disabled
  692. await messageHandler({ type: "updateSettings", updatedSettings: { soundEnabled: false } })
  693. expect(mockContext.globalState.update).toHaveBeenCalledWith("soundEnabled", false)
  694. expect(mockPostMessage).toHaveBeenCalled()
  695. // Simulate setting tts to enabled
  696. await messageHandler({ type: "updateSettings", updatedSettings: { ttsEnabled: true } })
  697. expect(setTtsEnabled).toHaveBeenCalledWith(true)
  698. expect(mockContext.globalState.update).toHaveBeenCalledWith("ttsEnabled", true)
  699. expect(mockPostMessage).toHaveBeenCalled()
  700. // Simulate setting tts to disabled
  701. await messageHandler({ type: "updateSettings", updatedSettings: { ttsEnabled: false } })
  702. expect(setTtsEnabled).toHaveBeenCalledWith(false)
  703. expect(mockContext.globalState.update).toHaveBeenCalledWith("ttsEnabled", false)
  704. expect(mockPostMessage).toHaveBeenCalled()
  705. })
  706. test("autoCondenseContext defaults to true", async () => {
  707. // Mock globalState.get to return undefined for autoCondenseContext
  708. ;(mockContext.globalState.get as any).mockImplementation((key: string) =>
  709. key === "autoCondenseContext" ? undefined : null,
  710. )
  711. const state = await provider.getState()
  712. expect(state.autoCondenseContext).toBe(true)
  713. })
  714. test("handles autoCondenseContext message", async () => {
  715. await provider.resolveWebviewView(mockWebviewView)
  716. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  717. await messageHandler({ type: "updateSettings", updatedSettings: { autoCondenseContext: false } })
  718. expect(updateGlobalStateSpy).toHaveBeenCalledWith("autoCondenseContext", false)
  719. expect(mockContext.globalState.update).toHaveBeenCalledWith("autoCondenseContext", false)
  720. expect(mockPostMessage).toHaveBeenCalled()
  721. })
  722. test("autoCondenseContextPercent defaults to 100", async () => {
  723. // Mock globalState.get to return undefined for autoCondenseContextPercent
  724. ;(mockContext.globalState.get as any).mockImplementation((key: string) =>
  725. key === "autoCondenseContextPercent" ? undefined : null,
  726. )
  727. const state = await provider.getState()
  728. expect(state.autoCondenseContextPercent).toBe(100)
  729. })
  730. test("handles autoCondenseContextPercent message", async () => {
  731. await provider.resolveWebviewView(mockWebviewView)
  732. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  733. await messageHandler({ type: "updateSettings", updatedSettings: { autoCondenseContextPercent: 75 } })
  734. expect(updateGlobalStateSpy).toHaveBeenCalledWith("autoCondenseContextPercent", 75)
  735. expect(mockContext.globalState.update).toHaveBeenCalledWith("autoCondenseContextPercent", 75)
  736. expect(mockPostMessage).toHaveBeenCalled()
  737. })
  738. it("loads saved API config when switching modes", async () => {
  739. await provider.resolveWebviewView(mockWebviewView)
  740. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  741. const profile: ProviderSettingsEntry = { name: "test-config", id: "test-id", apiProvider: "anthropic" }
  742. ;(provider as any).providerSettingsManager = {
  743. getModeConfigId: vi.fn().mockResolvedValue("test-id"),
  744. listConfig: vi.fn().mockResolvedValue([profile]),
  745. activateProfile: vi.fn().mockResolvedValue(profile),
  746. setModeConfig: vi.fn(),
  747. getProfile: vi.fn().mockResolvedValue(profile),
  748. } as any
  749. // Switch to architect mode
  750. await messageHandler({ type: "mode", text: "architect" })
  751. // Should load the saved config for architect mode
  752. expect(provider.providerSettingsManager.getModeConfigId).toHaveBeenCalledWith("architect")
  753. expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ name: "test-config" })
  754. expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
  755. })
  756. it("saves current config when switching to mode without config", async () => {
  757. await provider.resolveWebviewView(mockWebviewView)
  758. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  759. ;(provider as any).providerSettingsManager = {
  760. getModeConfigId: vi.fn().mockResolvedValue(undefined),
  761. listConfig: vi
  762. .fn()
  763. .mockResolvedValue([{ name: "current-config", id: "current-id", apiProvider: "anthropic" }]),
  764. setModeConfig: vi.fn(),
  765. } as any
  766. provider.setValue("currentApiConfigName", "current-config")
  767. // Switch to architect mode
  768. await messageHandler({ type: "mode", text: "architect" })
  769. // Should save current config as default for architect mode
  770. expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "current-id")
  771. })
  772. it("saves config as default for current mode when loading config", async () => {
  773. await provider.resolveWebviewView(mockWebviewView)
  774. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  775. const profile: ProviderSettingsEntry = { apiProvider: "anthropic", id: "new-id", name: "new-config" }
  776. ;(provider as any).providerSettingsManager = {
  777. activateProfile: vi.fn().mockResolvedValue(profile),
  778. listConfig: vi.fn().mockResolvedValue([profile]),
  779. setModeConfig: vi.fn(),
  780. getModeConfigId: vi.fn().mockResolvedValue(undefined),
  781. } as any
  782. // First set the mode
  783. await messageHandler({ type: "mode", text: "architect" })
  784. // Then load the config
  785. await messageHandler({ type: "loadApiConfiguration", text: "new-config" })
  786. // Should save new config as default for architect mode
  787. expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "new-id")
  788. })
  789. it("load API configuration by ID works and updates mode config", async () => {
  790. await provider.resolveWebviewView(mockWebviewView)
  791. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  792. const profile: ProviderSettingsEntry = {
  793. name: "config-by-id",
  794. id: "config-id-123",
  795. apiProvider: "anthropic",
  796. }
  797. ;(provider as any).providerSettingsManager = {
  798. activateProfile: vi.fn().mockResolvedValue(profile),
  799. listConfig: vi.fn().mockResolvedValue([profile]),
  800. setModeConfig: vi.fn(),
  801. getModeConfigId: vi.fn().mockResolvedValue(undefined),
  802. } as any
  803. // First set the mode
  804. await messageHandler({ type: "mode", text: "architect" })
  805. // Then load the config by ID
  806. await messageHandler({ type: "loadApiConfigurationById", text: "config-id-123" })
  807. // Should save new config as default for architect mode
  808. expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "config-id-123")
  809. // Ensure the `activateProfile` method was called with the correct ID
  810. expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ id: "config-id-123" })
  811. })
  812. test("handles browserToolEnabled setting", async () => {
  813. await provider.resolveWebviewView(mockWebviewView)
  814. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  815. // Test browserToolEnabled
  816. await messageHandler({ type: "updateSettings", updatedSettings: { browserToolEnabled: true } })
  817. expect(mockContext.globalState.update).toHaveBeenCalledWith("browserToolEnabled", true)
  818. expect(mockPostMessage).toHaveBeenCalled()
  819. // Verify state includes browserToolEnabled
  820. const state = await provider.getState()
  821. expect(state).toHaveProperty("browserToolEnabled")
  822. expect(state.browserToolEnabled).toBe(true) // Default value should be true
  823. })
  824. test("handles showRooIgnoredFiles setting", async () => {
  825. await provider.resolveWebviewView(mockWebviewView)
  826. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  827. // Default value should be false
  828. expect((await provider.getState()).showRooIgnoredFiles).toBe(false)
  829. // Test showRooIgnoredFiles with true
  830. await messageHandler({ type: "updateSettings", updatedSettings: { showRooIgnoredFiles: true } })
  831. expect(mockContext.globalState.update).toHaveBeenCalledWith("showRooIgnoredFiles", true)
  832. expect(mockPostMessage).toHaveBeenCalled()
  833. expect((await provider.getState()).showRooIgnoredFiles).toBe(true)
  834. // Test showRooIgnoredFiles with false
  835. await messageHandler({ type: "updateSettings", updatedSettings: { showRooIgnoredFiles: false } })
  836. expect(mockContext.globalState.update).toHaveBeenCalledWith("showRooIgnoredFiles", false)
  837. expect(mockPostMessage).toHaveBeenCalled()
  838. expect((await provider.getState()).showRooIgnoredFiles).toBe(false)
  839. })
  840. test("handles updatePrompt message correctly", async () => {
  841. await provider.resolveWebviewView(mockWebviewView)
  842. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  843. // Mock existing prompts
  844. const existingPrompts = {
  845. code: {
  846. roleDefinition: "existing code role",
  847. customInstructions: "existing code prompt",
  848. },
  849. architect: {
  850. roleDefinition: "existing architect role",
  851. customInstructions: "existing architect prompt",
  852. },
  853. }
  854. provider.setValue("customModePrompts", existingPrompts)
  855. // Test updating a prompt
  856. await messageHandler({
  857. type: "updatePrompt",
  858. promptMode: "code",
  859. customPrompt: "new code prompt",
  860. })
  861. // Verify state was updated correctly
  862. expect(mockContext.globalState.update).toHaveBeenCalledWith("customModePrompts", {
  863. ...existingPrompts,
  864. code: "new code prompt",
  865. })
  866. // Verify state was posted to webview
  867. expect(mockPostMessage).toHaveBeenCalledWith(
  868. expect.objectContaining({
  869. type: "state",
  870. state: expect.objectContaining({
  871. customModePrompts: {
  872. ...existingPrompts,
  873. code: "new code prompt",
  874. },
  875. }),
  876. }),
  877. )
  878. })
  879. test("customModePrompts defaults to empty object", async () => {
  880. // Mock globalState.get to return undefined for customModePrompts
  881. ;(mockContext.globalState.get as any).mockImplementation((key: string) => {
  882. if (key === "customModePrompts") {
  883. return undefined
  884. }
  885. return null
  886. })
  887. const state = await provider.getState()
  888. expect(state.customModePrompts).toEqual({})
  889. })
  890. test("handles maxWorkspaceFiles message", async () => {
  891. await provider.resolveWebviewView(mockWebviewView)
  892. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  893. await messageHandler({ type: "updateSettings", updatedSettings: { maxWorkspaceFiles: 300 } })
  894. expect(updateGlobalStateSpy).toHaveBeenCalledWith("maxWorkspaceFiles", 300)
  895. expect(mockContext.globalState.update).toHaveBeenCalledWith("maxWorkspaceFiles", 300)
  896. expect(mockPostMessage).toHaveBeenCalled()
  897. })
  898. test("handles mode-specific custom instructions updates", async () => {
  899. await provider.resolveWebviewView(mockWebviewView)
  900. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  901. // Mock existing prompts
  902. const existingPrompts = {
  903. code: {
  904. roleDefinition: "Code role",
  905. customInstructions: "Old instructions",
  906. },
  907. }
  908. mockContext.globalState.get = vi.fn((key: string) => {
  909. if (key === "customModePrompts") {
  910. return existingPrompts
  911. }
  912. return undefined
  913. })
  914. // Update custom instructions for code mode
  915. await messageHandler({
  916. type: "updatePrompt",
  917. promptMode: "code",
  918. customPrompt: {
  919. roleDefinition: "Code role",
  920. customInstructions: "New instructions",
  921. },
  922. })
  923. // Verify state was updated correctly
  924. expect(mockContext.globalState.update).toHaveBeenCalledWith("customModePrompts", {
  925. code: {
  926. roleDefinition: "Code role",
  927. customInstructions: "New instructions",
  928. },
  929. })
  930. })
  931. it("saves mode config when updating API configuration", async () => {
  932. // Setup mock context with mode and config name
  933. mockContext = {
  934. ...mockContext,
  935. globalState: {
  936. ...mockContext.globalState,
  937. get: vi.fn((key: string) => {
  938. if (key === "mode") {
  939. return "code"
  940. } else if (key === "currentApiConfigName") {
  941. return "test-config"
  942. }
  943. return undefined
  944. }),
  945. update: vi.fn(),
  946. keys: vi.fn().mockReturnValue([]),
  947. },
  948. } as unknown as vscode.ExtensionContext
  949. // Create new provider with updated mock context
  950. provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
  951. await provider.resolveWebviewView(mockWebviewView)
  952. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  953. ;(provider as any).providerSettingsManager = {
  954. listConfig: vi.fn().mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
  955. saveConfig: vi.fn().mockResolvedValue("test-id"),
  956. setModeConfig: vi.fn(),
  957. } as any
  958. // Update API configuration
  959. await messageHandler({
  960. type: "upsertApiConfiguration",
  961. text: "test-config",
  962. apiConfiguration: { apiProvider: "anthropic" },
  963. })
  964. // Should save config as default for current mode
  965. expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("code", "test-id")
  966. })
  967. test("file content includes line numbers", async () => {
  968. const { extractTextFromFile } = await import("../../../integrations/misc/extract-text")
  969. const result = await extractTextFromFile("test.js")
  970. expect(result).toBe("1 | const x = 1;\n2 | const y = 2;\n3 | const z = 3;")
  971. })
  972. describe("deleteMessage", () => {
  973. beforeEach(async () => {
  974. await provider.resolveWebviewView(mockWebviewView)
  975. })
  976. test("handles deletion with confirmation dialog", async () => {
  977. // Setup mock messages
  978. const mockMessages = [
  979. { ts: 1000, type: "say", say: "user_feedback" }, // User message 1
  980. { ts: 2000, type: "say", say: "tool" }, // Tool message
  981. { ts: 3000, type: "say", say: "text" }, // Message before delete
  982. { ts: 4000, type: "say", say: "browser_action" }, // Message to delete
  983. { ts: 5000, type: "say", say: "user_feedback" }, // Next user message
  984. { ts: 6000, type: "say", say: "user_feedback" }, // Final message
  985. ] as ClineMessage[]
  986. const mockApiHistory = [
  987. { ts: 1000 },
  988. { ts: 2000 },
  989. { ts: 3000 },
  990. { ts: 4000 },
  991. { ts: 5000 },
  992. { ts: 6000 },
  993. ] as (Anthropic.MessageParam & { ts?: number })[]
  994. // Setup Task instance with auto-mock from the top of the file
  995. const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
  996. mockCline.clineMessages = mockMessages // Set test-specific messages
  997. mockCline.apiConversationHistory = mockApiHistory // Set API history
  998. await provider.addClineToStack(mockCline) // Add the mocked instance to the stack
  999. // Mock getTaskWithId
  1000. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  1001. historyItem: { id: "test-task-id" },
  1002. })
  1003. // Mock createTaskWithHistoryItem
  1004. ;(provider as any).createTaskWithHistoryItem = vi.fn()
  1005. // Trigger message deletion
  1006. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  1007. await messageHandler({ type: "deleteMessage", value: 4000 })
  1008. // Verify that the dialog message was sent to webview
  1009. expect(mockPostMessage).toHaveBeenCalledWith({
  1010. type: "showDeleteMessageDialog",
  1011. messageTs: 4000,
  1012. hasCheckpoint: false,
  1013. })
  1014. // Simulate user confirming deletion through the dialog
  1015. await messageHandler({ type: "deleteMessageConfirm", messageTs: 4000 })
  1016. // Verify only messages before the deleted message were kept
  1017. expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([
  1018. mockMessages[0],
  1019. mockMessages[1],
  1020. mockMessages[2],
  1021. ])
  1022. // Verify only API messages before the deleted message were kept
  1023. expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
  1024. mockApiHistory[0],
  1025. mockApiHistory[1],
  1026. mockApiHistory[2],
  1027. ])
  1028. // createTaskWithHistoryItem is only called when restoring checkpoints or aborting tasks
  1029. expect((provider as any).createTaskWithHistoryItem).not.toHaveBeenCalled()
  1030. })
  1031. test("handles case when no current task exists", async () => {
  1032. // Clear the cline stack
  1033. ;(provider as any).clineStack = []
  1034. // Trigger message deletion
  1035. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  1036. await messageHandler({ type: "deleteMessage", value: 2000 })
  1037. // Verify no dialog was shown since there's no current cline
  1038. expect(mockPostMessage).not.toHaveBeenCalledWith(
  1039. expect.objectContaining({
  1040. type: "showDeleteMessageDialog",
  1041. }),
  1042. )
  1043. })
  1044. })
  1045. describe("editMessage", () => {
  1046. beforeEach(async () => {
  1047. await provider.resolveWebviewView(mockWebviewView)
  1048. })
  1049. test("handles edit with confirmation dialog", async () => {
  1050. // Setup mock messages
  1051. const mockMessages = [
  1052. { ts: 1000, type: "say", say: "user_feedback" }, // User message 1
  1053. { ts: 2000, type: "say", say: "tool" }, // Tool message
  1054. { ts: 3000, type: "say", say: "text" }, // Message before edit
  1055. { ts: 4000, type: "say", say: "browser_action" }, // Message to edit
  1056. { ts: 5000, type: "say", say: "user_feedback" }, // Next user message
  1057. { ts: 6000, type: "say", say: "user_feedback" }, // Final message
  1058. ] as ClineMessage[]
  1059. const mockApiHistory = [
  1060. { ts: 1000 },
  1061. { ts: 2000 },
  1062. { ts: 3000 },
  1063. { ts: 4000 },
  1064. { ts: 5000 },
  1065. { ts: 6000 },
  1066. ] as (Anthropic.MessageParam & { ts?: number })[]
  1067. // Setup Task instance with auto-mock from the top of the file
  1068. const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
  1069. mockCline.clineMessages = mockMessages // Set test-specific messages
  1070. mockCline.apiConversationHistory = mockApiHistory // Set API history
  1071. // Explicitly mock the overwrite methods since they're not being called in the tests
  1072. mockCline.overwriteClineMessages = vi.fn()
  1073. mockCline.overwriteApiConversationHistory = vi.fn()
  1074. mockCline.handleWebviewAskResponse = vi.fn()
  1075. await provider.addClineToStack(mockCline) // Add the mocked instance to the stack
  1076. // Mock getTaskWithId
  1077. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  1078. historyItem: { id: "test-task-id" },
  1079. })
  1080. // Trigger message edit
  1081. // Get the message handler function that was registered with the webview
  1082. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  1083. // Call the message handler with a submitEditedMessage message
  1084. await messageHandler({
  1085. type: "submitEditedMessage",
  1086. value: 4000,
  1087. editedMessageContent: "Edited message content",
  1088. })
  1089. // Verify that the dialog message was sent to webview
  1090. expect(mockPostMessage).toHaveBeenCalledWith({
  1091. type: "showEditMessageDialog",
  1092. messageTs: 4000,
  1093. text: "Edited message content",
  1094. hasCheckpoint: false,
  1095. images: undefined,
  1096. })
  1097. // Simulate user confirming edit through the dialog
  1098. await messageHandler({
  1099. type: "editMessageConfirm",
  1100. messageTs: 4000,
  1101. text: "Edited message content",
  1102. })
  1103. // Verify correct messages were kept - delete from the preceding user message to truly replace it
  1104. expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([])
  1105. // Verify correct API messages were kept
  1106. expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([])
  1107. // The new flow calls webviewMessageHandler recursively with askResponse
  1108. // We need to verify the recursive call happened by checking if the handler was called again
  1109. expect((mockWebviewView.webview.onDidReceiveMessage as any).mock.calls.length).toBeGreaterThanOrEqual(1)
  1110. })
  1111. })
  1112. describe("getSystemPrompt", () => {
  1113. beforeEach(async () => {
  1114. mockPostMessage.mockClear()
  1115. await provider.resolveWebviewView(mockWebviewView)
  1116. // Reset and setup mock
  1117. mockAddCustomInstructions.mockClear()
  1118. mockAddCustomInstructions.mockImplementation(
  1119. (modeInstructions: string, globalInstructions: string, _cwd: string) => {
  1120. return Promise.resolve(modeInstructions || globalInstructions || "")
  1121. },
  1122. )
  1123. })
  1124. const getMessageHandler = () => {
  1125. const mockCalls = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls
  1126. expect(mockCalls.length).toBeGreaterThan(0)
  1127. return mockCalls[0][0]
  1128. }
  1129. test("handles mcpEnabled setting correctly", async () => {
  1130. await provider.resolveWebviewView(mockWebviewView)
  1131. const handler = getMessageHandler()
  1132. expect(typeof handler).toBe("function")
  1133. // Test with mcpEnabled: true
  1134. vi.spyOn(provider, "getState").mockResolvedValueOnce({
  1135. apiConfiguration: {
  1136. apiProvider: "openrouter" as const,
  1137. },
  1138. mcpEnabled: true,
  1139. mode: "code" as const,
  1140. experiments: experimentDefault,
  1141. } as any)
  1142. await handler({ type: "getSystemPrompt", mode: "code" })
  1143. // Verify system prompt was generated and sent
  1144. expect(mockPostMessage).toHaveBeenCalledWith(
  1145. expect.objectContaining({
  1146. type: "systemPrompt",
  1147. text: expect.any(String),
  1148. mode: "code",
  1149. }),
  1150. )
  1151. // Reset for second test
  1152. mockPostMessage.mockClear()
  1153. // Test with mcpEnabled: false
  1154. vi.spyOn(provider, "getState").mockResolvedValueOnce({
  1155. apiConfiguration: {
  1156. apiProvider: "openrouter" as const,
  1157. },
  1158. mcpEnabled: false,
  1159. mode: "code" as const,
  1160. experiments: experimentDefault,
  1161. } as any)
  1162. await handler({ type: "getSystemPrompt", mode: "code" })
  1163. // Verify system prompt was generated and sent
  1164. expect(mockPostMessage).toHaveBeenCalledWith(
  1165. expect.objectContaining({
  1166. type: "systemPrompt",
  1167. text: expect.any(String),
  1168. mode: "code",
  1169. }),
  1170. )
  1171. })
  1172. test("handles errors gracefully", async () => {
  1173. // Mock SYSTEM_PROMPT to throw an error
  1174. const { SYSTEM_PROMPT } = await import("../../prompts/system")
  1175. vi.mocked(SYSTEM_PROMPT).mockRejectedValueOnce(new Error("Test error"))
  1176. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  1177. await messageHandler({ type: "getSystemPrompt", mode: "code" })
  1178. expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.get_system_prompt")
  1179. })
  1180. test("uses code mode custom instructions", async () => {
  1181. await provider.resolveWebviewView(mockWebviewView)
  1182. // Mock getState to return custom instructions for code mode
  1183. vi.spyOn(provider, "getState").mockResolvedValue({
  1184. apiConfiguration: {
  1185. apiProvider: "openrouter" as const,
  1186. },
  1187. customModePrompts: {
  1188. code: { customInstructions: "Code mode specific instructions" },
  1189. },
  1190. mode: "code" as const,
  1191. experiments: experimentDefault,
  1192. } as any)
  1193. // Trigger getSystemPrompt
  1194. const handler = getMessageHandler()
  1195. await handler({ type: "getSystemPrompt", mode: "code" })
  1196. // Verify system prompt was generated and sent
  1197. expect(mockPostMessage).toHaveBeenCalledWith(
  1198. expect.objectContaining({
  1199. type: "systemPrompt",
  1200. text: expect.any(String),
  1201. mode: "code",
  1202. }),
  1203. )
  1204. })
  1205. test("uses correct mode-specific instructions when mode is specified", async () => {
  1206. await provider.resolveWebviewView(mockWebviewView)
  1207. // Mock getState to return architect mode instructions
  1208. vi.spyOn(provider, "getState").mockResolvedValue({
  1209. apiConfiguration: {
  1210. apiProvider: "openrouter",
  1211. },
  1212. customModePrompts: {
  1213. architect: { customInstructions: "Architect mode instructions" },
  1214. },
  1215. mode: "architect",
  1216. mcpEnabled: false,
  1217. browserViewportSize: "900x600",
  1218. experiments: experimentDefault,
  1219. } as any)
  1220. // Trigger getSystemPrompt for architect mode
  1221. const handler = getMessageHandler()
  1222. await handler({ type: "getSystemPrompt", mode: "architect" })
  1223. // Verify system prompt was generated and sent
  1224. expect(mockPostMessage).toHaveBeenCalledWith(
  1225. expect.objectContaining({
  1226. type: "systemPrompt",
  1227. text: expect.any(String),
  1228. mode: "architect",
  1229. }),
  1230. )
  1231. })
  1232. // Tests for browser tool support - simplified to focus on behavior
  1233. test("generates system prompt with different browser tool configurations", async () => {
  1234. await provider.resolveWebviewView(mockWebviewView)
  1235. const handler = getMessageHandler()
  1236. // Test 1: Browser tools enabled with compatible model and mode
  1237. vi.spyOn(provider, "getState").mockResolvedValueOnce({
  1238. apiConfiguration: {
  1239. apiProvider: "openrouter",
  1240. },
  1241. browserToolEnabled: true,
  1242. mode: "code", // code mode includes browser tool group
  1243. experiments: experimentDefault,
  1244. } as any)
  1245. await handler({ type: "getSystemPrompt", mode: "code" })
  1246. expect(mockPostMessage).toHaveBeenCalledWith(
  1247. expect.objectContaining({
  1248. type: "systemPrompt",
  1249. text: expect.any(String),
  1250. mode: "code",
  1251. }),
  1252. )
  1253. mockPostMessage.mockClear()
  1254. // Test 2: Browser tools disabled
  1255. vi.spyOn(provider, "getState").mockResolvedValueOnce({
  1256. apiConfiguration: {
  1257. apiProvider: "openrouter",
  1258. },
  1259. browserToolEnabled: false,
  1260. mode: "code",
  1261. experiments: experimentDefault,
  1262. } as any)
  1263. await handler({ type: "getSystemPrompt", mode: "code" })
  1264. expect(mockPostMessage).toHaveBeenCalledWith(
  1265. expect.objectContaining({
  1266. type: "systemPrompt",
  1267. text: expect.any(String),
  1268. mode: "code",
  1269. }),
  1270. )
  1271. })
  1272. })
  1273. describe("handleModeSwitch", () => {
  1274. beforeEach(async () => {
  1275. // Set up webview for each test
  1276. await provider.resolveWebviewView(mockWebviewView)
  1277. })
  1278. it("loads saved API config when switching modes", async () => {
  1279. const profile: ProviderSettingsEntry = {
  1280. name: "saved-config",
  1281. id: "saved-config-id",
  1282. apiProvider: "anthropic",
  1283. }
  1284. ;(provider as any).providerSettingsManager = {
  1285. getModeConfigId: vi.fn().mockResolvedValue("saved-config-id"),
  1286. listConfig: vi.fn().mockResolvedValue([profile]),
  1287. activateProfile: vi.fn().mockResolvedValue(profile),
  1288. setModeConfig: vi.fn(),
  1289. getProfile: vi.fn().mockResolvedValue(profile),
  1290. } as any
  1291. // Switch to architect mode
  1292. await provider.handleModeSwitch("architect")
  1293. // Verify mode was updated
  1294. expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "architect")
  1295. // Verify saved config was loaded
  1296. expect(provider.providerSettingsManager.getModeConfigId).toHaveBeenCalledWith("architect")
  1297. expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ name: "saved-config" })
  1298. expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "saved-config")
  1299. // Verify state was posted to webview
  1300. expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
  1301. })
  1302. test("saves current config when switching to mode without config", async () => {
  1303. ;(provider as any).providerSettingsManager = {
  1304. getModeConfigId: vi.fn().mockResolvedValue(undefined),
  1305. listConfig: vi
  1306. .fn()
  1307. .mockResolvedValue([{ name: "current-config", id: "current-id", apiProvider: "anthropic" }]),
  1308. setModeConfig: vi.fn(),
  1309. } as any
  1310. // Mock the ContextProxy's getValue method to return the current config name
  1311. const contextProxy = (provider as any).contextProxy
  1312. const getValueSpy = vi.spyOn(contextProxy, "getValue")
  1313. getValueSpy.mockImplementation((key: any) => {
  1314. if (key === "currentApiConfigName") return "current-config"
  1315. return undefined
  1316. })
  1317. // Switch to architect mode
  1318. await provider.handleModeSwitch("architect")
  1319. // Verify mode was updated
  1320. expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "architect")
  1321. // Verify current config was saved as default for new mode
  1322. expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "current-id")
  1323. // Verify state was posted to webview
  1324. expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
  1325. })
  1326. })
  1327. describe("createTaskWithHistoryItem mode validation", () => {
  1328. test("validates and falls back to default mode when restored mode no longer exists", async () => {
  1329. await provider.resolveWebviewView(mockWebviewView)
  1330. // Mock custom modes that don't include the saved mode
  1331. const mockCustomModesManager = {
  1332. getCustomModes: vi.fn().mockResolvedValue([
  1333. {
  1334. slug: "existing-mode",
  1335. name: "Existing Mode",
  1336. roleDefinition: "Test role",
  1337. groups: ["read"] as const,
  1338. },
  1339. ]),
  1340. dispose: vi.fn(),
  1341. }
  1342. ;(provider as any).customModesManager = mockCustomModesManager
  1343. // Mock getModeBySlug to return undefined for non-existent mode
  1344. const { getModeBySlug } = await import("../../../shared/modes")
  1345. vi.mocked(getModeBySlug)
  1346. .mockReturnValueOnce(undefined) // First call returns undefined (mode doesn't exist)
  1347. .mockReturnValue({
  1348. slug: "code",
  1349. name: "Code Mode",
  1350. roleDefinition: "You are a code assistant",
  1351. groups: ["read", "edit", "browser"],
  1352. }) // Subsequent calls return default mode
  1353. // Mock provider settings manager
  1354. ;(provider as any).providerSettingsManager = {
  1355. getModeConfigId: vi.fn().mockResolvedValue(undefined),
  1356. listConfig: vi.fn().mockResolvedValue([]),
  1357. }
  1358. // Spy on log method to verify warning was logged
  1359. const logSpy = vi.spyOn(provider, "log")
  1360. // Create history item with non-existent mode
  1361. const historyItem = {
  1362. id: "test-id",
  1363. ts: Date.now(),
  1364. task: "Test task",
  1365. mode: "non-existent-mode", // This mode doesn't exist
  1366. number: 1,
  1367. tokensIn: 0,
  1368. tokensOut: 0,
  1369. totalCost: 0,
  1370. }
  1371. // Initialize with history item
  1372. await provider.createTaskWithHistoryItem(historyItem)
  1373. // Verify mode validation occurred
  1374. expect(mockCustomModesManager.getCustomModes).toHaveBeenCalled()
  1375. expect(getModeBySlug).toHaveBeenCalledWith("non-existent-mode", expect.any(Array))
  1376. // Verify fallback to default mode
  1377. expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "code")
  1378. expect(logSpy).toHaveBeenCalledWith(
  1379. "Mode 'non-existent-mode' from history no longer exists. Falling back to default mode 'code'.",
  1380. )
  1381. // Verify history item was updated with default mode
  1382. expect(historyItem.mode).toBe("code")
  1383. })
  1384. test("preserves mode when it exists in custom modes", async () => {
  1385. await provider.resolveWebviewView(mockWebviewView)
  1386. // Mock custom modes that include the saved mode
  1387. const mockCustomModesManager = {
  1388. getCustomModes: vi.fn().mockResolvedValue([
  1389. {
  1390. slug: "custom-mode",
  1391. name: "Custom Mode",
  1392. roleDefinition: "Custom role",
  1393. groups: ["read", "edit"] as const,
  1394. },
  1395. ]),
  1396. dispose: vi.fn(),
  1397. }
  1398. ;(provider as any).customModesManager = mockCustomModesManager
  1399. // Mock getModeBySlug to return the custom mode
  1400. const { getModeBySlug } = await import("../../../shared/modes")
  1401. vi.mocked(getModeBySlug).mockReturnValue({
  1402. slug: "custom-mode",
  1403. name: "Custom Mode",
  1404. roleDefinition: "Custom role",
  1405. groups: ["read", "edit"],
  1406. })
  1407. // Mock provider settings manager
  1408. ;(provider as any).providerSettingsManager = {
  1409. getModeConfigId: vi.fn().mockResolvedValue("config-id"),
  1410. listConfig: vi
  1411. .fn()
  1412. .mockResolvedValue([{ name: "test-config", id: "config-id", apiProvider: "anthropic" }]),
  1413. activateProfile: vi
  1414. .fn()
  1415. .mockResolvedValue({ name: "test-config", id: "config-id", apiProvider: "anthropic" }),
  1416. }
  1417. // Spy on log method to verify no warning was logged
  1418. const logSpy = vi.spyOn(provider, "log")
  1419. // Create history item with existing custom mode
  1420. const historyItem = {
  1421. id: "test-id",
  1422. ts: Date.now(),
  1423. task: "Test task",
  1424. mode: "custom-mode",
  1425. number: 1,
  1426. tokensIn: 0,
  1427. tokensOut: 0,
  1428. totalCost: 0,
  1429. }
  1430. // Initialize with history item
  1431. await provider.createTaskWithHistoryItem(historyItem)
  1432. // Verify mode validation occurred
  1433. expect(mockCustomModesManager.getCustomModes).toHaveBeenCalled()
  1434. expect(getModeBySlug).toHaveBeenCalledWith("custom-mode", expect.any(Array))
  1435. // Verify mode was preserved
  1436. expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "custom-mode")
  1437. expect(logSpy).not.toHaveBeenCalledWith(expect.stringContaining("no longer exists"))
  1438. // Verify history item mode was not changed
  1439. expect(historyItem.mode).toBe("custom-mode")
  1440. })
  1441. test("preserves mode when it exists in built-in modes", async () => {
  1442. await provider.resolveWebviewView(mockWebviewView)
  1443. // Mock no custom modes
  1444. const mockCustomModesManager = {
  1445. getCustomModes: vi.fn().mockResolvedValue([]),
  1446. dispose: vi.fn(),
  1447. }
  1448. ;(provider as any).customModesManager = mockCustomModesManager
  1449. // Mock getModeBySlug to return built-in architect mode
  1450. const { getModeBySlug } = await import("../../../shared/modes")
  1451. vi.mocked(getModeBySlug).mockReturnValue({
  1452. slug: "architect",
  1453. name: "Architect Mode",
  1454. roleDefinition: "You are an architect",
  1455. groups: ["read", "edit"],
  1456. })
  1457. // Mock provider settings manager
  1458. ;(provider as any).providerSettingsManager = {
  1459. getModeConfigId: vi.fn().mockResolvedValue(undefined),
  1460. listConfig: vi.fn().mockResolvedValue([]),
  1461. }
  1462. // Create history item with built-in mode
  1463. const historyItem = {
  1464. id: "test-id",
  1465. ts: Date.now(),
  1466. task: "Test task",
  1467. mode: "architect",
  1468. number: 1,
  1469. tokensIn: 0,
  1470. tokensOut: 0,
  1471. totalCost: 0,
  1472. }
  1473. // Initialize with history item
  1474. await provider.createTaskWithHistoryItem(historyItem)
  1475. // Verify mode was preserved
  1476. expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "architect")
  1477. // Verify history item mode was not changed
  1478. expect(historyItem.mode).toBe("architect")
  1479. })
  1480. test("handles history items without mode property", async () => {
  1481. await provider.resolveWebviewView(mockWebviewView)
  1482. // Mock provider settings manager
  1483. ;(provider as any).providerSettingsManager = {
  1484. getModeConfigId: vi.fn().mockResolvedValue(undefined),
  1485. listConfig: vi.fn().mockResolvedValue([]),
  1486. }
  1487. // Create history item without mode
  1488. const historyItem = {
  1489. id: "test-id",
  1490. ts: Date.now(),
  1491. task: "Test task",
  1492. // No mode property
  1493. number: 1,
  1494. tokensIn: 0,
  1495. tokensOut: 0,
  1496. totalCost: 0,
  1497. }
  1498. // Initialize with history item
  1499. await provider.createTaskWithHistoryItem(historyItem)
  1500. // Verify no mode validation occurred (mode update not called)
  1501. expect(mockContext.globalState.update).not.toHaveBeenCalledWith("mode", expect.any(String))
  1502. })
  1503. test("continues with task restoration even if mode config loading fails", async () => {
  1504. await provider.resolveWebviewView(mockWebviewView)
  1505. // Mock custom modes
  1506. const mockCustomModesManager = {
  1507. getCustomModes: vi.fn().mockResolvedValue([]),
  1508. dispose: vi.fn(),
  1509. }
  1510. ;(provider as any).customModesManager = mockCustomModesManager
  1511. // Mock getModeBySlug to return built-in mode
  1512. const { getModeBySlug } = await import("../../../shared/modes")
  1513. vi.mocked(getModeBySlug).mockReturnValue({
  1514. slug: "code",
  1515. name: "Code Mode",
  1516. roleDefinition: "You are a code assistant",
  1517. groups: ["read", "edit", "browser"],
  1518. })
  1519. // Mock provider settings manager to throw error
  1520. ;(provider as any).providerSettingsManager = {
  1521. getModeConfigId: vi.fn().mockResolvedValue("config-id"),
  1522. listConfig: vi
  1523. .fn()
  1524. .mockResolvedValue([{ name: "test-config", id: "config-id", apiProvider: "anthropic" }]),
  1525. activateProfile: vi.fn().mockRejectedValue(new Error("Failed to load config")),
  1526. }
  1527. // Spy on log method
  1528. const logSpy = vi.spyOn(provider, "log")
  1529. // Create history item
  1530. const historyItem = {
  1531. id: "test-id",
  1532. ts: Date.now(),
  1533. task: "Test task",
  1534. mode: "code",
  1535. number: 1,
  1536. tokensIn: 0,
  1537. tokensOut: 0,
  1538. totalCost: 0,
  1539. }
  1540. // Initialize with history item - should not throw
  1541. await expect(provider.createTaskWithHistoryItem(historyItem)).resolves.not.toThrow()
  1542. // Verify error was logged but task restoration continued
  1543. expect(logSpy).toHaveBeenCalledWith(
  1544. expect.stringContaining("Failed to restore API configuration for mode 'code'"),
  1545. )
  1546. })
  1547. })
  1548. describe("updateCustomMode", () => {
  1549. test("updates both file and state when updating custom mode", async () => {
  1550. await provider.resolveWebviewView(mockWebviewView)
  1551. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  1552. // Mock CustomModesManager methods
  1553. ;(provider as any).customModesManager = {
  1554. updateCustomMode: vi.fn().mockResolvedValue(undefined),
  1555. getCustomModes: vi.fn().mockResolvedValue([
  1556. {
  1557. slug: "test-mode",
  1558. name: "Test Mode",
  1559. roleDefinition: "Updated role definition",
  1560. groups: ["read"] as const,
  1561. },
  1562. ]),
  1563. dispose: vi.fn(),
  1564. } as any
  1565. // Test updating a custom mode
  1566. await messageHandler({
  1567. type: "updateCustomMode",
  1568. modeConfig: {
  1569. slug: "test-mode",
  1570. name: "Test Mode",
  1571. roleDefinition: "Updated role definition",
  1572. groups: ["read"] as const,
  1573. },
  1574. })
  1575. // Verify CustomModesManager.updateCustomMode was called
  1576. expect(provider.customModesManager.updateCustomMode).toHaveBeenCalledWith(
  1577. "test-mode",
  1578. expect.objectContaining({
  1579. slug: "test-mode",
  1580. roleDefinition: "Updated role definition",
  1581. }),
  1582. )
  1583. // Verify state was updated
  1584. expect(mockContext.globalState.update).toHaveBeenCalledWith("customModes", [
  1585. { groups: ["read"], name: "Test Mode", roleDefinition: "Updated role definition", slug: "test-mode" },
  1586. ])
  1587. // Verify state was posted to webview
  1588. // Verify state was posted to webview with correct format
  1589. expect(mockPostMessage).toHaveBeenCalledWith(
  1590. expect.objectContaining({
  1591. type: "state",
  1592. state: expect.objectContaining({
  1593. customModes: [
  1594. expect.objectContaining({
  1595. slug: "test-mode",
  1596. roleDefinition: "Updated role definition",
  1597. }),
  1598. ],
  1599. }),
  1600. }),
  1601. )
  1602. })
  1603. })
  1604. describe("upsertApiConfiguration", () => {
  1605. test("handles error in upsertApiConfiguration gracefully", async () => {
  1606. await provider.resolveWebviewView(mockWebviewView)
  1607. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  1608. ;(provider as any).providerSettingsManager = {
  1609. setModeConfig: vi.fn().mockRejectedValue(new Error("Failed to update mode config")),
  1610. listConfig: vi
  1611. .fn()
  1612. .mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
  1613. } as any
  1614. // Mock getState to provide necessary data
  1615. vi.spyOn(provider, "getState").mockResolvedValue({
  1616. mode: "code",
  1617. currentApiConfigName: "test-config",
  1618. } as any)
  1619. // Trigger upsertApiConfiguration
  1620. await messageHandler({
  1621. type: "upsertApiConfiguration",
  1622. text: "test-config",
  1623. apiConfiguration: { apiProvider: "anthropic", apiKey: "test-key" },
  1624. })
  1625. // Verify error was logged and user was notified
  1626. expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
  1627. expect.stringContaining("Error create new api configuration"),
  1628. )
  1629. expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.create_api_config")
  1630. })
  1631. test("handles successful upsertApiConfiguration", async () => {
  1632. await provider.resolveWebviewView(mockWebviewView)
  1633. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  1634. ;(provider as any).providerSettingsManager = {
  1635. setModeConfig: vi.fn(),
  1636. saveConfig: vi.fn().mockResolvedValue(undefined),
  1637. listConfig: vi
  1638. .fn()
  1639. .mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
  1640. } as any
  1641. const testApiConfig = {
  1642. apiProvider: "anthropic" as const,
  1643. apiKey: "test-key",
  1644. }
  1645. // Trigger upsertApiConfiguration
  1646. await messageHandler({
  1647. type: "upsertApiConfiguration",
  1648. text: "test-config",
  1649. apiConfiguration: testApiConfig,
  1650. })
  1651. // Verify config was saved
  1652. expect(provider.providerSettingsManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
  1653. // Verify state updates
  1654. expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
  1655. { name: "test-config", id: "test-id", apiProvider: "anthropic" },
  1656. ])
  1657. expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
  1658. // Verify state was posted to webview
  1659. expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
  1660. })
  1661. test("handles buildApiHandler error in updateApiConfiguration", async () => {
  1662. await provider.resolveWebviewView(mockWebviewView)
  1663. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  1664. // Mock buildApiHandler to throw an error
  1665. const { buildApiHandler } = await import("../../../api")
  1666. ;(buildApiHandler as any).mockImplementationOnce(() => {
  1667. throw new Error("API handler error")
  1668. })
  1669. ;(provider as any).providerSettingsManager = {
  1670. setModeConfig: vi.fn(),
  1671. saveConfig: vi.fn().mockResolvedValue(undefined),
  1672. listConfig: vi
  1673. .fn()
  1674. .mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
  1675. } as any
  1676. // Setup Task instance with auto-mock from the top of the file
  1677. const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
  1678. await provider.addClineToStack(mockCline)
  1679. const testApiConfig = {
  1680. apiProvider: "anthropic" as const,
  1681. apiKey: "test-key",
  1682. }
  1683. // Trigger upsertApiConfiguration
  1684. await messageHandler({
  1685. type: "upsertApiConfiguration",
  1686. text: "test-config",
  1687. apiConfiguration: testApiConfig,
  1688. })
  1689. // Verify error handling
  1690. expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
  1691. expect.stringContaining("Error create new api configuration"),
  1692. )
  1693. expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.create_api_config")
  1694. // Verify state was still updated
  1695. expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
  1696. { name: "test-config", id: "test-id", apiProvider: "anthropic" },
  1697. ])
  1698. expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
  1699. })
  1700. test("handles successful saveApiConfiguration", async () => {
  1701. await provider.resolveWebviewView(mockWebviewView)
  1702. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  1703. ;(provider as any).providerSettingsManager = {
  1704. setModeConfig: vi.fn(),
  1705. saveConfig: vi.fn().mockResolvedValue(undefined),
  1706. listConfig: vi
  1707. .fn()
  1708. .mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
  1709. } as any
  1710. const testApiConfig = {
  1711. apiProvider: "anthropic" as const,
  1712. apiKey: "test-key",
  1713. }
  1714. // Trigger upsertApiConfiguration
  1715. await messageHandler({
  1716. type: "saveApiConfiguration",
  1717. text: "test-config",
  1718. apiConfiguration: testApiConfig,
  1719. })
  1720. // Verify config was saved
  1721. expect(provider.providerSettingsManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
  1722. // Verify state updates
  1723. expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
  1724. { name: "test-config", id: "test-id", apiProvider: "anthropic" },
  1725. ])
  1726. expect(updateGlobalStateSpy).toHaveBeenCalledWith("listApiConfigMeta", [
  1727. { name: "test-config", id: "test-id", apiProvider: "anthropic" },
  1728. ])
  1729. })
  1730. })
  1731. describe("browser connection features", () => {
  1732. beforeEach(async () => {
  1733. // Reset mocks
  1734. vi.clearAllMocks()
  1735. await provider.resolveWebviewView(mockWebviewView)
  1736. })
  1737. // These mocks are already defined at the top of the file
  1738. test("handles testBrowserConnection with provided URL", async () => {
  1739. // Get the message handler
  1740. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  1741. // Test with valid URL
  1742. await messageHandler({
  1743. type: "testBrowserConnection",
  1744. text: "http://localhost:9222",
  1745. })
  1746. // Verify postMessage was called with success result
  1747. expect(mockPostMessage).toHaveBeenCalledWith(
  1748. expect.objectContaining({
  1749. type: "browserConnectionResult",
  1750. success: true,
  1751. text: expect.stringContaining("Successfully connected to Chrome"),
  1752. }),
  1753. )
  1754. // Reset mock
  1755. mockPostMessage.mockClear()
  1756. // Test with invalid URL
  1757. await messageHandler({
  1758. type: "testBrowserConnection",
  1759. text: "http://inlocalhost:9222",
  1760. })
  1761. // Verify postMessage was called with failure result
  1762. expect(mockPostMessage).toHaveBeenCalledWith(
  1763. expect.objectContaining({
  1764. type: "browserConnectionResult",
  1765. success: false,
  1766. text: expect.stringContaining("Failed to connect to Chrome"),
  1767. }),
  1768. )
  1769. })
  1770. test("handles testBrowserConnection with auto-discovery", async () => {
  1771. // Get the message handler
  1772. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  1773. // Test auto-discovery (no URL provided)
  1774. await messageHandler({
  1775. type: "testBrowserConnection",
  1776. })
  1777. // Verify discoverChromeHostUrl was called
  1778. const { discoverChromeHostUrl } = await import("../../../services/browser/browserDiscovery")
  1779. expect(discoverChromeHostUrl).toHaveBeenCalled()
  1780. // Verify postMessage was called with success result
  1781. expect(mockPostMessage).toHaveBeenCalledWith(
  1782. expect.objectContaining({
  1783. type: "browserConnectionResult",
  1784. success: true,
  1785. text: expect.stringContaining("Auto-discovered and tested connection to Chrome"),
  1786. }),
  1787. )
  1788. })
  1789. })
  1790. })
  1791. describe("Project MCP Settings", () => {
  1792. let provider: ClineProvider
  1793. let mockContext: vscode.ExtensionContext
  1794. let mockOutputChannel: vscode.OutputChannel
  1795. let mockWebviewView: vscode.WebviewView
  1796. let mockPostMessage: any
  1797. beforeEach(() => {
  1798. vi.clearAllMocks()
  1799. mockContext = {
  1800. extensionPath: "/test/path",
  1801. extensionUri: {} as vscode.Uri,
  1802. globalState: {
  1803. get: vi.fn(),
  1804. update: vi.fn(),
  1805. keys: vi.fn().mockReturnValue([]),
  1806. },
  1807. secrets: {
  1808. get: vi.fn(),
  1809. store: vi.fn(),
  1810. delete: vi.fn(),
  1811. },
  1812. workspaceState: {
  1813. get: vi.fn().mockReturnValue(undefined),
  1814. update: vi.fn().mockResolvedValue(undefined),
  1815. keys: vi.fn().mockReturnValue([]),
  1816. },
  1817. subscriptions: [],
  1818. extension: {
  1819. packageJSON: { version: "1.0.0" },
  1820. },
  1821. globalStorageUri: {
  1822. fsPath: "/test/storage/path",
  1823. },
  1824. } as unknown as vscode.ExtensionContext
  1825. mockOutputChannel = {
  1826. appendLine: vi.fn(),
  1827. clear: vi.fn(),
  1828. dispose: vi.fn(),
  1829. } as unknown as vscode.OutputChannel
  1830. mockPostMessage = vi.fn()
  1831. mockWebviewView = {
  1832. webview: {
  1833. postMessage: mockPostMessage,
  1834. html: "",
  1835. options: {},
  1836. onDidReceiveMessage: vi.fn(),
  1837. asWebviewUri: vi.fn(),
  1838. cspSource: "vscode-webview://test-csp-source",
  1839. },
  1840. visible: true,
  1841. onDidDispose: vi.fn(),
  1842. onDidChangeVisibility: vi.fn(),
  1843. } as unknown as vscode.WebviewView
  1844. provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
  1845. })
  1846. test.skip("handles openProjectMcpSettings message", async () => {
  1847. // Mock workspace folders first
  1848. ;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
  1849. // Mock fs functions
  1850. const fs = await import("fs/promises")
  1851. const mockedFs = vi.mocked(fs)
  1852. mockedFs.mkdir.mockClear()
  1853. mockedFs.mkdir.mockResolvedValue(undefined)
  1854. mockedFs.writeFile.mockClear()
  1855. mockedFs.writeFile.mockResolvedValue(undefined)
  1856. // Mock fileExistsAtPath to return false (file doesn't exist)
  1857. const fsUtils = await import("../../../utils/fs")
  1858. vi.spyOn(fsUtils, "fileExistsAtPath").mockResolvedValue(false)
  1859. // Mock openFile
  1860. const openFileModule = await import("../../../integrations/misc/open-file")
  1861. const openFileSpy = vi.spyOn(openFileModule, "openFile").mockClear().mockResolvedValue(undefined)
  1862. // Set up the webview
  1863. await provider.resolveWebviewView(mockWebviewView)
  1864. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  1865. // Ensure the message handler is properly set up
  1866. expect(messageHandler).toBeDefined()
  1867. expect(typeof messageHandler).toBe("function")
  1868. // Trigger openProjectMcpSettings through the message handler
  1869. await messageHandler({
  1870. type: "openProjectMcpSettings",
  1871. })
  1872. // Check that fs.mkdir was called with the correct path
  1873. expect(mockedFs.mkdir).toHaveBeenCalledWith("/test/workspace/.roo", { recursive: true })
  1874. // Verify file was created with default content
  1875. expect(safeWriteJson).toHaveBeenCalledWith("/test/workspace/.roo/mcp.json", { mcpServers: {} })
  1876. // Check that openFile was called
  1877. expect(openFileSpy).toHaveBeenCalledWith("/test/workspace/.roo/mcp.json")
  1878. })
  1879. test("handles openProjectMcpSettings when workspace is not open", async () => {
  1880. await provider.resolveWebviewView(mockWebviewView)
  1881. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  1882. // Mock no workspace folders
  1883. ;(vscode.workspace as any).workspaceFolders = []
  1884. // Trigger openProjectMcpSettings
  1885. await messageHandler({ type: "openProjectMcpSettings" })
  1886. // Verify error message was shown
  1887. expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.no_workspace")
  1888. })
  1889. test.skip("handles openProjectMcpSettings file creation error", async () => {
  1890. await provider.resolveWebviewView(mockWebviewView)
  1891. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  1892. // Mock workspace folders
  1893. ;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
  1894. // Mock fs functions to fail
  1895. const fs = require("fs/promises")
  1896. fs.mkdir.mockRejectedValue(new Error("Failed to create directory"))
  1897. // Trigger openProjectMcpSettings
  1898. await messageHandler({
  1899. type: "openProjectMcpSettings",
  1900. })
  1901. // Verify error message was shown
  1902. expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
  1903. expect.stringContaining("Failed to create or open .roo/mcp.json"),
  1904. )
  1905. })
  1906. })
  1907. describe.skip("ContextProxy integration", () => {
  1908. let provider: ClineProvider
  1909. let mockContext: vscode.ExtensionContext
  1910. let mockOutputChannel: vscode.OutputChannel
  1911. let mockContextProxy: any
  1912. beforeEach(() => {
  1913. // Reset mocks
  1914. vi.clearAllMocks()
  1915. // Setup basic mocks
  1916. mockContext = {
  1917. globalState: {
  1918. get: vi.fn(),
  1919. update: vi.fn(),
  1920. keys: vi.fn().mockReturnValue([]),
  1921. },
  1922. workspaceState: {
  1923. get: vi.fn().mockReturnValue(undefined),
  1924. update: vi.fn().mockResolvedValue(undefined),
  1925. keys: vi.fn().mockReturnValue([]),
  1926. },
  1927. secrets: { get: vi.fn(), store: vi.fn(), delete: vi.fn() },
  1928. extensionUri: {} as vscode.Uri,
  1929. globalStorageUri: { fsPath: "/test/path" },
  1930. extension: { packageJSON: { version: "1.0.0" } },
  1931. } as unknown as vscode.ExtensionContext
  1932. mockOutputChannel = { appendLine: vi.fn() } as unknown as vscode.OutputChannel
  1933. mockContextProxy = new ContextProxy(mockContext)
  1934. provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", mockContextProxy)
  1935. })
  1936. test("updateGlobalState uses contextProxy", async () => {
  1937. await provider.setValue("currentApiConfigName", "testValue")
  1938. expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("currentApiConfigName", "testValue")
  1939. })
  1940. test("getGlobalState uses contextProxy", async () => {
  1941. mockContextProxy.getGlobalState.mockResolvedValueOnce("testValue")
  1942. const result = await provider.getValue("currentApiConfigName")
  1943. expect(mockContextProxy.getGlobalState).toHaveBeenCalledWith("currentApiConfigName")
  1944. expect(result).toBe("testValue")
  1945. })
  1946. test("storeSecret uses contextProxy", async () => {
  1947. await provider.setValue("apiKey", "test-secret")
  1948. expect(mockContextProxy.storeSecret).toHaveBeenCalledWith("apiKey", "test-secret")
  1949. })
  1950. test("contextProxy methods are available", () => {
  1951. // Verify the contextProxy has all the required methods
  1952. expect(mockContextProxy.getGlobalState).toBeDefined()
  1953. expect(mockContextProxy.updateGlobalState).toBeDefined()
  1954. expect(mockContextProxy.storeSecret).toBeDefined()
  1955. expect(mockContextProxy.setValue).toBeDefined()
  1956. expect(mockContextProxy.setValues).toBeDefined()
  1957. })
  1958. })
  1959. describe("getTelemetryProperties", () => {
  1960. let defaultTaskOptions: TaskOptions
  1961. let provider: ClineProvider
  1962. let mockContext: vscode.ExtensionContext
  1963. let mockOutputChannel: vscode.OutputChannel
  1964. let mockCline: any
  1965. beforeEach(() => {
  1966. // Reset mocks
  1967. vi.clearAllMocks()
  1968. // Initialize TelemetryService if not already initialized
  1969. if (!TelemetryService.hasInstance()) {
  1970. TelemetryService.createInstance([])
  1971. }
  1972. // Setup basic mocks
  1973. mockContext = {
  1974. globalState: {
  1975. get: vi.fn().mockImplementation((key: string) => {
  1976. if (key === "mode") return "code"
  1977. if (key === "apiProvider") return "anthropic"
  1978. return undefined
  1979. }),
  1980. update: vi.fn(),
  1981. keys: vi.fn().mockReturnValue([]),
  1982. },
  1983. workspaceState: {
  1984. get: vi.fn().mockReturnValue(undefined),
  1985. update: vi.fn().mockResolvedValue(undefined),
  1986. keys: vi.fn().mockReturnValue([]),
  1987. },
  1988. secrets: { get: vi.fn(), store: vi.fn(), delete: vi.fn() },
  1989. extensionUri: {} as vscode.Uri,
  1990. globalStorageUri: { fsPath: "/test/path" },
  1991. extension: { packageJSON: { version: "1.0.0" } },
  1992. } as unknown as vscode.ExtensionContext
  1993. mockOutputChannel = { appendLine: vi.fn() } as unknown as vscode.OutputChannel
  1994. provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
  1995. defaultTaskOptions = {
  1996. provider,
  1997. apiConfiguration: {
  1998. apiProvider: "openrouter",
  1999. },
  2000. }
  2001. // Setup Task instance with mocked getModel method
  2002. mockCline = new Task(defaultTaskOptions)
  2003. mockCline.api = {
  2004. getModel: vi.fn().mockReturnValue({
  2005. id: "claude-sonnet-4-20250514",
  2006. info: { contextWindow: 200000 },
  2007. }),
  2008. }
  2009. })
  2010. test("includes basic properties in telemetry", async () => {
  2011. const properties = await provider.getTelemetryProperties()
  2012. expect(properties).toHaveProperty("vscodeVersion")
  2013. expect(properties).toHaveProperty("platform")
  2014. expect(properties).toHaveProperty("appVersion", "1.0.0")
  2015. })
  2016. test("includes model ID from current Cline instance if available", async () => {
  2017. // Add mock Cline to stack
  2018. await provider.addClineToStack(mockCline)
  2019. const properties = await provider.getTelemetryProperties()
  2020. expect(properties).toHaveProperty("modelId", "claude-sonnet-4-20250514")
  2021. })
  2022. describe("cloud authentication telemetry", () => {
  2023. beforeEach(() => {
  2024. // Reset all mocks before each test
  2025. vi.clearAllMocks()
  2026. })
  2027. test("includes cloud authentication property when user is authenticated", async () => {
  2028. // Import the CloudService mock and update it
  2029. const { CloudService } = await import("@roo-code/cloud")
  2030. const mockCloudService = {
  2031. isAuthenticated: vi.fn().mockReturnValue(true),
  2032. }
  2033. // Update the existing mock
  2034. Object.defineProperty(CloudService, "instance", {
  2035. get: vi.fn().mockReturnValue(mockCloudService),
  2036. configurable: true,
  2037. })
  2038. const properties = await provider.getTelemetryProperties()
  2039. expect(properties).toHaveProperty("cloudIsAuthenticated", true)
  2040. })
  2041. test("includes cloud authentication property when user is not authenticated", async () => {
  2042. // Import the CloudService mock and update it
  2043. const { CloudService } = await import("@roo-code/cloud")
  2044. const mockCloudService = {
  2045. isAuthenticated: vi.fn().mockReturnValue(false),
  2046. }
  2047. // Update the existing mock
  2048. Object.defineProperty(CloudService, "instance", {
  2049. get: vi.fn().mockReturnValue(mockCloudService),
  2050. configurable: true,
  2051. })
  2052. const properties = await provider.getTelemetryProperties()
  2053. expect(properties).toHaveProperty("cloudIsAuthenticated", false)
  2054. })
  2055. test("handles CloudService errors gracefully", async () => {
  2056. // Import the CloudService mock and update it to throw an error
  2057. const { CloudService } = await import("@roo-code/cloud")
  2058. Object.defineProperty(CloudService, "instance", {
  2059. get: vi.fn().mockImplementation(() => {
  2060. throw new Error("CloudService not available")
  2061. }),
  2062. configurable: true,
  2063. })
  2064. const properties = await provider.getTelemetryProperties()
  2065. // Should still include basic telemetry properties
  2066. expect(properties).toHaveProperty("vscodeVersion")
  2067. expect(properties).toHaveProperty("platform")
  2068. expect(properties).toHaveProperty("appVersion", "1.0.0")
  2069. // Cloud property should be undefined when CloudService is not available
  2070. expect(properties).toHaveProperty("cloudIsAuthenticated", undefined)
  2071. })
  2072. test("handles CloudService method errors gracefully", async () => {
  2073. // Import the CloudService mock and update it
  2074. const { CloudService } = await import("@roo-code/cloud")
  2075. const mockCloudService = {
  2076. isAuthenticated: vi.fn().mockImplementation(() => {
  2077. throw new Error("Authentication check error")
  2078. }),
  2079. }
  2080. // Update the existing mock
  2081. Object.defineProperty(CloudService, "instance", {
  2082. get: vi.fn().mockReturnValue(mockCloudService),
  2083. configurable: true,
  2084. })
  2085. const properties = await provider.getTelemetryProperties()
  2086. // Should still include basic telemetry properties
  2087. expect(properties).toHaveProperty("vscodeVersion")
  2088. expect(properties).toHaveProperty("platform")
  2089. expect(properties).toHaveProperty("appVersion", "1.0.0")
  2090. // Property that errored should be undefined
  2091. expect(properties).toHaveProperty("cloudIsAuthenticated", undefined)
  2092. })
  2093. })
  2094. })
  2095. describe("ClineProvider - Router Models", () => {
  2096. let provider: ClineProvider
  2097. let mockContext: vscode.ExtensionContext
  2098. let mockOutputChannel: vscode.OutputChannel
  2099. let mockWebviewView: vscode.WebviewView
  2100. let mockPostMessage: any
  2101. beforeEach(() => {
  2102. vi.clearAllMocks()
  2103. const globalState: Record<string, string | undefined> = {}
  2104. const secrets: Record<string, string | undefined> = {}
  2105. mockContext = {
  2106. extensionPath: "/test/path",
  2107. extensionUri: {} as vscode.Uri,
  2108. globalState: {
  2109. get: vi.fn().mockImplementation((key: string) => globalState[key]),
  2110. update: vi
  2111. .fn()
  2112. .mockImplementation((key: string, value: string | undefined) => (globalState[key] = value)),
  2113. keys: vi.fn().mockImplementation(() => Object.keys(globalState)),
  2114. },
  2115. secrets: {
  2116. get: vi.fn().mockImplementation((key: string) => secrets[key]),
  2117. store: vi.fn().mockImplementation((key: string, value: string | undefined) => (secrets[key] = value)),
  2118. delete: vi.fn().mockImplementation((key: string) => delete secrets[key]),
  2119. },
  2120. workspaceState: {
  2121. get: vi.fn().mockReturnValue(undefined),
  2122. update: vi.fn().mockResolvedValue(undefined),
  2123. keys: vi.fn().mockReturnValue([]),
  2124. },
  2125. subscriptions: [],
  2126. extension: {
  2127. packageJSON: { version: "1.0.0" },
  2128. },
  2129. globalStorageUri: {
  2130. fsPath: "/test/storage/path",
  2131. },
  2132. } as unknown as vscode.ExtensionContext
  2133. mockOutputChannel = {
  2134. appendLine: vi.fn(),
  2135. clear: vi.fn(),
  2136. dispose: vi.fn(),
  2137. } as unknown as vscode.OutputChannel
  2138. mockPostMessage = vi.fn()
  2139. mockWebviewView = {
  2140. webview: {
  2141. postMessage: mockPostMessage,
  2142. html: "",
  2143. options: {},
  2144. onDidReceiveMessage: vi.fn(),
  2145. asWebviewUri: vi.fn(),
  2146. },
  2147. visible: true,
  2148. onDidDispose: vi.fn().mockImplementation((callback) => {
  2149. callback()
  2150. return { dispose: vi.fn() }
  2151. }),
  2152. onDidChangeVisibility: vi.fn().mockImplementation(() => ({ dispose: vi.fn() })),
  2153. } as unknown as vscode.WebviewView
  2154. if (!TelemetryService.hasInstance()) {
  2155. TelemetryService.createInstance([])
  2156. }
  2157. provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
  2158. })
  2159. test("handles requestRouterModels with successful responses", async () => {
  2160. await provider.resolveWebviewView(mockWebviewView)
  2161. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2162. // Mock getState to return API configuration
  2163. vi.spyOn(provider, "getState").mockResolvedValue({
  2164. apiConfiguration: {
  2165. openRouterApiKey: "openrouter-key",
  2166. requestyApiKey: "requesty-key",
  2167. unboundApiKey: "unbound-key",
  2168. litellmApiKey: "litellm-key",
  2169. litellmBaseUrl: "http://localhost:4000",
  2170. },
  2171. } as any)
  2172. const mockModels = {
  2173. "model-1": {
  2174. maxTokens: 4096,
  2175. contextWindow: 8192,
  2176. description: "Test model 1",
  2177. supportsPromptCache: false,
  2178. },
  2179. "model-2": {
  2180. maxTokens: 8192,
  2181. contextWindow: 16384,
  2182. description: "Test model 2",
  2183. supportsPromptCache: false,
  2184. },
  2185. }
  2186. const { getModels } = await import("../../../api/providers/fetchers/modelCache")
  2187. vi.mocked(getModels).mockResolvedValue(mockModels)
  2188. await messageHandler({ type: "requestRouterModels" })
  2189. // Verify getModels was called for each provider with correct options
  2190. expect(getModels).toHaveBeenCalledWith({ provider: "openrouter" })
  2191. expect(getModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" })
  2192. expect(getModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" })
  2193. expect(getModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" })
  2194. expect(getModels).toHaveBeenCalledWith({ provider: "deepinfra" })
  2195. expect(getModels).toHaveBeenCalledWith(
  2196. expect.objectContaining({
  2197. provider: "roo",
  2198. baseUrl: expect.any(String),
  2199. }),
  2200. )
  2201. expect(getModels).toHaveBeenCalledWith({
  2202. provider: "litellm",
  2203. apiKey: "litellm-key",
  2204. baseUrl: "http://localhost:4000",
  2205. })
  2206. expect(getModels).toHaveBeenCalledWith({ provider: "chutes" })
  2207. // Verify response was sent
  2208. expect(mockPostMessage).toHaveBeenCalledWith({
  2209. type: "routerModels",
  2210. routerModels: {
  2211. deepinfra: mockModels,
  2212. openrouter: mockModels,
  2213. requesty: mockModels,
  2214. unbound: mockModels,
  2215. roo: mockModels,
  2216. chutes: mockModels,
  2217. litellm: mockModels,
  2218. ollama: {},
  2219. lmstudio: {},
  2220. "vercel-ai-gateway": mockModels,
  2221. huggingface: {},
  2222. "io-intelligence": {},
  2223. },
  2224. values: undefined,
  2225. })
  2226. })
  2227. test("handles requestRouterModels with individual provider failures", async () => {
  2228. await provider.resolveWebviewView(mockWebviewView)
  2229. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2230. vi.spyOn(provider, "getState").mockResolvedValue({
  2231. apiConfiguration: {
  2232. openRouterApiKey: "openrouter-key",
  2233. requestyApiKey: "requesty-key",
  2234. unboundApiKey: "unbound-key",
  2235. litellmApiKey: "litellm-key",
  2236. litellmBaseUrl: "http://localhost:4000",
  2237. },
  2238. } as any)
  2239. const mockModels = {
  2240. "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model", supportsPromptCache: false },
  2241. }
  2242. const { getModels } = await import("../../../api/providers/fetchers/modelCache")
  2243. // Mock some providers to succeed and others to fail
  2244. vi.mocked(getModels)
  2245. .mockResolvedValueOnce(mockModels) // openrouter success
  2246. .mockRejectedValueOnce(new Error("Requesty API error")) // requesty fail
  2247. .mockRejectedValueOnce(new Error("Unbound API error")) // unbound fail
  2248. .mockResolvedValueOnce(mockModels) // vercel-ai-gateway success
  2249. .mockResolvedValueOnce(mockModels) // deepinfra success
  2250. .mockResolvedValueOnce(mockModels) // roo success
  2251. .mockRejectedValueOnce(new Error("Chutes API error")) // chutes fail
  2252. .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail
  2253. await messageHandler({ type: "requestRouterModels" })
  2254. // Verify main response includes successful providers and empty objects for failed ones
  2255. expect(mockPostMessage).toHaveBeenCalledWith({
  2256. type: "routerModels",
  2257. routerModels: {
  2258. deepinfra: mockModels,
  2259. openrouter: mockModels,
  2260. requesty: {},
  2261. unbound: {},
  2262. roo: mockModels,
  2263. chutes: {},
  2264. ollama: {},
  2265. lmstudio: {},
  2266. litellm: {},
  2267. "vercel-ai-gateway": mockModels,
  2268. huggingface: {},
  2269. "io-intelligence": {},
  2270. },
  2271. values: undefined,
  2272. })
  2273. // Verify error messages were sent for failed providers
  2274. expect(mockPostMessage).toHaveBeenCalledWith({
  2275. type: "singleRouterModelFetchResponse",
  2276. success: false,
  2277. error: "Requesty API error",
  2278. values: { provider: "requesty" },
  2279. })
  2280. expect(mockPostMessage).toHaveBeenCalledWith({
  2281. type: "singleRouterModelFetchResponse",
  2282. success: false,
  2283. error: "Unbound API error",
  2284. values: { provider: "unbound" },
  2285. })
  2286. expect(mockPostMessage).toHaveBeenCalledWith({
  2287. type: "singleRouterModelFetchResponse",
  2288. success: false,
  2289. error: "Unbound API error",
  2290. values: { provider: "unbound" },
  2291. })
  2292. expect(mockPostMessage).toHaveBeenCalledWith({
  2293. type: "singleRouterModelFetchResponse",
  2294. success: false,
  2295. error: "Chutes API error",
  2296. values: { provider: "chutes" },
  2297. })
  2298. expect(mockPostMessage).toHaveBeenCalledWith({
  2299. type: "singleRouterModelFetchResponse",
  2300. success: false,
  2301. error: "LiteLLM connection failed",
  2302. values: { provider: "litellm" },
  2303. })
  2304. })
  2305. test("handles requestRouterModels with LiteLLM values from message", async () => {
  2306. await provider.resolveWebviewView(mockWebviewView)
  2307. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2308. // Mock state without LiteLLM config
  2309. vi.spyOn(provider, "getState").mockResolvedValue({
  2310. apiConfiguration: {
  2311. openRouterApiKey: "openrouter-key",
  2312. requestyApiKey: "requesty-key",
  2313. unboundApiKey: "unbound-key",
  2314. // No litellm config
  2315. },
  2316. } as any)
  2317. const mockModels = {
  2318. "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model", supportsPromptCache: false },
  2319. }
  2320. const { getModels } = await import("../../../api/providers/fetchers/modelCache")
  2321. vi.mocked(getModels).mockResolvedValue(mockModels)
  2322. await messageHandler({
  2323. type: "requestRouterModels",
  2324. values: {
  2325. litellmApiKey: "message-litellm-key",
  2326. litellmBaseUrl: "http://message-url:4000",
  2327. },
  2328. })
  2329. // Verify LiteLLM was called with values from message
  2330. expect(getModels).toHaveBeenCalledWith({
  2331. provider: "litellm",
  2332. apiKey: "message-litellm-key",
  2333. baseUrl: "http://message-url:4000",
  2334. })
  2335. })
  2336. test("skips LiteLLM when neither config nor message values are provided", async () => {
  2337. await provider.resolveWebviewView(mockWebviewView)
  2338. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2339. vi.spyOn(provider, "getState").mockResolvedValue({
  2340. apiConfiguration: {
  2341. openRouterApiKey: "openrouter-key",
  2342. requestyApiKey: "requesty-key",
  2343. unboundApiKey: "unbound-key",
  2344. // No litellm config
  2345. },
  2346. } as any)
  2347. const mockModels = {
  2348. "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model", supportsPromptCache: false },
  2349. }
  2350. const { getModels } = await import("../../../api/providers/fetchers/modelCache")
  2351. vi.mocked(getModels).mockResolvedValue(mockModels)
  2352. await messageHandler({ type: "requestRouterModels" })
  2353. // Verify LiteLLM was NOT called
  2354. expect(getModels).not.toHaveBeenCalledWith(
  2355. expect.objectContaining({
  2356. provider: "litellm",
  2357. }),
  2358. )
  2359. // Verify response includes empty object for LiteLLM
  2360. expect(mockPostMessage).toHaveBeenCalledWith({
  2361. type: "routerModels",
  2362. routerModels: {
  2363. deepinfra: mockModels,
  2364. openrouter: mockModels,
  2365. requesty: mockModels,
  2366. unbound: mockModels,
  2367. roo: mockModels,
  2368. chutes: mockModels,
  2369. litellm: {},
  2370. ollama: {},
  2371. lmstudio: {},
  2372. "vercel-ai-gateway": mockModels,
  2373. huggingface: {},
  2374. "io-intelligence": {},
  2375. },
  2376. values: undefined,
  2377. })
  2378. })
  2379. test("handles requestLmStudioModels with proper response", async () => {
  2380. await provider.resolveWebviewView(mockWebviewView)
  2381. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2382. vi.spyOn(provider, "getState").mockResolvedValue({
  2383. apiConfiguration: {
  2384. lmStudioModelId: "model-1",
  2385. lmStudioBaseUrl: "http://localhost:1234",
  2386. },
  2387. } as any)
  2388. const mockModels = {
  2389. "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model", supportsPromptCache: false },
  2390. }
  2391. const { getModels } = await import("../../../api/providers/fetchers/modelCache")
  2392. vi.mocked(getModels).mockResolvedValue(mockModels)
  2393. await messageHandler({
  2394. type: "requestLmStudioModels",
  2395. })
  2396. expect(getModels).toHaveBeenCalledWith({
  2397. provider: "lmstudio",
  2398. baseUrl: "http://localhost:1234",
  2399. })
  2400. })
  2401. })
  2402. describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
  2403. let provider: ClineProvider
  2404. let mockContext: vscode.ExtensionContext
  2405. let mockOutputChannel: vscode.OutputChannel
  2406. let mockWebviewView: vscode.WebviewView
  2407. let mockPostMessage: any
  2408. let defaultTaskOptions: TaskOptions
  2409. beforeEach(() => {
  2410. vi.clearAllMocks()
  2411. if (!TelemetryService.hasInstance()) {
  2412. TelemetryService.createInstance([])
  2413. }
  2414. const globalState: Record<string, string | undefined> = {
  2415. mode: "code",
  2416. currentApiConfigName: "current-config",
  2417. }
  2418. const secrets: Record<string, string | undefined> = {}
  2419. mockContext = {
  2420. extensionPath: "/test/path",
  2421. extensionUri: {} as vscode.Uri,
  2422. globalState: {
  2423. get: vi.fn().mockImplementation((key: string) => globalState[key]),
  2424. update: vi
  2425. .fn()
  2426. .mockImplementation((key: string, value: string | undefined) => (globalState[key] = value)),
  2427. keys: vi.fn().mockImplementation(() => Object.keys(globalState)),
  2428. },
  2429. secrets: {
  2430. get: vi.fn().mockImplementation((key: string) => secrets[key]),
  2431. store: vi.fn().mockImplementation((key: string, value: string | undefined) => (secrets[key] = value)),
  2432. delete: vi.fn().mockImplementation((key: string) => delete secrets[key]),
  2433. },
  2434. workspaceState: {
  2435. get: vi.fn().mockReturnValue(undefined),
  2436. update: vi.fn().mockResolvedValue(undefined),
  2437. keys: vi.fn().mockReturnValue([]),
  2438. },
  2439. subscriptions: [],
  2440. extension: {
  2441. packageJSON: { version: "1.0.0" },
  2442. },
  2443. globalStorageUri: {
  2444. fsPath: "/test/storage/path",
  2445. },
  2446. } as unknown as vscode.ExtensionContext
  2447. mockOutputChannel = {
  2448. appendLine: vi.fn(),
  2449. clear: vi.fn(),
  2450. dispose: vi.fn(),
  2451. } as unknown as vscode.OutputChannel
  2452. mockPostMessage = vi.fn()
  2453. mockWebviewView = {
  2454. webview: {
  2455. postMessage: mockPostMessage,
  2456. html: "",
  2457. options: {},
  2458. onDidReceiveMessage: vi.fn(),
  2459. asWebviewUri: vi.fn(),
  2460. },
  2461. visible: true,
  2462. onDidDispose: vi.fn().mockImplementation((callback) => {
  2463. callback()
  2464. return { dispose: vi.fn() }
  2465. }),
  2466. onDidChangeVisibility: vi.fn().mockImplementation(() => ({ dispose: vi.fn() })),
  2467. } as unknown as vscode.WebviewView
  2468. provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
  2469. defaultTaskOptions = {
  2470. provider,
  2471. apiConfiguration: {
  2472. apiProvider: "openrouter",
  2473. },
  2474. }
  2475. // Mock getMcpHub method
  2476. provider.getMcpHub = vi.fn().mockReturnValue({
  2477. listTools: vi.fn().mockResolvedValue([]),
  2478. callTool: vi.fn().mockResolvedValue({ content: [] }),
  2479. listResources: vi.fn().mockResolvedValue([]),
  2480. readResource: vi.fn().mockResolvedValue({ contents: [] }),
  2481. getAllServers: vi.fn().mockReturnValue([]),
  2482. })
  2483. })
  2484. describe("Edit Messages with Images and Attachments", () => {
  2485. beforeEach(async () => {
  2486. await provider.resolveWebviewView(mockWebviewView)
  2487. })
  2488. test("handles editing messages containing images", async () => {
  2489. const mockMessages = [
  2490. { ts: 1000, type: "say", say: "user_feedback", text: "Original message" },
  2491. {
  2492. ts: 2000,
  2493. type: "say",
  2494. say: "user_feedback",
  2495. text: "Message with image",
  2496. images: [
  2497. "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
  2498. ],
  2499. value: 3000,
  2500. },
  2501. { ts: 3000, type: "say", say: "text", text: "AI response" },
  2502. ] as ClineMessage[]
  2503. const mockCline = new Task(defaultTaskOptions)
  2504. mockCline.clineMessages = mockMessages
  2505. mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }] as any[]
  2506. mockCline.overwriteClineMessages = vi.fn()
  2507. mockCline.overwriteApiConversationHistory = vi.fn()
  2508. mockCline.submitUserMessage = vi.fn()
  2509. await provider.addClineToStack(mockCline)
  2510. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  2511. historyItem: { id: "test-task-id" },
  2512. })
  2513. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2514. await messageHandler({
  2515. type: "submitEditedMessage",
  2516. value: 3000,
  2517. editedMessageContent: "Edited message with preserved images",
  2518. })
  2519. // Verify dialog was shown
  2520. expect(mockPostMessage).toHaveBeenCalledWith({
  2521. type: "showEditMessageDialog",
  2522. messageTs: 3000,
  2523. text: "Edited message with preserved images",
  2524. hasCheckpoint: false,
  2525. images: undefined,
  2526. })
  2527. // Simulate confirmation
  2528. await messageHandler({
  2529. type: "editMessageConfirm",
  2530. messageTs: 3000,
  2531. text: "Edited message with preserved images",
  2532. })
  2533. // Verify messages were edited correctly - the ORIGINAL user message and all subsequent messages are removed
  2534. expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]])
  2535. expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }])
  2536. // Verify submitUserMessage was called with the edited content
  2537. expect(mockCline.submitUserMessage).toHaveBeenCalledWith("Edited message with preserved images", [])
  2538. })
  2539. test("handles editing messages with file attachments", async () => {
  2540. const mockMessages = [
  2541. { ts: 1000, type: "say", say: "user_feedback", text: "Original message" },
  2542. {
  2543. ts: 2000,
  2544. type: "say",
  2545. say: "user_feedback",
  2546. text: "Message with file",
  2547. attachments: [{ path: "/path/to/file.txt", type: "file" }],
  2548. value: 3000,
  2549. },
  2550. { ts: 3000, type: "say", say: "text", text: "AI response" },
  2551. ] as ClineMessage[]
  2552. const mockCline = new Task(defaultTaskOptions)
  2553. mockCline.clineMessages = mockMessages
  2554. mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }] as any[]
  2555. mockCline.overwriteClineMessages = vi.fn()
  2556. mockCline.overwriteApiConversationHistory = vi.fn()
  2557. mockCline.submitUserMessage = vi.fn()
  2558. await provider.addClineToStack(mockCline)
  2559. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  2560. historyItem: { id: "test-task-id" },
  2561. })
  2562. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2563. await messageHandler({
  2564. type: "submitEditedMessage",
  2565. value: 3000,
  2566. editedMessageContent: "Edited message with file attachment",
  2567. })
  2568. // Verify dialog was shown
  2569. expect(mockPostMessage).toHaveBeenCalledWith({
  2570. type: "showEditMessageDialog",
  2571. messageTs: 3000,
  2572. text: "Edited message with file attachment",
  2573. hasCheckpoint: false,
  2574. images: undefined,
  2575. })
  2576. // Simulate user confirming the edit
  2577. await messageHandler({
  2578. type: "editMessageConfirm",
  2579. messageTs: 3000,
  2580. text: "Edited message with file attachment",
  2581. })
  2582. expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
  2583. expect(mockCline.submitUserMessage).toHaveBeenCalledWith("Edited message with file attachment", [])
  2584. })
  2585. })
  2586. describe("Network Failure Scenarios", () => {
  2587. beforeEach(async () => {
  2588. ;(vscode.window.showInformationMessage as any) = vi.fn()
  2589. await provider.resolveWebviewView(mockWebviewView)
  2590. })
  2591. test("handles network timeout during edit submission", async () => {
  2592. const mockCline = new Task(defaultTaskOptions)
  2593. mockCline.clineMessages = [
  2594. { ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 },
  2595. { ts: 2000, type: "say", say: "text", text: "AI response" },
  2596. ] as ClineMessage[]
  2597. mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
  2598. mockCline.overwriteClineMessages = vi.fn()
  2599. mockCline.overwriteApiConversationHistory = vi.fn()
  2600. mockCline.handleWebviewAskResponse = vi.fn().mockRejectedValue(new Error("Network timeout"))
  2601. await provider.addClineToStack(mockCline)
  2602. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  2603. historyItem: { id: "test-task-id" },
  2604. })
  2605. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2606. // Should not throw error, but handle gracefully
  2607. await expect(
  2608. messageHandler({
  2609. type: "submitEditedMessage",
  2610. value: 2000,
  2611. editedMessageContent: "Edited message",
  2612. }),
  2613. ).resolves.toBeUndefined()
  2614. // Verify dialog was shown
  2615. expect(mockPostMessage).toHaveBeenCalledWith({
  2616. type: "showEditMessageDialog",
  2617. messageTs: 2000,
  2618. text: "Edited message",
  2619. hasCheckpoint: false,
  2620. images: undefined,
  2621. })
  2622. // Simulate user confirming the edit
  2623. await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message" })
  2624. expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
  2625. })
  2626. test("handles connection drops during edit operation", async () => {
  2627. const mockCline = new Task(defaultTaskOptions)
  2628. mockCline.clineMessages = [
  2629. { ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 },
  2630. { ts: 2000, type: "say", say: "text", text: "AI response" },
  2631. ] as ClineMessage[]
  2632. mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
  2633. mockCline.overwriteClineMessages = vi.fn().mockRejectedValue(new Error("Connection lost"))
  2634. mockCline.overwriteApiConversationHistory = vi.fn()
  2635. mockCline.handleWebviewAskResponse = vi.fn()
  2636. await provider.addClineToStack(mockCline)
  2637. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  2638. historyItem: { id: "test-task-id" },
  2639. })
  2640. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2641. // Should handle connection error gracefully
  2642. await expect(
  2643. messageHandler({
  2644. type: "submitEditedMessage",
  2645. value: 2000,
  2646. editedMessageContent: "Edited message",
  2647. }),
  2648. ).resolves.toBeUndefined()
  2649. // Verify dialog was shown
  2650. expect(mockPostMessage).toHaveBeenCalledWith({
  2651. type: "showEditMessageDialog",
  2652. messageTs: 2000,
  2653. text: "Edited message",
  2654. hasCheckpoint: false,
  2655. images: undefined,
  2656. })
  2657. // Simulate user confirming the edit
  2658. await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message" })
  2659. // The error should be caught and shown
  2660. expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_editing_message")
  2661. })
  2662. })
  2663. describe("Concurrent Edit Operations", () => {
  2664. beforeEach(async () => {
  2665. ;(vscode.window.showInformationMessage as any) = vi.fn()
  2666. await provider.resolveWebviewView(mockWebviewView)
  2667. })
  2668. test("handles race conditions with simultaneous edits", async () => {
  2669. const mockCline = new Task(defaultTaskOptions)
  2670. mockCline.clineMessages = [
  2671. { ts: 1000, type: "say", say: "user_feedback", text: "Message 1", value: 2000 },
  2672. { ts: 2000, type: "say", say: "text", text: "AI response 1" },
  2673. { ts: 3000, type: "say", say: "user_feedback", text: "Message 2", value: 4000 },
  2674. { ts: 4000, type: "say", say: "text", text: "AI response 2" },
  2675. ] as ClineMessage[]
  2676. mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }, { ts: 4000 }] as any[]
  2677. mockCline.overwriteClineMessages = vi.fn()
  2678. mockCline.overwriteApiConversationHistory = vi.fn()
  2679. mockCline.handleWebviewAskResponse = vi.fn()
  2680. await provider.addClineToStack(mockCline)
  2681. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  2682. historyItem: { id: "test-task-id" },
  2683. })
  2684. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2685. // Simulate concurrent edit operations
  2686. const edit1Promise = messageHandler({
  2687. type: "submitEditedMessage",
  2688. value: 2000,
  2689. editedMessageContent: "Edited message 1",
  2690. })
  2691. const edit2Promise = messageHandler({
  2692. type: "submitEditedMessage",
  2693. value: 4000,
  2694. editedMessageContent: "Edited message 2",
  2695. })
  2696. await Promise.all([edit1Promise, edit2Promise])
  2697. // Verify dialogs were shown for both edits
  2698. expect(mockPostMessage).toHaveBeenCalledWith({
  2699. type: "showEditMessageDialog",
  2700. messageTs: 2000,
  2701. text: "Edited message 1",
  2702. hasCheckpoint: false,
  2703. images: undefined,
  2704. })
  2705. expect(mockPostMessage).toHaveBeenCalledWith({
  2706. type: "showEditMessageDialog",
  2707. messageTs: 4000,
  2708. text: "Edited message 2",
  2709. hasCheckpoint: false,
  2710. images: undefined,
  2711. })
  2712. // Simulate user confirming both edits
  2713. await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message 1" })
  2714. await messageHandler({ type: "editMessageConfirm", messageTs: 4000, text: "Edited message 2" })
  2715. // Both operations should complete without throwing
  2716. expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
  2717. })
  2718. })
  2719. describe("Edit Permissions and Authorization", () => {
  2720. beforeEach(async () => {
  2721. ;(vscode.window.showInformationMessage as any) = vi.fn()
  2722. await provider.resolveWebviewView(mockWebviewView)
  2723. })
  2724. test("handles edit permission failures", async () => {
  2725. // Mock no current cline (simulating permission failure)
  2726. vi.spyOn(provider, "getCurrentTask").mockReturnValue(undefined)
  2727. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2728. await messageHandler({
  2729. type: "submitEditedMessage",
  2730. value: 2000,
  2731. editedMessageContent: "Edited message",
  2732. })
  2733. // Should not show confirmation dialog when no current cline
  2734. expect(vscode.window.showInformationMessage).not.toHaveBeenCalled()
  2735. })
  2736. test("handles authorization failures during edit", async () => {
  2737. const mockCline = new Task(defaultTaskOptions)
  2738. mockCline.clineMessages = [
  2739. { ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 },
  2740. { ts: 2000, type: "say", say: "text", text: "AI response" },
  2741. ] as ClineMessage[]
  2742. mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
  2743. mockCline.overwriteClineMessages = vi.fn().mockRejectedValue(new Error("Unauthorized"))
  2744. mockCline.overwriteApiConversationHistory = vi.fn()
  2745. mockCline.handleWebviewAskResponse = vi.fn()
  2746. await provider.addClineToStack(mockCline)
  2747. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  2748. historyItem: { id: "test-task-id" },
  2749. })
  2750. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2751. await messageHandler({
  2752. type: "submitEditedMessage",
  2753. value: 2000,
  2754. editedMessageContent: "Edited message",
  2755. })
  2756. // Simulate confirmation
  2757. await messageHandler({
  2758. type: "editMessageConfirm",
  2759. messageTs: 2000,
  2760. text: "Edited message",
  2761. })
  2762. expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_editing_message")
  2763. })
  2764. describe("Malformed Requests and Invalid Formats", () => {
  2765. beforeEach(async () => {
  2766. await provider.resolveWebviewView(mockWebviewView)
  2767. })
  2768. test("handles malformed edit requests", async () => {
  2769. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2770. // Test with missing value
  2771. await messageHandler({
  2772. type: "submitEditedMessage",
  2773. editedMessageContent: "Edited message",
  2774. })
  2775. // Test with invalid value type
  2776. await messageHandler({
  2777. type: "submitEditedMessage",
  2778. value: "invalid",
  2779. editedMessageContent: "Edited message",
  2780. })
  2781. // Test with missing editedMessageContent
  2782. await messageHandler({
  2783. type: "submitEditedMessage",
  2784. value: 2000,
  2785. })
  2786. // Should not show confirmation dialog for malformed requests
  2787. expect(vscode.window.showInformationMessage).not.toHaveBeenCalled()
  2788. })
  2789. test("handles invalid message formats", async () => {
  2790. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2791. // Test with null message - should throw error
  2792. await expect(messageHandler(null)).rejects.toThrow()
  2793. // Test with undefined message - should throw error
  2794. await expect(messageHandler(undefined)).rejects.toThrow()
  2795. // Test with message missing type
  2796. await expect(
  2797. messageHandler({
  2798. value: 2000,
  2799. editedMessageContent: "Edited message",
  2800. }),
  2801. ).resolves.toBeUndefined()
  2802. // Should handle gracefully without errors
  2803. expect(vscode.window.showInformationMessage).not.toHaveBeenCalled()
  2804. })
  2805. test("handles invalid timestamp values", async () => {
  2806. ;(vscode.window.showInformationMessage as any) = vi.fn()
  2807. const mockCline = new Task(defaultTaskOptions)
  2808. mockCline.clineMessages = [
  2809. { ts: 1000, type: "say", say: "user_feedback", text: "Original message" },
  2810. { ts: 2000, type: "say", say: "text", text: "AI response" },
  2811. ] as ClineMessage[]
  2812. mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
  2813. await provider.addClineToStack(mockCline)
  2814. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2815. // Test with negative timestamp
  2816. await messageHandler({
  2817. type: "deleteMessage",
  2818. value: -1000,
  2819. })
  2820. // Test with zero timestamp
  2821. await messageHandler({
  2822. type: "deleteMessage",
  2823. value: 0,
  2824. })
  2825. // Invalid timestamps may still trigger confirmation dialog
  2826. // This is expected behavior as the system tries to process the message
  2827. })
  2828. })
  2829. describe("Operations on Deleted or Non-existent Messages", () => {
  2830. beforeEach(async () => {
  2831. ;(vscode.window.showInformationMessage as any) = vi.fn()
  2832. await provider.resolveWebviewView(mockWebviewView)
  2833. })
  2834. test("handles edit operations on deleted messages", async () => {
  2835. const mockCline = new Task(defaultTaskOptions)
  2836. mockCline.clineMessages = [
  2837. { ts: 1000, type: "say", say: "user_feedback", text: "Existing message" },
  2838. ] as ClineMessage[]
  2839. mockCline.apiConversationHistory = [{ ts: 1000 }] as any[]
  2840. mockCline.overwriteClineMessages = vi.fn()
  2841. mockCline.overwriteApiConversationHistory = vi.fn()
  2842. mockCline.handleWebviewAskResponse = vi.fn()
  2843. await provider.addClineToStack(mockCline)
  2844. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  2845. historyItem: { id: "test-task-id" },
  2846. })
  2847. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2848. // Try to edit a message that doesn't exist (timestamp 5000)
  2849. await messageHandler({
  2850. type: "submitEditedMessage",
  2851. value: 5000,
  2852. editedMessageContent: "Edited non-existent message",
  2853. })
  2854. // Should show edit dialog
  2855. expect(mockPostMessage).toHaveBeenCalledWith({
  2856. type: "showEditMessageDialog",
  2857. messageTs: 5000,
  2858. text: "Edited non-existent message",
  2859. hasCheckpoint: false,
  2860. images: undefined,
  2861. })
  2862. // Simulate user confirming the edit
  2863. await messageHandler({
  2864. type: "editMessageConfirm",
  2865. messageTs: 5000,
  2866. text: "Edited non-existent message",
  2867. })
  2868. // Should not perform any operations since message doesn't exist
  2869. expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled()
  2870. expect(mockCline.handleWebviewAskResponse).not.toHaveBeenCalled()
  2871. })
  2872. test("handles delete operations on non-existent messages", async () => {
  2873. const mockCline = new Task(defaultTaskOptions)
  2874. mockCline.clineMessages = [
  2875. { ts: 1000, type: "say", say: "user_feedback", text: "Existing message" },
  2876. ] as ClineMessage[]
  2877. mockCline.apiConversationHistory = [{ ts: 1000 }] as any[]
  2878. mockCline.overwriteClineMessages = vi.fn()
  2879. mockCline.overwriteApiConversationHistory = vi.fn()
  2880. await provider.addClineToStack(mockCline)
  2881. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  2882. historyItem: { id: "test-task-id" },
  2883. })
  2884. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2885. // Try to delete a message that doesn't exist (timestamp 5000)
  2886. await messageHandler({
  2887. type: "deleteMessage",
  2888. value: 5000,
  2889. })
  2890. // Should show delete dialog
  2891. expect(mockPostMessage).toHaveBeenCalledWith({
  2892. type: "showDeleteMessageDialog",
  2893. messageTs: 5000,
  2894. hasCheckpoint: false,
  2895. })
  2896. // Simulate user confirming the delete
  2897. await messageHandler({ type: "deleteMessageConfirm", messageTs: 5000 })
  2898. // Should not perform any operations since message doesn't exist
  2899. expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled()
  2900. })
  2901. })
  2902. describe("Resource Cleanup During Failed Operations", () => {
  2903. beforeEach(async () => {
  2904. ;(vscode.window.showInformationMessage as any) = vi.fn()
  2905. await provider.resolveWebviewView(mockWebviewView)
  2906. })
  2907. test("validates proper cleanup during failed edit operations", async () => {
  2908. const mockCline = new Task(defaultTaskOptions)
  2909. mockCline.clineMessages = [
  2910. { ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 },
  2911. { ts: 2000, type: "say", say: "text", text: "AI response" },
  2912. ] as ClineMessage[]
  2913. mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
  2914. // Mock cleanup tracking
  2915. const cleanupSpy = vi.fn()
  2916. mockCline.overwriteClineMessages = vi.fn().mockImplementation(() => {
  2917. cleanupSpy()
  2918. throw new Error("Operation failed")
  2919. })
  2920. mockCline.overwriteApiConversationHistory = vi.fn()
  2921. mockCline.handleWebviewAskResponse = vi.fn()
  2922. await provider.addClineToStack(mockCline)
  2923. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  2924. historyItem: { id: "test-task-id" },
  2925. })
  2926. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2927. await messageHandler({
  2928. type: "submitEditedMessage",
  2929. value: 2000,
  2930. editedMessageContent: "Edited message",
  2931. })
  2932. // Should show edit dialog
  2933. expect(mockPostMessage).toHaveBeenCalledWith({
  2934. type: "showEditMessageDialog",
  2935. messageTs: 2000,
  2936. text: "Edited message",
  2937. hasCheckpoint: false,
  2938. images: undefined,
  2939. })
  2940. // Simulate user confirming the edit
  2941. await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message" })
  2942. // Verify cleanup was attempted before failure
  2943. expect(cleanupSpy).toHaveBeenCalled()
  2944. expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_editing_message")
  2945. })
  2946. test("validates proper cleanup during failed delete operations", async () => {
  2947. const mockCline = new Task(defaultTaskOptions)
  2948. mockCline.clineMessages = [
  2949. { ts: 1000, type: "say", say: "user_feedback", text: "Message to delete" },
  2950. { ts: 2000, type: "say", say: "text", text: "AI response" },
  2951. ] as ClineMessage[]
  2952. mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
  2953. // Mock cleanup tracking
  2954. const cleanupSpy = vi.fn()
  2955. mockCline.overwriteClineMessages = vi.fn().mockImplementation(() => {
  2956. cleanupSpy()
  2957. throw new Error("Delete operation failed")
  2958. })
  2959. mockCline.overwriteApiConversationHistory = vi.fn()
  2960. await provider.addClineToStack(mockCline)
  2961. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  2962. historyItem: { id: "test-task-id" },
  2963. })
  2964. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  2965. await messageHandler({ type: "deleteMessage", value: 2000 })
  2966. // Should show delete dialog
  2967. expect(mockPostMessage).toHaveBeenCalledWith({
  2968. type: "showDeleteMessageDialog",
  2969. messageTs: 2000,
  2970. hasCheckpoint: false,
  2971. })
  2972. // Simulate user confirming the delete
  2973. await messageHandler({ type: "deleteMessageConfirm", messageTs: 2000 })
  2974. // Verify cleanup was attempted before failure
  2975. expect(cleanupSpy).toHaveBeenCalled()
  2976. expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_deleting_message")
  2977. })
  2978. })
  2979. describe("Large Message Payloads", () => {
  2980. beforeEach(async () => {
  2981. ;(vscode.window.showInformationMessage as any) = vi.fn()
  2982. await provider.resolveWebviewView(mockWebviewView)
  2983. })
  2984. test("handles editing messages with large text content", async () => {
  2985. // Create a large message (10KB of text)
  2986. const largeText = "A".repeat(10000)
  2987. const mockMessages = [
  2988. { ts: 1000, type: "say", say: "user_feedback", text: largeText, value: 2000 },
  2989. { ts: 2000, type: "say", say: "text", text: "AI response" },
  2990. ] as ClineMessage[]
  2991. const mockCline = new Task(defaultTaskOptions)
  2992. mockCline.clineMessages = mockMessages
  2993. mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
  2994. mockCline.overwriteClineMessages = vi.fn()
  2995. mockCline.overwriteApiConversationHistory = vi.fn()
  2996. mockCline.submitUserMessage = vi.fn()
  2997. await provider.addClineToStack(mockCline)
  2998. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  2999. historyItem: { id: "test-task-id" },
  3000. })
  3001. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  3002. const largeEditedContent = "B".repeat(15000)
  3003. await messageHandler({
  3004. type: "submitEditedMessage",
  3005. value: 2000,
  3006. editedMessageContent: largeEditedContent,
  3007. })
  3008. // Should show edit dialog
  3009. expect(mockPostMessage).toHaveBeenCalledWith({
  3010. type: "showEditMessageDialog",
  3011. messageTs: 2000,
  3012. text: largeEditedContent,
  3013. hasCheckpoint: false,
  3014. images: undefined,
  3015. })
  3016. // Simulate user confirming the edit
  3017. await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: largeEditedContent })
  3018. expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
  3019. expect(mockCline.submitUserMessage).toHaveBeenCalledWith(largeEditedContent, [])
  3020. })
  3021. test("handles deleting messages with large payloads", async () => {
  3022. // Create messages with large payloads
  3023. const largeText = "X".repeat(50000)
  3024. const mockMessages = [
  3025. { ts: 1000, type: "say", say: "user_feedback", text: "Small message" },
  3026. { ts: 2000, type: "say", say: "user_feedback", text: largeText },
  3027. { ts: 3000, type: "say", say: "text", text: "AI response" },
  3028. { ts: 4000, type: "say", say: "user_feedback", text: "Another large message: " + largeText },
  3029. ] as ClineMessage[]
  3030. const mockCline = new Task(defaultTaskOptions)
  3031. mockCline.clineMessages = mockMessages
  3032. mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }, { ts: 4000 }] as any[]
  3033. mockCline.overwriteClineMessages = vi.fn()
  3034. mockCline.overwriteApiConversationHistory = vi.fn()
  3035. await provider.addClineToStack(mockCline)
  3036. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  3037. historyItem: { id: "test-task-id" },
  3038. })
  3039. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  3040. await messageHandler({ type: "deleteMessage", value: 3000 })
  3041. // Should show delete dialog
  3042. expect(mockPostMessage).toHaveBeenCalledWith({
  3043. type: "showDeleteMessageDialog",
  3044. messageTs: 3000,
  3045. hasCheckpoint: false,
  3046. })
  3047. // Simulate user confirming the delete
  3048. await messageHandler({ type: "deleteMessageConfirm", messageTs: 3000 })
  3049. // Should handle large payloads without issues - keeps messages before the deleted one
  3050. expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]])
  3051. expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }, { ts: 2000 }])
  3052. })
  3053. })
  3054. describe("Error Messaging and User Feedback", () => {
  3055. beforeEach(async () => {
  3056. await provider.resolveWebviewView(mockWebviewView)
  3057. })
  3058. // Note: Error messaging test removed as the implementation may not have proper error handling in place
  3059. test("provides user feedback for successful operations", async () => {
  3060. const mockCline = new Task(defaultTaskOptions)
  3061. mockCline.clineMessages = [
  3062. { ts: 1000, type: "say", say: "user_feedback", text: "Message to delete" },
  3063. { ts: 2000, type: "say", say: "text", text: "AI response" },
  3064. ] as ClineMessage[]
  3065. mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
  3066. mockCline.overwriteClineMessages = vi.fn()
  3067. mockCline.overwriteApiConversationHistory = vi.fn()
  3068. await provider.addClineToStack(mockCline)
  3069. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  3070. historyItem: { id: "test-task-id" },
  3071. })
  3072. ;(provider as any).createTaskWithHistoryItem = vi.fn()
  3073. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  3074. await messageHandler({ type: "deleteMessage", value: 2000 })
  3075. // Should show delete dialog
  3076. expect(mockPostMessage).toHaveBeenCalledWith({
  3077. type: "showDeleteMessageDialog",
  3078. messageTs: 2000,
  3079. hasCheckpoint: false,
  3080. })
  3081. // Simulate user confirming the delete
  3082. await messageHandler({ type: "deleteMessageConfirm", messageTs: 2000 })
  3083. // Verify successful operation completed
  3084. expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
  3085. // createTaskWithHistoryItem is only called when restoring checkpoints or aborting tasks
  3086. expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
  3087. })
  3088. test("handles user cancellation gracefully", async () => {
  3089. // Test cancellation by not sending confirmation
  3090. const mockCline = new Task(defaultTaskOptions)
  3091. mockCline.clineMessages = [
  3092. { ts: 1000, type: "say", say: "user_feedback", text: "Message to edit" },
  3093. { ts: 2000, type: "say", say: "text", text: "AI response" },
  3094. ] as ClineMessage[]
  3095. mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
  3096. mockCline.overwriteClineMessages = vi.fn()
  3097. mockCline.overwriteApiConversationHistory = vi.fn()
  3098. mockCline.handleWebviewAskResponse = vi.fn()
  3099. await provider.addClineToStack(mockCline)
  3100. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  3101. await messageHandler({
  3102. type: "submitEditedMessage",
  3103. value: 2000,
  3104. editedMessageContent: "Edited message",
  3105. })
  3106. // Verify no operations were performed when user canceled
  3107. expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled()
  3108. expect(mockCline.overwriteApiConversationHistory).not.toHaveBeenCalled()
  3109. expect(mockCline.handleWebviewAskResponse).not.toHaveBeenCalled()
  3110. expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
  3111. })
  3112. })
  3113. describe("Edge Cases with Message Timestamps", () => {
  3114. beforeEach(async () => {
  3115. ;(vscode.window.showInformationMessage as any) = vi.fn()
  3116. await provider.resolveWebviewView(mockWebviewView)
  3117. })
  3118. test("handles messages with identical timestamps", async () => {
  3119. const mockCline = new Task(defaultTaskOptions)
  3120. mockCline.clineMessages = [
  3121. { ts: 1000, type: "say", say: "user_feedback", text: "Message 1" },
  3122. { ts: 1000, type: "say", say: "text", text: "Message 2 (same timestamp)" },
  3123. { ts: 1000, type: "say", say: "user_feedback", text: "Message 3 (same timestamp)" },
  3124. { ts: 2000, type: "say", say: "text", text: "Message 4" },
  3125. ] as ClineMessage[]
  3126. mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 1000 }, { ts: 1000 }, { ts: 2000 }] as any[]
  3127. mockCline.overwriteClineMessages = vi.fn()
  3128. mockCline.overwriteApiConversationHistory = vi.fn()
  3129. await provider.addClineToStack(mockCline)
  3130. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  3131. historyItem: { id: "test-task-id" },
  3132. })
  3133. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  3134. await messageHandler({ type: "deleteMessage", value: 1000 })
  3135. // Should show delete dialog
  3136. expect(mockPostMessage).toHaveBeenCalledWith({
  3137. type: "showDeleteMessageDialog",
  3138. messageTs: 1000,
  3139. hasCheckpoint: false,
  3140. })
  3141. // Simulate user confirming the delete
  3142. await messageHandler({ type: "deleteMessageConfirm", messageTs: 1000 })
  3143. // Should handle identical timestamps gracefully
  3144. expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
  3145. })
  3146. test("handles messages with future timestamps", async () => {
  3147. const futureTimestamp = Date.now() + 100000 // Future timestamp
  3148. const mockCline = new Task(defaultTaskOptions)
  3149. mockCline.clineMessages = [
  3150. { ts: 1000, type: "say", say: "user_feedback", text: "Past message" },
  3151. {
  3152. ts: futureTimestamp,
  3153. type: "say",
  3154. say: "user_feedback",
  3155. text: "Future message",
  3156. value: futureTimestamp + 1000,
  3157. },
  3158. { ts: futureTimestamp + 1000, type: "say", say: "text", text: "AI response" },
  3159. ] as ClineMessage[]
  3160. mockCline.apiConversationHistory = [
  3161. { ts: 1000 },
  3162. { ts: futureTimestamp },
  3163. { ts: futureTimestamp + 1000 },
  3164. ] as any[]
  3165. mockCline.overwriteClineMessages = vi.fn()
  3166. mockCline.overwriteApiConversationHistory = vi.fn()
  3167. mockCline.submitUserMessage = vi.fn()
  3168. await provider.addClineToStack(mockCline)
  3169. ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
  3170. historyItem: { id: "test-task-id" },
  3171. })
  3172. const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
  3173. await messageHandler({
  3174. type: "submitEditedMessage",
  3175. value: futureTimestamp + 1000,
  3176. editedMessageContent: "Edited future message",
  3177. })
  3178. // Should show edit dialog
  3179. expect(mockPostMessage).toHaveBeenCalledWith({
  3180. type: "showEditMessageDialog",
  3181. messageTs: futureTimestamp + 1000,
  3182. text: "Edited future message",
  3183. hasCheckpoint: false,
  3184. images: undefined,
  3185. })
  3186. // Simulate user confirming the edit
  3187. await messageHandler({
  3188. type: "editMessageConfirm",
  3189. messageTs: futureTimestamp + 1000,
  3190. text: "Edited future message",
  3191. })
  3192. // Should handle future timestamps correctly
  3193. expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
  3194. expect(mockCline.submitUserMessage).toHaveBeenCalled()
  3195. })
  3196. })
  3197. })
  3198. describe("getTaskWithId", () => {
  3199. it("returns empty apiConversationHistory when file is missing", async () => {
  3200. const historyItem = { id: "missing-api-file-task", task: "test task", ts: Date.now() }
  3201. vi.mocked(mockContext.globalState.get).mockImplementation((key: string) => {
  3202. if (key === "taskHistory") {
  3203. return [historyItem]
  3204. }
  3205. return undefined
  3206. })
  3207. const deleteTaskSpy = vi.spyOn(provider, "deleteTaskFromState")
  3208. const result = await (provider as any).getTaskWithId("missing-api-file-task")
  3209. expect(result.historyItem).toEqual(historyItem)
  3210. expect(result.apiConversationHistory).toEqual([])
  3211. expect(deleteTaskSpy).not.toHaveBeenCalled()
  3212. })
  3213. it("returns empty apiConversationHistory when file contains invalid JSON", async () => {
  3214. const historyItem = { id: "corrupt-api-task", task: "test task", ts: Date.now() }
  3215. vi.mocked(mockContext.globalState.get).mockImplementation((key: string) => {
  3216. if (key === "taskHistory") {
  3217. return [historyItem]
  3218. }
  3219. return undefined
  3220. })
  3221. // Make fileExistsAtPath return true so the read path is exercised
  3222. const fsUtils = await import("../../../utils/fs")
  3223. vi.spyOn(fsUtils, "fileExistsAtPath").mockResolvedValue(true)
  3224. // Make readFile return corrupted JSON
  3225. const fsp = await import("fs/promises")
  3226. vi.mocked(fsp.readFile).mockResolvedValueOnce("{not valid json!!!" as never)
  3227. const deleteTaskSpy = vi.spyOn(provider, "deleteTaskFromState")
  3228. const result = await (provider as any).getTaskWithId("corrupt-api-task")
  3229. expect(result.historyItem).toEqual(historyItem)
  3230. expect(result.apiConversationHistory).toEqual([])
  3231. expect(deleteTaskSpy).not.toHaveBeenCalled()
  3232. // Restore the spy
  3233. vi.mocked(fsUtils.fileExistsAtPath).mockRestore()
  3234. })
  3235. })
  3236. })