@@ -399,6 +399,57 @@ test("V2 setup preserves existing commands and configured command-name collision
399399 await cleanup ( )
400400} )
401401
402+ test ( "V2 prompt hook pauses compatibility commands before admission" , async ( ) => {
403+ const mock = makeMockContext ( { auto_continue : false } , [ "goal" , "pause_goal" , "resume_goal" ] )
404+ const cleanup = await setupPlugin ( mock as never )
405+ await createGoalViaV2Tool ( mock , "pause before acknowledgement" )
406+ const v1 = await plugin . server ( { client : { session : { promptAsync : async ( ) => { } } } } as never , {
407+ auto_continue : false ,
408+ } )
409+ const config = { } as { command ?: Record < string , { template : string } > }
410+ await v1 . config ?.( config as never )
411+ const pauseTemplate = config . command ?. pause_goal ?. template
412+ const resumeTemplate = config . command ?. resume_goal ?. template
413+ if ( ! pauseTemplate ) throw new Error ( "expected pause_goal compatibility command" )
414+ if ( ! resumeTemplate ) throw new Error ( "expected resume_goal compatibility command" )
415+ await v1 . dispose ?.( )
416+ const input = {
417+ sessionID : "ses_v2" ,
418+ messageID : "msg_pause" ,
419+ prompt : {
420+ text : `${ pauseTemplate } \nuntrusted arguments` ,
421+ files : [ { uri : "file:///tmp/untrusted.txt" } ] ,
422+ agents : [ { name : "plan" } ] ,
423+ skills : [ { id : "untrusted" } ] ,
424+ } ,
425+ delivery : "steer" ,
426+ }
427+
428+ await mock . hooks . prompt ?.( input )
429+
430+ expect ( await getGoal ( "ses_v2" ) ) . toMatchObject ( { status : "paused" , objective : "pause before acknowledgement" } )
431+ expect ( input . prompt . text ) . toBe ( pauseTemplate )
432+ expect ( input . prompt . files ) . toBeUndefined ( )
433+ expect ( input . prompt . agents ) . toBeUndefined ( )
434+ expect ( input . prompt . skills ) . toBeUndefined ( )
435+
436+ const resumeInput = {
437+ sessionID : "ses_v2" ,
438+ messageID : "msg_resume" ,
439+ prompt : {
440+ text : `${ resumeTemplate } \nuntrusted arguments` ,
441+ files : [ { uri : "file:///tmp/untrusted.txt" } ] ,
442+ } ,
443+ delivery : "steer" ,
444+ }
445+ await mock . hooks . prompt ?.( resumeInput )
446+ expect ( ( await getGoal ( "ses_v2" ) ) ?. status ) . toBe ( "paused" )
447+ expect ( resumeInput . prompt . text ) . toBe ( resumeTemplate )
448+ expect ( resumeInput . prompt . files ) . toBeUndefined ( )
449+ mock . stream . end ( )
450+ await cleanup ( )
451+ } )
452+
402453test ( "V2 command transform remains stable when the registry replays it" , async ( ) => {
403454 const mock = makeMockContext ( { auto_continue : false } )
404455 let transform : ( ( draft : MockCommandDraft ) => void ) | undefined
@@ -463,6 +514,7 @@ test("V2 setup skips command registration when register_command is false", async
463514
464515 expect ( mock . commands ) . toHaveLength ( 0 )
465516 expect ( mock . disposals ) . not . toContain ( "command.transform" )
517+ expect ( mock . hooks . prompt ) . toBeUndefined ( )
466518
467519 mock . stream . end ( )
468520 await cleanup ( )
@@ -757,7 +809,14 @@ test("V2 cleanup disposes registrations and stops the event consumer", async ()
757809 await cleanup ( )
758810
759811 expect ( mock . disposals ) . toEqual (
760- expect . arrayContaining ( [ "command.transform" , "tool.transform" , "tool.hook:execute.before" , "tool.hook:execute.after" , "session.hook:context" ] ) ,
812+ expect . arrayContaining ( [
813+ "command.transform" ,
814+ "session.hook:prompt" ,
815+ "tool.transform" ,
816+ "tool.hook:execute.before" ,
817+ "tool.hook:execute.after" ,
818+ "session.hook:context" ,
819+ ] ) ,
761820 )
762821 // Events pushed after cleanup must not throw or mutate state.
763822 mock . stream . push ( {
0 commit comments