From 8e50967279e85d077d7a5b9b96a7486b76634ab9 Mon Sep 17 00:00:00 2001 From: Phil Larson Date: Thu, 30 Jul 2026 06:24:57 -0700 Subject: [PATCH] fix: restore current main regression invariants (#2755) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - align the renamed-review CLI regression with the classifier’s resolved-boolean contract - keep GitHub tracking controls expanded across same-task detail and sparse SSE updates - strengthen the sticky tracking regression to wait for the sparse update ## Test plan - `pnpm --filter @runfusion/fusion exec vitest run src/__tests__/cli-active-count-lanes.test.ts` - `pnpm --filter @fusion/engine exec vitest run src/__tests__/restart-recovery-coordinator.test.ts` - `FUSION_DASHBOARD_DEEP=1 pnpm --filter @fusion/dashboard exec vitest run app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx` - `pnpm --filter @fusion/dashboard typecheck` - `pnpm --filter @runfusion/fusion typecheck` - `pnpm check:changesets` --- ...b-tracking-disclosure-and-review-fixture.md | 7 +++++++ .../__tests__/cli-active-count-lanes.test.ts | 18 +++++++++--------- .../app/components/TaskDetailModal.tsx | 7 ++++++- ...al.inline-editing-and-integrations.test.tsx | 8 ++++++++ 4 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 .changeset/github-tracking-disclosure-and-review-fixture.md diff --git a/.changeset/github-tracking-disclosure-and-review-fixture.md b/.changeset/github-tracking-disclosure-and-review-fixture.md new file mode 100644 index 0000000000..91ce40a89c --- /dev/null +++ b/.changeset/github-tracking-disclosure-and-review-fixture.md @@ -0,0 +1,7 @@ +--- +"@fusion/dashboard": patch +--- + +summary: GitHub tracking controls now stay expanded while same-task detail updates preserve disabled tracking state. +category: fix +dev: Reset GitHub tracking disclosure state only when the selected task id changes, and align the renamed-review CLI regression with the classifier's resolved-boolean contract. diff --git a/packages/cli/src/__tests__/cli-active-count-lanes.test.ts b/packages/cli/src/__tests__/cli-active-count-lanes.test.ts index dcad77d8d1..87f9c08e61 100644 --- a/packages/cli/src/__tests__/cli-active-count-lanes.test.ts +++ b/packages/cli/src/__tests__/cli-active-count-lanes.test.ts @@ -131,28 +131,28 @@ and then delegated to a predicate that did not: the card was refused for the ONE allow, and no message anywhere mentions columns. Testing the classifier directly rather than through three route harnesses: it is a pure function and the -defect lives in it. The three call sites passing their sets is asserted structurally below, because a call +defect lives in it. The three call sites passing their resolved answers are asserted structurally below, because a call site that accepts the parameter and does not pass it is the failure mode a unit test cannot see. */ -describe("the shared missing-worktree classifier takes the caller's review lane", () => { +describe("the shared missing-worktree classifier takes the caller's resolved review answer", () => { const FAILURE = "Refusing to start coding agent in missing worktree: /gone"; - it("recognises a renamed review lane when the caller supplies it", async () => { + it("recognises a renamed review lane when the caller resolves it", async () => { const { isInReviewMissingWorktreeSessionStartFailure } = await import("@fusion/engine"); const task = { id: "FN-1", column: "signoff", error: FAILURE } as never; // Pre-fix: `signoff` !== "in-review", so the retry bypass this classifier exists for never applied. - expect(isInReviewMissingWorktreeSessionStartFailure(task, new Set(["signoff"]))).toBe(true); + expect(isInReviewMissingWorktreeSessionStartFailure(task, true)).toBe(true); }); - it("still refuses a column outside the supplied set", async () => { + it("still refuses a column the caller did not resolve as review", async () => { const { isInReviewMissingWorktreeSessionStartFailure } = await import("@fusion/engine"); const task = { id: "FN-2", column: "building", error: FAILURE } as never; - expect(isInReviewMissingWorktreeSessionStartFailure(task, new Set(["signoff"]))).toBe(false); + expect(isInReviewMissingWorktreeSessionStartFailure(task, false)).toBe(false); }); - it("keeps the legacy literal when no set is supplied", async () => { + it("keeps the legacy literal when no resolved answer is supplied", async () => { // Backwards compatibility is the reason the parameter is optional: existing callers must not change. const { isInReviewMissingWorktreeSessionStartFailure } = await import("@fusion/engine"); @@ -160,7 +160,7 @@ describe("the shared missing-worktree classifier takes the caller's review lane" expect(isInReviewMissingWorktreeSessionStartFailure({ id: "FN-4", column: "signoff", error: FAILURE } as never)).toBe(false); }); - it("is called WITH a resolved set at all three surfaces", async () => { + it("is called WITH a resolved review answer at all three surfaces", async () => { /* A call site that accepts the parameter and forgets to pass it is exactly the half-conversion this thread was about, and no unit test on the classifier can see it. Structural, and it names the file so a fourth @@ -177,7 +177,7 @@ describe("the shared missing-worktree classifier takes the caller's review lane" const code = (await readFile(surface, "utf8")).replace(/\/\*[\s\S]*?\*\//g, ""); const call = code.match(/isInReviewMissingWorktreeSessionStartFailure\(([^)]*)\)/); expect(call, `${surface.pathname} does not call the classifier`).toBeTruthy(); - expect(call?.[1], `${surface.pathname} calls it without a resolved review set`).toContain(","); + expect(call?.[1], `${surface.pathname} calls it without a resolved review answer`).toContain(","); } }); }); diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index 7623bdfd95..f7c42dc483 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -1446,13 +1446,18 @@ export function TaskDetailContent({ setEditSourceIssueUrl(task.sourceIssue?.url ?? ""); setEditExecutionMode(normalizeExecutionModeValue(task.executionMode)); setSourceIssueExpanded(false); - setGithubTrackingExpanded(false); setGithubRepoOverrideDraft(workingTask.githubTracking?.repoOverride ?? ""); setGithubTrackingEnabledDraft(null); setGithubRepoOverrideError(null); setIsEditing(false); }, [task.id, task.title, task.description, task.branch, task.baseBranch, task.sourceIssue, task.executionMode, workingTask.githubTracking]); + // Disclosure state belongs to the selected task, not to same-task detail + // refreshes such as GitHub tracking updates or sparse SSE payloads. + useEffect(() => { + setGithubTrackingExpanded(false); + }, [task.id]); + useEffect(() => { setWorkflowEnabledSteps(task.enabledWorkflowSteps); }, [task.id, task.enabledWorkflowSteps]); diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx index 8b06ccdcff..6d95103017 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx @@ -3221,6 +3221,11 @@ describe("TaskDetailModal", () => { }, } as Task); + let signalSparseUpdate!: () => void; + const sparseUpdateApplied = new Promise((resolve) => { + signalSparseUpdate = resolve; + }); + function Harness(): JSX.Element { const [taskState, setTaskState] = useState(baseTask); @@ -3240,6 +3245,7 @@ describe("TaskDetailModal", () => { ...current, githubTracking: undefined, })); + signalSparseUpdate(); }, 0); }} addToast={noop} @@ -3259,6 +3265,8 @@ describe("TaskDetailModal", () => { expect(mockUpdate).toHaveBeenCalledWith("FN-001", { githubTracking: { enabled: false } }, undefined); }); + await sparseUpdateApplied; + await waitFor(() => { expect((screen.getByRole("checkbox", { name: "Enable GitHub tracking" }) as HTMLInputElement).checked).toBe(false); });