diff --git a/ui/src/pages/InstanceExperimentalSettings.test.tsx b/ui/src/pages/InstanceExperimentalSettings.test.tsx index 893990e56a..9a6605fd8a 100644 --- a/ui/src/pages/InstanceExperimentalSettings.test.tsx +++ b/ui/src/pages/InstanceExperimentalSettings.test.tsx @@ -3,7 +3,10 @@ import { flushSync } from "react-dom"; import { createRoot, type Root } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import type { InstanceExperimentalSettings as InstanceExperimentalSettingsPayload } from "@paperclipai/shared"; +import type { + InstanceExperimentalSettings as InstanceExperimentalSettingsPayload, + IssueGraphLivenessAutoRecoveryPreview, +} from "@paperclipai/shared"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { InstanceExperimentalSettings } from "./InstanceExperimentalSettings"; @@ -52,6 +55,8 @@ const SERVER_INFO_TOGGLE_SELECTOR = 'button[aria-label="Toggle server info debug view experimental setting"]'; const BUILT_IN_AGENTS_TOGGLE_SELECTOR = 'button[aria-label="Toggle built-in agents experimental setting"]'; +const AUTO_RECOVERY_TOGGLE_SELECTOR = + 'button[aria-label="Toggle task graph liveness auto-recovery"]'; function defaultExperimentalSettings(): InstanceExperimentalSettingsPayload { return { @@ -81,6 +86,18 @@ function defaultExperimentalSettings(): InstanceExperimentalSettingsPayload { }; } +function emptyRecoveryPreview(): IssueGraphLivenessAutoRecoveryPreview { + return { + lookbackHours: 24, + cutoff: "2026-07-12T16:00:00.000Z", + generatedAt: "2026-07-13T16:00:00.000Z", + findings: 0, + recoverableFindings: 0, + skippedOutsideLookback: 0, + items: [], + }; +} + const WORKTREE_RUN_EXECUTION_TOGGLE_SELECTOR = 'button[aria-label="Toggle worktree run execution setting"]'; @@ -426,4 +443,88 @@ describe("InstanceExperimentalSettings — Conference Room Chat card (PAP-11233) }); expect(toggle?.getAttribute("aria-checked")).toBe("true"); }); + + it("removes the auto-recovery confirmation overlay after enabling only", async () => { + mockInstanceSettingsApi.previewIssueGraphLivenessAutoRecovery.mockResolvedValue(emptyRecoveryPreview()); + await renderPage(); + + const toggle = container.querySelector(AUTO_RECOVERY_TOGGLE_SELECTOR); + expect(toggle?.getAttribute("aria-checked")).toBe("false"); + + await act(async () => { + toggle?.click(); + }); + await flushReact(); + + expect(mockInstanceSettingsApi.previewIssueGraphLivenessAutoRecovery).toHaveBeenCalledWith({ + lookbackHours: 24, + }); + expect(document.body.textContent).toContain("Confirm auto-recovery"); + expect(document.body.querySelector('[data-slot="dialog-overlay"]')).not.toBeNull(); + + const enableOnlyButton = [...document.body.querySelectorAll("button")].find( + (button) => button.textContent === "Enable only", + ); + + await act(async () => { + enableOnlyButton?.click(); + }); + await flushReact(); + + expect(mockInstanceSettingsApi.updateExperimental).toHaveBeenCalledWith({ + enableIssueGraphLivenessAutoRecovery: true, + issueGraphLivenessAutoRecoveryLookbackHours: 24, + }); + expect(document.body.textContent).not.toContain("Confirm auto-recovery"); + expect(document.body.querySelector('[data-slot="dialog-overlay"]')).toBeNull(); + const enabledToggle = container.querySelector(AUTO_RECOVERY_TOGGLE_SELECTOR); + expect(enabledToggle?.getAttribute("aria-checked")).toBe("true"); + }); + + it("removes the auto-recovery confirmation overlay after enabling and running", async () => { + mockInstanceSettingsApi.previewIssueGraphLivenessAutoRecovery.mockResolvedValue(emptyRecoveryPreview()); + mockInstanceSettingsApi.runIssueGraphLivenessAutoRecovery.mockResolvedValue({ + findings: 0, + autoRecoveryEnabled: true, + lookbackHours: 24, + cutoff: "2026-07-12T16:00:00.000Z", + escalationsCreated: 0, + existingEscalations: 0, + skipped: 0, + skippedAutoRecoveryDisabled: 0, + }); + await renderPage(); + + const toggle = container.querySelector(AUTO_RECOVERY_TOGGLE_SELECTOR); + expect(toggle?.getAttribute("aria-checked")).toBe("false"); + + await act(async () => { + toggle?.click(); + }); + await flushReact(); + + expect(document.body.textContent).toContain("Confirm auto-recovery"); + expect(document.body.querySelector('[data-slot="dialog-overlay"]')).not.toBeNull(); + + const enableAndRunButton = [...document.body.querySelectorAll("button")].find( + (button) => button.textContent === "Enable", + ); + + await act(async () => { + enableAndRunButton?.click(); + }); + await flushReact(); + + expect(mockInstanceSettingsApi.updateExperimental).toHaveBeenCalledWith({ + enableIssueGraphLivenessAutoRecovery: true, + issueGraphLivenessAutoRecoveryLookbackHours: 24, + }); + expect(mockInstanceSettingsApi.runIssueGraphLivenessAutoRecovery).toHaveBeenCalledWith({ + lookbackHours: 24, + }); + expect(document.body.textContent).not.toContain("Confirm auto-recovery"); + expect(document.body.querySelector('[data-slot="dialog-overlay"]')).toBeNull(); + const enabledToggle = container.querySelector(AUTO_RECOVERY_TOGGLE_SELECTOR); + expect(enabledToggle?.getAttribute("aria-checked")).toBe("true"); + }); }); diff --git a/ui/src/pages/InstanceExperimentalSettings.tsx b/ui/src/pages/InstanceExperimentalSettings.tsx index c3ee757893..b77f205a3d 100644 --- a/ui/src/pages/InstanceExperimentalSettings.tsx +++ b/ui/src/pages/InstanceExperimentalSettings.tsx @@ -170,6 +170,11 @@ export function InstanceExperimentalSettings() { const [previewDialogOpen, setPreviewDialogOpen] = useState(false); const [pendingPreview, setPendingPreview] = useState(null); + function closeRecoveryPreview() { + setPreviewDialogOpen(false); + setPendingPreview(null); + } + useEffect(() => { setBreadcrumbs([ { label: "Settings", href: "/company/settings" }, @@ -239,7 +244,7 @@ export function InstanceExperimentalSettings() { instanceSettingsApi.runIssueGraphLivenessAutoRecovery({ lookbackHours }), onSuccess: async () => { setActionError(null); - setPreviewDialogOpen(false); + closeRecoveryPreview(); await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.instance.experimentalSettings }), queryClient.invalidateQueries({ queryKey: queryKeys.health }), @@ -310,21 +315,22 @@ export function InstanceExperimentalSettings() { setActionError("Lookback hours must be a whole number from 1 to 720."); return; } + closeRecoveryPreview(); previewMutation.mutate(parsedLookbackHours); } function enableOnly() { if (!lookbackHoursIsValid) return; + closeRecoveryPreview(); toggleMutation.mutate({ enableIssueGraphLivenessAutoRecovery: true, issueGraphLivenessAutoRecoveryLookbackHours: parsedLookbackHours, - }, { - onSuccess: () => setPreviewDialogOpen(false), }); } function enableAndRun() { if (!lookbackHoursIsValid) return; + closeRecoveryPreview(); toggleMutation.mutate({ enableIssueGraphLivenessAutoRecovery: true, issueGraphLivenessAutoRecoveryLookbackHours: parsedLookbackHours, @@ -784,14 +790,20 @@ export function InstanceExperimentalSettings() { - + {previewDialogOpen ? ( + { + if (!open) { + closeRecoveryPreview(); + } + }} + preview={pendingPreview} + onEnableOnly={enableOnly} + onEnableAndRun={enableAndRun} + isPending={recoveryActionPending} + /> + ) : null} ); }