From d23c5d901f549a1d4cf3bedfd9ea4330821a53d7 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 11 Jun 2026 22:11:44 -0700 Subject: [PATCH] FN-6247: refresh PR controls from live auto-merge setting Keep task detail PR and review affordances synchronized with the current project auto-merge setting. - Thread the live auto-merge value into task detail modals and list split-pane detail views. - Prefer the live auto-merge setting over the stale fetched modal snapshot while preserving task-level overrides. - Cover PR and review tab behavior for auto-merge on/off and document the dashboard behavior. - Evict flaky engine gate entries and add a patch changeset for the published CLI package. Files changed: .changeset/fn-6247-automerge-off-modal-stale.md | 5 + docs/dashboard-guide.md | 1 + packages/dashboard/app/App.tsx | 3 +- packages/dashboard/app/components/AppModals.tsx | 2 + packages/dashboard/app/components/ListView.tsx | 3 + .../dashboard/app/components/TaskDetailModal.tsx | 4 +- .../__tests__/TaskDetailModal.create-pr.test.tsx | 171 ++++++++++++++++++++- packages/engine/vitest.config.ts | 2 - 8 files changed, 182 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-6247 Fusion-Task-Lineage: 1321c03a-216d-4b15-bf4f-95621d68c9ae --- .../fn-6247-automerge-off-modal-stale.md | 5 + docs/dashboard-guide.md | 1 + packages/dashboard/app/App.tsx | 3 +- .../dashboard/app/components/AppModals.tsx | 2 + .../dashboard/app/components/ListView.tsx | 3 + .../app/components/TaskDetailModal.tsx | 4 +- .../TaskDetailModal.create-pr.test.tsx | 171 +++++++++++++++++- packages/engine/vitest.config.ts | 2 - 8 files changed, 182 insertions(+), 9 deletions(-) create mode 100644 .changeset/fn-6247-automerge-off-modal-stale.md diff --git a/.changeset/fn-6247-automerge-off-modal-stale.md b/.changeset/fn-6247-automerge-off-modal-stale.md new file mode 100644 index 0000000000..c36c911f97 --- /dev/null +++ b/.changeset/fn-6247-automerge-off-modal-stale.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Fix task detail Pull Request and Review surfaces so they use the live project auto-merge setting instead of a stale modal-open snapshot. Create PR / manual merge affordances now appear immediately when auto-merge is toggled off, and the automatic auto-merge hint returns when it is toggled back on. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index ab68c09a1d..cc0d8a00b1 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -635,6 +635,7 @@ Inspect task definition, logs, review feedback, comments, documents, workflow ou - In shared task edit/create forms, GitHub Tracking appears at the bottom of **More options**, after **Workflow Steps**. - From this section you can explicitly enable/disable tracking and manage a per-task repo override (`owner/repo`). Clearing the override saves `null` and falls back to project/global defaults. - In `in-review`, pull-request controls/status (including stall badges) are in a dedicated **Pull Request** tab instead of the Definition tab. +- Task Detail and list split-pane PR affordances follow the live project auto-merge setting: when auto-merge is off, manual **Create PR** / merge actions are shown; when it is on, the tab shows the automatic auto-merge hint unless a per-task override changes the effective behavior. - The **Create Pull Request** modal now offers in-app remediation for every blocking preflight check. If `branchOnRemote` is false, use **Push branch to remote** and Fusion will publish `fusion/` to `origin` and refresh preflight. If `conflictsWithBase` is true, use **Resolve conflicts with AI** and Fusion will use an AI coding agent to resolve merge markers on the task branch, commit the result, push the branch, and refresh preflight so normal PR creation can continue once all checks pass. - The modal shell renders immediately: preflight checks and PR options load independently of AI-generated title/body metadata, so slow AI suggestions no longer block base-branch selection, diagnostics, or manual PR authoring. - AI title/body generation is bounded to 60 seconds and is canceled if the dialog request disconnects; on timeout/cancel, Fusion falls back to deterministic task-based PR title/body content instead of leaving the spinner stuck forever. diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index d4e6272c93..d3f11ac14d 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -1815,6 +1815,7 @@ function AppInner() { searchQuery={searchQuery} lastFetchTimeMs={lastFetchTimeMs} prAuthAvailable={prAuthAvailable} + autoMerge={autoMerge} onCreateWorkflow={openCreateWorkflowWithNav} /> @@ -2136,7 +2137,7 @@ function AppInner() { }} taskOperations={{ moveTask, deleteTask, mergeTask, archiveTask, retryTask, resetTask, duplicateTask }} deepLink={{ handleDetailClose }} - settings={{ prAuthAvailable, themeMode, colorTheme, dashboardFontScalePct, setThemeMode, setColorTheme, setDashboardFontScalePct }} + settings={{ prAuthAvailable, autoMerge, themeMode, colorTheme, dashboardFontScalePct, setThemeMode, setColorTheme, setDashboardFontScalePct }} onSettingsClose={handleSettingsCloseWithNav} onReopenOnboarding={reopenOnboardingWithNav} onOpenApprovals={(_approvalId) => handleTaskViewChange("mailbox")} diff --git a/packages/dashboard/app/components/AppModals.tsx b/packages/dashboard/app/components/AppModals.tsx index 5bb9b13458..c626cd2700 100644 --- a/packages/dashboard/app/components/AppModals.tsx +++ b/packages/dashboard/app/components/AppModals.tsx @@ -73,6 +73,7 @@ interface AppModalsProps { }; settings: { prAuthAvailable: boolean; + autoMerge: boolean; themeMode: ThemeMode; colorTheme: ColorTheme; dashboardFontScalePct: number; @@ -298,6 +299,7 @@ export function AppModals({ onTaskUpdated={modalManager.updateDetailTask} addToast={addToast} prAuthAvailable={settings.prAuthAvailable} + autoMergeEnabled={settings.autoMerge} onOpenWorkflowEditor={() => modalManager.openWorkflowEditor()} initialTab={modalManager.detailTaskInitialTab} /> diff --git a/packages/dashboard/app/components/ListView.tsx b/packages/dashboard/app/components/ListView.tsx index 66365f18e6..b1de9f3d22 100644 --- a/packages/dashboard/app/components/ListView.tsx +++ b/packages/dashboard/app/components/ListView.tsx @@ -233,6 +233,7 @@ interface ListViewProps { /** Timestamp (ms) when task data was last confirmed fresh from the server. Used for freshness-aware stuck detection. */ lastFetchTimeMs?: number; prAuthAvailable?: boolean; + autoMerge?: boolean; onCreateWorkflow?: () => void; } @@ -296,6 +297,7 @@ export function ListView({ searchQuery = "", lastFetchTimeMs, prAuthAvailable, + autoMerge, onCreateWorkflow, }: ListViewProps) { const { t } = useTranslation("app"); @@ -2332,6 +2334,7 @@ export function ListView({ }} addToast={addToast} prAuthAvailable={prAuthAvailable} + autoMergeEnabled={autoMerge} /> )} diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index afa3271e08..34e165921c 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -368,6 +368,7 @@ export interface TaskDetailModalProps { onTaskUpdated?: (task: Task) => void; addToast: (message: string, type?: ToastType) => void; prAuthAvailable?: boolean; + autoMergeEnabled?: boolean; onOpenWorkflowEditor?: () => void; /** Open the modal with this tab active instead of "definition" */ initialTab?: TabId; @@ -549,6 +550,7 @@ export function TaskDetailContent({ onTaskUpdated, addToast, prAuthAvailable, + autoMergeEnabled: autoMergeEnabledProp, onOpenWorkflowEditor, initialTab = "definition", mobileHeaderMode = "close", @@ -2605,7 +2607,7 @@ export function TaskDetailContent({ }; const prAutomationLabel = task.status ? prAutomationStatusLabels[task.status] : undefined; const mergeStrategy = settings?.mergeStrategy ?? "direct"; - const autoMergeEnabled = settings?.autoMerge ?? false; + const autoMergeEnabled = autoMergeEnabledProp ?? (settings?.autoMerge ?? false); const effectiveAutoMerge = resolveEffectiveAutoMerge({ autoMerge: task.autoMerge }, { autoMerge: autoMergeEnabled }); const isManualPrFlow = mergeStrategy === "pull-request" && !autoMergeEnabled; diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.create-pr.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.create-pr.test.tsx index e5db10884d..068029e6b3 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.create-pr.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.create-pr.test.tsx @@ -5,6 +5,7 @@ import type { PrInfo } from "@fusion/core"; const prPanelState = vi.hoisted(() => ({ latestPrInfo: undefined as PrInfo | undefined, latestAutoMerge: undefined as boolean | undefined, + latestIsManualPrFlow: undefined as boolean | undefined, })); const prCreateModalState = vi.hoisted(() => ({ @@ -19,11 +20,16 @@ vi.mock("../PrPanel", () => ({ PrPanel: (props: any) => { prPanelState.latestPrInfo = props.prInfo; prPanelState.latestAutoMerge = props.autoMerge; + prPanelState.latestIsManualPrFlow = props.isManualPrFlow; return (
- + {props.autoMerge ? ( +
Auto-merge will handle this task automatically.
+ ) : ( + + )}
{props.prInfo?.number ?? "none"}
); @@ -65,11 +71,18 @@ vi.mock("../PrCreateModal", () => ({ vi.mock("../TaskReviewTab", () => ({ TaskReviewTab: (props: any) => { taskReviewTabState.latestProps = props; - return ( + const effectiveAutoMerge = props.task.autoMerge ?? props.autoMergeEnabled; + const showCreatePr = + props.task.column === "in-review" && + !props.task.prInfo && + props.prAuthAvailable === true && + !effectiveAutoMerge && + typeof props.onRequestCreatePr === "function"; + return showCreatePr ? ( - ); + ) : null; }, })); @@ -92,6 +105,7 @@ describe("TaskDetailModal create-PR wiring", () => { vi.clearAllMocks(); prPanelState.latestPrInfo = undefined; prPanelState.latestAutoMerge = undefined; + prPanelState.latestIsManualPrFlow = undefined; prCreateModalState.latestProps = null; taskReviewTabState.latestProps = null; }); @@ -234,4 +248,151 @@ describe("TaskDetailModal create-PR wiring", () => { expect(screen.queryByTestId("pr-create-modal-stub")).toBeNull(); expect(prCreateModalState.latestProps?.open).toBe(false); }); + + it("prefers live auto-merge off over a stale fetched snapshot for PR surfaces", async () => { + (fetchSettings as ReturnType).mockResolvedValue({ + modelPresets: [], + autoSelectModelPreset: false, + defaultPresetBySize: {}, + autoMerge: true, + }); + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "Pull Request" })); + await waitFor(() => expect(prPanelState.latestAutoMerge).toBe(false)); + expect(screen.getByRole("button", { name: "Create PR" })).toBeInTheDocument(); + expect(screen.queryByText("Auto-merge will handle this task automatically.")).toBeNull(); + + fireEvent.click(screen.getByRole("button", { name: "Review" })); + await waitFor(() => expect(taskReviewTabState.latestProps?.autoMergeEnabled).toBe(false)); + expect(screen.getByTestId("task-review-create-pr")).toBeInTheDocument(); + }); + + it("prefers live auto-merge on over a stale fetched snapshot for PR surfaces", async () => { + (fetchSettings as ReturnType).mockResolvedValue({ + modelPresets: [], + autoSelectModelPreset: false, + defaultPresetBySize: {}, + autoMerge: false, + }); + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "Pull Request" })); + await waitFor(() => expect(prPanelState.latestAutoMerge).toBe(true)); + expect(screen.getByText("Auto-merge will handle this task automatically.")).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Create PR" })).toBeNull(); + + fireEvent.click(screen.getByRole("button", { name: "Review" })); + await waitFor(() => expect(taskReviewTabState.latestProps?.autoMergeEnabled).toBe(true)); + expect(screen.queryByTestId("task-review-create-pr")).toBeNull(); + }); + + it.each([ + { taskAutoMerge: undefined, liveAutoMerge: false, expectedEffective: false }, + { taskAutoMerge: undefined, liveAutoMerge: true, expectedEffective: true }, + { taskAutoMerge: true, liveAutoMerge: false, expectedEffective: true }, + { taskAutoMerge: true, liveAutoMerge: true, expectedEffective: true }, + { taskAutoMerge: false, liveAutoMerge: false, expectedEffective: false }, + { taskAutoMerge: false, liveAutoMerge: true, expectedEffective: false }, + ])( + "resolves effective auto-merge for task override $taskAutoMerge with live global $liveAutoMerge", + async ({ taskAutoMerge, liveAutoMerge, expectedEffective }) => { + (fetchSettings as ReturnType).mockResolvedValue({ + modelPresets: [], + autoSelectModelPreset: false, + defaultPresetBySize: {}, + autoMerge: !liveAutoMerge, + }); + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "Pull Request" })); + await waitFor(() => expect(prPanelState.latestAutoMerge).toBe(expectedEffective)); + if (expectedEffective) { + expect(screen.getByText("Auto-merge will handle this task automatically.")).toBeInTheDocument(); + } else { + expect(screen.queryByText("Auto-merge will handle this task automatically.")).toBeNull(); + expect(screen.getByRole("button", { name: "Create PR" })).toBeInTheDocument(); + } + + fireEvent.click(screen.getByRole("button", { name: "Review" })); + await waitFor(() => expect(taskReviewTabState.latestProps?.autoMergeEnabled).toBe(liveAutoMerge)); + if (expectedEffective) { + expect(screen.queryByTestId("task-review-create-pr")).toBeNull(); + } else { + expect(screen.getByTestId("task-review-create-pr")).toBeInTheDocument(); + } + }, + ); + + it("keeps manual PR flow driven by live global auto-merge rather than effective override", async () => { + (fetchSettings as ReturnType).mockResolvedValue({ + modelPresets: [], + autoSelectModelPreset: false, + defaultPresetBySize: {}, + autoMerge: true, + mergeStrategy: "pull-request", + }); + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "Pull Request" })); + await waitFor(() => expect(prPanelState.latestAutoMerge).toBe(true)); + expect(prPanelState.latestIsManualPrFlow).toBe(true); + expect(screen.getByText("Auto-merge will handle this task automatically.")).toBeInTheDocument(); + }); }); diff --git a/packages/engine/vitest.config.ts b/packages/engine/vitest.config.ts index 7532b738f2..c118c5cfc3 100644 --- a/packages/engine/vitest.config.ts +++ b/packages/engine/vitest.config.ts @@ -74,13 +74,11 @@ export default defineConfig({ "src/__tests__/executor-recovery.test.ts", "src/__tests__/executor-base-commit-capture.test.ts", "src/__tests__/executor-capture-modified-files-attribution.test.ts", - "src/__tests__/triage.test.ts", "src/__tests__/triage-preflight.test.ts", "src/__tests__/scheduler.test.ts", "src/__tests__/scheduler-node-routing.test.ts", "src/__tests__/scheduler-overlap-requeue.test.ts", "src/__tests__/mission-scheduler.test.ts", - "src/__tests__/self-healing.test.ts", "src/__tests__/heartbeat-monitor.test.ts", "src/__tests__/workflow-node-handlers.test.ts", "src/__tests__/workflow-policy-ownership-map.test.ts",