FN-6983: remove branch reattachment warning

Remove the stale branch reattachment affordance from Task Detail while preserving self-healing as the recovery path.

- Remove unused rebind banner styles and mobile action-shell rules.
- Drop stale recoverBranchBinding mocks from TaskDetailModal tests.
- Add rendering coverage proving Task Detail and embedded content do not show reattachment affordances across missing, populated, workspace, and mobile branch states.
- Add a patch changeset for the published Fusion package.

Files changed:
 .changeset/fn-6983-task-detail-rebind-warning.md   |  7 ++
 .../dashboard/app/components/TaskDetailModal.css   | 42 +-----------
 .../TaskDetailModal.create-pr-e2e.test.tsx         |  1 -
 .../TaskDetailModal.create-pr-integration.test.tsx |  1 -
 .../__tests__/TaskDetailModal.rendering.test.tsx   | 80 ++++++++++++++++++++++
 .../__tests__/TaskDetailModal.test-helpers.ts      |  1 -
 6 files changed, 88 insertions(+), 44 deletions(-)

Fusion-Task-Id: FN-6983

Fusion-Task-Lineage: 1a45c38e-f7d0-4df7-94cf-7d1309758e0d
This commit is contained in:
gsxdsm
2026-06-25 01:43:59 -07:00
parent 2d2dd50f32
commit 0c53f46afb
6 changed files with 88 additions and 44 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Stop showing branch reattachment warnings in Task Detail.
category: fix
dev: Removes stale TaskDetailModal rebind-banner CSS/mocks and covers missing-branch workspace shapes.

View File

@@ -2271,48 +2271,8 @@ Narrow mobile task detail surfaces from both Board and List must allow horizonta
gap: var(--space-sm);
}
.rebind-banner {
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding: var(--space-lg);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--color-warning) 8%, transparent);
border: 1px solid color-mix(in srgb, var(--color-warning) 30%, transparent);
}
.rebind-banner-header {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.rebind-banner-headline {
font-weight: 600;
color: var(--text);
}
.rebind-banner-copy {
margin: 0;
color: var(--text);
}
.rebind-banner-result {
color: var(--text-muted);
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.rebind-banner-actions {
display: flex;
align-items: center;
gap: var(--space-sm);
}
@media (max-width: 768px) {
.detail-near-duplicate-banner__actions,
.rebind-banner-actions {
.detail-near-duplicate-banner__actions {
flex-direction: column;
align-items: flex-start;
}

View File

@@ -31,7 +31,6 @@ vi.mock("../../api", async (importOriginal) => {
updateGlobalSettings: vi.fn().mockResolvedValue({}),
pauseTask: vi.fn().mockResolvedValue({}),
unpauseTask: vi.fn().mockResolvedValue({}),
recoverBranchBinding: vi.fn(),
fetchWorkflowResults: vi.fn().mockResolvedValue([]),
fetchTaskReview: vi.fn().mockResolvedValue({ reviewState: { source: "reviewer-agent", items: [], addressing: [] }, automationStatus: null, emptyMessage: "No reviewer feedback yet" }),
refreshTaskReview: vi.fn().mockResolvedValue({ reviewState: undefined, automationStatus: null }),

View File

@@ -37,7 +37,6 @@ vi.mock("../../api", async (importOriginal) => {
updateGlobalSettings: vi.fn().mockResolvedValue({}),
pauseTask: vi.fn().mockResolvedValue({}),
unpauseTask: vi.fn().mockResolvedValue({}),
recoverBranchBinding: vi.fn(),
fetchWorkflowResults: vi.fn().mockResolvedValue([]),
fetchTaskReview: vi.fn().mockResolvedValue({ reviewState: { source: "reviewer-agent", items: [], addressing: [] }, automationStatus: null, emptyMessage: "No reviewer feedback yet" }),
refreshTaskReview: vi.fn().mockResolvedValue({ reviewState: undefined, automationStatus: null }),

View File

@@ -742,6 +742,86 @@ describe("TaskDetailModal", () => {
expect(onRequestClose).toHaveBeenCalledTimes(1);
});
function expectNoBranchReattachmentAffordance(container: HTMLElement): void {
// FN-6983: Task Detail must not ask users to manually reattach branches; self-healing owns recovery.
expect(screen.queryByText(/Branch needs reattachment/i)).not.toBeInTheDocument();
expect(screen.queryByText(/Branch binding lost/i)).not.toBeInTheDocument();
expect(screen.queryByText(/isn't currently attached to a fusion branch/i)).not.toBeInTheDocument();
expect(screen.queryByText(/Reattached branch/i)).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /Reattach branch/i })).not.toBeInTheDocument();
expect(container.querySelector(".rebind-banner")).toBeNull();
expect(container.querySelector(".rebind-banner-actions")).toBeNull();
expect(container.querySelector(".rebind-banner-result")).toBeNull();
}
function renderTaskDetail(task: ReturnType<typeof makeTask>, mobileHeaderMode?: "back") {
return render(
<TaskDetailModal
initialTab="definition"
task={task}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
mobileHeaderMode={mobileHeaderMode}
/>,
);
}
describe("branch reattachment affordance absence", () => {
it.each([
["null branch and worktree", makeTask({ column: "in-review", branch: null, worktree: null })],
["undefined branch with missing worktree", makeTask({ column: "in-review", branch: undefined, worktree: null })],
["populated branch control", makeTask({ column: "in-review", branch: "fusion/fn-099", worktree: "/tmp/fn-099" })],
["non-in-review missing branch", makeTask({ column: "todo", branch: null, worktree: null })],
] as const)("renders no reattachment banner for %s", (_label, task) => {
const { container } = renderTaskDetail(task);
expectNoBranchReattachmentAffordance(container);
});
it("renders no reattachment banner for workspace in-review tasks with no singular branch", () => {
const task = makeTask({
column: "in-review",
worktree: null,
workspaceWorktrees: {
"repo-a": { worktreePath: "/tmp/fn-099/repo-a", branch: "fusion/fn-099", baseCommitSha: "abc123" },
"repo-b": { worktreePath: "/tmp/fn-099/repo-b", branch: "fusion/fn-099" },
},
});
delete (task as { branch?: string | null }).branch;
const { container } = renderTaskDetail(task);
expectNoBranchReattachmentAffordance(container);
});
it("keeps the removed mobile rebind action shell absent in narrow task detail rendering", () => {
const { container } = renderTaskDetail(makeTask({ column: "in-review", branch: null, worktree: null }), "back");
expect(screen.getByRole("button", { name: "Back to task list" })).toBeInTheDocument();
expectNoBranchReattachmentAffordance(container);
});
it("renders no reattachment banner from embedded TaskDetailContent", () => {
const { container } = render(
<TaskDetailContent
task={makeTask({ column: "in-review", branch: null, worktree: null })}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
embedded
/>,
);
expectNoBranchReattachmentAffordance(container);
});
});
it("styles detail-body scrollbar rules", () => {
const css = readDashboardStylesSource();

View File

@@ -37,7 +37,6 @@ vi.mock("../../api", async (importOriginal) => {
updateGlobalSettings: vi.fn().mockResolvedValue({}),
pauseTask: vi.fn().mockResolvedValue({}),
unpauseTask: vi.fn().mockResolvedValue({}),
recoverBranchBinding: vi.fn(),
refreshPrStatus: vi.fn(),
fetchWorkflowResults: vi.fn().mockResolvedValue([]),
fetchTaskReview: vi.fn().mockResolvedValue({ reviewState: { source: "reviewer-agent", items: [], addressing: [] }, automationStatus: null, emptyMessage: "No reviewer feedback yet — this task has not produced reviewer-agent feedback in direct mode." }),