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:
@@ -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.
|
||||||
@@ -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(",");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user