@@ -63,6 +63,17 @@ function setup() {
6363const value = [ { usageControlExpression : 'force' } ]
6464const update = { blockId : 'agent-1' , subblockId : 'tools' , value, operationId : 'op-1' , timestamp : 1 }
6565
66+ function holdNextWorkflowLookup ( ) {
67+ let finish : ( error ?: Error ) => void = ( ) => { }
68+ const result = new Promise < Array < { id : string } > > ( ( resolve , reject ) => {
69+ finish = ( error ) => ( error ? reject ( error ) : resolve ( [ { id : 'workflow-1' } ] ) )
70+ } )
71+ mockSelect . mockReturnValueOnce ( {
72+ from : ( ) => ( { where : ( ) => ( { limit : ( ) => result } ) } ) ,
73+ } )
74+ return finish
75+ }
76+
6677describe ( 'debounced variable permission writes' , ( ) => {
6778 beforeEach ( ( ) => {
6879 vi . useFakeTimers ( )
@@ -145,4 +156,94 @@ describe('debounced variable permission writes', () => {
145156 expect . objectContaining ( { operationId : 'op-2' } )
146157 )
147158 } )
159+
160+ it ( 'preserves save and confirmation order when the older workflow lookup stalls' , async ( ) => {
161+ const { handlers, emit } = setup ( )
162+ const finishLookup = holdNextWorkflowLookup ( )
163+ const newerValue = [ { usageControlExpression : 'none' } ]
164+
165+ await handlers [ 'subblock-update' ] ( update )
166+ await vi . advanceTimersByTimeAsync ( 25 )
167+ await handlers [ 'subblock-update' ] ( { ...update , operationId : 'op-2' , value : newerValue } )
168+ await vi . advanceTimersByTimeAsync ( 25 )
169+ const writesBeforeRelease = mockSet . mock . calls . length
170+ finishLookup ( )
171+ await vi . advanceTimersByTimeAsync ( 0 )
172+
173+ expect ( writesBeforeRelease ) . toBe ( 0 )
174+ expect (
175+ mockSet . mock . calls
176+ . filter ( ( [ fields ] ) => fields . subBlocks )
177+ . map ( ( [ fields ] ) => fields . subBlocks . tools . value )
178+ ) . toEqual ( [ value , newerValue ] )
179+ expect (
180+ emit . mock . calls
181+ . filter ( ( [ event ] ) => event === 'operation-confirmed' )
182+ . map ( ( [ , payload ] ) => payload . operationId )
183+ ) . toEqual ( [ 'op-1' , 'op-2' ] )
184+ } )
185+
186+ it . each ( [ false , true ] ) (
187+ 'coalesces waiting edits and continues after an older failure: %s' ,
188+ async ( failOlder ) => {
189+ const { handlers, emit } = setup ( )
190+ const finishLookup = holdNextWorkflowLookup ( )
191+ const newestValue = [ { usageControlExpression : 'auto' } ]
192+
193+ await handlers [ 'subblock-update' ] ( update )
194+ await vi . advanceTimersByTimeAsync ( 25 )
195+ await handlers [ 'subblock-update' ] ( {
196+ ...update ,
197+ operationId : 'op-2' ,
198+ value : [ { usageControlExpression : 'none' } ] ,
199+ } )
200+ await vi . advanceTimersByTimeAsync ( 25 )
201+ await handlers [ 'subblock-update' ] ( { ...update , operationId : 'op-3' , value : newestValue } )
202+ await vi . advanceTimersByTimeAsync ( 25 )
203+ finishLookup ( failOlder ? new Error ( 'connection reset' ) : undefined )
204+ await vi . advanceTimersByTimeAsync ( 0 )
205+
206+ expect (
207+ mockSet . mock . calls
208+ . filter ( ( [ fields ] ) => fields . subBlocks )
209+ . map ( ( [ fields ] ) => fields . subBlocks . tools . value )
210+ ) . toEqual ( failOlder ? [ newestValue ] : [ value , newestValue ] )
211+ expect (
212+ emit . mock . calls
213+ . filter ( ( [ event ] ) => event === 'operation-confirmed' )
214+ . map ( ( [ , payload ] ) => payload . operationId )
215+ ) . toEqual ( failOlder ? [ 'op-2' , 'op-3' ] : [ 'op-1' , 'op-2' , 'op-3' ] )
216+ if ( failOlder ) {
217+ expect ( emit ) . toHaveBeenCalledWith (
218+ 'operation-failed' ,
219+ expect . objectContaining ( { operationId : 'op-1' , retryable : true } )
220+ )
221+ }
222+ }
223+ )
224+
225+ it ( 'allows a different subblock to save while one subblock is stalled' , async ( ) => {
226+ const { handlers, emit } = setup ( )
227+ const finishLookup = holdNextWorkflowLookup ( )
228+ await handlers [ 'subblock-update' ] ( update )
229+ await vi . advanceTimersByTimeAsync ( 25 )
230+ await handlers [ 'subblock-update' ] ( {
231+ ...update ,
232+ subblockId : 'systemPrompt' ,
233+ operationId : 'op-other' ,
234+ value : 'hello' ,
235+ } )
236+ await vi . advanceTimersByTimeAsync ( 25 )
237+ const confirmedBeforeRelease = emit . mock . calls
238+ . filter ( ( [ event ] ) => event === 'operation-confirmed' )
239+ . map ( ( [ , payload ] ) => payload . operationId )
240+ finishLookup ( )
241+ await vi . advanceTimersByTimeAsync ( 0 )
242+
243+ expect ( confirmedBeforeRelease ) . toEqual ( [ 'op-other' ] )
244+ expect ( emit ) . toHaveBeenCalledWith (
245+ 'operation-confirmed' ,
246+ expect . objectContaining ( { operationId : 'op-1' } )
247+ )
248+ } )
148249} )
0 commit comments