@@ -203,6 +203,247 @@ describe("AgentServer HTTP Mode", () => {
203203 } ) ;
204204
205205 describe ( "turn completion" , ( ) => {
206+ function stubSessionCleanup ( testServer : unknown ) : {
207+ cleanupSession : ( options ?: {
208+ completeEventStream ?: boolean ;
209+ } ) => Promise < void > ;
210+ eventStreamSender : {
211+ enqueue : ReturnType < typeof vi . fn > ;
212+ stop : ReturnType < typeof vi . fn > ;
213+ } ;
214+ } {
215+ const cleanupServer = testServer as {
216+ session : unknown ;
217+ eventStreamSender : {
218+ enqueue : ReturnType < typeof vi . fn > ;
219+ stop : ReturnType < typeof vi . fn > ;
220+ } ;
221+ captureCheckpointState : ReturnType < typeof vi . fn > ;
222+ cleanupSession : ( options ?: {
223+ completeEventStream ?: boolean ;
224+ } ) => Promise < void > ;
225+ } ;
226+ cleanupServer . captureCheckpointState = vi . fn ( async ( ) => { } ) ;
227+ cleanupServer . eventStreamSender = {
228+ enqueue : vi . fn ( ) ,
229+ stop : vi . fn ( async ( ) => { } ) ,
230+ } ;
231+ cleanupServer . session = {
232+ payload : { run_id : "run-1" } ,
233+ pendingHandoffGitState : undefined ,
234+ logWriter : { flush : vi . fn ( async ( ) => { } ) } ,
235+ acpConnection : { cleanup : vi . fn ( async ( ) => { } ) } ,
236+ sseController : { close : vi . fn ( ) } ,
237+ } ;
238+ return cleanupServer ;
239+ }
240+
241+ it ( "keeps event ingest open for non-terminal session cleanup" , async ( ) => {
242+ const testServer = stubSessionCleanup ( createServer ( ) ) ;
243+
244+ await testServer . cleanupSession ( ) ;
245+
246+ expect ( testServer . eventStreamSender . enqueue ) . not . toHaveBeenCalled ( ) ;
247+ expect ( testServer . eventStreamSender . stop ) . not . toHaveBeenCalled ( ) ;
248+ } ) ;
249+
250+ it ( "stops event ingest for terminal session cleanup without fake task completion" , async ( ) => {
251+ const testServer = stubSessionCleanup ( createServer ( ) ) ;
252+
253+ await testServer . cleanupSession ( { completeEventStream : true } ) ;
254+
255+ expect ( testServer . eventStreamSender . enqueue ) . not . toHaveBeenCalled ( ) ;
256+ expect ( testServer . eventStreamSender . stop ) . toHaveBeenCalledOnce ( ) ;
257+ } ) ;
258+
259+ it ( "writes terminal failure status before completing event ingest" , async ( ) => {
260+ const order : string [ ] = [ ] ;
261+ const testServer = new AgentServer ( {
262+ port,
263+ jwtPublicKey : TEST_PUBLIC_KEY ,
264+ repositoryPath : repo . path ,
265+ apiUrl : "http://localhost:8000" ,
266+ apiKey : "test-api-key" ,
267+ projectId : 1 ,
268+ mode : "interactive" ,
269+ taskId : "test-task-id" ,
270+ runId : "test-run-id" ,
271+ } ) as unknown as {
272+ eventStreamSender : {
273+ enqueue : ( event : Record < string , unknown > ) => void ;
274+ stop : ( ) => Promise < void > ;
275+ } ;
276+ posthogAPI : {
277+ updateTaskRun : (
278+ taskId : string ,
279+ runId : string ,
280+ payload : Record < string , unknown > ,
281+ ) => Promise < unknown > ;
282+ } ;
283+ signalTaskComplete (
284+ payload : JwtPayload ,
285+ stopReason : string ,
286+ errorMessage ?: string ,
287+ ) : Promise < void > ;
288+ } ;
289+ testServer . eventStreamSender = {
290+ enqueue : vi . fn ( ( ) => {
291+ order . push ( "enqueue" ) ;
292+ } ) ,
293+ stop : vi . fn ( async ( ) => {
294+ order . push ( "stop" ) ;
295+ } ) ,
296+ } ;
297+ testServer . posthogAPI = {
298+ updateTaskRun : vi . fn ( async ( ) => {
299+ order . push ( "update" ) ;
300+ return { } ;
301+ } ) ,
302+ } ;
303+
304+ await testServer . signalTaskComplete (
305+ {
306+ run_id : "run-1" ,
307+ task_id : "task-1" ,
308+ team_id : 1 ,
309+ user_id : 1 ,
310+ distinct_id : "distinct-id" ,
311+ mode : "interactive" ,
312+ } ,
313+ "error" ,
314+ "boom" ,
315+ ) ;
316+
317+ expect ( order ) . toEqual ( [ "enqueue" , "update" , "stop" ] ) ;
318+ expect ( testServer . eventStreamSender . enqueue ) . toHaveBeenCalledWith (
319+ expect . objectContaining ( {
320+ type : "notification" ,
321+ notification : expect . objectContaining ( {
322+ method : "_posthog/error" ,
323+ params : expect . objectContaining ( { error : "boom" } ) ,
324+ } ) ,
325+ } ) ,
326+ ) ;
327+ expect ( testServer . posthogAPI . updateTaskRun ) . toHaveBeenCalledWith (
328+ "task-1" ,
329+ "run-1" ,
330+ {
331+ status : "failed" ,
332+ error_message : "boom" ,
333+ } ,
334+ ) ;
335+ } ) ;
336+
337+ it ( "still stops event ingest when terminal failure status update fails" , async ( ) => {
338+ const testServer = new AgentServer ( {
339+ port,
340+ jwtPublicKey : TEST_PUBLIC_KEY ,
341+ repositoryPath : repo . path ,
342+ apiUrl : "http://localhost:8000" ,
343+ apiKey : "test-api-key" ,
344+ projectId : 1 ,
345+ mode : "interactive" ,
346+ taskId : "test-task-id" ,
347+ runId : "test-run-id" ,
348+ } ) as unknown as {
349+ eventStreamSender : {
350+ enqueue : ( event : Record < string , unknown > ) => void ;
351+ stop : ( ) => Promise < void > ;
352+ } ;
353+ posthogAPI : {
354+ updateTaskRun : (
355+ taskId : string ,
356+ runId : string ,
357+ payload : Record < string , unknown > ,
358+ ) => Promise < unknown > ;
359+ } ;
360+ signalTaskComplete (
361+ payload : JwtPayload ,
362+ stopReason : string ,
363+ errorMessage ?: string ,
364+ ) : Promise < void > ;
365+ } ;
366+ testServer . eventStreamSender = {
367+ enqueue : vi . fn ( ) ,
368+ stop : vi . fn ( async ( ) => { } ) ,
369+ } ;
370+ testServer . posthogAPI = {
371+ updateTaskRun : vi . fn ( async ( ) => {
372+ throw new Error ( "update failed" ) ;
373+ } ) ,
374+ } ;
375+
376+ await testServer . signalTaskComplete (
377+ {
378+ run_id : "run-1" ,
379+ task_id : "task-1" ,
380+ team_id : 1 ,
381+ user_id : 1 ,
382+ distinct_id : "distinct-id" ,
383+ mode : "interactive" ,
384+ } ,
385+ "error" ,
386+ "boom" ,
387+ ) ;
388+
389+ expect ( testServer . eventStreamSender . enqueue ) . toHaveBeenCalledOnce ( ) ;
390+ expect ( testServer . posthogAPI . updateTaskRun ) . toHaveBeenCalledOnce ( ) ;
391+ expect ( testServer . eventStreamSender . stop ) . toHaveBeenCalledOnce ( ) ;
392+ } ) ;
393+
394+ it ( "leaves event ingest open for non-error stop reasons" , async ( ) => {
395+ const testServer = new AgentServer ( {
396+ port,
397+ jwtPublicKey : TEST_PUBLIC_KEY ,
398+ repositoryPath : repo . path ,
399+ apiUrl : "http://localhost:8000" ,
400+ apiKey : "test-api-key" ,
401+ projectId : 1 ,
402+ mode : "interactive" ,
403+ taskId : "test-task-id" ,
404+ runId : "test-run-id" ,
405+ } ) as unknown as {
406+ eventStreamSender : {
407+ enqueue : ( event : Record < string , unknown > ) => void ;
408+ stop : ( ) => Promise < void > ;
409+ } ;
410+ posthogAPI : {
411+ updateTaskRun : (
412+ taskId : string ,
413+ runId : string ,
414+ payload : Record < string , unknown > ,
415+ ) => Promise < unknown > ;
416+ } ;
417+ signalTaskComplete (
418+ payload : JwtPayload ,
419+ stopReason : string ,
420+ ) : Promise < void > ;
421+ } ;
422+ testServer . eventStreamSender = {
423+ enqueue : vi . fn ( ) ,
424+ stop : vi . fn ( async ( ) => { } ) ,
425+ } ;
426+ testServer . posthogAPI = {
427+ updateTaskRun : vi . fn ( async ( ) => ( { } ) ) ,
428+ } ;
429+
430+ await testServer . signalTaskComplete (
431+ {
432+ run_id : "run-1" ,
433+ task_id : "task-1" ,
434+ team_id : 1 ,
435+ user_id : 1 ,
436+ distinct_id : "distinct-id" ,
437+ mode : "interactive" ,
438+ } ,
439+ "end_turn" ,
440+ ) ;
441+
442+ expect ( testServer . eventStreamSender . enqueue ) . not . toHaveBeenCalled ( ) ;
443+ expect ( testServer . eventStreamSender . stop ) . not . toHaveBeenCalled ( ) ;
444+ expect ( testServer . posthogAPI . updateTaskRun ) . not . toHaveBeenCalled ( ) ;
445+ } ) ;
446+
206447 it ( "persists structured turn completion notifications" , ( ) => {
207448 const appendRawLine = vi . fn ( ) ;
208449 const testServer = new AgentServer ( {
0 commit comments