@@ -2757,7 +2757,10 @@ describe("CodexAppServerAgent", () => {
27572757 }
27582758
27592759 // permissionOutcome may be a value or a function (per-call, e.g. a pending promise).
2760- function makePlanAgent ( permissionOutcome : unknown ) {
2760+ function makePlanAgent (
2761+ permissionOutcome : unknown ,
2762+ options : { rejectPlanToolUpdates ?: boolean } = { } ,
2763+ ) {
27612764 const stub = makeStubRpc ( {
27622765 "thread/start" : { thread : { id : "t" } } ,
27632766 "turn/start" : { turn : { id : "turn_1" } } ,
@@ -2770,7 +2773,15 @@ describe("CodexAppServerAgent", () => {
27702773 } > = [ ] ;
27712774 const client = {
27722775 sessionUpdate : async ( n : unknown ) => {
2773- sessionUpdates . push ( n as { update ?: Record < string , unknown > } ) ;
2776+ const notification = n as { update ?: Record < string , unknown > } ;
2777+ if (
2778+ options . rejectPlanToolUpdates &&
2779+ ( notification . update ?. sessionUpdate === "tool_call" ||
2780+ notification . update ?. sessionUpdate === "tool_call_update" )
2781+ ) {
2782+ throw new Error ( "renderer disconnected" ) ;
2783+ }
2784+ sessionUpdates . push ( notification ) ;
27742785 } ,
27752786 requestPermission : async ( params : {
27762787 toolCall : Record < string , unknown > ;
@@ -2898,6 +2909,42 @@ describe("CodexAppServerAgent", () => {
28982909 expect ( permissionRequests [ 0 ] . options . map ( ( o ) => o . optionId ) ) . toContain (
28992910 "auto" ,
29002911 ) ;
2912+ expect ( sessionUpdates ) . toContainEqual ( {
2913+ sessionId : "t" ,
2914+ update : {
2915+ sessionUpdate : "tool_call" ,
2916+ toolCallId : "p1:implement" ,
2917+ title : "Ready to code?" ,
2918+ kind : "switch_mode" ,
2919+ content : [
2920+ {
2921+ type : "content" ,
2922+ content : { type : "text" , text : "# The plan\n\n1. do it" } ,
2923+ } ,
2924+ ] ,
2925+ rawInput : { plan : "# The plan\n\n1. do it" } ,
2926+ status : "in_progress" ,
2927+ } ,
2928+ } ) ;
2929+ expect ( sessionUpdates ) . toContainEqual ( {
2930+ sessionId : "t" ,
2931+ update : {
2932+ sessionUpdate : "tool_call_update" ,
2933+ toolCallId : "p1:implement" ,
2934+ status : "completed" ,
2935+ } ,
2936+ } ) ;
2937+ expect (
2938+ sessionUpdates . some ( ( notification ) => {
2939+ if ( notification . update ?. sessionUpdate !== "agent_message_chunk" ) {
2940+ return false ;
2941+ }
2942+ const content = notification . update . content as
2943+ | { type ?: string ; text ?: string }
2944+ | undefined ;
2945+ return content ?. text ?. includes ( "# The plan" ) === true ;
2946+ } ) ,
2947+ ) . toBe ( false ) ;
29012948
29022949 // Mode flipped to auto and the host was told.
29032950 expect ( sessionUpdates ) . toContainEqual (
@@ -2924,7 +2971,7 @@ describe("CodexAppServerAgent", () => {
29242971 } ) ;
29252972
29262973 it ( "stays in plan mode when the handoff is rejected without feedback" , async ( ) => {
2927- const { agent, stub, permissionRequests } = makePlanAgent ( {
2974+ const { agent, stub, sessionUpdates , permissionRequests } = makePlanAgent ( {
29282975 outcome : { outcome : "selected" , optionId : "reject_with_feedback" } ,
29292976 } ) ;
29302977 const { done } = await startPlanTurn ( agent , stub ) ;
@@ -2938,12 +2985,43 @@ describe("CodexAppServerAgent", () => {
29382985
29392986 expect ( ( await done ) . stopReason ) . toBe ( "end_turn" ) ;
29402987 expect ( permissionRequests ) . toHaveLength ( 1 ) ;
2988+ expect ( sessionUpdates ) . toContainEqual ( {
2989+ sessionId : "t" ,
2990+ update : {
2991+ sessionUpdate : "tool_call_update" ,
2992+ toolCallId : "p1:implement" ,
2993+ status : "failed" ,
2994+ } ,
2995+ } ) ;
29412996 // No implementation turn started; the picker stays on plan.
29422997 expect ( stub . requests . filter ( ( r ) => r . method === "turn/start" ) ) . toHaveLength (
29432998 1 ,
29442999 ) ;
29453000 } ) ;
29463001
3002+ it ( "settles the handoff when plan tool-call updates cannot be delivered" , async ( ) => {
3003+ const { agent, stub, permissionRequests } = makePlanAgent (
3004+ {
3005+ outcome : { outcome : "selected" , optionId : "reject_with_feedback" } ,
3006+ } ,
3007+ { rejectPlanToolUpdates : true } ,
3008+ ) ;
3009+ const { done } = await startPlanTurn ( agent , stub ) ;
3010+
3011+ stub . emit ( "item/completed" , {
3012+ item : { type : "plan" , id : "p1" , text : "# The plan" } ,
3013+ } ) ;
3014+ stub . emit ( "turn/completed" , {
3015+ turn : { id : "turn_1" , status : "completed" } ,
3016+ } ) ;
3017+
3018+ expect ( ( await done ) . stopReason ) . toBe ( "end_turn" ) ;
3019+ expect ( permissionRequests ) . toHaveLength ( 1 ) ;
3020+ expect ( stub . requests . filter ( ( r ) => r . method === "turn/start" ) ) . toHaveLength (
3021+ 1 ,
3022+ ) ;
3023+ } ) ;
3024+
29473025 it ( "feeds handoff feedback into another plan turn" , async ( ) => {
29483026 const { agent, stub, permissionRequests } = makePlanAgent ( {
29493027 outcome : { outcome : "selected" , optionId : "reject_with_feedback" } ,
0 commit comments