fix: restore current main regression invariants (#2755)

## 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`
This commit is contained in:
Phil Larson
2026-07-30 06:24:57 -07:00
committed by GitHub
parent 3da8b90ed9
commit 8e50967279
4 changed files with 30 additions and 10 deletions

View File

@@ -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.

View File

@@ -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. allow, and no message anywhere mentions columns.
Testing the classifier directly rather than through three route harnesses: it is a pure function and the 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. 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"; 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 { isInReviewMissingWorktreeSessionStartFailure } = await import("@fusion/engine");
const task = { id: "FN-1", column: "signoff", error: FAILURE } as never; 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. // 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 { isInReviewMissingWorktreeSessionStartFailure } = await import("@fusion/engine");
const task = { id: "FN-2", column: "building", error: FAILURE } as never; 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. // Backwards compatibility is the reason the parameter is optional: existing callers must not change.
const { isInReviewMissingWorktreeSessionStartFailure } = await import("@fusion/engine"); 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); 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 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 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 code = (await readFile(surface, "utf8")).replace(/\/\*[\s\S]*?\*\//g, "");
const call = code.match(/isInReviewMissingWorktreeSessionStartFailure\(([^)]*)\)/); const call = code.match(/isInReviewMissingWorktreeSessionStartFailure\(([^)]*)\)/);
expect(call, `${surface.pathname} does not call the classifier`).toBeTruthy(); 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(",");
} }
}); });
}); });

View File

@@ -1446,13 +1446,18 @@ export function TaskDetailContent({
setEditSourceIssueUrl(task.sourceIssue?.url ?? ""); setEditSourceIssueUrl(task.sourceIssue?.url ?? "");
setEditExecutionMode(normalizeExecutionModeValue(task.executionMode)); setEditExecutionMode(normalizeExecutionModeValue(task.executionMode));
setSourceIssueExpanded(false); setSourceIssueExpanded(false);
setGithubTrackingExpanded(false);
setGithubRepoOverrideDraft(workingTask.githubTracking?.repoOverride ?? ""); setGithubRepoOverrideDraft(workingTask.githubTracking?.repoOverride ?? "");
setGithubTrackingEnabledDraft(null); setGithubTrackingEnabledDraft(null);
setGithubRepoOverrideError(null); setGithubRepoOverrideError(null);
setIsEditing(false); setIsEditing(false);
}, [task.id, task.title, task.description, task.branch, task.baseBranch, task.sourceIssue, task.executionMode, workingTask.githubTracking]); }, [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(() => { useEffect(() => {
setWorkflowEnabledSteps(task.enabledWorkflowSteps); setWorkflowEnabledSteps(task.enabledWorkflowSteps);
}, [task.id, task.enabledWorkflowSteps]); }, [task.id, task.enabledWorkflowSteps]);

View File

@@ -3221,6 +3221,11 @@ describe("TaskDetailModal", () => {
}, },
} as Task); } as Task);
let signalSparseUpdate!: () => void;
const sparseUpdateApplied = new Promise<void>((resolve) => {
signalSparseUpdate = resolve;
});
function Harness(): JSX.Element { function Harness(): JSX.Element {
const [taskState, setTaskState] = useState(baseTask); const [taskState, setTaskState] = useState(baseTask);
@@ -3240,6 +3245,7 @@ describe("TaskDetailModal", () => {
...current, ...current,
githubTracking: undefined, githubTracking: undefined,
})); }));
signalSparseUpdate();
}, 0); }, 0);
}} }}
addToast={noop} addToast={noop}
@@ -3259,6 +3265,8 @@ describe("TaskDetailModal", () => {
expect(mockUpdate).toHaveBeenCalledWith("FN-001", { githubTracking: { enabled: false } }, undefined); expect(mockUpdate).toHaveBeenCalledWith("FN-001", { githubTracking: { enabled: false } }, undefined);
}); });
await sparseUpdateApplied;
await waitFor(() => { await waitFor(() => {
expect((screen.getByRole("checkbox", { name: "Enable GitHub tracking" }) as HTMLInputElement).checked).toBe(false); expect((screen.getByRole("checkbox", { name: "Enable GitHub tracking" }) as HTMLInputElement).checked).toBe(false);
}); });