11import { beforeAll , beforeEach , describe , expect , mock , test } from "bun:test"
2- import type { Prompt } from "@/context/prompt"
2+ import type { ContextItem , Prompt } from "@/context/prompt"
33
44let createPromptSubmit : typeof import ( "./submit" ) . createPromptSubmit
55
@@ -16,6 +16,7 @@ const optimistic: Array<{
1616 }
1717} > = [ ]
1818const optimisticSeeded : boolean [ ] = [ ]
19+ const optimisticRemoved : string [ ] = [ ]
1920const storedSessions : Record < string , Array < { id : string ; title ?: string } > > = { }
2021const promoted : Array < { directory : string ; sessionID : string } > = [ ]
2122const sentShell : string [ ] = [ ]
@@ -32,6 +33,8 @@ const sentPromptAsync: Array<{
3233 directory : string
3334 metadata ?: unknown
3435 text ?: string
36+ parts ?: Array < { id ?: string ; type : string ; text ?: string } >
37+ messageID ?: string
3538 intentID ?: string
3639 intentSource ?: string
3740 intentVariant ?: string
@@ -47,8 +50,10 @@ let variant: string | undefined
4750let promptMode : "direct" | "intelligence" | "wish" = "direct"
4851let appLocale = "en"
4952let releaseDelayedPrompt : ( ( ) => void ) | undefined
53+ const rejectedAdmissionReceipts = new Set < string > ( )
5054
5155const promptValue : Prompt = [ { type : "text" , content : "ls" , start : 0 , end : 2 } ]
56+ const promptContextItems : Array < ContextItem & { key : string } > = [ ]
5257const flushAsyncSubmit = ( ) => new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) )
5358
5459const clientFor = ( directory : string ) => {
@@ -72,6 +77,7 @@ const clientFor = (directory: string) => {
7277 promptAsync : async ( payload ?: {
7378 metadata ?: unknown
7479 parts ?: Array < { type : string ; text ?: string } >
80+ messageID ?: string
7581 intentID ?: string
7682 intentSource ?: string
7783 intentVariant ?: string
@@ -80,17 +86,26 @@ const clientFor = (directory: string) => {
8086 directory,
8187 metadata : payload ?. metadata ,
8288 text : payload ?. parts ?. find ( ( part ) => part . type === "text" ) ?. text ,
89+ parts : payload ?. parts ,
90+ messageID : payload ?. messageID ,
8391 intentID : payload ?. intentID ,
8492 intentSource : payload ?. intentSource ,
8593 intentVariant : payload ?. intentVariant ,
8694 }
8795 sentPromptAsync . push ( sent )
96+ if (
97+ ( sent . text === "receipt lost" || sent . text === "Edited retry goal" ) &&
98+ ! rejectedAdmissionReceipts . has ( sent . text )
99+ ) {
100+ rejectedAdmissionReceipts . add ( sent . text )
101+ throw new Error ( "connection closed after durable admission" )
102+ }
88103 if ( sent . text === "prompt waits after admission" ) {
89104 await new Promise < void > ( ( resolve ) => {
90105 releaseDelayedPrompt = resolve
91106 } )
92107 }
93- return { data : undefined }
108+ return { data : { messageID : "msg_server_admitted" , delivery : "steer" } }
94109 } ,
95110 command : async ( ) => ( { data : undefined } ) ,
96111 abort : async ( ) => ( { data : undefined } ) ,
@@ -139,8 +154,8 @@ const clientFor = (directory: string) => {
139154 }
140155 }
141156 const result = {
142- prompt_draft_id : " prompt_draft:test:1" ,
143- context_plan_id : " context_plan:test:1" ,
157+ prompt_draft_id : ` prompt_draft:test:${ preparedDrafts . length } ` ,
158+ context_plan_id : ` context_plan:test:${ preparedDrafts . length } ` ,
144159 state : "draft_ready" ,
145160 mode : payload . body ?. mode ?? "intelligence" ,
146161 route : text === "hello" ? "general" : "code" ,
@@ -227,9 +242,14 @@ beforeAll(async () => {
227242 reset : ( ) => undefined ,
228243 set : ( ) => undefined ,
229244 context : {
230- add : ( ) => undefined ,
231- remove : ( ) => undefined ,
232- items : ( ) => [ ] ,
245+ add : ( item : ContextItem ) => {
246+ promptContextItems . push ( { ...item , key : `restored:${ promptContextItems . length } :${ item . path } ` } )
247+ } ,
248+ remove : ( key : string ) => {
249+ const index = promptContextItems . findIndex ( ( item ) => item . key === key )
250+ if ( index >= 0 ) promptContextItems . splice ( index , 1 )
251+ } ,
252+ items : ( ) => promptContextItems ,
233253 } ,
234254 } ) ,
235255 } ) )
@@ -274,7 +294,9 @@ beforeAll(async () => {
274294 ! ! storedSessions [ value . directory ] ?. find ( ( item ) => item . id === value . sessionID ) ?. title ,
275295 )
276296 } ,
277- remove : ( ) => undefined ,
297+ remove : ( value : { messageID : string } ) => {
298+ optimisticRemoved . push ( value . messageID )
299+ } ,
278300 } ,
279301 } ,
280302 set : ( ) => undefined ,
@@ -338,6 +360,7 @@ beforeEach(() => {
338360 enabledAutoAccept . length = 0
339361 optimistic . length = 0
340362 optimisticSeeded . length = 0
363+ optimisticRemoved . length = 0
341364 promoted . length = 0
342365 params = { }
343366 sentShell . length = 0
@@ -347,13 +370,15 @@ beforeEach(() => {
347370 sentPromptAsync . length = 0
348371 promptPrepareEvents . length = 0
349372 promptPrepareProgress . length = 0
373+ promptContextItems . length = 0
350374 promptValue [ 0 ] = { type : "text" , content : "ls" , start : 0 , end : 2 }
351375 selected = "/repo/worktree-a"
352376 variant = undefined
353377 promptMode = "direct"
354378 appLocale = "en"
355379 releaseDelayedPrompt ?.( )
356380 releaseDelayedPrompt = undefined
381+ rejectedAdmissionReceipts . clear ( )
357382 for ( const key of Object . keys ( storedSessions ) ) delete storedSessions [ key ]
358383} )
359384
@@ -455,6 +480,7 @@ describe("prompt submit worktree selection", () => {
455480 model : { providerID : "provider" , modelID : "model" , variant : "high" } ,
456481 } ,
457482 } )
483+ expect ( optimisticRemoved ) . toHaveLength ( 1 )
458484 } )
459485
460486 test ( "seeds new sessions before optimistic prompts are added" , async ( ) => {
@@ -784,4 +810,113 @@ describe("prompt submit worktree selection", () => {
784810 expect ( sentPromptAsync ) . toHaveLength ( 1 )
785811 promptValue [ 0 ] = { type : "text" , content : "ls" , start : 0 , end : 2 }
786812 } )
813+
814+ test ( "reuses submission identity when the durable admission receipt is lost" , async ( ) => {
815+ params = { id : "session-1" }
816+ promptValue [ 0 ] = { type : "text" , content : "receipt lost" , start : 0 , end : 12 }
817+ const context = {
818+ type : "file" as const ,
819+ path : "src/retry.ts" ,
820+ comment : "keep this review context" ,
821+ commentID : "comment-1" ,
822+ key : "original-ui-key" ,
823+ }
824+ promptContextItems . push ( context , {
825+ type : "file" ,
826+ path : "src/reference.ts" ,
827+ key : "reference-ui-key" ,
828+ } )
829+
830+ const submit = createPromptSubmit ( {
831+ info : ( ) => ( { id : "session-1" } ) ,
832+ imageAttachments : ( ) => [ ] ,
833+ commentCount : ( ) => 0 ,
834+ autoAccept : ( ) => false ,
835+ mode : ( ) => "normal" ,
836+ working : ( ) => false ,
837+ editor : ( ) => undefined ,
838+ queueScroll : ( ) => undefined ,
839+ promptLength : ( value ) => value . reduce ( ( sum , part ) => sum + ( "content" in part ? part . content . length : 0 ) , 0 ) ,
840+ addToHistory : ( ) => undefined ,
841+ resetHistoryNavigation : ( ) => undefined ,
842+ setMode : ( ) => undefined ,
843+ setPopover : ( ) => undefined ,
844+ onSubmit : ( ) => undefined ,
845+ } )
846+
847+ const event = { preventDefault : ( ) => undefined } as unknown as Event
848+ await submit . handleSubmit ( event )
849+ await flushAsyncSubmit ( )
850+
851+ expect ( promptContextItems . find ( ( item ) => item . commentID === context . commentID ) ?. key ) . not . toBe ( context . key )
852+
853+ await submit . handleSubmit ( event )
854+ await flushAsyncSubmit ( )
855+
856+ expect ( sentPromptAsync ) . toHaveLength ( 2 )
857+ expect ( sentPromptAsync [ 0 ] ?. messageID ) . toBeDefined ( )
858+ expect ( sentPromptAsync [ 1 ] ?. messageID ) . toBe ( sentPromptAsync [ 0 ] ?. messageID )
859+ expect ( sentPromptAsync [ 0 ] ?. intentID ) . toBeDefined ( )
860+ expect ( sentPromptAsync [ 1 ] ?. intentID ) . toBe ( sentPromptAsync [ 0 ] ?. intentID )
861+ expect ( sentPromptAsync [ 1 ] ?. parts ) . toEqual ( sentPromptAsync [ 0 ] ?. parts )
862+
863+ promptContextItems . push ( { ...context , key : "new-ui-key-after-success" } )
864+ await submit . handleSubmit ( event )
865+ await flushAsyncSubmit ( )
866+
867+ expect ( sentPromptAsync ) . toHaveLength ( 3 )
868+ expect ( sentPromptAsync [ 2 ] ?. messageID ) . not . toBe ( sentPromptAsync [ 0 ] ?. messageID )
869+ expect ( sentPromptAsync [ 2 ] ?. intentID ) . not . toBe ( sentPromptAsync [ 0 ] ?. intentID )
870+ promptValue [ 0 ] = { type : "text" , content : "ls" , start : 0 , end : 2 }
871+ } )
872+
873+ test ( "reuses the final prepared payload when an intelligence admission receipt is lost" , async ( ) => {
874+ params = { id : "session-1" }
875+ promptMode = "intelligence"
876+ promptValue [ 0 ] = { type : "text" , content : "intelligence receipt lost" , start : 0 , end : 25 }
877+ let confirms = 0
878+
879+ const submit = createPromptSubmit ( {
880+ info : ( ) => ( { id : "session-1" } ) ,
881+ imageAttachments : ( ) => [ ] ,
882+ commentCount : ( ) => 0 ,
883+ autoAccept : ( ) => false ,
884+ mode : ( ) => "normal" ,
885+ working : ( ) => false ,
886+ editor : ( ) => undefined ,
887+ queueScroll : ( ) => undefined ,
888+ promptLength : ( value ) => value . reduce ( ( sum , part ) => sum + ( "content" in part ? part . content . length : 0 ) , 0 ) ,
889+ addToHistory : ( ) => undefined ,
890+ resetHistoryNavigation : ( ) => undefined ,
891+ setMode : ( ) => undefined ,
892+ setPopover : ( ) => undefined ,
893+ confirmPromptDraft : async ( ) => {
894+ confirms += 1
895+ return { editedGoal : "Edited retry goal" }
896+ } ,
897+ onSubmit : ( ) => undefined ,
898+ } )
899+
900+ const event = { preventDefault : ( ) => undefined } as unknown as Event
901+ await submit . handleSubmit ( event )
902+ await flushAsyncSubmit ( )
903+ await submit . handleSubmit ( event )
904+ await flushAsyncSubmit ( )
905+
906+ expect ( preparedDrafts ) . toHaveLength ( 1 )
907+ expect ( confirms ) . toBe ( 1 )
908+ expect ( sentPromptAsync ) . toHaveLength ( 2 )
909+ expect ( sentPromptAsync [ 1 ] ?. messageID ) . toBe ( sentPromptAsync [ 0 ] ?. messageID )
910+ expect ( sentPromptAsync [ 1 ] ?. intentID ) . toBe ( sentPromptAsync [ 0 ] ?. intentID )
911+ expect ( sentPromptAsync [ 1 ] ?. parts ) . toEqual ( sentPromptAsync [ 0 ] ?. parts )
912+ expect ( sentPromptAsync [ 1 ] ?. metadata ) . toEqual ( sentPromptAsync [ 0 ] ?. metadata )
913+ expect ( sentPromptAsync [ 1 ] ?. metadata ) . toMatchObject ( {
914+ deepagent : {
915+ prompt_pipeline : {
916+ confirmed_draft_id : "prompt_draft:test:1" ,
917+ } ,
918+ } ,
919+ } )
920+ promptValue [ 0 ] = { type : "text" , content : "ls" , start : 0 , end : 2 }
921+ } )
787922} )
0 commit comments