From 1f19dbe6d50b8ac9b96cba2a1e5a2002bbc551ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Wed, 29 Jul 2026 14:05:38 +0100 Subject: [PATCH] fix: keep watcher save state visible (#2037) --- apps/backend/internal/github/controller.go | 7 +- .../internal/github/controller_test.go | 35 +++++ .../use-watcher-enabled-drafts.test.tsx | 118 ++++++++++++++++- .../use-watcher-enabled-drafts.ts | 33 ++++- .../tests/gitlab/mobile-gitlab-parity.spec.ts | 4 + .../github-workspace-settings.spec.ts | 67 ++++++++++ .../integration-watcher-save-state/plan.md | 123 ++++++++++++++++++ .../task-01-github-review-watch-response.md | 54 ++++++++ .../task-02-shared-watcher-saved-baseline.md | 56 ++++++++ .../task-03-watcher-save-e2e.md | 59 +++++++++ docs/specs/ui/settings-manual-save.md | 11 +- 11 files changed, 557 insertions(+), 10 deletions(-) create mode 100644 docs/plans/integration-watcher-save-state/plan.md create mode 100644 docs/plans/integration-watcher-save-state/task-01-github-review-watch-response.md create mode 100644 docs/plans/integration-watcher-save-state/task-02-shared-watcher-saved-baseline.md create mode 100644 docs/plans/integration-watcher-save-state/task-03-watcher-save-e2e.md diff --git a/apps/backend/internal/github/controller.go b/apps/backend/internal/github/controller.go index fba50387b0..da4b80469f 100644 --- a/apps/backend/internal/github/controller.go +++ b/apps/backend/internal/github/controller.go @@ -787,7 +787,12 @@ func (c *Controller) httpUpdateReviewWatch(ctx *gin.Context) { ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - ctx.JSON(http.StatusOK, gin.H{"updated": true}) + updated, err := c.service.GetReviewWatch(ctx.Request.Context(), id) + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + ctx.JSON(http.StatusOK, updated) } func (c *Controller) httpDeleteReviewWatch(ctx *gin.Context) { diff --git a/apps/backend/internal/github/controller_test.go b/apps/backend/internal/github/controller_test.go index 94e02d73d5..9e48ba1408 100644 --- a/apps/backend/internal/github/controller_test.go +++ b/apps/backend/internal/github/controller_test.go @@ -481,6 +481,41 @@ func TestHttpTriggerReviewWatchPublishesNewPREvents(t *testing.T) { } } +func TestHttpUpdateReviewWatchReturnsUpdatedWatch(t *testing.T) { + router, store := setupControllerStoreTest(t) + watch := &ReviewWatch{ + WorkspaceID: "ws-1", + WorkflowID: "wf-1", + WorkflowStepID: "step-1", + Enabled: true, + PollIntervalSeconds: defaultWatchPollIntervalSec, + CleanupPolicy: CleanupPolicyNever, + } + if err := store.CreateReviewWatch(context.Background(), watch); err != nil { + t.Fatalf("create review watch: %v", err) + } + + req := httptest.NewRequest( + http.MethodPut, + "/api/v1/github/watches/review/"+watch.ID+"?workspace_id=ws-1", + strings.NewReader(`{"enabled":false}`), + ) + req.Header.Set("Content-Type", "application/json") + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + + if response.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", response.Code, response.Body.String()) + } + var updated ReviewWatch + if err := json.NewDecoder(response.Body).Decode(&updated); err != nil { + t.Fatalf("decode update response: %v", err) + } + if updated.ID != watch.ID || updated.Enabled { + t.Fatalf("updated watch = %+v, want id %q and enabled=false", updated, watch.ID) + } +} + func TestHttpListReviewWatchesRequiresWorkspaceForRequests(t *testing.T) { store := newTestStore(t) watch := &ReviewWatch{WorkspaceID: "victim-workspace", Prompt: "secret review prompt"} diff --git a/apps/web/components/integrations/use-watcher-enabled-drafts.test.tsx b/apps/web/components/integrations/use-watcher-enabled-drafts.test.tsx index 6751a24194..656476c877 100644 --- a/apps/web/components/integrations/use-watcher-enabled-drafts.test.tsx +++ b/apps/web/components/integrations/use-watcher-enabled-drafts.test.tsx @@ -1,9 +1,11 @@ import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { useState } from "react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { SettingsSaveProvider } from "@/components/settings/settings-save-provider"; import { useWatcherEnabledDrafts } from "./use-watcher-enabled-drafts"; const watch = { id: "watch-1", enabled: true }; +const saveChangesLabel = "Save changes"; afterEach(cleanup); @@ -23,6 +25,61 @@ function Harness({ save }: { save: (enabled: boolean) => Promise }) { ); } +function ReconciliationHarness({ save }: { save: (enabled: boolean) => Promise }) { + const [items, setItems] = useState([watch]); + const drafts = useWatcherEnabledDrafts({ + id: "test-watches", + items, + saveEnabled: (_watch, enabled) => save(enabled), + }); + return ( + <> + + +