@@ -17,14 +17,16 @@ function createTestContext(): {
1717 ctx : ToolHandlerContext ;
1818 warn : ReturnType < typeof vi . fn > ;
1919 onBlockReplyFlush : ReturnType < typeof vi . fn > ;
20+ onAgentEvent : ReturnType < typeof vi . fn > ;
2021} {
2122 const onBlockReplyFlush = vi . fn ( ) ;
23+ const onAgentEvent = vi . fn ( ) ;
2224 const warn = vi . fn ( ) ;
2325 const ctx : ToolHandlerContext = {
2426 params : {
2527 runId : "run-test" ,
2628 onBlockReplyFlush,
27- onAgentEvent : undefined ,
29+ onAgentEvent,
2830 onToolResult : undefined ,
2931 } ,
3032 flushBlockReplyBuffer : vi . fn ( ) ,
@@ -59,7 +61,7 @@ function createTestContext(): {
5961 trimMessagingToolSent : vi . fn ( ) ,
6062 } ;
6163
62- return { ctx, warn, onBlockReplyFlush } ;
64+ return { ctx, warn, onBlockReplyFlush, onAgentEvent } ;
6365}
6466
6567describe ( "handleToolExecutionStart read path checks" , ( ) => {
@@ -124,8 +126,9 @@ describe("handleToolExecutionStart read path checks", () => {
124126 await pending ;
125127
126128 expect ( ctx . state . toolMetaById . has ( "tool-await-flush" ) ) . toBe ( true ) ;
127- expect ( ctx . state . itemStartedCount ) . toBe ( 1 ) ;
129+ expect ( ctx . state . itemStartedCount ) . toBe ( 2 ) ;
128130 expect ( ctx . state . itemActiveIds . has ( "tool:tool-await-flush" ) ) . toBe ( true ) ;
131+ expect ( ctx . state . itemActiveIds . has ( "command:tool-await-flush" ) ) . toBe ( true ) ;
129132 } ) ;
130133} ) ;
131134
@@ -482,6 +485,157 @@ describe("handleToolExecutionEnd exec approval prompts", () => {
482485
483486 expect ( ctx . state . deterministicApprovalPromptSent ) . toBe ( false ) ;
484487 } ) ;
488+
489+ it ( "emits approval + blocked command item events when exec needs approval" , async ( ) => {
490+ const { ctx, onAgentEvent } = createTestContext ( ) ;
491+
492+ await handleToolExecutionStart (
493+ ctx as never ,
494+ {
495+ type : "tool_execution_start" ,
496+ toolName : "exec" ,
497+ toolCallId : "tool-exec-approval-events" ,
498+ args : { command : "npm test" } ,
499+ } as never ,
500+ ) ;
501+
502+ await handleToolExecutionEnd (
503+ ctx as never ,
504+ {
505+ type : "tool_execution_end" ,
506+ toolName : "exec" ,
507+ toolCallId : "tool-exec-approval-events" ,
508+ isError : false ,
509+ result : {
510+ details : {
511+ status : "approval-pending" ,
512+ approvalId : "12345678-1234-1234-1234-123456789012" ,
513+ approvalSlug : "12345678" ,
514+ host : "gateway" ,
515+ command : "npm test" ,
516+ } ,
517+ } ,
518+ } as never ,
519+ ) ;
520+
521+ expect ( onAgentEvent ) . toHaveBeenCalledWith (
522+ expect . objectContaining ( {
523+ stream : "approval" ,
524+ data : expect . objectContaining ( {
525+ phase : "requested" ,
526+ status : "pending" ,
527+ itemId : "command:tool-exec-approval-events" ,
528+ approvalId : "12345678-1234-1234-1234-123456789012" ,
529+ approvalSlug : "12345678" ,
530+ } ) ,
531+ } ) ,
532+ ) ;
533+ expect ( onAgentEvent ) . toHaveBeenCalledWith (
534+ expect . objectContaining ( {
535+ stream : "item" ,
536+ data : expect . objectContaining ( {
537+ itemId : "command:tool-exec-approval-events" ,
538+ phase : "end" ,
539+ status : "blocked" ,
540+ summary : "Awaiting approval before command can run." ,
541+ } ) ,
542+ } ) ,
543+ ) ;
544+ } ) ;
545+ } ) ;
546+
547+ describe ( "handleToolExecutionEnd derived tool events" , ( ) => {
548+ it ( "emits command output events for exec results" , async ( ) => {
549+ const { ctx, onAgentEvent } = createTestContext ( ) ;
550+
551+ await handleToolExecutionStart (
552+ ctx as never ,
553+ {
554+ type : "tool_execution_start" ,
555+ toolName : "exec" ,
556+ toolCallId : "tool-exec-output" ,
557+ args : { command : "ls" } ,
558+ } as never ,
559+ ) ;
560+
561+ await handleToolExecutionEnd (
562+ ctx as never ,
563+ {
564+ type : "tool_execution_end" ,
565+ toolName : "exec" ,
566+ toolCallId : "tool-exec-output" ,
567+ isError : false ,
568+ result : {
569+ details : {
570+ status : "completed" ,
571+ aggregated : "README.md" ,
572+ exitCode : 0 ,
573+ durationMs : 10 ,
574+ cwd : "/tmp/work" ,
575+ } ,
576+ } ,
577+ } as never ,
578+ ) ;
579+
580+ expect ( onAgentEvent ) . toHaveBeenCalledWith (
581+ expect . objectContaining ( {
582+ stream : "command_output" ,
583+ data : expect . objectContaining ( {
584+ itemId : "command:tool-exec-output" ,
585+ phase : "end" ,
586+ output : "README.md" ,
587+ exitCode : 0 ,
588+ cwd : "/tmp/work" ,
589+ } ) ,
590+ } ) ,
591+ ) ;
592+ } ) ;
593+
594+ it ( "emits patch summary events for apply_patch results" , async ( ) => {
595+ const { ctx, onAgentEvent } = createTestContext ( ) ;
596+
597+ await handleToolExecutionStart (
598+ ctx as never ,
599+ {
600+ type : "tool_execution_start" ,
601+ toolName : "apply_patch" ,
602+ toolCallId : "tool-patch-summary" ,
603+ args : { patch : "*** Begin Patch" } ,
604+ } as never ,
605+ ) ;
606+
607+ await handleToolExecutionEnd (
608+ ctx as never ,
609+ {
610+ type : "tool_execution_end" ,
611+ toolName : "apply_patch" ,
612+ toolCallId : "tool-patch-summary" ,
613+ isError : false ,
614+ result : {
615+ details : {
616+ summary : {
617+ added : [ "a.ts" ] ,
618+ modified : [ "b.ts" ] ,
619+ deleted : [ "c.ts" ] ,
620+ } ,
621+ } ,
622+ } ,
623+ } as never ,
624+ ) ;
625+
626+ expect ( onAgentEvent ) . toHaveBeenCalledWith (
627+ expect . objectContaining ( {
628+ stream : "patch" ,
629+ data : expect . objectContaining ( {
630+ itemId : "patch:tool-patch-summary" ,
631+ added : [ "a.ts" ] ,
632+ modified : [ "b.ts" ] ,
633+ deleted : [ "c.ts" ] ,
634+ summary : "1 added, 1 modified, 1 deleted" ,
635+ } ) ,
636+ } ) ,
637+ ) ;
638+ } ) ;
485639} ) ;
486640
487641describe ( "messaging tool media URL tracking" , ( ) => {
0 commit comments