@@ -26,10 +26,11 @@ function makeTx(selectResults: unknown[][]) {
2626 const inserted : Array < Record < string , unknown > > = [ ]
2727 const updates : Array < Record < string , unknown > > = [ ]
2828 let call = 0
29+ const select = vi . fn ( ( ) => ( {
30+ from : ( ) => ( { where : ( ) => Promise . resolve ( selectResults [ call ++ ] ?? [ ] ) } ) ,
31+ } ) )
2932 const tx = {
30- select : ( ) => ( {
31- from : ( ) => ( { where : ( ) => Promise . resolve ( selectResults [ call ++ ] ?? [ ] ) } ) ,
32- } ) ,
33+ select,
3334 insert : ( ) => ( {
3435 values : ( values : Array < Record < string , unknown > > ) => {
3536 inserted . push ( ...values )
@@ -45,7 +46,7 @@ function makeTx(selectResults: unknown[][]) {
4546 } ) ,
4647 } ) ,
4748 }
48- return { tx : tx as unknown as DbOrTx , inserted, updates }
49+ return { tx : tx as unknown as DbOrTx , inserted, updates, select }
4950}
5051
5152const attachment = ( overrides : Record < string , unknown > = { } ) => ( {
@@ -69,7 +70,8 @@ const serverMappingRow = {
6970describe ( 'reconcileForkWorkflowMcpAttachments' , ( ) => {
7071 it ( 'creates the target attachment for a mapped server + written pair (push: child -> parent)' , async ( ) => {
7172 mockGetEdgeMappingRows . mockResolvedValue ( [ serverMappingRow ] )
72- const { tx, inserted } = makeTx ( [
73+ mockAcquireLock . mockClear ( )
74+ const { tx, inserted, select } = makeTx ( [
7375 [ { id : 'srv-child' } , { id : 'srv-parent' } ] , // both mapped servers still live
7476 [ attachment ( { serverId : 'srv-child' , workflowId : 'wf-child' } ) ] ,
7577 [ ] , // no existing target attachments
@@ -89,6 +91,11 @@ describe('reconcileForkWorkflowMcpAttachments', () => {
8991 } )
9092 expect ( result . affectedServerIds ) . toEqual ( [ 'srv-parent' ] )
9193 expect ( mockAcquireLock ) . toHaveBeenCalledWith ( tx , 'srv-parent' )
94+ // The lock must precede every read: locking after the diff is computed would let a
95+ // concurrent attach commit in between and abort the promote on the unique constraint.
96+ expect ( mockAcquireLock . mock . invocationCallOrder [ 0 ] ) . toBeLessThan (
97+ select . mock . invocationCallOrder [ 0 ]
98+ )
9299 } )
93100
94101 it ( 'archives a target attachment whose source counterpart was detached' , async ( ) => {
0 commit comments