@@ -67,6 +67,14 @@ async function pathExists(path: string): Promise<boolean> {
6767 }
6868}
6969
70+ async function waitFor ( predicate : ( ) => boolean | Promise < boolean > ) : Promise < void > {
71+ for ( let attempt = 0 ; attempt < 500 ; attempt ++ ) {
72+ if ( await predicate ( ) ) return ;
73+ await new Promise ( ( resolve ) => setTimeout ( resolve , 1 ) ) ;
74+ }
75+ throw new Error ( "condition was not reached" ) ;
76+ }
77+
7078function deferred < T > ( ) : {
7179 promise : Promise < T > ;
7280 resolve : ( v : T ) => void ;
@@ -170,6 +178,62 @@ describe("spawn_agent worktree isolation", () => {
170178 expect ( typeof ends [ 0 ] ?. properties . duration_ms ) . toBe ( "number" ) ;
171179 } ) ;
172180
181+ test ( "pairs pre-progress cancellation with a cancelled terminal event" , async ( ) => {
182+ const repo = await makeRepo ( ) ;
183+ tempDirs . push ( repo ) ;
184+ const { telemetry, events } = telemetryCapture ( ) ;
185+ const sessions = createSubAgentSessionStore ( ) ;
186+ const tool = createSpawnAgentTool ( {
187+ permissionGate : testPermissionGate ,
188+ cwd : repo ,
189+ getWorkdirBase : ( ) => repo ,
190+ provider,
191+ telemetry,
192+ run : async ( params ) => {
193+ params . onRunSettled ?.( {
194+ turn_count : 0 ,
195+ input_tokens : 0 ,
196+ output_tokens : 0 ,
197+ cache_read_tokens : 0 ,
198+ cache_write_tokens : 0 ,
199+ reasoning_tokens : 0 ,
200+ tool_call_count : 0 ,
201+ tool_error_count : 0 ,
202+ error_count : 1 ,
203+ duration_ms : 1 ,
204+ model : "test-model" ,
205+ terminal_reason : "cancelled" ,
206+ } ) ;
207+ const error = new Error ( "aborted" ) ;
208+ error . name = "AbortError" ;
209+ throw error ;
210+ } ,
211+ sessions,
212+ fleetRecords : createFleetRecords ( ) ,
213+ } ) ;
214+ if ( tool . kind !== "full" ) throw new Error ( "expected full tool" ) ;
215+
216+ const result = await tool . handler (
217+ {
218+ id : "cancelled-spawn" ,
219+ name : "spawn_agent" ,
220+ arguments : { description : "cancelled" , prompt : "Do the work" , intent : "explore" } ,
221+ } ,
222+ new AbortController ( ) . signal ,
223+ ) ;
224+
225+ expect ( result . isError ) . not . toBe ( true ) ;
226+ await waitFor ( ( ) => events . some ( ( event ) => event . event === "subagent_end" ) ) ;
227+ expect ( sessions . list ( ) [ 0 ] ?. status ) . toBe ( "cancelled" ) ;
228+ expect ( events . filter ( ( event ) => event . event === "subagent_start" ) ) . toHaveLength ( 1 ) ;
229+ const ends = events . filter ( ( event ) => event . event === "subagent_end" ) ;
230+ expect ( ends ) . toHaveLength ( 1 ) ;
231+ expect ( ends [ 0 ] ?. properties ) . toMatchObject ( {
232+ status : "cancelled" ,
233+ stop_reason : "cancelled" ,
234+ } ) ;
235+ } ) ;
236+
173237 test ( "defers worktree cleanup while the session is retained for followup" , async ( ) => {
174238 const repo = await makeRepo ( ) ;
175239 tempDirs . push ( repo ) ;
@@ -229,13 +293,17 @@ describe("spawn_agent worktree isolation", () => {
229293
230294 const settle = deferred < RunSubAgentResult > ( ) ;
231295 let workerCwd : string | undefined ;
296+ let settlementCount = 0 ;
297+ let settlementWasFrozen = false ;
298+ const { telemetry, events } = telemetryCapture ( ) ;
232299 const sessions = createSubAgentSessionStore ( ) ;
233300 const tool = createSpawnAgentTool ( {
234301 permissionGate : testPermissionGate ,
235302 cwd : repo ,
236303 getWorkdirBase : ( ) => workdirBase ,
237304 provider,
238305 useWorktree : true ,
306+ telemetry,
239307 run : async ( params ) => {
240308 workerCwd = params . cwd ;
241309 params . onAgentReady ?.( {
@@ -244,7 +312,25 @@ describe("spawn_agent worktree isolation", () => {
244312 followup : async ( ) => "" ,
245313 deliver : ( ) => { } ,
246314 } ) ;
247- return settle . promise ;
315+ const result = await settle . promise ;
316+ const summary = Object . freeze ( {
317+ turn_count : 0 ,
318+ input_tokens : 0 ,
319+ output_tokens : 0 ,
320+ cache_read_tokens : 0 ,
321+ cache_write_tokens : 0 ,
322+ reasoning_tokens : 0 ,
323+ tool_call_count : 0 ,
324+ tool_error_count : 0 ,
325+ error_count : 0 ,
326+ duration_ms : 1 ,
327+ model : "test-model" ,
328+ terminal_reason : "cancelled" as const ,
329+ } ) ;
330+ settlementCount += 1 ;
331+ settlementWasFrozen = Object . isFrozen ( summary ) ;
332+ params . onRunSettled ?.( summary ) ;
333+ return result ;
248334 } ,
249335 sessions,
250336 fleetRecords : createFleetRecords ( ) ,
@@ -263,10 +349,20 @@ describe("spawn_agent worktree isolation", () => {
263349
264350 settle . resolve ( {
265351 report : "## Summary\nStopped.\n## Findings\npartial\n## Blockers\ninterrupted\n## Paths\n" ,
352+ stopReason : "cancelled" ,
266353 interrupted : true ,
267354 } ) ;
268- await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
355+ await waitFor ( ( ) => events . some ( ( event ) => event . event === "subagent_end" ) ) ;
269356
357+ expect ( settlementCount ) . toBe ( 1 ) ;
358+ expect ( settlementWasFrozen ) . toBe ( true ) ;
359+ expect ( sessions . get ( agentId ) ?. lifecycleStatus ) . toBe ( "interrupted" ) ;
360+ const ends = events . filter ( ( event ) => event . event === "subagent_end" ) ;
361+ expect ( ends ) . toHaveLength ( 1 ) ;
362+ expect ( ends [ 0 ] ?. properties ) . toMatchObject ( {
363+ status : "interrupted" ,
364+ stop_reason : "cancelled" ,
365+ } ) ;
270366 expect ( workerCwd ) . toBeDefined ( ) ;
271367 expect ( await pathExists ( workerCwd ! ) ) . toBe ( true ) ;
272368
@@ -305,9 +401,11 @@ describe("spawn_agent worktree isolation", () => {
305401 } ,
306402 new AbortController ( ) . signal ,
307403 ) ;
308- await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
404+ await waitFor ( ( ) => workerCwd !== undefined ) ;
405+ if ( workerCwd === undefined ) throw new Error ( "worker cwd was not captured" ) ;
406+ const completedWorkerCwd = workerCwd ;
407+ await waitFor ( async ( ) => ! ( await pathExists ( completedWorkerCwd ) ) ) ;
309408
310- expect ( workerCwd ) . toBeDefined ( ) ;
311- expect ( await pathExists ( workerCwd ! ) ) . toBe ( false ) ;
409+ expect ( await pathExists ( completedWorkerCwd ) ) . toBe ( false ) ;
312410 } ) ;
313411} ) ;
0 commit comments