11import { NodeFileSystem } from "@effect/platform-node"
2- import { expect } from "bun:test"
2+ import { expect , spyOn } from "bun:test"
33import { Cause , Effect , Exit , Fiber , Layer , ServiceMap } from "effect"
44import * as Stream from "effect/Stream"
5+ import z from "zod"
56import type { Agent } from "../../src/agent/agent"
67import { Agent as AgentSvc } from "../../src/agent/agent"
78import { Bus } from "../../src/bus"
@@ -25,6 +26,7 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema"
2526import { SessionStatus } from "../../src/session/status"
2627import { Shell } from "../../src/shell/shell"
2728import { Snapshot } from "../../src/snapshot"
29+ import { TaskTool } from "../../src/tool/task"
2830import { ToolRegistry } from "../../src/tool/registry"
2931import { Truncate } from "../../src/tool/truncate"
3032import { Log } from "../../src/util/log"
@@ -630,6 +632,69 @@ it.effect(
630632 30_000 ,
631633)
632634
635+ it . effect (
636+ "cancel finalizes subtask tool state" ,
637+ ( ) =>
638+ provideTmpdirInstance (
639+ ( dir ) =>
640+ Effect . gen ( function * ( ) {
641+ const ready = defer < void > ( )
642+ const aborted = defer < void > ( )
643+ const init = spyOn ( TaskTool , "init" ) . mockImplementation ( async ( ) => ( {
644+ description : "task" ,
645+ parameters : z . object ( {
646+ description : z . string ( ) ,
647+ prompt : z . string ( ) ,
648+ subagent_type : z . string ( ) ,
649+ task_id : z . string ( ) . optional ( ) ,
650+ command : z . string ( ) . optional ( ) ,
651+ } ) ,
652+ execute : async ( _args , ctx ) => {
653+ ready . resolve ( )
654+ ctx . abort . addEventListener ( "abort" , ( ) => aborted . resolve ( ) , { once : true } )
655+ await new Promise < void > ( ( ) => { } )
656+ return {
657+ title : "" ,
658+ metadata : {
659+ sessionId : SessionID . make ( "task" ) ,
660+ model : ref ,
661+ } ,
662+ output : "" ,
663+ }
664+ } ,
665+ } ) )
666+ yield * Effect . addFinalizer ( ( ) => Effect . sync ( ( ) => init . mockRestore ( ) ) )
667+
668+ const { prompt, chat } = yield * boot ( )
669+ const msg = yield * user ( chat . id , "hello" )
670+ yield * addSubtask ( chat . id , msg . id )
671+
672+ const fiber = yield * prompt . loop ( { sessionID : chat . id } ) . pipe ( Effect . forkChild )
673+ yield * Effect . promise ( ( ) => ready . promise )
674+ yield * prompt . cancel ( chat . id )
675+ yield * Effect . promise ( ( ) => aborted . promise )
676+
677+ const exit = yield * Fiber . await ( fiber )
678+ expect ( Exit . isSuccess ( exit ) ) . toBe ( true )
679+
680+ const msgs = yield * Effect . promise ( ( ) => MessageV2 . filterCompacted ( MessageV2 . stream ( chat . id ) ) )
681+ const taskMsg = msgs . find ( ( item ) => item . info . role === "assistant" && item . info . agent === "general" )
682+ expect ( taskMsg ?. info . role ) . toBe ( "assistant" )
683+ if ( ! taskMsg || taskMsg . info . role !== "assistant" ) return
684+
685+ const tool = toolPart ( taskMsg . parts )
686+ expect ( tool ?. type ) . toBe ( "tool" )
687+ if ( ! tool ) return
688+
689+ expect ( tool . state . status ) . not . toBe ( "running" )
690+ expect ( taskMsg . info . time . completed ) . toBeDefined ( )
691+ expect ( taskMsg . info . finish ) . toBeDefined ( )
692+ } ) ,
693+ { git : true , config : cfg } ,
694+ ) ,
695+ 30_000 ,
696+ )
697+
633698it . effect (
634699 "cancel with queued callers resolves all cleanly" ,
635700 ( ) =>
0 commit comments