@@ -18,6 +18,7 @@ vi.mock("../../../shell/logger", () => ({
1818} ) ) ;
1919
2020import { useWarmTask } from "./useWarmTask" ;
21+ import { takeWarmTaskLease } from "./warmTaskLease" ;
2122
2223interface Props {
2324 workspaceMode : WorkspaceMode ;
@@ -28,6 +29,8 @@ interface Props {
2829 runtimeAdapter ?: string | null ;
2930 model ?: string | null ;
3031 reasoningEffort ?: string | null ;
32+ sandboxEnvironmentId ?: string | null ;
33+ customImageId ?: string | null ;
3134}
3235
3336const cloudTyping : Props = {
@@ -201,6 +204,103 @@ describe("useWarmTask", () => {
201204 expect ( mockClient . warmTask ) . toHaveBeenCalledTimes ( 2 ) ;
202205 } ) ;
203206
207+ it ( "forwards sandbox configuration and re-warms when the image changes" , async ( ) => {
208+ const { rerender } = renderHook ( ( props : Props ) => useWarmTask ( props ) , {
209+ initialProps : {
210+ ...cloudTyping ,
211+ sandboxEnvironmentId : "environment-123" ,
212+ customImageId : "image-123" ,
213+ } ,
214+ } ) ;
215+ await flushDebounce ( ) ;
216+ expect ( mockClient . warmTask ) . toHaveBeenLastCalledWith ( {
217+ repository : "acme/repo" ,
218+ github_integration : 42 ,
219+ branch : "main" ,
220+ ...NULL_RUNTIME ,
221+ sandbox_environment_id : "environment-123" ,
222+ custom_image_id : "image-123" ,
223+ } ) ;
224+
225+ rerender ( {
226+ ...cloudTyping ,
227+ sandboxEnvironmentId : "environment-123" ,
228+ customImageId : "image-456" ,
229+ } ) ;
230+ await flushDebounce ( ) ;
231+ expect ( mockClient . warmTask ) . toHaveBeenLastCalledWith ( {
232+ repository : "acme/repo" ,
233+ github_integration : 42 ,
234+ branch : "main" ,
235+ ...NULL_RUNTIME ,
236+ sandbox_environment_id : "environment-123" ,
237+ custom_image_id : "image-456" ,
238+ } ) ;
239+ expect ( mockClient . warmTask ) . toHaveBeenCalledTimes ( 2 ) ;
240+ } ) ;
241+
242+ it ( "warms only the latest image when selection changes during the debounce" , async ( ) => {
243+ const { rerender } = renderHook ( ( props : Props ) => useWarmTask ( props ) , {
244+ initialProps : { ...cloudTyping , customImageId : "image-123" } ,
245+ } ) ;
246+
247+ rerender ( { ...cloudTyping , customImageId : "image-456" } ) ;
248+ await flushDebounce ( ) ;
249+
250+ expect ( mockClient . warmTask ) . toHaveBeenCalledOnce ( ) ;
251+ expect ( mockClient . warmTask ) . toHaveBeenCalledWith ( {
252+ repository : "acme/repo" ,
253+ github_integration : 42 ,
254+ branch : "main" ,
255+ ...NULL_RUNTIME ,
256+ custom_image_id : "image-456" ,
257+ } ) ;
258+ } ) ;
259+
260+ it ( "keeps the latest image lease when warm responses complete out of order" , async ( ) => {
261+ type WarmResponse = { task_id : string ; run_id : string } ;
262+ let resolveFirstWarm ! : ( value : WarmResponse ) => void ;
263+ let resolveSecondWarm ! : ( value : WarmResponse ) => void ;
264+ const firstWarm = new Promise < WarmResponse > ( ( resolve ) => {
265+ resolveFirstWarm = resolve ;
266+ } ) ;
267+ const secondWarm = new Promise < WarmResponse > ( ( resolve ) => {
268+ resolveSecondWarm = resolve ;
269+ } ) ;
270+ mockClient . warmTask
271+ . mockReturnValueOnce ( firstWarm )
272+ . mockReturnValueOnce ( secondWarm ) ;
273+
274+ const { rerender } = renderHook ( ( props : Props ) => useWarmTask ( props ) , {
275+ initialProps : { ...cloudTyping , customImageId : "image-123" } ,
276+ } ) ;
277+ await flushDebounce ( ) ;
278+
279+ rerender ( { ...cloudTyping , customImageId : "image-456" } ) ;
280+ await flushDebounce ( ) ;
281+
282+ await act ( async ( ) => {
283+ resolveSecondWarm ( { task_id : "task-2" , run_id : "run-2" } ) ;
284+ await secondWarm ;
285+ } ) ;
286+ await act ( async ( ) => {
287+ resolveFirstWarm ( { task_id : "task-1" , run_id : "run-1" } ) ;
288+ await firstWarm ;
289+ } ) ;
290+
291+ expect (
292+ takeWarmTaskLease ( {
293+ repository : "acme/repo" ,
294+ branch : "main" ,
295+ runtimeAdapter : null ,
296+ model : null ,
297+ reasoningEffort : null ,
298+ sandboxEnvironmentId : null ,
299+ customImageId : "image-456" ,
300+ } ) ,
301+ ) . toEqual ( { taskId : "task-2" , runId : "run-2" } ) ;
302+ } ) ;
303+
204304 it ( "warms again for a new selection after a failed warm" , async ( ) => {
205305 mockClient . warmTask . mockRejectedValueOnce ( new Error ( "boom" ) ) ;
206306 const { rerender } = renderHook ( ( props : Props ) => useWarmTask ( props ) , {
0 commit comments