@@ -337,75 +337,100 @@ func TestMergeStackEnvVars_Branches(t *testing.T) {
337337 ctx := context .Background ()
338338 id := uuid .New ()
339339 patch := map [string ]string {"A" : "1" }
340-
341- // 1) BeginTx error.
340+ // selRe matches the status+env_vars FOR UPDATE select (post-hardening).
341+ selRe := `SELECT status, COALESCE\(env_vars.*FOR UPDATE`
342+ lockRe := `SET LOCAL lock_timeout`
343+ okRows := func (status , env string ) * sqlmock.Rows {
344+ return sqlmock .NewRows ([]string {"status" , "env_vars" }).AddRow (status , []byte (env ))
345+ }
346+
347+ // 1) BeginTx error (before SET LOCAL).
342348 dbB , mockB := newMock (t )
343349 mockB .ExpectBegin ().WillReturnError (errors .New ("begin-boom" ))
344350 _ , _ , err := MergeStackEnvVars (ctx , dbB , id , patch )
345351 require .ErrorContains (t , err , "begin-boom" )
346352
347- // 2) SELECT ... FOR UPDATE → no rows → ErrStackNotFound (rolls back).
353+ // 2) SET LOCAL lock_timeout error (rolls back).
354+ dbLT , mockLT := newMock (t )
355+ mockLT .ExpectBegin ()
356+ mockLT .ExpectExec (lockRe ).WillReturnError (errors .New ("lock-boom" ))
357+ mockLT .ExpectRollback ()
358+ _ , _ , err = MergeStackEnvVars (ctx , dbLT , id , patch )
359+ require .ErrorContains (t , err , "lock_timeout" )
360+
361+ // 3) SELECT ... FOR UPDATE → no rows → ErrStackNotFound (rolls back).
348362 dbNF , mockNF := newMock (t )
349363 mockNF .ExpectBegin ()
350- mockNF .ExpectQuery (`SELECT COALESCE\(env_vars.*FOR UPDATE` ).WillReturnError (errNoRows ())
364+ mockNF .ExpectExec (lockRe ).WillReturnResult (sqlmock .NewResult (0 , 0 ))
365+ mockNF .ExpectQuery (selRe ).WillReturnError (errNoRows ())
351366 mockNF .ExpectRollback ()
352367 var nf * ErrStackNotFound
353368 _ , _ , err = MergeStackEnvVars (ctx , dbNF , id , patch )
354369 require .ErrorAs (t , err , & nf )
355370
356- // 3 ) SELECT error (non-NoRows).
371+ // 4 ) SELECT error (non-NoRows).
357372 dbSE , mockSE := newMock (t )
358373 mockSE .ExpectBegin ()
359- mockSE .ExpectQuery (`SELECT COALESCE\(env_vars.*FOR UPDATE` ).WillReturnError (errors .New ("sel-boom" ))
374+ mockSE .ExpectExec (lockRe ).WillReturnResult (sqlmock .NewResult (0 , 0 ))
375+ mockSE .ExpectQuery (selRe ).WillReturnError (errors .New ("sel-boom" ))
360376 mockSE .ExpectRollback ()
361377 _ , _ , err = MergeStackEnvVars (ctx , dbSE , id , patch )
362378 require .ErrorContains (t , err , "sel-boom" )
363379
364- // 4) unmarshal error (malformed jsonb).
380+ // 5) status='deleting' → ErrStackDeleting (in-tx teardown guard, rolls back).
381+ dbDel , mockDel := newMock (t )
382+ mockDel .ExpectBegin ()
383+ mockDel .ExpectExec (lockRe ).WillReturnResult (sqlmock .NewResult (0 , 0 ))
384+ mockDel .ExpectQuery (selRe ).WillReturnRows (okRows (StackStatusDeleting , `{}` ))
385+ mockDel .ExpectRollback ()
386+ _ , _ , err = MergeStackEnvVars (ctx , dbDel , id , patch )
387+ require .ErrorIs (t , err , ErrStackDeleting )
388+
389+ // 6) unmarshal error (malformed jsonb).
365390 dbUM , mockUM := newMock (t )
366391 mockUM .ExpectBegin ()
367- mockUM .ExpectQuery ( `SELECT COALESCE\(env_vars.*FOR UPDATE` ).
368- WillReturnRows ( sqlmock . NewRows ([] string { "env_vars" }). AddRow ([] byte ( `not json` ) ))
392+ mockUM .ExpectExec ( lockRe ). WillReturnResult ( sqlmock . NewResult ( 0 , 0 ))
393+ mockUM . ExpectQuery ( selRe ). WillReturnRows ( okRows ( "healthy" , `not json` ))
369394 mockUM .ExpectRollback ()
370395 _ , _ , err = MergeStackEnvVars (ctx , dbUM , id , patch )
371396 require .ErrorContains (t , err , "unmarshal" )
372397
373- // 5 ) over-cap → ErrStackEnvVarsTooLarge, checked BEFORE the UPDATE (rolls back).
398+ // 7 ) over-cap → ErrStackEnvVarsTooLarge, checked BEFORE the UPDATE (rolls back).
374399 dbTL , mockTL := newMock (t )
375400 mockTL .ExpectBegin ()
376- mockTL .ExpectQuery ( `SELECT COALESCE\(env_vars.*FOR UPDATE` ).
377- WillReturnRows ( sqlmock . NewRows ([] string { "env_vars" }). AddRow ([] byte ( `{}` ) ))
401+ mockTL .ExpectExec ( lockRe ). WillReturnResult ( sqlmock . NewResult ( 0 , 0 ))
402+ mockTL . ExpectQuery ( selRe ). WillReturnRows ( okRows ( "healthy" , `{}` ))
378403 mockTL .ExpectRollback ()
379404 big := map [string ]string {"K" : strings .Repeat ("x" , maxStackEnvVarsBytes + 1 )}
380405 _ , _ , err = MergeStackEnvVars (ctx , dbTL , id , big )
381406 require .ErrorIs (t , err , ErrStackEnvVarsTooLarge )
382407
383- // 6 ) UPDATE error.
408+ // 8 ) UPDATE error.
384409 dbUE , mockUE := newMock (t )
385410 mockUE .ExpectBegin ()
386- mockUE .ExpectQuery ( `SELECT COALESCE\(env_vars.*FOR UPDATE` ).
387- WillReturnRows ( sqlmock . NewRows ([] string { "env_vars" }). AddRow ([] byte ( `{}` ) ))
411+ mockUE .ExpectExec ( lockRe ). WillReturnResult ( sqlmock . NewResult ( 0 , 0 ))
412+ mockUE . ExpectQuery ( selRe ). WillReturnRows ( okRows ( "healthy" , `{}` ))
388413 mockUE .ExpectExec (`UPDATE stacks SET env_vars` ).WillReturnError (errors .New ("upd-boom" ))
389414 mockUE .ExpectRollback ()
390415 _ , _ , err = MergeStackEnvVars (ctx , dbUE , id , patch )
391416 require .ErrorContains (t , err , "upd-boom" )
392417
393- // 7 ) Commit error.
418+ // 9 ) Commit error.
394419 dbCE , mockCE := newMock (t )
395420 mockCE .ExpectBegin ()
396- mockCE .ExpectQuery ( `SELECT COALESCE\(env_vars.*FOR UPDATE` ).
397- WillReturnRows ( sqlmock . NewRows ([] string { "env_vars" }). AddRow ([] byte ( `{}` ) ))
421+ mockCE .ExpectExec ( lockRe ). WillReturnResult ( sqlmock . NewResult ( 0 , 0 ))
422+ mockCE . ExpectQuery ( selRe ). WillReturnRows ( okRows ( "healthy" , `{}` ))
398423 mockCE .ExpectExec (`UPDATE stacks SET env_vars` ).WillReturnResult (sqlmock .NewResult (0 , 1 ))
399424 mockCE .ExpectCommit ().WillReturnError (errors .New ("commit-boom" ))
400425 _ , _ , err = MergeStackEnvVars (ctx , dbCE , id , patch )
401426 require .ErrorContains (t , err , "commit-boom" )
402427
403- // 8 ) Happy path: existing {A:old, B:keep}; patch upserts A, adds C, deletes B
404- // (present → counted) and deletes MISSING (absent → NOT counted).
428+ // 10 ) Happy path: existing {A:old, B:keep}; patch upserts A, adds C, deletes B
429+ // (present → counted) and deletes MISSING (absent → NOT counted).
405430 dbOK , mockOK := newMock (t )
406431 mockOK .ExpectBegin ()
407- mockOK .ExpectQuery ( `SELECT COALESCE\(env_vars.*FOR UPDATE` ).
408- WillReturnRows ( sqlmock . NewRows ([] string { "env_vars" }). AddRow ([] byte ( `{"A":"old","B":"keep"}` ) ))
432+ mockOK .ExpectExec ( lockRe ). WillReturnResult ( sqlmock . NewResult ( 0 , 0 ))
433+ mockOK . ExpectQuery ( selRe ). WillReturnRows ( okRows ( "healthy" , `{"A":"old","B":"keep"}` ))
409434 mockOK .ExpectExec (`UPDATE stacks SET env_vars` ).WillReturnResult (sqlmock .NewResult (0 , 1 ))
410435 mockOK .ExpectCommit ()
411436 merged , deletes , err := MergeStackEnvVars (ctx , dbOK , id ,
0 commit comments