From d52cac1ec60652a4299cef5f1117c2d0a98b72f2 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Fri, 2 Oct 2026 14:43:00 +0200 Subject: [PATCH] Toggle only a target's active state, so it cannot undo an edit (closes #431) The target toggle loaded the target, flipped its active flag and saved the whole row, so an edit of the same target's name or settings saved in between was written back over and lost, although it reported success. The toggle now updates only the active column, so it can no longer undo an edit. A test saves an edit just after the toggle has read the target and shows the edit survives and the state flips. The entrypoint toggle is unchanged, since an entrypoint has no edit form. Model: opus-5-5 --- internal/handlers/source_management.go | 8 ++- internal/handlers/target_toggle_test.go | 66 +++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 internal/handlers/target_toggle_test.go diff --git a/internal/handlers/source_management.go b/internal/handlers/source_management.go index 0e8aa1b..bc5d2c9 100644 --- a/internal/handlers/source_management.go +++ b/internal/handlers/source_management.go @@ -1911,9 +1911,13 @@ func (h *Handlers) HandleTargetToggle() http.HandlerFunc { return false, err } - tgt.Active = !tgt.Active + // Only the active column: saving the whole row would + // write back the name and settings read above over an + // edit saved since. + active := !tgt.Active - return tgt.Active, h.db.DB().Save(&tgt).Error + return active, h.db.DB().Model(&tgt). + Update("active", active).Error }, "failed to toggle target", targetActivated, targetDeactivated, diff --git a/internal/handlers/target_toggle_test.go b/internal/handlers/target_toggle_test.go new file mode 100644 index 0000000..05a9695 --- /dev/null +++ b/internal/handlers/target_toggle_test.go @@ -0,0 +1,66 @@ +package handlers_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +// TestHandleTargetToggle_DoesNotUndoAnEdit proves that a toggle which +// loaded the target before an edit of it was saved does not write the +// old name and settings back over the edit. The edit is submitted from +// a callback on the toggle's own read of the target, so it is saved +// after that read and before the toggle writes. +func TestHandleTargetToggle_DoesNotUndoAnEdit(t *testing.T) { + t.Parallel() + + env := setupSourceTest(t) + wh, tgt := seedHTTPTarget(t, env, "", "") + require.True(t, tgt.Active) + + var ( + edited bool + editCode int + ) + + require.NoError(t, env.db.DB().Callback().Query(). + After("gorm:query"). + Register("test:edit_after_toggle_read", func(tx *gorm.DB) { + // The edit reads the target too; only the toggle's read, + // the first, submits it. + if tx.Statement.Table != "targets" || edited { + return + } + + edited = true + editCode = submitTargetEdit( + env, wh.ID, tgt.ID, + editForm(editReplacedURL, "", ""), + ).Code + }), + ) + + req := postRequest( + "/hook/"+wh.ID+"/targets/"+tgt.ID+"/toggle", + env.cookies, + map[string]string{paramSourceID: wh.ID, paramTargetID: tgt.ID}, + ) + w := httptest.NewRecorder() + + env.handlers.HandleTargetToggle().ServeHTTP(w, req) + + require.Equal(t, http.StatusSeeOther, w.Code) + require.Equal(t, http.StatusSeeOther, editCode) + + stored := storedTarget(t, env, tgt.ID) + assert.False(t, stored.Active) + assert.Equal(t, "edited-name", stored.Name) + assert.Equal(t, 5, stored.MaxRetries) + assert.Equal( + t, editReplacedURL, storedHTTPConfig(t, env, tgt.ID).URL, + ) +}