| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852 |
- // pnpm --filter roo-cline test core/webview/__tests__/ClineProvider.spec.ts
- import Anthropic from "@anthropic-ai/sdk"
- import * as vscode from "vscode"
- import axios from "axios"
- import {
- type ProviderSettingsEntry,
- type ClineMessage,
- type ExtensionMessage,
- type ExtensionState,
- ORGANIZATION_ALLOW_ALL,
- DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
- } from "@roo-code/types"
- import { TelemetryService } from "@roo-code/telemetry"
- import { defaultModeSlug } from "../../../shared/modes"
- import { experimentDefault } from "../../../shared/experiments"
- import { setTtsEnabled } from "../../../utils/tts"
- import { ContextProxy } from "../../config/ContextProxy"
- import { Task, TaskOptions } from "../../task/Task"
- import { safeWriteJson } from "../../../utils/safeWriteJson"
- import { ClineProvider } from "../ClineProvider"
- import { MessageManager } from "../../message-manager"
- // Mock setup must come before imports.
- vi.mock("../../prompts/sections/custom-instructions")
- vi.mock("p-wait-for", () => ({
- __esModule: true,
- default: vi.fn().mockResolvedValue(undefined),
- }))
- vi.mock("fs/promises", () => ({
- mkdir: vi.fn().mockResolvedValue(undefined),
- writeFile: vi.fn().mockResolvedValue(undefined),
- readFile: vi.fn().mockResolvedValue(""),
- unlink: vi.fn().mockResolvedValue(undefined),
- rmdir: vi.fn().mockResolvedValue(undefined),
- }))
- vi.mock("axios", () => ({
- default: {
- get: vi.fn().mockResolvedValue({ data: { data: [] } }),
- post: vi.fn(),
- },
- get: vi.fn().mockResolvedValue({ data: { data: [] } }),
- post: vi.fn(),
- }))
- vi.mock("../../../utils/safeWriteJson")
- vi.mock("../../../utils/storage", () => ({
- getSettingsDirectoryPath: vi.fn().mockResolvedValue("/test/settings/path"),
- getTaskDirectoryPath: vi.fn().mockResolvedValue("/test/task/path"),
- getGlobalStoragePath: vi.fn().mockResolvedValue("/test/storage/path"),
- }))
- vi.mock("@modelcontextprotocol/sdk/types.js", () => ({
- CallToolResultSchema: {},
- ListResourcesResultSchema: {},
- ListResourceTemplatesResultSchema: {},
- ListToolsResultSchema: {},
- ReadResourceResultSchema: {},
- ErrorCode: {
- InvalidRequest: "InvalidRequest",
- MethodNotFound: "MethodNotFound",
- InternalError: "InternalError",
- },
- McpError: class McpError extends Error {
- code: string
- constructor(code: string, message: string) {
- super(message)
- this.code = code
- this.name = "McpError"
- }
- },
- }))
- vi.mock("../../../services/browser/BrowserSession", () => ({
- BrowserSession: vi.fn().mockImplementation(() => ({
- testConnection: vi.fn().mockImplementation(async (url) => {
- if (url === "http://localhost:9222") {
- return {
- success: true,
- message: "Successfully connected to Chrome",
- endpoint: "ws://localhost:9222/devtools/browser/123",
- }
- } else {
- return {
- success: false,
- message: "Failed to connect to Chrome",
- endpoint: undefined,
- }
- }
- }),
- })),
- }))
- vi.mock("../../../services/browser/browserDiscovery", () => ({
- discoverChromeHostUrl: vi.fn().mockResolvedValue("http://localhost:9222"),
- tryChromeHostUrl: vi.fn().mockImplementation(async (url) => {
- return url === "http://localhost:9222"
- }),
- testBrowserConnection: vi.fn(),
- }))
- // Remove duplicate mock - it's already defined below.
- const mockAddCustomInstructions = vi.fn().mockResolvedValue("Combined instructions")
- ;(vi.mocked(await import("../../prompts/sections/custom-instructions")) as any).addCustomInstructions =
- mockAddCustomInstructions
- vi.mock("delay", () => {
- const delayFn = (_ms: number) => Promise.resolve()
- delayFn.createDelay = () => delayFn
- delayFn.reject = () => Promise.reject(new Error("Delay rejected"))
- delayFn.range = () => Promise.resolve()
- return { default: delayFn }
- })
- // MCP-related modules are mocked once above (lines 87-109).
- vi.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
- Client: vi.fn().mockImplementation(() => ({
- connect: vi.fn().mockResolvedValue(undefined),
- close: vi.fn().mockResolvedValue(undefined),
- listTools: vi.fn().mockResolvedValue({ tools: [] }),
- callTool: vi.fn().mockResolvedValue({ content: [] }),
- })),
- }))
- vi.mock("@modelcontextprotocol/sdk/client/stdio.js", () => ({
- StdioClientTransport: vi.fn().mockImplementation(() => ({
- connect: vi.fn().mockResolvedValue(undefined),
- close: vi.fn().mockResolvedValue(undefined),
- })),
- }))
- vi.mock("vscode", () => ({
- ExtensionContext: vi.fn(),
- OutputChannel: vi.fn(),
- WebviewView: vi.fn(),
- Uri: {
- joinPath: vi.fn(),
- file: vi.fn(),
- },
- CodeActionKind: {
- QuickFix: { value: "quickfix" },
- RefactorRewrite: { value: "refactor.rewrite" },
- },
- commands: {
- executeCommand: vi.fn().mockResolvedValue(undefined),
- },
- window: {
- showInformationMessage: vi.fn(),
- showWarningMessage: vi.fn(),
- showErrorMessage: vi.fn(),
- onDidChangeActiveTextEditor: vi.fn(() => ({ dispose: vi.fn() })),
- },
- workspace: {
- getConfiguration: vi.fn().mockReturnValue({
- get: vi.fn().mockReturnValue([]),
- update: vi.fn(),
- }),
- onDidChangeConfiguration: vi.fn().mockImplementation(() => ({
- dispose: vi.fn(),
- })),
- onDidSaveTextDocument: vi.fn(() => ({ dispose: vi.fn() })),
- onDidChangeTextDocument: vi.fn(() => ({ dispose: vi.fn() })),
- onDidOpenTextDocument: vi.fn(() => ({ dispose: vi.fn() })),
- onDidCloseTextDocument: vi.fn(() => ({ dispose: vi.fn() })),
- },
- env: {
- uriScheme: "vscode",
- language: "en",
- appName: "Visual Studio Code",
- },
- ExtensionMode: {
- Production: 1,
- Development: 2,
- Test: 3,
- },
- version: "1.85.0",
- }))
- vi.mock("../../../utils/tts", () => ({
- setTtsEnabled: vi.fn(),
- setTtsSpeed: vi.fn(),
- }))
- vi.mock("../../../api", () => ({
- buildApiHandler: vi.fn(),
- }))
- vi.mock("../../prompts/system", () => ({
- SYSTEM_PROMPT: vi.fn().mockImplementation(async () => "mocked system prompt"),
- codeMode: "code",
- }))
- vi.mock("../../../integrations/workspace/WorkspaceTracker", () => {
- return {
- default: vi.fn().mockImplementation(() => ({
- initializeFilePaths: vi.fn(),
- dispose: vi.fn(),
- })),
- }
- })
- vi.mock("../../task/Task", () => ({
- Task: vi.fn().mockImplementation((options: any) => ({
- api: undefined,
- abortTask: vi.fn(),
- handleWebviewAskResponse: vi.fn(),
- clineMessages: [],
- apiConversationHistory: [],
- overwriteClineMessages: vi.fn(),
- overwriteApiConversationHistory: vi.fn(),
- getTaskNumber: vi.fn().mockReturnValue(0),
- setTaskNumber: vi.fn(),
- setParentTask: vi.fn(),
- setRootTask: vi.fn(),
- taskId: options?.historyItem?.id || "test-task-id",
- emit: vi.fn(),
- })),
- }))
- vi.mock("../../../integrations/misc/extract-text", () => ({
- extractTextFromFile: vi.fn().mockImplementation(async (_filePath: string) => {
- const content = "const x = 1;\nconst y = 2;\nconst z = 3;"
- const lines = content.split("\n")
- return lines.map((line, index) => `${index + 1} | ${line}`).join("\n")
- }),
- }))
- vi.mock("../../../api/providers/fetchers/modelCache", () => ({
- getModels: vi.fn().mockResolvedValue({}),
- flushModels: vi.fn(),
- getModelsFromCache: vi.fn().mockReturnValue(undefined),
- }))
- vi.mock("../../../shared/modes", () => ({
- modes: [
- {
- slug: "code",
- name: "Code Mode",
- roleDefinition: "You are a code assistant",
- groups: ["read", "edit", "browser"],
- },
- {
- slug: "architect",
- name: "Architect Mode",
- roleDefinition: "You are an architect",
- groups: ["read", "edit"],
- },
- {
- slug: "ask",
- name: "Ask Mode",
- roleDefinition: "You are a helpful assistant",
- groups: ["read"],
- },
- ],
- getModeBySlug: vi.fn().mockReturnValue({
- slug: "code",
- name: "Code Mode",
- roleDefinition: "You are a code assistant",
- groups: ["read", "edit", "browser"],
- }),
- getGroupName: vi.fn().mockImplementation((group: string) => {
- // Return appropriate group names for different tool groups
- switch (group) {
- case "read":
- return "Read Tools"
- case "edit":
- return "Edit Tools"
- case "browser":
- return "Browser Tools"
- case "mcp":
- return "MCP Tools"
- default:
- return "General Tools"
- }
- }),
- defaultModeSlug: "code",
- }))
- vi.mock("../../prompts/system", () => ({
- SYSTEM_PROMPT: vi.fn().mockResolvedValue("mocked system prompt"),
- codeMode: "code",
- }))
- vi.mock("../../../api", () => ({
- buildApiHandler: vi.fn().mockReturnValue({
- getModel: vi.fn().mockReturnValue({
- id: "claude-3-sonnet",
- }),
- }),
- }))
- vi.mock("../../../integrations/misc/extract-text", () => ({
- extractTextFromFile: vi.fn().mockImplementation(async (_filePath: string) => {
- const content = "const x = 1;\nconst y = 2;\nconst z = 3;"
- const lines = content.split("\n")
- return lines.map((line, index) => `${index + 1} | ${line}`).join("\n")
- }),
- }))
- vi.mock("../../../api/providers/fetchers/modelCache", () => ({
- getModels: vi.fn().mockResolvedValue({}),
- flushModels: vi.fn(),
- getModelsFromCache: vi.fn().mockReturnValue(undefined),
- }))
- vi.mock("../diff/strategies/multi-search-replace", () => ({
- MultiSearchReplaceDiffStrategy: vi.fn().mockImplementation(() => ({
- getToolDescription: () => "test",
- getName: () => "test-strategy",
- applyDiff: vi.fn(),
- })),
- }))
- vi.mock("@roo-code/cloud", () => ({
- CloudService: {
- hasInstance: vi.fn().mockReturnValue(true),
- get instance() {
- return {
- isAuthenticated: vi.fn().mockReturnValue(false),
- }
- },
- },
- BridgeOrchestrator: {
- isEnabled: vi.fn().mockReturnValue(false),
- },
- getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"),
- }))
- afterAll(() => {
- vi.restoreAllMocks()
- })
- describe("ClineProvider", () => {
- beforeAll(() => {
- vi.mocked(Task).mockImplementation((options: any) => {
- const task: any = {
- api: undefined,
- abortTask: vi.fn(),
- handleWebviewAskResponse: vi.fn(),
- clineMessages: [],
- apiConversationHistory: [],
- overwriteClineMessages: vi.fn(),
- overwriteApiConversationHistory: vi.fn(),
- getTaskNumber: vi.fn().mockReturnValue(0),
- setTaskNumber: vi.fn(),
- setParentTask: vi.fn(),
- setRootTask: vi.fn(),
- taskId: options?.historyItem?.id || "test-task-id",
- emit: vi.fn(),
- }
- Object.defineProperty(task, "messageManager", {
- get: () => new MessageManager(task),
- })
- return task
- })
- })
- let defaultTaskOptions: TaskOptions
- let provider: ClineProvider
- let mockContext: vscode.ExtensionContext
- let mockOutputChannel: vscode.OutputChannel
- let mockWebviewView: vscode.WebviewView
- let mockPostMessage: any
- let updateGlobalStateSpy: any
- beforeEach(() => {
- vi.clearAllMocks()
- if (!TelemetryService.hasInstance()) {
- TelemetryService.createInstance([])
- }
- const globalState: Record<string, string | undefined> = {
- mode: "architect",
- currentApiConfigName: "current-config",
- }
- const secrets: Record<string, string | undefined> = {}
- mockContext = {
- extensionPath: "/test/path",
- extensionUri: {} as vscode.Uri,
- globalState: {
- get: vi.fn().mockImplementation((key: string) => globalState[key]),
- update: vi
- .fn()
- .mockImplementation((key: string, value: string | undefined) => (globalState[key] = value)),
- keys: vi.fn().mockImplementation(() => Object.keys(globalState)),
- },
- secrets: {
- get: vi.fn().mockImplementation((key: string) => secrets[key]),
- store: vi.fn().mockImplementation((key: string, value: string | undefined) => (secrets[key] = value)),
- delete: vi.fn().mockImplementation((key: string) => delete secrets[key]),
- },
- workspaceState: {
- get: vi.fn().mockReturnValue(undefined),
- update: vi.fn().mockResolvedValue(undefined),
- keys: vi.fn().mockReturnValue([]),
- },
- subscriptions: [],
- extension: {
- packageJSON: { version: "1.0.0" },
- },
- globalStorageUri: {
- fsPath: "/test/storage/path",
- },
- } as unknown as vscode.ExtensionContext
- // Mock CustomModesManager
- const mockCustomModesManager = {
- updateCustomMode: vi.fn().mockResolvedValue(undefined),
- getCustomModes: vi.fn().mockResolvedValue([]),
- dispose: vi.fn(),
- }
- // Mock output channel
- mockOutputChannel = {
- appendLine: vi.fn(),
- clear: vi.fn(),
- dispose: vi.fn(),
- } as unknown as vscode.OutputChannel
- // Mock webview
- mockPostMessage = vi.fn()
- mockWebviewView = {
- webview: {
- postMessage: mockPostMessage,
- html: "",
- options: {},
- onDidReceiveMessage: vi.fn(),
- asWebviewUri: vi.fn(),
- cspSource: "vscode-webview://test-csp-source",
- },
- visible: true,
- onDidDispose: vi.fn().mockImplementation((callback) => {
- callback()
- return { dispose: vi.fn() }
- }),
- onDidChangeVisibility: vi.fn().mockImplementation(() => ({ dispose: vi.fn() })),
- } as unknown as vscode.WebviewView
- provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
- defaultTaskOptions = {
- provider,
- apiConfiguration: {
- apiProvider: "openrouter",
- },
- }
- // @ts-ignore - Access private property for testing
- updateGlobalStateSpy = vi.spyOn(provider.contextProxy, "setValue")
- // @ts-ignore - Accessing private property for testing.
- provider.customModesManager = mockCustomModesManager
- // Mock getMcpHub method for generateSystemPrompt
- provider.getMcpHub = vi.fn().mockReturnValue({
- listTools: vi.fn().mockResolvedValue([]),
- callTool: vi.fn().mockResolvedValue({ content: [] }),
- listResources: vi.fn().mockResolvedValue([]),
- readResource: vi.fn().mockResolvedValue({ contents: [] }),
- getAllServers: vi.fn().mockReturnValue([]),
- })
- })
- test("constructor initializes correctly", () => {
- expect(provider).toBeInstanceOf(ClineProvider)
- // Since getVisibleInstance returns the last instance where view.visible is true
- // @ts-ignore - accessing private property for testing
- provider.view = mockWebviewView
- expect(ClineProvider.getVisibleInstance()).toBe(provider)
- })
- test("resolveWebviewView sets up webview correctly", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- expect(mockWebviewView.webview.options).toEqual({
- enableScripts: true,
- localResourceRoots: [mockContext.extensionUri],
- })
- expect(mockWebviewView.webview.html).toContain("<!DOCTYPE html>")
- })
- test("resolveWebviewView sets up webview correctly in development mode even if local server is not running", async () => {
- provider = new ClineProvider(
- { ...mockContext, extensionMode: vscode.ExtensionMode.Development },
- mockOutputChannel,
- "sidebar",
- new ContextProxy(mockContext),
- )
- ;(axios.get as any).mockRejectedValueOnce(new Error("Network error"))
- await provider.resolveWebviewView(mockWebviewView)
- expect(mockWebviewView.webview.options).toEqual({
- enableScripts: true,
- localResourceRoots: [mockContext.extensionUri],
- })
- expect(mockWebviewView.webview.html).toContain("<!DOCTYPE html>")
- // Verify Content Security Policy contains the necessary PostHog domains
- expect(mockWebviewView.webview.html).toContain(
- "connect-src vscode-webview://test-csp-source https://openrouter.ai https://api.requesty.ai https://ph.roocode.com",
- )
- // Extract the script-src directive section and verify required security elements
- const html = mockWebviewView.webview.html
- const scriptSrcMatch = html.match(/script-src[^;]*;/)
- expect(scriptSrcMatch).not.toBeNull()
- expect(scriptSrcMatch![0]).toContain("'nonce-")
- // Verify wasm-unsafe-eval is present for Shiki syntax highlighting
- expect(scriptSrcMatch![0]).toContain("'wasm-unsafe-eval'")
- })
- test("postMessageToWebview sends message to webview", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const mockState: ExtensionState = {
- version: "1.0.0",
- isBrowserSessionActive: false,
- clineMessages: [],
- taskHistory: [],
- shouldShowAnnouncement: false,
- apiConfiguration: {
- apiProvider: "openrouter",
- },
- customInstructions: undefined,
- alwaysAllowReadOnly: false,
- alwaysAllowReadOnlyOutsideWorkspace: false,
- alwaysAllowWrite: false,
- codebaseIndexConfig: {
- codebaseIndexEnabled: true,
- codebaseIndexQdrantUrl: "",
- codebaseIndexEmbedderProvider: "openai",
- codebaseIndexEmbedderBaseUrl: "",
- codebaseIndexEmbedderModelId: "",
- },
- alwaysAllowWriteOutsideWorkspace: false,
- alwaysAllowExecute: false,
- alwaysAllowBrowser: false,
- alwaysAllowMcp: false,
- uriScheme: "vscode",
- soundEnabled: false,
- ttsEnabled: false,
- enableCheckpoints: false,
- writeDelayMs: 1000,
- browserViewportSize: "900x600",
- mcpEnabled: true,
- mode: defaultModeSlug,
- customModes: [],
- experiments: experimentDefault,
- maxOpenTabsContext: 20,
- maxWorkspaceFiles: 200,
- browserToolEnabled: true,
- telemetrySetting: "unset",
- showRooIgnoredFiles: false,
- enableSubfolderRules: false,
- renderContext: "sidebar",
- maxImageFileSize: 5,
- maxTotalImageSize: 20,
- cloudUserInfo: null,
- organizationAllowList: ORGANIZATION_ALLOW_ALL,
- autoCondenseContext: true,
- autoCondenseContextPercent: 100,
- cloudIsAuthenticated: false,
- sharingEnabled: false,
- publicSharingEnabled: false,
- profileThresholds: {},
- hasOpenedModeSelector: false,
- diagnosticsEnabled: true,
- openRouterImageApiKey: undefined,
- openRouterImageGenerationSelectedModel: undefined,
- remoteControlEnabled: false,
- taskSyncEnabled: false,
- featureRoomoteControlEnabled: false,
- checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
- }
- const message: ExtensionMessage = {
- type: "state",
- state: mockState,
- }
- await provider.postMessageToWebview(message)
- expect(mockPostMessage).toHaveBeenCalledWith(message)
- })
- test("handles webviewDidLaunch message", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- // Get the message handler from onDidReceiveMessage
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Simulate webviewDidLaunch message
- await messageHandler({ type: "webviewDidLaunch" })
- // Should post state and theme to webview
- expect(mockPostMessage).toHaveBeenCalled()
- })
- test("clearTask aborts current task", async () => {
- // Setup Cline instance with auto-mock from the top of the file
- const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
- // add the mock object to the stack
- await provider.addClineToStack(mockCline)
- // get the stack size before the abort call
- const stackSizeBeforeAbort = provider.getTaskStackSize()
- // call the removeClineFromStack method so it will call the current cline abort and remove it from the stack
- await provider.removeClineFromStack()
- // get the stack size after the abort call
- const stackSizeAfterAbort = provider.getTaskStackSize()
- // check if the abort method was called
- expect(mockCline.abortTask).toHaveBeenCalled()
- // check if the stack size was decreased
- expect(stackSizeBeforeAbort - stackSizeAfterAbort).toBe(1)
- })
- describe("clearTask message handler", () => {
- beforeEach(async () => {
- await provider.resolveWebviewView(mockWebviewView)
- })
- test("calls clearTask (delegation handled via metadata)", async () => {
- // Setup a single task without parent
- const mockCline = new Task(defaultTaskOptions)
- // Mock the provider methods
- const clearTaskSpy = vi.spyOn(provider, "clearTask").mockResolvedValue(undefined)
- const postStateToWebviewSpy = vi.spyOn(provider, "postStateToWebview").mockResolvedValue(undefined)
- // Add task to stack
- await provider.addClineToStack(mockCline)
- // Get the message handler
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Trigger clearTask message
- await messageHandler({ type: "clearTask" })
- // Verify clearTask was called
- expect(clearTaskSpy).toHaveBeenCalled()
- expect(postStateToWebviewSpy).toHaveBeenCalled()
- })
- test("calls clearTask even with parent task (delegation via metadata)", async () => {
- // Setup parent and child tasks
- const parentTask = new Task(defaultTaskOptions)
- const childTask = new Task(defaultTaskOptions)
- // Set up parent-child relationship
- ;(childTask as any).parentTask = parentTask
- ;(childTask as any).rootTask = parentTask
- // Mock the provider methods
- const clearTaskSpy = vi.spyOn(provider, "clearTask").mockResolvedValue(undefined)
- const postStateToWebviewSpy = vi.spyOn(provider, "postStateToWebview").mockResolvedValue(undefined)
- // Add both tasks to stack (parent first, then child)
- await provider.addClineToStack(parentTask)
- await provider.addClineToStack(childTask)
- // Get the message handler
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Trigger clearTask message
- await messageHandler({ type: "clearTask" })
- // Verify clearTask was called (delegation happens via metadata, not finishSubTask)
- expect(clearTaskSpy).toHaveBeenCalled()
- expect(postStateToWebviewSpy).toHaveBeenCalled()
- })
- test("handles case when no current task exists", async () => {
- // Don't add any tasks to the stack
- // Mock the provider methods
- const clearTaskSpy = vi.spyOn(provider, "clearTask").mockResolvedValue(undefined)
- const postStateToWebviewSpy = vi.spyOn(provider, "postStateToWebview").mockResolvedValue(undefined)
- // Get the message handler
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Trigger clearTask message
- await messageHandler({ type: "clearTask" })
- // When there's no current task, clearTask is still called (it handles the no-task case internally)
- expect(clearTaskSpy).toHaveBeenCalled()
- expect(postStateToWebviewSpy).toHaveBeenCalled()
- })
- test("correctly identifies task scenario for issue #4602", async () => {
- // This test validates the fix for issue #4602
- // where canceling during API retry correctly uses clearTask
- const mockCline = new Task(defaultTaskOptions)
- // Mock the provider methods
- const clearTaskSpy = vi.spyOn(provider, "clearTask").mockResolvedValue(undefined)
- // Add only one task to stack
- await provider.addClineToStack(mockCline)
- // Verify stack size is 1
- expect(provider.getTaskStackSize()).toBe(1)
- // Get the message handler
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Trigger clearTask message (simulating cancel during API retry)
- await messageHandler({ type: "clearTask" })
- // clearTask should be called (delegation handled via metadata)
- expect(clearTaskSpy).toHaveBeenCalled()
- })
- })
- test("addClineToStack adds multiple Cline instances to the stack", async () => {
- // Setup Cline instance with auto-mock from the top of the file
- const mockCline1 = new Task(defaultTaskOptions) // Create a new mocked instance
- const mockCline2 = new Task(defaultTaskOptions) // Create a new mocked instance
- Object.defineProperty(mockCline1, "taskId", { value: "test-task-id-1", writable: true })
- Object.defineProperty(mockCline2, "taskId", { value: "test-task-id-2", writable: true })
- // add Cline instances to the stack
- await provider.addClineToStack(mockCline1)
- await provider.addClineToStack(mockCline2)
- // verify cline instances were added to the stack
- expect(provider.getTaskStackSize()).toBe(2)
- // verify current cline instance is the last one added
- expect(provider.getCurrentTask()).toBe(mockCline2)
- })
- test("getState returns correct initial state", async () => {
- const state = await provider.getState()
- expect(state).toHaveProperty("apiConfiguration")
- expect(state.apiConfiguration).toHaveProperty("apiProvider")
- expect(state).toHaveProperty("customInstructions")
- expect(state).toHaveProperty("alwaysAllowReadOnly")
- expect(state).toHaveProperty("alwaysAllowWrite")
- expect(state).toHaveProperty("alwaysAllowExecute")
- expect(state).toHaveProperty("alwaysAllowBrowser")
- expect(state).toHaveProperty("taskHistory")
- expect(state).toHaveProperty("soundEnabled")
- expect(state).toHaveProperty("ttsEnabled")
- expect(state).toHaveProperty("writeDelayMs")
- })
- test("language is set to VSCode language", async () => {
- // Mock VSCode language as Spanish
- ;(vscode.env as any).language = "pt-BR"
- const state = await provider.getState()
- expect(state.language).toBe("pt-BR")
- })
- test("writeDelayMs defaults to 1000ms", async () => {
- // Mock globalState.get to return undefined for writeDelayMs
- ;(mockContext.globalState.get as any).mockImplementation((key: string) =>
- key === "writeDelayMs" ? undefined : null,
- )
- const state = await provider.getState()
- expect(state.writeDelayMs).toBe(1000)
- })
- test("handles writeDelayMs message", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({ type: "updateSettings", updatedSettings: { writeDelayMs: 2000 } })
- expect(updateGlobalStateSpy).toHaveBeenCalledWith("writeDelayMs", 2000)
- expect(mockContext.globalState.update).toHaveBeenCalledWith("writeDelayMs", 2000)
- expect(mockPostMessage).toHaveBeenCalled()
- })
- test("updates sound utility when sound setting changes", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- // Get the message handler from onDidReceiveMessage
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Simulate setting sound to enabled
- await messageHandler({ type: "updateSettings", updatedSettings: { soundEnabled: true } })
- expect(updateGlobalStateSpy).toHaveBeenCalledWith("soundEnabled", true)
- expect(mockContext.globalState.update).toHaveBeenCalledWith("soundEnabled", true)
- expect(mockPostMessage).toHaveBeenCalled()
- // Simulate setting sound to disabled
- await messageHandler({ type: "updateSettings", updatedSettings: { soundEnabled: false } })
- expect(mockContext.globalState.update).toHaveBeenCalledWith("soundEnabled", false)
- expect(mockPostMessage).toHaveBeenCalled()
- // Simulate setting tts to enabled
- await messageHandler({ type: "updateSettings", updatedSettings: { ttsEnabled: true } })
- expect(setTtsEnabled).toHaveBeenCalledWith(true)
- expect(mockContext.globalState.update).toHaveBeenCalledWith("ttsEnabled", true)
- expect(mockPostMessage).toHaveBeenCalled()
- // Simulate setting tts to disabled
- await messageHandler({ type: "updateSettings", updatedSettings: { ttsEnabled: false } })
- expect(setTtsEnabled).toHaveBeenCalledWith(false)
- expect(mockContext.globalState.update).toHaveBeenCalledWith("ttsEnabled", false)
- expect(mockPostMessage).toHaveBeenCalled()
- })
- test("autoCondenseContext defaults to true", async () => {
- // Mock globalState.get to return undefined for autoCondenseContext
- ;(mockContext.globalState.get as any).mockImplementation((key: string) =>
- key === "autoCondenseContext" ? undefined : null,
- )
- const state = await provider.getState()
- expect(state.autoCondenseContext).toBe(true)
- })
- test("handles autoCondenseContext message", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({ type: "updateSettings", updatedSettings: { autoCondenseContext: false } })
- expect(updateGlobalStateSpy).toHaveBeenCalledWith("autoCondenseContext", false)
- expect(mockContext.globalState.update).toHaveBeenCalledWith("autoCondenseContext", false)
- expect(mockPostMessage).toHaveBeenCalled()
- })
- test("autoCondenseContextPercent defaults to 100", async () => {
- // Mock globalState.get to return undefined for autoCondenseContextPercent
- ;(mockContext.globalState.get as any).mockImplementation((key: string) =>
- key === "autoCondenseContextPercent" ? undefined : null,
- )
- const state = await provider.getState()
- expect(state.autoCondenseContextPercent).toBe(100)
- })
- test("handles autoCondenseContextPercent message", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({ type: "updateSettings", updatedSettings: { autoCondenseContextPercent: 75 } })
- expect(updateGlobalStateSpy).toHaveBeenCalledWith("autoCondenseContextPercent", 75)
- expect(mockContext.globalState.update).toHaveBeenCalledWith("autoCondenseContextPercent", 75)
- expect(mockPostMessage).toHaveBeenCalled()
- })
- it("loads saved API config when switching modes", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- const profile: ProviderSettingsEntry = { name: "test-config", id: "test-id", apiProvider: "anthropic" }
- ;(provider as any).providerSettingsManager = {
- getModeConfigId: vi.fn().mockResolvedValue("test-id"),
- listConfig: vi.fn().mockResolvedValue([profile]),
- activateProfile: vi.fn().mockResolvedValue(profile),
- setModeConfig: vi.fn(),
- getProfile: vi.fn().mockResolvedValue(profile),
- } as any
- // Switch to architect mode
- await messageHandler({ type: "mode", text: "architect" })
- // Should load the saved config for architect mode
- expect(provider.providerSettingsManager.getModeConfigId).toHaveBeenCalledWith("architect")
- expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ name: "test-config" })
- expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
- })
- it("saves current config when switching to mode without config", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- ;(provider as any).providerSettingsManager = {
- getModeConfigId: vi.fn().mockResolvedValue(undefined),
- listConfig: vi
- .fn()
- .mockResolvedValue([{ name: "current-config", id: "current-id", apiProvider: "anthropic" }]),
- setModeConfig: vi.fn(),
- } as any
- provider.setValue("currentApiConfigName", "current-config")
- // Switch to architect mode
- await messageHandler({ type: "mode", text: "architect" })
- // Should save current config as default for architect mode
- expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "current-id")
- })
- it("saves config as default for current mode when loading config", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- const profile: ProviderSettingsEntry = { apiProvider: "anthropic", id: "new-id", name: "new-config" }
- ;(provider as any).providerSettingsManager = {
- activateProfile: vi.fn().mockResolvedValue(profile),
- listConfig: vi.fn().mockResolvedValue([profile]),
- setModeConfig: vi.fn(),
- getModeConfigId: vi.fn().mockResolvedValue(undefined),
- } as any
- // First set the mode
- await messageHandler({ type: "mode", text: "architect" })
- // Then load the config
- await messageHandler({ type: "loadApiConfiguration", text: "new-config" })
- // Should save new config as default for architect mode
- expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "new-id")
- })
- it("load API configuration by ID works and updates mode config", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- const profile: ProviderSettingsEntry = {
- name: "config-by-id",
- id: "config-id-123",
- apiProvider: "anthropic",
- }
- ;(provider as any).providerSettingsManager = {
- activateProfile: vi.fn().mockResolvedValue(profile),
- listConfig: vi.fn().mockResolvedValue([profile]),
- setModeConfig: vi.fn(),
- getModeConfigId: vi.fn().mockResolvedValue(undefined),
- } as any
- // First set the mode
- await messageHandler({ type: "mode", text: "architect" })
- // Then load the config by ID
- await messageHandler({ type: "loadApiConfigurationById", text: "config-id-123" })
- // Should save new config as default for architect mode
- expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "config-id-123")
- // Ensure the `activateProfile` method was called with the correct ID
- expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ id: "config-id-123" })
- })
- test("handles browserToolEnabled setting", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Test browserToolEnabled
- await messageHandler({ type: "updateSettings", updatedSettings: { browserToolEnabled: true } })
- expect(mockContext.globalState.update).toHaveBeenCalledWith("browserToolEnabled", true)
- expect(mockPostMessage).toHaveBeenCalled()
- // Verify state includes browserToolEnabled
- const state = await provider.getState()
- expect(state).toHaveProperty("browserToolEnabled")
- expect(state.browserToolEnabled).toBe(true) // Default value should be true
- })
- test("handles showRooIgnoredFiles setting", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Default value should be false
- expect((await provider.getState()).showRooIgnoredFiles).toBe(false)
- // Test showRooIgnoredFiles with true
- await messageHandler({ type: "updateSettings", updatedSettings: { showRooIgnoredFiles: true } })
- expect(mockContext.globalState.update).toHaveBeenCalledWith("showRooIgnoredFiles", true)
- expect(mockPostMessage).toHaveBeenCalled()
- expect((await provider.getState()).showRooIgnoredFiles).toBe(true)
- // Test showRooIgnoredFiles with false
- await messageHandler({ type: "updateSettings", updatedSettings: { showRooIgnoredFiles: false } })
- expect(mockContext.globalState.update).toHaveBeenCalledWith("showRooIgnoredFiles", false)
- expect(mockPostMessage).toHaveBeenCalled()
- expect((await provider.getState()).showRooIgnoredFiles).toBe(false)
- })
- test("handles updatePrompt message correctly", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Mock existing prompts
- const existingPrompts = {
- code: {
- roleDefinition: "existing code role",
- customInstructions: "existing code prompt",
- },
- architect: {
- roleDefinition: "existing architect role",
- customInstructions: "existing architect prompt",
- },
- }
- provider.setValue("customModePrompts", existingPrompts)
- // Test updating a prompt
- await messageHandler({
- type: "updatePrompt",
- promptMode: "code",
- customPrompt: "new code prompt",
- })
- // Verify state was updated correctly
- expect(mockContext.globalState.update).toHaveBeenCalledWith("customModePrompts", {
- ...existingPrompts,
- code: "new code prompt",
- })
- // Verify state was posted to webview
- expect(mockPostMessage).toHaveBeenCalledWith(
- expect.objectContaining({
- type: "state",
- state: expect.objectContaining({
- customModePrompts: {
- ...existingPrompts,
- code: "new code prompt",
- },
- }),
- }),
- )
- })
- test("customModePrompts defaults to empty object", async () => {
- // Mock globalState.get to return undefined for customModePrompts
- ;(mockContext.globalState.get as any).mockImplementation((key: string) => {
- if (key === "customModePrompts") {
- return undefined
- }
- return null
- })
- const state = await provider.getState()
- expect(state.customModePrompts).toEqual({})
- })
- test("handles maxWorkspaceFiles message", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({ type: "updateSettings", updatedSettings: { maxWorkspaceFiles: 300 } })
- expect(updateGlobalStateSpy).toHaveBeenCalledWith("maxWorkspaceFiles", 300)
- expect(mockContext.globalState.update).toHaveBeenCalledWith("maxWorkspaceFiles", 300)
- expect(mockPostMessage).toHaveBeenCalled()
- })
- test("handles mode-specific custom instructions updates", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Mock existing prompts
- const existingPrompts = {
- code: {
- roleDefinition: "Code role",
- customInstructions: "Old instructions",
- },
- }
- mockContext.globalState.get = vi.fn((key: string) => {
- if (key === "customModePrompts") {
- return existingPrompts
- }
- return undefined
- })
- // Update custom instructions for code mode
- await messageHandler({
- type: "updatePrompt",
- promptMode: "code",
- customPrompt: {
- roleDefinition: "Code role",
- customInstructions: "New instructions",
- },
- })
- // Verify state was updated correctly
- expect(mockContext.globalState.update).toHaveBeenCalledWith("customModePrompts", {
- code: {
- roleDefinition: "Code role",
- customInstructions: "New instructions",
- },
- })
- })
- it("saves mode config when updating API configuration", async () => {
- // Setup mock context with mode and config name
- mockContext = {
- ...mockContext,
- globalState: {
- ...mockContext.globalState,
- get: vi.fn((key: string) => {
- if (key === "mode") {
- return "code"
- } else if (key === "currentApiConfigName") {
- return "test-config"
- }
- return undefined
- }),
- update: vi.fn(),
- keys: vi.fn().mockReturnValue([]),
- },
- } as unknown as vscode.ExtensionContext
- // Create new provider with updated mock context
- provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- ;(provider as any).providerSettingsManager = {
- listConfig: vi.fn().mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
- saveConfig: vi.fn().mockResolvedValue("test-id"),
- setModeConfig: vi.fn(),
- } as any
- // Update API configuration
- await messageHandler({
- type: "upsertApiConfiguration",
- text: "test-config",
- apiConfiguration: { apiProvider: "anthropic" },
- })
- // Should save config as default for current mode
- expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("code", "test-id")
- })
- test("file content includes line numbers", async () => {
- const { extractTextFromFile } = await import("../../../integrations/misc/extract-text")
- const result = await extractTextFromFile("test.js")
- expect(result).toBe("1 | const x = 1;\n2 | const y = 2;\n3 | const z = 3;")
- })
- describe("deleteMessage", () => {
- beforeEach(async () => {
- await provider.resolveWebviewView(mockWebviewView)
- })
- test("handles deletion with confirmation dialog", async () => {
- // Setup mock messages
- const mockMessages = [
- { ts: 1000, type: "say", say: "user_feedback" }, // User message 1
- { ts: 2000, type: "say", say: "tool" }, // Tool message
- { ts: 3000, type: "say", say: "text" }, // Message before delete
- { ts: 4000, type: "say", say: "browser_action" }, // Message to delete
- { ts: 5000, type: "say", say: "user_feedback" }, // Next user message
- { ts: 6000, type: "say", say: "user_feedback" }, // Final message
- ] as ClineMessage[]
- const mockApiHistory = [
- { ts: 1000 },
- { ts: 2000 },
- { ts: 3000 },
- { ts: 4000 },
- { ts: 5000 },
- { ts: 6000 },
- ] as (Anthropic.MessageParam & { ts?: number })[]
- // Setup Task instance with auto-mock from the top of the file
- const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
- mockCline.clineMessages = mockMessages // Set test-specific messages
- mockCline.apiConversationHistory = mockApiHistory // Set API history
- await provider.addClineToStack(mockCline) // Add the mocked instance to the stack
- // Mock getTaskWithId
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- // Mock createTaskWithHistoryItem
- ;(provider as any).createTaskWithHistoryItem = vi.fn()
- // Trigger message deletion
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({ type: "deleteMessage", value: 4000 })
- // Verify that the dialog message was sent to webview
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showDeleteMessageDialog",
- messageTs: 4000,
- hasCheckpoint: false,
- })
- // Simulate user confirming deletion through the dialog
- await messageHandler({ type: "deleteMessageConfirm", messageTs: 4000 })
- // Verify only messages before the deleted message were kept
- expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([
- mockMessages[0],
- mockMessages[1],
- mockMessages[2],
- ])
- // Verify only API messages before the deleted message were kept
- expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
- mockApiHistory[0],
- mockApiHistory[1],
- mockApiHistory[2],
- ])
- // createTaskWithHistoryItem is only called when restoring checkpoints or aborting tasks
- expect((provider as any).createTaskWithHistoryItem).not.toHaveBeenCalled()
- })
- test("handles case when no current task exists", async () => {
- // Clear the cline stack
- ;(provider as any).clineStack = []
- // Trigger message deletion
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({ type: "deleteMessage", value: 2000 })
- // Verify no dialog was shown since there's no current cline
- expect(mockPostMessage).not.toHaveBeenCalledWith(
- expect.objectContaining({
- type: "showDeleteMessageDialog",
- }),
- )
- })
- })
- describe("editMessage", () => {
- beforeEach(async () => {
- await provider.resolveWebviewView(mockWebviewView)
- })
- test("handles edit with confirmation dialog", async () => {
- // Setup mock messages
- const mockMessages = [
- { ts: 1000, type: "say", say: "user_feedback" }, // User message 1
- { ts: 2000, type: "say", say: "tool" }, // Tool message
- { ts: 3000, type: "say", say: "text" }, // Message before edit
- { ts: 4000, type: "say", say: "browser_action" }, // Message to edit
- { ts: 5000, type: "say", say: "user_feedback" }, // Next user message
- { ts: 6000, type: "say", say: "user_feedback" }, // Final message
- ] as ClineMessage[]
- const mockApiHistory = [
- { ts: 1000 },
- { ts: 2000 },
- { ts: 3000 },
- { ts: 4000 },
- { ts: 5000 },
- { ts: 6000 },
- ] as (Anthropic.MessageParam & { ts?: number })[]
- // Setup Task instance with auto-mock from the top of the file
- const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
- mockCline.clineMessages = mockMessages // Set test-specific messages
- mockCline.apiConversationHistory = mockApiHistory // Set API history
- // Explicitly mock the overwrite methods since they're not being called in the tests
- mockCline.overwriteClineMessages = vi.fn()
- mockCline.overwriteApiConversationHistory = vi.fn()
- mockCline.handleWebviewAskResponse = vi.fn()
- await provider.addClineToStack(mockCline) // Add the mocked instance to the stack
- // Mock getTaskWithId
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- // Trigger message edit
- // Get the message handler function that was registered with the webview
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Call the message handler with a submitEditedMessage message
- await messageHandler({
- type: "submitEditedMessage",
- value: 4000,
- editedMessageContent: "Edited message content",
- })
- // Verify that the dialog message was sent to webview
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showEditMessageDialog",
- messageTs: 4000,
- text: "Edited message content",
- hasCheckpoint: false,
- images: undefined,
- })
- // Simulate user confirming edit through the dialog
- await messageHandler({
- type: "editMessageConfirm",
- messageTs: 4000,
- text: "Edited message content",
- })
- // Verify correct messages were kept - delete from the preceding user message to truly replace it
- expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([])
- // Verify correct API messages were kept
- expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([])
- // The new flow calls webviewMessageHandler recursively with askResponse
- // We need to verify the recursive call happened by checking if the handler was called again
- expect((mockWebviewView.webview.onDidReceiveMessage as any).mock.calls.length).toBeGreaterThanOrEqual(1)
- })
- })
- describe("getSystemPrompt", () => {
- beforeEach(async () => {
- mockPostMessage.mockClear()
- await provider.resolveWebviewView(mockWebviewView)
- // Reset and setup mock
- mockAddCustomInstructions.mockClear()
- mockAddCustomInstructions.mockImplementation(
- (modeInstructions: string, globalInstructions: string, _cwd: string) => {
- return Promise.resolve(modeInstructions || globalInstructions || "")
- },
- )
- })
- const getMessageHandler = () => {
- const mockCalls = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls
- expect(mockCalls.length).toBeGreaterThan(0)
- return mockCalls[0][0]
- }
- test("handles mcpEnabled setting correctly", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const handler = getMessageHandler()
- expect(typeof handler).toBe("function")
- // Test with mcpEnabled: true
- vi.spyOn(provider, "getState").mockResolvedValueOnce({
- apiConfiguration: {
- apiProvider: "openrouter" as const,
- },
- mcpEnabled: true,
- mode: "code" as const,
- experiments: experimentDefault,
- } as any)
- await handler({ type: "getSystemPrompt", mode: "code" })
- // Verify system prompt was generated and sent
- expect(mockPostMessage).toHaveBeenCalledWith(
- expect.objectContaining({
- type: "systemPrompt",
- text: expect.any(String),
- mode: "code",
- }),
- )
- // Reset for second test
- mockPostMessage.mockClear()
- // Test with mcpEnabled: false
- vi.spyOn(provider, "getState").mockResolvedValueOnce({
- apiConfiguration: {
- apiProvider: "openrouter" as const,
- },
- mcpEnabled: false,
- mode: "code" as const,
- experiments: experimentDefault,
- } as any)
- await handler({ type: "getSystemPrompt", mode: "code" })
- // Verify system prompt was generated and sent
- expect(mockPostMessage).toHaveBeenCalledWith(
- expect.objectContaining({
- type: "systemPrompt",
- text: expect.any(String),
- mode: "code",
- }),
- )
- })
- test("handles errors gracefully", async () => {
- // Mock SYSTEM_PROMPT to throw an error
- const { SYSTEM_PROMPT } = await import("../../prompts/system")
- vi.mocked(SYSTEM_PROMPT).mockRejectedValueOnce(new Error("Test error"))
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({ type: "getSystemPrompt", mode: "code" })
- expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.get_system_prompt")
- })
- test("uses code mode custom instructions", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- // Mock getState to return custom instructions for code mode
- vi.spyOn(provider, "getState").mockResolvedValue({
- apiConfiguration: {
- apiProvider: "openrouter" as const,
- },
- customModePrompts: {
- code: { customInstructions: "Code mode specific instructions" },
- },
- mode: "code" as const,
- experiments: experimentDefault,
- } as any)
- // Trigger getSystemPrompt
- const handler = getMessageHandler()
- await handler({ type: "getSystemPrompt", mode: "code" })
- // Verify system prompt was generated and sent
- expect(mockPostMessage).toHaveBeenCalledWith(
- expect.objectContaining({
- type: "systemPrompt",
- text: expect.any(String),
- mode: "code",
- }),
- )
- })
- test("uses correct mode-specific instructions when mode is specified", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- // Mock getState to return architect mode instructions
- vi.spyOn(provider, "getState").mockResolvedValue({
- apiConfiguration: {
- apiProvider: "openrouter",
- },
- customModePrompts: {
- architect: { customInstructions: "Architect mode instructions" },
- },
- mode: "architect",
- mcpEnabled: false,
- browserViewportSize: "900x600",
- experiments: experimentDefault,
- } as any)
- // Trigger getSystemPrompt for architect mode
- const handler = getMessageHandler()
- await handler({ type: "getSystemPrompt", mode: "architect" })
- // Verify system prompt was generated and sent
- expect(mockPostMessage).toHaveBeenCalledWith(
- expect.objectContaining({
- type: "systemPrompt",
- text: expect.any(String),
- mode: "architect",
- }),
- )
- })
- // Tests for browser tool support - simplified to focus on behavior
- test("generates system prompt with different browser tool configurations", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const handler = getMessageHandler()
- // Test 1: Browser tools enabled with compatible model and mode
- vi.spyOn(provider, "getState").mockResolvedValueOnce({
- apiConfiguration: {
- apiProvider: "openrouter",
- },
- browserToolEnabled: true,
- mode: "code", // code mode includes browser tool group
- experiments: experimentDefault,
- } as any)
- await handler({ type: "getSystemPrompt", mode: "code" })
- expect(mockPostMessage).toHaveBeenCalledWith(
- expect.objectContaining({
- type: "systemPrompt",
- text: expect.any(String),
- mode: "code",
- }),
- )
- mockPostMessage.mockClear()
- // Test 2: Browser tools disabled
- vi.spyOn(provider, "getState").mockResolvedValueOnce({
- apiConfiguration: {
- apiProvider: "openrouter",
- },
- browserToolEnabled: false,
- mode: "code",
- experiments: experimentDefault,
- } as any)
- await handler({ type: "getSystemPrompt", mode: "code" })
- expect(mockPostMessage).toHaveBeenCalledWith(
- expect.objectContaining({
- type: "systemPrompt",
- text: expect.any(String),
- mode: "code",
- }),
- )
- })
- })
- describe("handleModeSwitch", () => {
- beforeEach(async () => {
- // Set up webview for each test
- await provider.resolveWebviewView(mockWebviewView)
- })
- it("loads saved API config when switching modes", async () => {
- const profile: ProviderSettingsEntry = {
- name: "saved-config",
- id: "saved-config-id",
- apiProvider: "anthropic",
- }
- ;(provider as any).providerSettingsManager = {
- getModeConfigId: vi.fn().mockResolvedValue("saved-config-id"),
- listConfig: vi.fn().mockResolvedValue([profile]),
- activateProfile: vi.fn().mockResolvedValue(profile),
- setModeConfig: vi.fn(),
- getProfile: vi.fn().mockResolvedValue(profile),
- } as any
- // Switch to architect mode
- await provider.handleModeSwitch("architect")
- // Verify mode was updated
- expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "architect")
- // Verify saved config was loaded
- expect(provider.providerSettingsManager.getModeConfigId).toHaveBeenCalledWith("architect")
- expect(provider.providerSettingsManager.activateProfile).toHaveBeenCalledWith({ name: "saved-config" })
- expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "saved-config")
- // Verify state was posted to webview
- expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
- })
- test("saves current config when switching to mode without config", async () => {
- ;(provider as any).providerSettingsManager = {
- getModeConfigId: vi.fn().mockResolvedValue(undefined),
- listConfig: vi
- .fn()
- .mockResolvedValue([{ name: "current-config", id: "current-id", apiProvider: "anthropic" }]),
- setModeConfig: vi.fn(),
- } as any
- // Mock the ContextProxy's getValue method to return the current config name
- const contextProxy = (provider as any).contextProxy
- const getValueSpy = vi.spyOn(contextProxy, "getValue")
- getValueSpy.mockImplementation((key: any) => {
- if (key === "currentApiConfigName") return "current-config"
- return undefined
- })
- // Switch to architect mode
- await provider.handleModeSwitch("architect")
- // Verify mode was updated
- expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "architect")
- // Verify current config was saved as default for new mode
- expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "current-id")
- // Verify state was posted to webview
- expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
- })
- })
- describe("createTaskWithHistoryItem mode validation", () => {
- test("validates and falls back to default mode when restored mode no longer exists", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- // Mock custom modes that don't include the saved mode
- const mockCustomModesManager = {
- getCustomModes: vi.fn().mockResolvedValue([
- {
- slug: "existing-mode",
- name: "Existing Mode",
- roleDefinition: "Test role",
- groups: ["read"] as const,
- },
- ]),
- dispose: vi.fn(),
- }
- ;(provider as any).customModesManager = mockCustomModesManager
- // Mock getModeBySlug to return undefined for non-existent mode
- const { getModeBySlug } = await import("../../../shared/modes")
- vi.mocked(getModeBySlug)
- .mockReturnValueOnce(undefined) // First call returns undefined (mode doesn't exist)
- .mockReturnValue({
- slug: "code",
- name: "Code Mode",
- roleDefinition: "You are a code assistant",
- groups: ["read", "edit", "browser"],
- }) // Subsequent calls return default mode
- // Mock provider settings manager
- ;(provider as any).providerSettingsManager = {
- getModeConfigId: vi.fn().mockResolvedValue(undefined),
- listConfig: vi.fn().mockResolvedValue([]),
- }
- // Spy on log method to verify warning was logged
- const logSpy = vi.spyOn(provider, "log")
- // Create history item with non-existent mode
- const historyItem = {
- id: "test-id",
- ts: Date.now(),
- task: "Test task",
- mode: "non-existent-mode", // This mode doesn't exist
- number: 1,
- tokensIn: 0,
- tokensOut: 0,
- totalCost: 0,
- }
- // Initialize with history item
- await provider.createTaskWithHistoryItem(historyItem)
- // Verify mode validation occurred
- expect(mockCustomModesManager.getCustomModes).toHaveBeenCalled()
- expect(getModeBySlug).toHaveBeenCalledWith("non-existent-mode", expect.any(Array))
- // Verify fallback to default mode
- expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "code")
- expect(logSpy).toHaveBeenCalledWith(
- "Mode 'non-existent-mode' from history no longer exists. Falling back to default mode 'code'.",
- )
- // Verify history item was updated with default mode
- expect(historyItem.mode).toBe("code")
- })
- test("preserves mode when it exists in custom modes", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- // Mock custom modes that include the saved mode
- const mockCustomModesManager = {
- getCustomModes: vi.fn().mockResolvedValue([
- {
- slug: "custom-mode",
- name: "Custom Mode",
- roleDefinition: "Custom role",
- groups: ["read", "edit"] as const,
- },
- ]),
- dispose: vi.fn(),
- }
- ;(provider as any).customModesManager = mockCustomModesManager
- // Mock getModeBySlug to return the custom mode
- const { getModeBySlug } = await import("../../../shared/modes")
- vi.mocked(getModeBySlug).mockReturnValue({
- slug: "custom-mode",
- name: "Custom Mode",
- roleDefinition: "Custom role",
- groups: ["read", "edit"],
- })
- // Mock provider settings manager
- ;(provider as any).providerSettingsManager = {
- getModeConfigId: vi.fn().mockResolvedValue("config-id"),
- listConfig: vi
- .fn()
- .mockResolvedValue([{ name: "test-config", id: "config-id", apiProvider: "anthropic" }]),
- activateProfile: vi
- .fn()
- .mockResolvedValue({ name: "test-config", id: "config-id", apiProvider: "anthropic" }),
- }
- // Spy on log method to verify no warning was logged
- const logSpy = vi.spyOn(provider, "log")
- // Create history item with existing custom mode
- const historyItem = {
- id: "test-id",
- ts: Date.now(),
- task: "Test task",
- mode: "custom-mode",
- number: 1,
- tokensIn: 0,
- tokensOut: 0,
- totalCost: 0,
- }
- // Initialize with history item
- await provider.createTaskWithHistoryItem(historyItem)
- // Verify mode validation occurred
- expect(mockCustomModesManager.getCustomModes).toHaveBeenCalled()
- expect(getModeBySlug).toHaveBeenCalledWith("custom-mode", expect.any(Array))
- // Verify mode was preserved
- expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "custom-mode")
- expect(logSpy).not.toHaveBeenCalledWith(expect.stringContaining("no longer exists"))
- // Verify history item mode was not changed
- expect(historyItem.mode).toBe("custom-mode")
- })
- test("preserves mode when it exists in built-in modes", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- // Mock no custom modes
- const mockCustomModesManager = {
- getCustomModes: vi.fn().mockResolvedValue([]),
- dispose: vi.fn(),
- }
- ;(provider as any).customModesManager = mockCustomModesManager
- // Mock getModeBySlug to return built-in architect mode
- const { getModeBySlug } = await import("../../../shared/modes")
- vi.mocked(getModeBySlug).mockReturnValue({
- slug: "architect",
- name: "Architect Mode",
- roleDefinition: "You are an architect",
- groups: ["read", "edit"],
- })
- // Mock provider settings manager
- ;(provider as any).providerSettingsManager = {
- getModeConfigId: vi.fn().mockResolvedValue(undefined),
- listConfig: vi.fn().mockResolvedValue([]),
- }
- // Create history item with built-in mode
- const historyItem = {
- id: "test-id",
- ts: Date.now(),
- task: "Test task",
- mode: "architect",
- number: 1,
- tokensIn: 0,
- tokensOut: 0,
- totalCost: 0,
- }
- // Initialize with history item
- await provider.createTaskWithHistoryItem(historyItem)
- // Verify mode was preserved
- expect(mockContext.globalState.update).toHaveBeenCalledWith("mode", "architect")
- // Verify history item mode was not changed
- expect(historyItem.mode).toBe("architect")
- })
- test("handles history items without mode property", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- // Mock provider settings manager
- ;(provider as any).providerSettingsManager = {
- getModeConfigId: vi.fn().mockResolvedValue(undefined),
- listConfig: vi.fn().mockResolvedValue([]),
- }
- // Create history item without mode
- const historyItem = {
- id: "test-id",
- ts: Date.now(),
- task: "Test task",
- // No mode property
- number: 1,
- tokensIn: 0,
- tokensOut: 0,
- totalCost: 0,
- }
- // Initialize with history item
- await provider.createTaskWithHistoryItem(historyItem)
- // Verify no mode validation occurred (mode update not called)
- expect(mockContext.globalState.update).not.toHaveBeenCalledWith("mode", expect.any(String))
- })
- test("continues with task restoration even if mode config loading fails", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- // Mock custom modes
- const mockCustomModesManager = {
- getCustomModes: vi.fn().mockResolvedValue([]),
- dispose: vi.fn(),
- }
- ;(provider as any).customModesManager = mockCustomModesManager
- // Mock getModeBySlug to return built-in mode
- const { getModeBySlug } = await import("../../../shared/modes")
- vi.mocked(getModeBySlug).mockReturnValue({
- slug: "code",
- name: "Code Mode",
- roleDefinition: "You are a code assistant",
- groups: ["read", "edit", "browser"],
- })
- // Mock provider settings manager to throw error
- ;(provider as any).providerSettingsManager = {
- getModeConfigId: vi.fn().mockResolvedValue("config-id"),
- listConfig: vi
- .fn()
- .mockResolvedValue([{ name: "test-config", id: "config-id", apiProvider: "anthropic" }]),
- activateProfile: vi.fn().mockRejectedValue(new Error("Failed to load config")),
- }
- // Spy on log method
- const logSpy = vi.spyOn(provider, "log")
- // Create history item
- const historyItem = {
- id: "test-id",
- ts: Date.now(),
- task: "Test task",
- mode: "code",
- number: 1,
- tokensIn: 0,
- tokensOut: 0,
- totalCost: 0,
- }
- // Initialize with history item - should not throw
- await expect(provider.createTaskWithHistoryItem(historyItem)).resolves.not.toThrow()
- // Verify error was logged but task restoration continued
- expect(logSpy).toHaveBeenCalledWith(
- expect.stringContaining("Failed to restore API configuration for mode 'code'"),
- )
- })
- })
- describe("updateCustomMode", () => {
- test("updates both file and state when updating custom mode", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Mock CustomModesManager methods
- ;(provider as any).customModesManager = {
- updateCustomMode: vi.fn().mockResolvedValue(undefined),
- getCustomModes: vi.fn().mockResolvedValue([
- {
- slug: "test-mode",
- name: "Test Mode",
- roleDefinition: "Updated role definition",
- groups: ["read"] as const,
- },
- ]),
- dispose: vi.fn(),
- } as any
- // Test updating a custom mode
- await messageHandler({
- type: "updateCustomMode",
- modeConfig: {
- slug: "test-mode",
- name: "Test Mode",
- roleDefinition: "Updated role definition",
- groups: ["read"] as const,
- },
- })
- // Verify CustomModesManager.updateCustomMode was called
- expect(provider.customModesManager.updateCustomMode).toHaveBeenCalledWith(
- "test-mode",
- expect.objectContaining({
- slug: "test-mode",
- roleDefinition: "Updated role definition",
- }),
- )
- // Verify state was updated
- expect(mockContext.globalState.update).toHaveBeenCalledWith("customModes", [
- { groups: ["read"], name: "Test Mode", roleDefinition: "Updated role definition", slug: "test-mode" },
- ])
- // Verify state was posted to webview
- // Verify state was posted to webview with correct format
- expect(mockPostMessage).toHaveBeenCalledWith(
- expect.objectContaining({
- type: "state",
- state: expect.objectContaining({
- customModes: [
- expect.objectContaining({
- slug: "test-mode",
- roleDefinition: "Updated role definition",
- }),
- ],
- }),
- }),
- )
- })
- })
- describe("upsertApiConfiguration", () => {
- test("handles error in upsertApiConfiguration gracefully", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- ;(provider as any).providerSettingsManager = {
- setModeConfig: vi.fn().mockRejectedValue(new Error("Failed to update mode config")),
- listConfig: vi
- .fn()
- .mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
- } as any
- // Mock getState to provide necessary data
- vi.spyOn(provider, "getState").mockResolvedValue({
- mode: "code",
- currentApiConfigName: "test-config",
- } as any)
- // Trigger upsertApiConfiguration
- await messageHandler({
- type: "upsertApiConfiguration",
- text: "test-config",
- apiConfiguration: { apiProvider: "anthropic", apiKey: "test-key" },
- })
- // Verify error was logged and user was notified
- expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
- expect.stringContaining("Error create new api configuration"),
- )
- expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.create_api_config")
- })
- test("handles successful upsertApiConfiguration", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- ;(provider as any).providerSettingsManager = {
- setModeConfig: vi.fn(),
- saveConfig: vi.fn().mockResolvedValue(undefined),
- listConfig: vi
- .fn()
- .mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
- } as any
- const testApiConfig = {
- apiProvider: "anthropic" as const,
- apiKey: "test-key",
- }
- // Trigger upsertApiConfiguration
- await messageHandler({
- type: "upsertApiConfiguration",
- text: "test-config",
- apiConfiguration: testApiConfig,
- })
- // Verify config was saved
- expect(provider.providerSettingsManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
- // Verify state updates
- expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
- { name: "test-config", id: "test-id", apiProvider: "anthropic" },
- ])
- expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
- // Verify state was posted to webview
- expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
- })
- test("handles buildApiHandler error in updateApiConfiguration", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Mock buildApiHandler to throw an error
- const { buildApiHandler } = await import("../../../api")
- ;(buildApiHandler as any).mockImplementationOnce(() => {
- throw new Error("API handler error")
- })
- ;(provider as any).providerSettingsManager = {
- setModeConfig: vi.fn(),
- saveConfig: vi.fn().mockResolvedValue(undefined),
- listConfig: vi
- .fn()
- .mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
- } as any
- // Setup Task instance with auto-mock from the top of the file
- const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
- await provider.addClineToStack(mockCline)
- const testApiConfig = {
- apiProvider: "anthropic" as const,
- apiKey: "test-key",
- }
- // Trigger upsertApiConfiguration
- await messageHandler({
- type: "upsertApiConfiguration",
- text: "test-config",
- apiConfiguration: testApiConfig,
- })
- // Verify error handling
- expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
- expect.stringContaining("Error create new api configuration"),
- )
- expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.create_api_config")
- // Verify state was still updated
- expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
- { name: "test-config", id: "test-id", apiProvider: "anthropic" },
- ])
- expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
- })
- test("handles successful saveApiConfiguration", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- ;(provider as any).providerSettingsManager = {
- setModeConfig: vi.fn(),
- saveConfig: vi.fn().mockResolvedValue(undefined),
- listConfig: vi
- .fn()
- .mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
- } as any
- const testApiConfig = {
- apiProvider: "anthropic" as const,
- apiKey: "test-key",
- }
- // Trigger upsertApiConfiguration
- await messageHandler({
- type: "saveApiConfiguration",
- text: "test-config",
- apiConfiguration: testApiConfig,
- })
- // Verify config was saved
- expect(provider.providerSettingsManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
- // Verify state updates
- expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
- { name: "test-config", id: "test-id", apiProvider: "anthropic" },
- ])
- expect(updateGlobalStateSpy).toHaveBeenCalledWith("listApiConfigMeta", [
- { name: "test-config", id: "test-id", apiProvider: "anthropic" },
- ])
- })
- })
- describe("browser connection features", () => {
- beforeEach(async () => {
- // Reset mocks
- vi.clearAllMocks()
- await provider.resolveWebviewView(mockWebviewView)
- })
- // These mocks are already defined at the top of the file
- test("handles testBrowserConnection with provided URL", async () => {
- // Get the message handler
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Test with valid URL
- await messageHandler({
- type: "testBrowserConnection",
- text: "http://localhost:9222",
- })
- // Verify postMessage was called with success result
- expect(mockPostMessage).toHaveBeenCalledWith(
- expect.objectContaining({
- type: "browserConnectionResult",
- success: true,
- text: expect.stringContaining("Successfully connected to Chrome"),
- }),
- )
- // Reset mock
- mockPostMessage.mockClear()
- // Test with invalid URL
- await messageHandler({
- type: "testBrowserConnection",
- text: "http://inlocalhost:9222",
- })
- // Verify postMessage was called with failure result
- expect(mockPostMessage).toHaveBeenCalledWith(
- expect.objectContaining({
- type: "browserConnectionResult",
- success: false,
- text: expect.stringContaining("Failed to connect to Chrome"),
- }),
- )
- })
- test("handles testBrowserConnection with auto-discovery", async () => {
- // Get the message handler
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Test auto-discovery (no URL provided)
- await messageHandler({
- type: "testBrowserConnection",
- })
- // Verify discoverChromeHostUrl was called
- const { discoverChromeHostUrl } = await import("../../../services/browser/browserDiscovery")
- expect(discoverChromeHostUrl).toHaveBeenCalled()
- // Verify postMessage was called with success result
- expect(mockPostMessage).toHaveBeenCalledWith(
- expect.objectContaining({
- type: "browserConnectionResult",
- success: true,
- text: expect.stringContaining("Auto-discovered and tested connection to Chrome"),
- }),
- )
- })
- })
- })
- describe("Project MCP Settings", () => {
- let provider: ClineProvider
- let mockContext: vscode.ExtensionContext
- let mockOutputChannel: vscode.OutputChannel
- let mockWebviewView: vscode.WebviewView
- let mockPostMessage: any
- beforeEach(() => {
- vi.clearAllMocks()
- mockContext = {
- extensionPath: "/test/path",
- extensionUri: {} as vscode.Uri,
- globalState: {
- get: vi.fn(),
- update: vi.fn(),
- keys: vi.fn().mockReturnValue([]),
- },
- secrets: {
- get: vi.fn(),
- store: vi.fn(),
- delete: vi.fn(),
- },
- workspaceState: {
- get: vi.fn().mockReturnValue(undefined),
- update: vi.fn().mockResolvedValue(undefined),
- keys: vi.fn().mockReturnValue([]),
- },
- subscriptions: [],
- extension: {
- packageJSON: { version: "1.0.0" },
- },
- globalStorageUri: {
- fsPath: "/test/storage/path",
- },
- } as unknown as vscode.ExtensionContext
- mockOutputChannel = {
- appendLine: vi.fn(),
- clear: vi.fn(),
- dispose: vi.fn(),
- } as unknown as vscode.OutputChannel
- mockPostMessage = vi.fn()
- mockWebviewView = {
- webview: {
- postMessage: mockPostMessage,
- html: "",
- options: {},
- onDidReceiveMessage: vi.fn(),
- asWebviewUri: vi.fn(),
- cspSource: "vscode-webview://test-csp-source",
- },
- visible: true,
- onDidDispose: vi.fn(),
- onDidChangeVisibility: vi.fn(),
- } as unknown as vscode.WebviewView
- provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
- })
- test.skip("handles openProjectMcpSettings message", async () => {
- // Mock workspace folders first
- ;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
- // Mock fs functions
- const fs = await import("fs/promises")
- const mockedFs = vi.mocked(fs)
- mockedFs.mkdir.mockClear()
- mockedFs.mkdir.mockResolvedValue(undefined)
- mockedFs.writeFile.mockClear()
- mockedFs.writeFile.mockResolvedValue(undefined)
- // Mock fileExistsAtPath to return false (file doesn't exist)
- const fsUtils = await import("../../../utils/fs")
- vi.spyOn(fsUtils, "fileExistsAtPath").mockResolvedValue(false)
- // Mock openFile
- const openFileModule = await import("../../../integrations/misc/open-file")
- const openFileSpy = vi.spyOn(openFileModule, "openFile").mockClear().mockResolvedValue(undefined)
- // Set up the webview
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Ensure the message handler is properly set up
- expect(messageHandler).toBeDefined()
- expect(typeof messageHandler).toBe("function")
- // Trigger openProjectMcpSettings through the message handler
- await messageHandler({
- type: "openProjectMcpSettings",
- })
- // Check that fs.mkdir was called with the correct path
- expect(mockedFs.mkdir).toHaveBeenCalledWith("/test/workspace/.roo", { recursive: true })
- // Verify file was created with default content
- expect(safeWriteJson).toHaveBeenCalledWith("/test/workspace/.roo/mcp.json", { mcpServers: {} })
- // Check that openFile was called
- expect(openFileSpy).toHaveBeenCalledWith("/test/workspace/.roo/mcp.json")
- })
- test("handles openProjectMcpSettings when workspace is not open", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Mock no workspace folders
- ;(vscode.workspace as any).workspaceFolders = []
- // Trigger openProjectMcpSettings
- await messageHandler({ type: "openProjectMcpSettings" })
- // Verify error message was shown
- expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.no_workspace")
- })
- test.skip("handles openProjectMcpSettings file creation error", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Mock workspace folders
- ;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
- // Mock fs functions to fail
- const fs = require("fs/promises")
- fs.mkdir.mockRejectedValue(new Error("Failed to create directory"))
- // Trigger openProjectMcpSettings
- await messageHandler({
- type: "openProjectMcpSettings",
- })
- // Verify error message was shown
- expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
- expect.stringContaining("Failed to create or open .roo/mcp.json"),
- )
- })
- })
- describe.skip("ContextProxy integration", () => {
- let provider: ClineProvider
- let mockContext: vscode.ExtensionContext
- let mockOutputChannel: vscode.OutputChannel
- let mockContextProxy: any
- beforeEach(() => {
- // Reset mocks
- vi.clearAllMocks()
- // Setup basic mocks
- mockContext = {
- globalState: {
- get: vi.fn(),
- update: vi.fn(),
- keys: vi.fn().mockReturnValue([]),
- },
- workspaceState: {
- get: vi.fn().mockReturnValue(undefined),
- update: vi.fn().mockResolvedValue(undefined),
- keys: vi.fn().mockReturnValue([]),
- },
- secrets: { get: vi.fn(), store: vi.fn(), delete: vi.fn() },
- extensionUri: {} as vscode.Uri,
- globalStorageUri: { fsPath: "/test/path" },
- extension: { packageJSON: { version: "1.0.0" } },
- } as unknown as vscode.ExtensionContext
- mockOutputChannel = { appendLine: vi.fn() } as unknown as vscode.OutputChannel
- mockContextProxy = new ContextProxy(mockContext)
- provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", mockContextProxy)
- })
- test("updateGlobalState uses contextProxy", async () => {
- await provider.setValue("currentApiConfigName", "testValue")
- expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("currentApiConfigName", "testValue")
- })
- test("getGlobalState uses contextProxy", async () => {
- mockContextProxy.getGlobalState.mockResolvedValueOnce("testValue")
- const result = await provider.getValue("currentApiConfigName")
- expect(mockContextProxy.getGlobalState).toHaveBeenCalledWith("currentApiConfigName")
- expect(result).toBe("testValue")
- })
- test("storeSecret uses contextProxy", async () => {
- await provider.setValue("apiKey", "test-secret")
- expect(mockContextProxy.storeSecret).toHaveBeenCalledWith("apiKey", "test-secret")
- })
- test("contextProxy methods are available", () => {
- // Verify the contextProxy has all the required methods
- expect(mockContextProxy.getGlobalState).toBeDefined()
- expect(mockContextProxy.updateGlobalState).toBeDefined()
- expect(mockContextProxy.storeSecret).toBeDefined()
- expect(mockContextProxy.setValue).toBeDefined()
- expect(mockContextProxy.setValues).toBeDefined()
- })
- })
- describe("getTelemetryProperties", () => {
- let defaultTaskOptions: TaskOptions
- let provider: ClineProvider
- let mockContext: vscode.ExtensionContext
- let mockOutputChannel: vscode.OutputChannel
- let mockCline: any
- beforeEach(() => {
- // Reset mocks
- vi.clearAllMocks()
- // Initialize TelemetryService if not already initialized
- if (!TelemetryService.hasInstance()) {
- TelemetryService.createInstance([])
- }
- // Setup basic mocks
- mockContext = {
- globalState: {
- get: vi.fn().mockImplementation((key: string) => {
- if (key === "mode") return "code"
- if (key === "apiProvider") return "anthropic"
- return undefined
- }),
- update: vi.fn(),
- keys: vi.fn().mockReturnValue([]),
- },
- workspaceState: {
- get: vi.fn().mockReturnValue(undefined),
- update: vi.fn().mockResolvedValue(undefined),
- keys: vi.fn().mockReturnValue([]),
- },
- secrets: { get: vi.fn(), store: vi.fn(), delete: vi.fn() },
- extensionUri: {} as vscode.Uri,
- globalStorageUri: { fsPath: "/test/path" },
- extension: { packageJSON: { version: "1.0.0" } },
- } as unknown as vscode.ExtensionContext
- mockOutputChannel = { appendLine: vi.fn() } as unknown as vscode.OutputChannel
- provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
- defaultTaskOptions = {
- provider,
- apiConfiguration: {
- apiProvider: "openrouter",
- },
- }
- // Setup Task instance with mocked getModel method
- mockCline = new Task(defaultTaskOptions)
- mockCline.api = {
- getModel: vi.fn().mockReturnValue({
- id: "claude-sonnet-4-20250514",
- info: { contextWindow: 200000 },
- }),
- }
- })
- test("includes basic properties in telemetry", async () => {
- const properties = await provider.getTelemetryProperties()
- expect(properties).toHaveProperty("vscodeVersion")
- expect(properties).toHaveProperty("platform")
- expect(properties).toHaveProperty("appVersion", "1.0.0")
- })
- test("includes model ID from current Cline instance if available", async () => {
- // Add mock Cline to stack
- await provider.addClineToStack(mockCline)
- const properties = await provider.getTelemetryProperties()
- expect(properties).toHaveProperty("modelId", "claude-sonnet-4-20250514")
- })
- describe("cloud authentication telemetry", () => {
- beforeEach(() => {
- // Reset all mocks before each test
- vi.clearAllMocks()
- })
- test("includes cloud authentication property when user is authenticated", async () => {
- // Import the CloudService mock and update it
- const { CloudService } = await import("@roo-code/cloud")
- const mockCloudService = {
- isAuthenticated: vi.fn().mockReturnValue(true),
- }
- // Update the existing mock
- Object.defineProperty(CloudService, "instance", {
- get: vi.fn().mockReturnValue(mockCloudService),
- configurable: true,
- })
- const properties = await provider.getTelemetryProperties()
- expect(properties).toHaveProperty("cloudIsAuthenticated", true)
- })
- test("includes cloud authentication property when user is not authenticated", async () => {
- // Import the CloudService mock and update it
- const { CloudService } = await import("@roo-code/cloud")
- const mockCloudService = {
- isAuthenticated: vi.fn().mockReturnValue(false),
- }
- // Update the existing mock
- Object.defineProperty(CloudService, "instance", {
- get: vi.fn().mockReturnValue(mockCloudService),
- configurable: true,
- })
- const properties = await provider.getTelemetryProperties()
- expect(properties).toHaveProperty("cloudIsAuthenticated", false)
- })
- test("handles CloudService errors gracefully", async () => {
- // Import the CloudService mock and update it to throw an error
- const { CloudService } = await import("@roo-code/cloud")
- Object.defineProperty(CloudService, "instance", {
- get: vi.fn().mockImplementation(() => {
- throw new Error("CloudService not available")
- }),
- configurable: true,
- })
- const properties = await provider.getTelemetryProperties()
- // Should still include basic telemetry properties
- expect(properties).toHaveProperty("vscodeVersion")
- expect(properties).toHaveProperty("platform")
- expect(properties).toHaveProperty("appVersion", "1.0.0")
- // Cloud property should be undefined when CloudService is not available
- expect(properties).toHaveProperty("cloudIsAuthenticated", undefined)
- })
- test("handles CloudService method errors gracefully", async () => {
- // Import the CloudService mock and update it
- const { CloudService } = await import("@roo-code/cloud")
- const mockCloudService = {
- isAuthenticated: vi.fn().mockImplementation(() => {
- throw new Error("Authentication check error")
- }),
- }
- // Update the existing mock
- Object.defineProperty(CloudService, "instance", {
- get: vi.fn().mockReturnValue(mockCloudService),
- configurable: true,
- })
- const properties = await provider.getTelemetryProperties()
- // Should still include basic telemetry properties
- expect(properties).toHaveProperty("vscodeVersion")
- expect(properties).toHaveProperty("platform")
- expect(properties).toHaveProperty("appVersion", "1.0.0")
- // Property that errored should be undefined
- expect(properties).toHaveProperty("cloudIsAuthenticated", undefined)
- })
- })
- })
- describe("ClineProvider - Router Models", () => {
- let provider: ClineProvider
- let mockContext: vscode.ExtensionContext
- let mockOutputChannel: vscode.OutputChannel
- let mockWebviewView: vscode.WebviewView
- let mockPostMessage: any
- beforeEach(() => {
- vi.clearAllMocks()
- const globalState: Record<string, string | undefined> = {}
- const secrets: Record<string, string | undefined> = {}
- mockContext = {
- extensionPath: "/test/path",
- extensionUri: {} as vscode.Uri,
- globalState: {
- get: vi.fn().mockImplementation((key: string) => globalState[key]),
- update: vi
- .fn()
- .mockImplementation((key: string, value: string | undefined) => (globalState[key] = value)),
- keys: vi.fn().mockImplementation(() => Object.keys(globalState)),
- },
- secrets: {
- get: vi.fn().mockImplementation((key: string) => secrets[key]),
- store: vi.fn().mockImplementation((key: string, value: string | undefined) => (secrets[key] = value)),
- delete: vi.fn().mockImplementation((key: string) => delete secrets[key]),
- },
- workspaceState: {
- get: vi.fn().mockReturnValue(undefined),
- update: vi.fn().mockResolvedValue(undefined),
- keys: vi.fn().mockReturnValue([]),
- },
- subscriptions: [],
- extension: {
- packageJSON: { version: "1.0.0" },
- },
- globalStorageUri: {
- fsPath: "/test/storage/path",
- },
- } as unknown as vscode.ExtensionContext
- mockOutputChannel = {
- appendLine: vi.fn(),
- clear: vi.fn(),
- dispose: vi.fn(),
- } as unknown as vscode.OutputChannel
- mockPostMessage = vi.fn()
- mockWebviewView = {
- webview: {
- postMessage: mockPostMessage,
- html: "",
- options: {},
- onDidReceiveMessage: vi.fn(),
- asWebviewUri: vi.fn(),
- },
- visible: true,
- onDidDispose: vi.fn().mockImplementation((callback) => {
- callback()
- return { dispose: vi.fn() }
- }),
- onDidChangeVisibility: vi.fn().mockImplementation(() => ({ dispose: vi.fn() })),
- } as unknown as vscode.WebviewView
- if (!TelemetryService.hasInstance()) {
- TelemetryService.createInstance([])
- }
- provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
- })
- test("handles requestRouterModels with successful responses", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Mock getState to return API configuration
- vi.spyOn(provider, "getState").mockResolvedValue({
- apiConfiguration: {
- openRouterApiKey: "openrouter-key",
- requestyApiKey: "requesty-key",
- unboundApiKey: "unbound-key",
- litellmApiKey: "litellm-key",
- litellmBaseUrl: "http://localhost:4000",
- },
- } as any)
- const mockModels = {
- "model-1": {
- maxTokens: 4096,
- contextWindow: 8192,
- description: "Test model 1",
- supportsPromptCache: false,
- },
- "model-2": {
- maxTokens: 8192,
- contextWindow: 16384,
- description: "Test model 2",
- supportsPromptCache: false,
- },
- }
- const { getModels } = await import("../../../api/providers/fetchers/modelCache")
- vi.mocked(getModels).mockResolvedValue(mockModels)
- await messageHandler({ type: "requestRouterModels" })
- // Verify getModels was called for each provider with correct options
- expect(getModels).toHaveBeenCalledWith({ provider: "openrouter" })
- expect(getModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" })
- expect(getModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" })
- expect(getModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" })
- expect(getModels).toHaveBeenCalledWith({ provider: "deepinfra" })
- expect(getModels).toHaveBeenCalledWith(
- expect.objectContaining({
- provider: "roo",
- baseUrl: expect.any(String),
- }),
- )
- expect(getModels).toHaveBeenCalledWith({
- provider: "litellm",
- apiKey: "litellm-key",
- baseUrl: "http://localhost:4000",
- })
- expect(getModels).toHaveBeenCalledWith({ provider: "chutes" })
- // Verify response was sent
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "routerModels",
- routerModels: {
- deepinfra: mockModels,
- openrouter: mockModels,
- requesty: mockModels,
- unbound: mockModels,
- roo: mockModels,
- chutes: mockModels,
- litellm: mockModels,
- ollama: {},
- lmstudio: {},
- "vercel-ai-gateway": mockModels,
- huggingface: {},
- "io-intelligence": {},
- },
- values: undefined,
- })
- })
- test("handles requestRouterModels with individual provider failures", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- vi.spyOn(provider, "getState").mockResolvedValue({
- apiConfiguration: {
- openRouterApiKey: "openrouter-key",
- requestyApiKey: "requesty-key",
- unboundApiKey: "unbound-key",
- litellmApiKey: "litellm-key",
- litellmBaseUrl: "http://localhost:4000",
- },
- } as any)
- const mockModels = {
- "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model", supportsPromptCache: false },
- }
- const { getModels } = await import("../../../api/providers/fetchers/modelCache")
- // Mock some providers to succeed and others to fail
- vi.mocked(getModels)
- .mockResolvedValueOnce(mockModels) // openrouter success
- .mockRejectedValueOnce(new Error("Requesty API error")) // requesty fail
- .mockRejectedValueOnce(new Error("Unbound API error")) // unbound fail
- .mockResolvedValueOnce(mockModels) // vercel-ai-gateway success
- .mockResolvedValueOnce(mockModels) // deepinfra success
- .mockResolvedValueOnce(mockModels) // roo success
- .mockRejectedValueOnce(new Error("Chutes API error")) // chutes fail
- .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail
- await messageHandler({ type: "requestRouterModels" })
- // Verify main response includes successful providers and empty objects for failed ones
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "routerModels",
- routerModels: {
- deepinfra: mockModels,
- openrouter: mockModels,
- requesty: {},
- unbound: {},
- roo: mockModels,
- chutes: {},
- ollama: {},
- lmstudio: {},
- litellm: {},
- "vercel-ai-gateway": mockModels,
- huggingface: {},
- "io-intelligence": {},
- },
- values: undefined,
- })
- // Verify error messages were sent for failed providers
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "singleRouterModelFetchResponse",
- success: false,
- error: "Requesty API error",
- values: { provider: "requesty" },
- })
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "singleRouterModelFetchResponse",
- success: false,
- error: "Unbound API error",
- values: { provider: "unbound" },
- })
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "singleRouterModelFetchResponse",
- success: false,
- error: "Unbound API error",
- values: { provider: "unbound" },
- })
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "singleRouterModelFetchResponse",
- success: false,
- error: "Chutes API error",
- values: { provider: "chutes" },
- })
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "singleRouterModelFetchResponse",
- success: false,
- error: "LiteLLM connection failed",
- values: { provider: "litellm" },
- })
- })
- test("handles requestRouterModels with LiteLLM values from message", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Mock state without LiteLLM config
- vi.spyOn(provider, "getState").mockResolvedValue({
- apiConfiguration: {
- openRouterApiKey: "openrouter-key",
- requestyApiKey: "requesty-key",
- unboundApiKey: "unbound-key",
- // No litellm config
- },
- } as any)
- const mockModels = {
- "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model", supportsPromptCache: false },
- }
- const { getModels } = await import("../../../api/providers/fetchers/modelCache")
- vi.mocked(getModels).mockResolvedValue(mockModels)
- await messageHandler({
- type: "requestRouterModels",
- values: {
- litellmApiKey: "message-litellm-key",
- litellmBaseUrl: "http://message-url:4000",
- },
- })
- // Verify LiteLLM was called with values from message
- expect(getModels).toHaveBeenCalledWith({
- provider: "litellm",
- apiKey: "message-litellm-key",
- baseUrl: "http://message-url:4000",
- })
- })
- test("skips LiteLLM when neither config nor message values are provided", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- vi.spyOn(provider, "getState").mockResolvedValue({
- apiConfiguration: {
- openRouterApiKey: "openrouter-key",
- requestyApiKey: "requesty-key",
- unboundApiKey: "unbound-key",
- // No litellm config
- },
- } as any)
- const mockModels = {
- "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model", supportsPromptCache: false },
- }
- const { getModels } = await import("../../../api/providers/fetchers/modelCache")
- vi.mocked(getModels).mockResolvedValue(mockModels)
- await messageHandler({ type: "requestRouterModels" })
- // Verify LiteLLM was NOT called
- expect(getModels).not.toHaveBeenCalledWith(
- expect.objectContaining({
- provider: "litellm",
- }),
- )
- // Verify response includes empty object for LiteLLM
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "routerModels",
- routerModels: {
- deepinfra: mockModels,
- openrouter: mockModels,
- requesty: mockModels,
- unbound: mockModels,
- roo: mockModels,
- chutes: mockModels,
- litellm: {},
- ollama: {},
- lmstudio: {},
- "vercel-ai-gateway": mockModels,
- huggingface: {},
- "io-intelligence": {},
- },
- values: undefined,
- })
- })
- test("handles requestLmStudioModels with proper response", async () => {
- await provider.resolveWebviewView(mockWebviewView)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- vi.spyOn(provider, "getState").mockResolvedValue({
- apiConfiguration: {
- lmStudioModelId: "model-1",
- lmStudioBaseUrl: "http://localhost:1234",
- },
- } as any)
- const mockModels = {
- "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model", supportsPromptCache: false },
- }
- const { getModels } = await import("../../../api/providers/fetchers/modelCache")
- vi.mocked(getModels).mockResolvedValue(mockModels)
- await messageHandler({
- type: "requestLmStudioModels",
- })
- expect(getModels).toHaveBeenCalledWith({
- provider: "lmstudio",
- baseUrl: "http://localhost:1234",
- })
- })
- })
- describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
- let provider: ClineProvider
- let mockContext: vscode.ExtensionContext
- let mockOutputChannel: vscode.OutputChannel
- let mockWebviewView: vscode.WebviewView
- let mockPostMessage: any
- let defaultTaskOptions: TaskOptions
- beforeEach(() => {
- vi.clearAllMocks()
- if (!TelemetryService.hasInstance()) {
- TelemetryService.createInstance([])
- }
- const globalState: Record<string, string | undefined> = {
- mode: "code",
- currentApiConfigName: "current-config",
- }
- const secrets: Record<string, string | undefined> = {}
- mockContext = {
- extensionPath: "/test/path",
- extensionUri: {} as vscode.Uri,
- globalState: {
- get: vi.fn().mockImplementation((key: string) => globalState[key]),
- update: vi
- .fn()
- .mockImplementation((key: string, value: string | undefined) => (globalState[key] = value)),
- keys: vi.fn().mockImplementation(() => Object.keys(globalState)),
- },
- secrets: {
- get: vi.fn().mockImplementation((key: string) => secrets[key]),
- store: vi.fn().mockImplementation((key: string, value: string | undefined) => (secrets[key] = value)),
- delete: vi.fn().mockImplementation((key: string) => delete secrets[key]),
- },
- workspaceState: {
- get: vi.fn().mockReturnValue(undefined),
- update: vi.fn().mockResolvedValue(undefined),
- keys: vi.fn().mockReturnValue([]),
- },
- subscriptions: [],
- extension: {
- packageJSON: { version: "1.0.0" },
- },
- globalStorageUri: {
- fsPath: "/test/storage/path",
- },
- } as unknown as vscode.ExtensionContext
- mockOutputChannel = {
- appendLine: vi.fn(),
- clear: vi.fn(),
- dispose: vi.fn(),
- } as unknown as vscode.OutputChannel
- mockPostMessage = vi.fn()
- mockWebviewView = {
- webview: {
- postMessage: mockPostMessage,
- html: "",
- options: {},
- onDidReceiveMessage: vi.fn(),
- asWebviewUri: vi.fn(),
- },
- visible: true,
- onDidDispose: vi.fn().mockImplementation((callback) => {
- callback()
- return { dispose: vi.fn() }
- }),
- onDidChangeVisibility: vi.fn().mockImplementation(() => ({ dispose: vi.fn() })),
- } as unknown as vscode.WebviewView
- provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
- defaultTaskOptions = {
- provider,
- apiConfiguration: {
- apiProvider: "openrouter",
- },
- }
- // Mock getMcpHub method
- provider.getMcpHub = vi.fn().mockReturnValue({
- listTools: vi.fn().mockResolvedValue([]),
- callTool: vi.fn().mockResolvedValue({ content: [] }),
- listResources: vi.fn().mockResolvedValue([]),
- readResource: vi.fn().mockResolvedValue({ contents: [] }),
- getAllServers: vi.fn().mockReturnValue([]),
- })
- })
- describe("Edit Messages with Images and Attachments", () => {
- beforeEach(async () => {
- await provider.resolveWebviewView(mockWebviewView)
- })
- test("handles editing messages containing images", async () => {
- const mockMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: "Original message" },
- {
- ts: 2000,
- type: "say",
- say: "user_feedback",
- text: "Message with image",
- images: [
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
- ],
- value: 3000,
- },
- { ts: 3000, type: "say", say: "text", text: "AI response" },
- ] as ClineMessage[]
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = mockMessages
- mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }] as any[]
- mockCline.overwriteClineMessages = vi.fn()
- mockCline.overwriteApiConversationHistory = vi.fn()
- mockCline.submitUserMessage = vi.fn()
- await provider.addClineToStack(mockCline)
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({
- type: "submitEditedMessage",
- value: 3000,
- editedMessageContent: "Edited message with preserved images",
- })
- // Verify dialog was shown
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showEditMessageDialog",
- messageTs: 3000,
- text: "Edited message with preserved images",
- hasCheckpoint: false,
- images: undefined,
- })
- // Simulate confirmation
- await messageHandler({
- type: "editMessageConfirm",
- messageTs: 3000,
- text: "Edited message with preserved images",
- })
- // Verify messages were edited correctly - the ORIGINAL user message and all subsequent messages are removed
- expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]])
- expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }])
- // Verify submitUserMessage was called with the edited content
- expect(mockCline.submitUserMessage).toHaveBeenCalledWith("Edited message with preserved images", [])
- })
- test("handles editing messages with file attachments", async () => {
- const mockMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: "Original message" },
- {
- ts: 2000,
- type: "say",
- say: "user_feedback",
- text: "Message with file",
- attachments: [{ path: "/path/to/file.txt", type: "file" }],
- value: 3000,
- },
- { ts: 3000, type: "say", say: "text", text: "AI response" },
- ] as ClineMessage[]
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = mockMessages
- mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }] as any[]
- mockCline.overwriteClineMessages = vi.fn()
- mockCline.overwriteApiConversationHistory = vi.fn()
- mockCline.submitUserMessage = vi.fn()
- await provider.addClineToStack(mockCline)
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({
- type: "submitEditedMessage",
- value: 3000,
- editedMessageContent: "Edited message with file attachment",
- })
- // Verify dialog was shown
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showEditMessageDialog",
- messageTs: 3000,
- text: "Edited message with file attachment",
- hasCheckpoint: false,
- images: undefined,
- })
- // Simulate user confirming the edit
- await messageHandler({
- type: "editMessageConfirm",
- messageTs: 3000,
- text: "Edited message with file attachment",
- })
- expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
- expect(mockCline.submitUserMessage).toHaveBeenCalledWith("Edited message with file attachment", [])
- })
- })
- describe("Network Failure Scenarios", () => {
- beforeEach(async () => {
- ;(vscode.window.showInformationMessage as any) = vi.fn()
- await provider.resolveWebviewView(mockWebviewView)
- })
- test("handles network timeout during edit submission", async () => {
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 },
- { ts: 2000, type: "say", say: "text", text: "AI response" },
- ] as ClineMessage[]
- mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
- mockCline.overwriteClineMessages = vi.fn()
- mockCline.overwriteApiConversationHistory = vi.fn()
- mockCline.handleWebviewAskResponse = vi.fn().mockRejectedValue(new Error("Network timeout"))
- await provider.addClineToStack(mockCline)
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Should not throw error, but handle gracefully
- await expect(
- messageHandler({
- type: "submitEditedMessage",
- value: 2000,
- editedMessageContent: "Edited message",
- }),
- ).resolves.toBeUndefined()
- // Verify dialog was shown
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showEditMessageDialog",
- messageTs: 2000,
- text: "Edited message",
- hasCheckpoint: false,
- images: undefined,
- })
- // Simulate user confirming the edit
- await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message" })
- expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
- })
- test("handles connection drops during edit operation", async () => {
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 },
- { ts: 2000, type: "say", say: "text", text: "AI response" },
- ] as ClineMessage[]
- mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
- mockCline.overwriteClineMessages = vi.fn().mockRejectedValue(new Error("Connection lost"))
- mockCline.overwriteApiConversationHistory = vi.fn()
- mockCline.handleWebviewAskResponse = vi.fn()
- await provider.addClineToStack(mockCline)
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Should handle connection error gracefully
- await expect(
- messageHandler({
- type: "submitEditedMessage",
- value: 2000,
- editedMessageContent: "Edited message",
- }),
- ).resolves.toBeUndefined()
- // Verify dialog was shown
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showEditMessageDialog",
- messageTs: 2000,
- text: "Edited message",
- hasCheckpoint: false,
- images: undefined,
- })
- // Simulate user confirming the edit
- await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message" })
- // The error should be caught and shown
- expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_editing_message")
- })
- })
- describe("Concurrent Edit Operations", () => {
- beforeEach(async () => {
- ;(vscode.window.showInformationMessage as any) = vi.fn()
- await provider.resolveWebviewView(mockWebviewView)
- })
- test("handles race conditions with simultaneous edits", async () => {
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: "Message 1", value: 2000 },
- { ts: 2000, type: "say", say: "text", text: "AI response 1" },
- { ts: 3000, type: "say", say: "user_feedback", text: "Message 2", value: 4000 },
- { ts: 4000, type: "say", say: "text", text: "AI response 2" },
- ] as ClineMessage[]
- mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }, { ts: 4000 }] as any[]
- mockCline.overwriteClineMessages = vi.fn()
- mockCline.overwriteApiConversationHistory = vi.fn()
- mockCline.handleWebviewAskResponse = vi.fn()
- await provider.addClineToStack(mockCline)
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Simulate concurrent edit operations
- const edit1Promise = messageHandler({
- type: "submitEditedMessage",
- value: 2000,
- editedMessageContent: "Edited message 1",
- })
- const edit2Promise = messageHandler({
- type: "submitEditedMessage",
- value: 4000,
- editedMessageContent: "Edited message 2",
- })
- await Promise.all([edit1Promise, edit2Promise])
- // Verify dialogs were shown for both edits
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showEditMessageDialog",
- messageTs: 2000,
- text: "Edited message 1",
- hasCheckpoint: false,
- images: undefined,
- })
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showEditMessageDialog",
- messageTs: 4000,
- text: "Edited message 2",
- hasCheckpoint: false,
- images: undefined,
- })
- // Simulate user confirming both edits
- await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message 1" })
- await messageHandler({ type: "editMessageConfirm", messageTs: 4000, text: "Edited message 2" })
- // Both operations should complete without throwing
- expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
- })
- })
- describe("Edit Permissions and Authorization", () => {
- beforeEach(async () => {
- ;(vscode.window.showInformationMessage as any) = vi.fn()
- await provider.resolveWebviewView(mockWebviewView)
- })
- test("handles edit permission failures", async () => {
- // Mock no current cline (simulating permission failure)
- vi.spyOn(provider, "getCurrentTask").mockReturnValue(undefined)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({
- type: "submitEditedMessage",
- value: 2000,
- editedMessageContent: "Edited message",
- })
- // Should not show confirmation dialog when no current cline
- expect(vscode.window.showInformationMessage).not.toHaveBeenCalled()
- })
- test("handles authorization failures during edit", async () => {
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 },
- { ts: 2000, type: "say", say: "text", text: "AI response" },
- ] as ClineMessage[]
- mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
- mockCline.overwriteClineMessages = vi.fn().mockRejectedValue(new Error("Unauthorized"))
- mockCline.overwriteApiConversationHistory = vi.fn()
- mockCline.handleWebviewAskResponse = vi.fn()
- await provider.addClineToStack(mockCline)
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({
- type: "submitEditedMessage",
- value: 2000,
- editedMessageContent: "Edited message",
- })
- // Simulate confirmation
- await messageHandler({
- type: "editMessageConfirm",
- messageTs: 2000,
- text: "Edited message",
- })
- expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_editing_message")
- })
- describe("Malformed Requests and Invalid Formats", () => {
- beforeEach(async () => {
- await provider.resolveWebviewView(mockWebviewView)
- })
- test("handles malformed edit requests", async () => {
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Test with missing value
- await messageHandler({
- type: "submitEditedMessage",
- editedMessageContent: "Edited message",
- })
- // Test with invalid value type
- await messageHandler({
- type: "submitEditedMessage",
- value: "invalid",
- editedMessageContent: "Edited message",
- })
- // Test with missing editedMessageContent
- await messageHandler({
- type: "submitEditedMessage",
- value: 2000,
- })
- // Should not show confirmation dialog for malformed requests
- expect(vscode.window.showInformationMessage).not.toHaveBeenCalled()
- })
- test("handles invalid message formats", async () => {
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Test with null message - should throw error
- await expect(messageHandler(null)).rejects.toThrow()
- // Test with undefined message - should throw error
- await expect(messageHandler(undefined)).rejects.toThrow()
- // Test with message missing type
- await expect(
- messageHandler({
- value: 2000,
- editedMessageContent: "Edited message",
- }),
- ).resolves.toBeUndefined()
- // Should handle gracefully without errors
- expect(vscode.window.showInformationMessage).not.toHaveBeenCalled()
- })
- test("handles invalid timestamp values", async () => {
- ;(vscode.window.showInformationMessage as any) = vi.fn()
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: "Original message" },
- { ts: 2000, type: "say", say: "text", text: "AI response" },
- ] as ClineMessage[]
- mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
- await provider.addClineToStack(mockCline)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Test with negative timestamp
- await messageHandler({
- type: "deleteMessage",
- value: -1000,
- })
- // Test with zero timestamp
- await messageHandler({
- type: "deleteMessage",
- value: 0,
- })
- // Invalid timestamps may still trigger confirmation dialog
- // This is expected behavior as the system tries to process the message
- })
- })
- describe("Operations on Deleted or Non-existent Messages", () => {
- beforeEach(async () => {
- ;(vscode.window.showInformationMessage as any) = vi.fn()
- await provider.resolveWebviewView(mockWebviewView)
- })
- test("handles edit operations on deleted messages", async () => {
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: "Existing message" },
- ] as ClineMessage[]
- mockCline.apiConversationHistory = [{ ts: 1000 }] as any[]
- mockCline.overwriteClineMessages = vi.fn()
- mockCline.overwriteApiConversationHistory = vi.fn()
- mockCline.handleWebviewAskResponse = vi.fn()
- await provider.addClineToStack(mockCline)
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Try to edit a message that doesn't exist (timestamp 5000)
- await messageHandler({
- type: "submitEditedMessage",
- value: 5000,
- editedMessageContent: "Edited non-existent message",
- })
- // Should show edit dialog
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showEditMessageDialog",
- messageTs: 5000,
- text: "Edited non-existent message",
- hasCheckpoint: false,
- images: undefined,
- })
- // Simulate user confirming the edit
- await messageHandler({
- type: "editMessageConfirm",
- messageTs: 5000,
- text: "Edited non-existent message",
- })
- // Should not perform any operations since message doesn't exist
- expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled()
- expect(mockCline.handleWebviewAskResponse).not.toHaveBeenCalled()
- })
- test("handles delete operations on non-existent messages", async () => {
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: "Existing message" },
- ] as ClineMessage[]
- mockCline.apiConversationHistory = [{ ts: 1000 }] as any[]
- mockCline.overwriteClineMessages = vi.fn()
- mockCline.overwriteApiConversationHistory = vi.fn()
- await provider.addClineToStack(mockCline)
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- // Try to delete a message that doesn't exist (timestamp 5000)
- await messageHandler({
- type: "deleteMessage",
- value: 5000,
- })
- // Should show delete dialog
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showDeleteMessageDialog",
- messageTs: 5000,
- hasCheckpoint: false,
- })
- // Simulate user confirming the delete
- await messageHandler({ type: "deleteMessageConfirm", messageTs: 5000 })
- // Should not perform any operations since message doesn't exist
- expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled()
- })
- })
- describe("Resource Cleanup During Failed Operations", () => {
- beforeEach(async () => {
- ;(vscode.window.showInformationMessage as any) = vi.fn()
- await provider.resolveWebviewView(mockWebviewView)
- })
- test("validates proper cleanup during failed edit operations", async () => {
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 },
- { ts: 2000, type: "say", say: "text", text: "AI response" },
- ] as ClineMessage[]
- mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
- // Mock cleanup tracking
- const cleanupSpy = vi.fn()
- mockCline.overwriteClineMessages = vi.fn().mockImplementation(() => {
- cleanupSpy()
- throw new Error("Operation failed")
- })
- mockCline.overwriteApiConversationHistory = vi.fn()
- mockCline.handleWebviewAskResponse = vi.fn()
- await provider.addClineToStack(mockCline)
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({
- type: "submitEditedMessage",
- value: 2000,
- editedMessageContent: "Edited message",
- })
- // Should show edit dialog
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showEditMessageDialog",
- messageTs: 2000,
- text: "Edited message",
- hasCheckpoint: false,
- images: undefined,
- })
- // Simulate user confirming the edit
- await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message" })
- // Verify cleanup was attempted before failure
- expect(cleanupSpy).toHaveBeenCalled()
- expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_editing_message")
- })
- test("validates proper cleanup during failed delete operations", async () => {
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: "Message to delete" },
- { ts: 2000, type: "say", say: "text", text: "AI response" },
- ] as ClineMessage[]
- mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
- // Mock cleanup tracking
- const cleanupSpy = vi.fn()
- mockCline.overwriteClineMessages = vi.fn().mockImplementation(() => {
- cleanupSpy()
- throw new Error("Delete operation failed")
- })
- mockCline.overwriteApiConversationHistory = vi.fn()
- await provider.addClineToStack(mockCline)
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({ type: "deleteMessage", value: 2000 })
- // Should show delete dialog
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showDeleteMessageDialog",
- messageTs: 2000,
- hasCheckpoint: false,
- })
- // Simulate user confirming the delete
- await messageHandler({ type: "deleteMessageConfirm", messageTs: 2000 })
- // Verify cleanup was attempted before failure
- expect(cleanupSpy).toHaveBeenCalled()
- expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_deleting_message")
- })
- })
- describe("Large Message Payloads", () => {
- beforeEach(async () => {
- ;(vscode.window.showInformationMessage as any) = vi.fn()
- await provider.resolveWebviewView(mockWebviewView)
- })
- test("handles editing messages with large text content", async () => {
- // Create a large message (10KB of text)
- const largeText = "A".repeat(10000)
- const mockMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: largeText, value: 2000 },
- { ts: 2000, type: "say", say: "text", text: "AI response" },
- ] as ClineMessage[]
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = mockMessages
- mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
- mockCline.overwriteClineMessages = vi.fn()
- mockCline.overwriteApiConversationHistory = vi.fn()
- mockCline.submitUserMessage = vi.fn()
- await provider.addClineToStack(mockCline)
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- const largeEditedContent = "B".repeat(15000)
- await messageHandler({
- type: "submitEditedMessage",
- value: 2000,
- editedMessageContent: largeEditedContent,
- })
- // Should show edit dialog
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showEditMessageDialog",
- messageTs: 2000,
- text: largeEditedContent,
- hasCheckpoint: false,
- images: undefined,
- })
- // Simulate user confirming the edit
- await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: largeEditedContent })
- expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
- expect(mockCline.submitUserMessage).toHaveBeenCalledWith(largeEditedContent, [])
- })
- test("handles deleting messages with large payloads", async () => {
- // Create messages with large payloads
- const largeText = "X".repeat(50000)
- const mockMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: "Small message" },
- { ts: 2000, type: "say", say: "user_feedback", text: largeText },
- { ts: 3000, type: "say", say: "text", text: "AI response" },
- { ts: 4000, type: "say", say: "user_feedback", text: "Another large message: " + largeText },
- ] as ClineMessage[]
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = mockMessages
- mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }, { ts: 4000 }] as any[]
- mockCline.overwriteClineMessages = vi.fn()
- mockCline.overwriteApiConversationHistory = vi.fn()
- await provider.addClineToStack(mockCline)
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({ type: "deleteMessage", value: 3000 })
- // Should show delete dialog
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showDeleteMessageDialog",
- messageTs: 3000,
- hasCheckpoint: false,
- })
- // Simulate user confirming the delete
- await messageHandler({ type: "deleteMessageConfirm", messageTs: 3000 })
- // Should handle large payloads without issues - keeps messages before the deleted one
- expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]])
- expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }, { ts: 2000 }])
- })
- })
- describe("Error Messaging and User Feedback", () => {
- beforeEach(async () => {
- await provider.resolveWebviewView(mockWebviewView)
- })
- // Note: Error messaging test removed as the implementation may not have proper error handling in place
- test("provides user feedback for successful operations", async () => {
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: "Message to delete" },
- { ts: 2000, type: "say", say: "text", text: "AI response" },
- ] as ClineMessage[]
- mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
- mockCline.overwriteClineMessages = vi.fn()
- mockCline.overwriteApiConversationHistory = vi.fn()
- await provider.addClineToStack(mockCline)
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- ;(provider as any).createTaskWithHistoryItem = vi.fn()
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({ type: "deleteMessage", value: 2000 })
- // Should show delete dialog
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showDeleteMessageDialog",
- messageTs: 2000,
- hasCheckpoint: false,
- })
- // Simulate user confirming the delete
- await messageHandler({ type: "deleteMessageConfirm", messageTs: 2000 })
- // Verify successful operation completed
- expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
- // createTaskWithHistoryItem is only called when restoring checkpoints or aborting tasks
- expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
- })
- test("handles user cancellation gracefully", async () => {
- // Test cancellation by not sending confirmation
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: "Message to edit" },
- { ts: 2000, type: "say", say: "text", text: "AI response" },
- ] as ClineMessage[]
- mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[]
- mockCline.overwriteClineMessages = vi.fn()
- mockCline.overwriteApiConversationHistory = vi.fn()
- mockCline.handleWebviewAskResponse = vi.fn()
- await provider.addClineToStack(mockCline)
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({
- type: "submitEditedMessage",
- value: 2000,
- editedMessageContent: "Edited message",
- })
- // Verify no operations were performed when user canceled
- expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled()
- expect(mockCline.overwriteApiConversationHistory).not.toHaveBeenCalled()
- expect(mockCline.handleWebviewAskResponse).not.toHaveBeenCalled()
- expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
- })
- })
- describe("Edge Cases with Message Timestamps", () => {
- beforeEach(async () => {
- ;(vscode.window.showInformationMessage as any) = vi.fn()
- await provider.resolveWebviewView(mockWebviewView)
- })
- test("handles messages with identical timestamps", async () => {
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: "Message 1" },
- { ts: 1000, type: "say", say: "text", text: "Message 2 (same timestamp)" },
- { ts: 1000, type: "say", say: "user_feedback", text: "Message 3 (same timestamp)" },
- { ts: 2000, type: "say", say: "text", text: "Message 4" },
- ] as ClineMessage[]
- mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 1000 }, { ts: 1000 }, { ts: 2000 }] as any[]
- mockCline.overwriteClineMessages = vi.fn()
- mockCline.overwriteApiConversationHistory = vi.fn()
- await provider.addClineToStack(mockCline)
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({ type: "deleteMessage", value: 1000 })
- // Should show delete dialog
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showDeleteMessageDialog",
- messageTs: 1000,
- hasCheckpoint: false,
- })
- // Simulate user confirming the delete
- await messageHandler({ type: "deleteMessageConfirm", messageTs: 1000 })
- // Should handle identical timestamps gracefully
- expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
- })
- test("handles messages with future timestamps", async () => {
- const futureTimestamp = Date.now() + 100000 // Future timestamp
- const mockCline = new Task(defaultTaskOptions)
- mockCline.clineMessages = [
- { ts: 1000, type: "say", say: "user_feedback", text: "Past message" },
- {
- ts: futureTimestamp,
- type: "say",
- say: "user_feedback",
- text: "Future message",
- value: futureTimestamp + 1000,
- },
- { ts: futureTimestamp + 1000, type: "say", say: "text", text: "AI response" },
- ] as ClineMessage[]
- mockCline.apiConversationHistory = [
- { ts: 1000 },
- { ts: futureTimestamp },
- { ts: futureTimestamp + 1000 },
- ] as any[]
- mockCline.overwriteClineMessages = vi.fn()
- mockCline.overwriteApiConversationHistory = vi.fn()
- mockCline.submitUserMessage = vi.fn()
- await provider.addClineToStack(mockCline)
- ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
- historyItem: { id: "test-task-id" },
- })
- const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
- await messageHandler({
- type: "submitEditedMessage",
- value: futureTimestamp + 1000,
- editedMessageContent: "Edited future message",
- })
- // Should show edit dialog
- expect(mockPostMessage).toHaveBeenCalledWith({
- type: "showEditMessageDialog",
- messageTs: futureTimestamp + 1000,
- text: "Edited future message",
- hasCheckpoint: false,
- images: undefined,
- })
- // Simulate user confirming the edit
- await messageHandler({
- type: "editMessageConfirm",
- messageTs: futureTimestamp + 1000,
- text: "Edited future message",
- })
- // Should handle future timestamps correctly
- expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
- expect(mockCline.submitUserMessage).toHaveBeenCalled()
- })
- })
- })
- describe("getTaskWithId", () => {
- it("returns empty apiConversationHistory when file is missing", async () => {
- const historyItem = { id: "missing-api-file-task", task: "test task", ts: Date.now() }
- vi.mocked(mockContext.globalState.get).mockImplementation((key: string) => {
- if (key === "taskHistory") {
- return [historyItem]
- }
- return undefined
- })
- const deleteTaskSpy = vi.spyOn(provider, "deleteTaskFromState")
- const result = await (provider as any).getTaskWithId("missing-api-file-task")
- expect(result.historyItem).toEqual(historyItem)
- expect(result.apiConversationHistory).toEqual([])
- expect(deleteTaskSpy).not.toHaveBeenCalled()
- })
- it("returns empty apiConversationHistory when file contains invalid JSON", async () => {
- const historyItem = { id: "corrupt-api-task", task: "test task", ts: Date.now() }
- vi.mocked(mockContext.globalState.get).mockImplementation((key: string) => {
- if (key === "taskHistory") {
- return [historyItem]
- }
- return undefined
- })
- // Make fileExistsAtPath return true so the read path is exercised
- const fsUtils = await import("../../../utils/fs")
- vi.spyOn(fsUtils, "fileExistsAtPath").mockResolvedValue(true)
- // Make readFile return corrupted JSON
- const fsp = await import("fs/promises")
- vi.mocked(fsp.readFile).mockResolvedValueOnce("{not valid json!!!" as never)
- const deleteTaskSpy = vi.spyOn(provider, "deleteTaskFromState")
- const result = await (provider as any).getTaskWithId("corrupt-api-task")
- expect(result.historyItem).toEqual(historyItem)
- expect(result.apiConversationHistory).toEqual([])
- expect(deleteTaskSpy).not.toHaveBeenCalled()
- // Restore the spy
- vi.mocked(fsUtils.fileExistsAtPath).mockRestore()
- })
- })
- })
|