@@ -32,6 +32,7 @@ type TaskTestAccess = {
3232 presentAssistantMessageSafe : ( ) => void
3333 updateClineMessage : ( message : import ( "@roo-code/types" ) . ClineMessage ) => Promise < void >
3434 saveClineMessages : ( ) => Promise < boolean >
35+ safeEnsureModelFetched : ( ) => Promise < void >
3536}
3637
3738function getTaskTestAccess ( task : Task ) : TaskTestAccess {
@@ -185,6 +186,14 @@ vi.mock("../../environment/getEnvironmentDetails", () => ({
185186 getEnvironmentDetails : vi . fn ( ) . mockResolvedValue ( "" ) ,
186187} ) )
187188
189+ vi . mock ( "../../mentions/processUserContentMentions" , async ( importOriginal ) => {
190+ const actual = await importOriginal < typeof import ( "../../mentions/processUserContentMentions" ) > ( )
191+ return {
192+ ...actual ,
193+ processUserContentMentions : vi . fn ( ) . mockImplementation ( actual . processUserContentMentions ) ,
194+ }
195+ } )
196+
188197vi . mock ( "../../ignore/RooIgnoreController" )
189198
190199vi . mock ( "../../../i18n" , ( ) => {
@@ -2507,6 +2516,230 @@ describe("Cline", () => {
25072516 } )
25082517 } )
25092518
2519+ describe ( "safeEnsureModelFetched" , ( ) => {
2520+ it ( "loads model metadata before getModel is used" , async ( ) => {
2521+ const task = new Task ( {
2522+ provider : mockProvider ,
2523+ apiConfiguration : mockApiConfig ,
2524+ task : "test task" ,
2525+ startTask : false ,
2526+ } )
2527+
2528+ const ensureModelFetched = vi . fn ( ) . mockResolvedValue ( undefined )
2529+ Object . assign ( task . api , { ensureModelFetched } )
2530+
2531+ await getTaskTestAccess ( task ) . safeEnsureModelFetched ( )
2532+
2533+ expect ( ensureModelFetched ) . toHaveBeenCalledTimes ( 1 )
2534+ } )
2535+
2536+ it ( "swallows fetch failures so callers can fall back to defaults" , async ( ) => {
2537+ const task = new Task ( {
2538+ provider : mockProvider ,
2539+ apiConfiguration : mockApiConfig ,
2540+ task : "test task" ,
2541+ startTask : false ,
2542+ } )
2543+
2544+ const ensureModelFetched = vi . fn ( ) . mockRejectedValue ( new Error ( "network down" ) )
2545+ Object . assign ( task . api , { ensureModelFetched } )
2546+ const errorSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } )
2547+
2548+ await expect ( getTaskTestAccess ( task ) . safeEnsureModelFetched ( ) ) . resolves . toBeUndefined ( )
2549+
2550+ expect ( errorSpy ) . toHaveBeenCalledWith (
2551+ expect . stringContaining ( "Failed to fetch model metadata" ) ,
2552+ "network down" ,
2553+ )
2554+ errorSpy . mockRestore ( )
2555+ } )
2556+
2557+ it ( "is a no-op when the api handler does not implement ensureModelFetched" , async ( ) => {
2558+ const task = new Task ( {
2559+ provider : mockProvider ,
2560+ apiConfiguration : mockApiConfig ,
2561+ task : "test task" ,
2562+ startTask : false ,
2563+ } )
2564+
2565+ await expect ( getTaskTestAccess ( task ) . safeEnsureModelFetched ( ) ) . resolves . toBeUndefined ( )
2566+ } )
2567+
2568+ it ( "calls safeEnsureModelFetched from attemptApiRequest when context tokens are present" , async ( ) => {
2569+ const task = new Task ( {
2570+ provider : mockProvider ,
2571+ apiConfiguration : mockApiConfig ,
2572+ task : "test task" ,
2573+ startTask : false ,
2574+ } )
2575+
2576+ vi . spyOn ( getTaskTestAccess ( task ) , "getSystemPrompt" ) . mockResolvedValue ( "mock system prompt" )
2577+ vi . spyOn ( task , "getTokenUsage" ) . mockReturnValue ( {
2578+ totalCost : 0 ,
2579+ totalTokensIn : 0 ,
2580+ totalTokensOut : 0 ,
2581+ contextTokens : 50_000 ,
2582+ } )
2583+ const safeSpy = vi . spyOn ( getTaskTestAccess ( task ) , "safeEnsureModelFetched" ) . mockResolvedValue ( undefined )
2584+ vi . spyOn ( task . api , "getModel" ) . mockReturnValue ( {
2585+ id : mockApiConfig . apiModelId ! ,
2586+ info : {
2587+ supportsImages : false ,
2588+ supportsPromptCache : true ,
2589+ contextWindow : 200_000 ,
2590+ maxTokens : 4096 ,
2591+ } as ModelInfo ,
2592+ } )
2593+ vi . spyOn ( task . api , "createMessage" ) . mockReturnValue ( {
2594+ async * [ Symbol . asyncIterator ] ( ) {
2595+ yield { type : "text" , text : "ok" }
2596+ } ,
2597+ async next ( ) {
2598+ return { done : true , value : undefined }
2599+ } ,
2600+ async return ( ) {
2601+ return { done : true , value : undefined }
2602+ } ,
2603+ async throw ( error : unknown ) {
2604+ throw error
2605+ } ,
2606+ async [ Symbol . asyncDispose ] ( ) { } ,
2607+ } as AsyncGenerator < ApiStreamChunk > )
2608+
2609+ task . apiConversationHistory = [
2610+ {
2611+ role : "user" as const ,
2612+ content : [ { type : "text" as const , text : "test message" } ] ,
2613+ ts : Date . now ( ) ,
2614+ } ,
2615+ ]
2616+
2617+ const iterator = task . attemptApiRequest ( 0 )
2618+ await iterator . next ( )
2619+
2620+ expect ( safeSpy ) . toHaveBeenCalled ( )
2621+ } )
2622+
2623+ it ( "continues attemptApiRequest when model metadata fetch fails" , async ( ) => {
2624+ const task = new Task ( {
2625+ provider : mockProvider ,
2626+ apiConfiguration : mockApiConfig ,
2627+ task : "test task" ,
2628+ startTask : false ,
2629+ } )
2630+
2631+ vi . spyOn ( getTaskTestAccess ( task ) , "getSystemPrompt" ) . mockResolvedValue ( "mock system prompt" )
2632+ vi . spyOn ( task , "getTokenUsage" ) . mockReturnValue ( {
2633+ totalCost : 0 ,
2634+ totalTokensIn : 0 ,
2635+ totalTokensOut : 0 ,
2636+ contextTokens : 50_000 ,
2637+ } )
2638+ const ensureModelFetched = vi . fn ( ) . mockRejectedValue ( new Error ( "fetch failed" ) )
2639+ Object . assign ( task . api , { ensureModelFetched } )
2640+ vi . spyOn ( task . api , "getModel" ) . mockReturnValue ( {
2641+ id : mockApiConfig . apiModelId ! ,
2642+ info : {
2643+ supportsImages : false ,
2644+ supportsPromptCache : true ,
2645+ contextWindow : 200_000 ,
2646+ maxTokens : 4096 ,
2647+ } as ModelInfo ,
2648+ } )
2649+ vi . spyOn ( task . api , "createMessage" ) . mockReturnValue ( {
2650+ async * [ Symbol . asyncIterator ] ( ) {
2651+ yield { type : "text" , text : "ok" }
2652+ } ,
2653+ async next ( ) {
2654+ return { done : false , value : { type : "text" , text : "ok" } }
2655+ } ,
2656+ async return ( ) {
2657+ return { done : true , value : undefined }
2658+ } ,
2659+ async throw ( error : unknown ) {
2660+ throw error
2661+ } ,
2662+ async [ Symbol . asyncDispose ] ( ) { } ,
2663+ } as AsyncGenerator < ApiStreamChunk > )
2664+ const errorSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } )
2665+
2666+ task . apiConversationHistory = [
2667+ {
2668+ role : "user" as const ,
2669+ content : [ { type : "text" as const , text : "test message" } ] ,
2670+ ts : Date . now ( ) ,
2671+ } ,
2672+ ]
2673+
2674+ const iterator = task . attemptApiRequest ( 0 )
2675+ await expect ( iterator . next ( ) ) . resolves . toMatchObject ( {
2676+ done : false ,
2677+ value : { type : "text" , text : "ok" } ,
2678+ } )
2679+ expect ( errorSpy ) . toHaveBeenCalled ( )
2680+ errorSpy . mockRestore ( )
2681+ } )
2682+
2683+ it ( "fetches model metadata before caching the streaming model" , async ( ) => {
2684+ const task = new Task ( {
2685+ provider : mockProvider ,
2686+ apiConfiguration : mockApiConfig ,
2687+ task : "test task" ,
2688+ startTask : false ,
2689+ } )
2690+
2691+ const ensureModelFetched = vi . fn ( ) . mockResolvedValue ( undefined )
2692+ Object . assign ( task . api , { ensureModelFetched } )
2693+ vi . spyOn ( task . api , "getModel" ) . mockReturnValue ( {
2694+ id : mockApiConfig . apiModelId ! ,
2695+ info : {
2696+ supportsImages : false ,
2697+ supportsPromptCache : true ,
2698+ contextWindow : 200_000 ,
2699+ maxTokens : 4096 ,
2700+ } as ModelInfo ,
2701+ } )
2702+ vi . mocked ( processUserContentMentions ) . mockResolvedValueOnce ( {
2703+ content : [ { type : "text" , text : "hello" } ] ,
2704+ mode : undefined ,
2705+ } )
2706+ const safeSpy = vi . spyOn ( getTaskTestAccess ( task ) , "safeEnsureModelFetched" )
2707+ vi . spyOn ( task , "attemptApiRequest" ) . mockImplementation ( ( ) => {
2708+ throw new Error ( "stop after model metadata fetch" )
2709+ } )
2710+ vi . spyOn ( getTaskTestAccess ( task ) , "saveClineMessages" ) . mockResolvedValue ( true )
2711+ vi . spyOn ( task . diffViewProvider , "reset" ) . mockResolvedValue ( undefined as never )
2712+ vi . spyOn ( task , "addToApiConversationHistory" ) . mockResolvedValue ( undefined as never )
2713+
2714+ task . clineMessages = [
2715+ {
2716+ ts : Date . now ( ) ,
2717+ type : "say" ,
2718+ say : "api_req_started" ,
2719+ text : "{}" ,
2720+ } ,
2721+ ]
2722+ vi . spyOn ( task , "say" ) . mockImplementation ( async ( type ) => {
2723+ if ( type === "api_req_started" ) {
2724+ task . clineMessages . push ( {
2725+ ts : Date . now ( ) ,
2726+ type : "say" ,
2727+ say : "api_req_started" ,
2728+ text : "{}" ,
2729+ } )
2730+ }
2731+ return undefined as never
2732+ } )
2733+
2734+ const result = await task . recursivelyMakeClineRequests ( [ { type : "text" , text : "hello" } ] , false )
2735+
2736+ expect ( result ) . toBe ( true )
2737+ expect ( safeSpy ) . toHaveBeenCalled ( )
2738+ expect ( ensureModelFetched ) . toHaveBeenCalled ( )
2739+ expect ( task . cachedStreamingModel ?. id ) . toBe ( mockApiConfig . apiModelId )
2740+ } )
2741+ } )
2742+
25102743 describe ( "start()" , ( ) => {
25112744 it ( "should be a no-op if the task was already started in the constructor" , ( ) => {
25122745 const task = new Task ( {
0 commit comments