fix(FN-8764): clear stale planning state after refresh
Fusion-Task-Id: FN-8764
This commit is contained in:
7
.changeset/clear-stale-planning-badges.md
Normal file
7
.changeset/clear-stale-planning-badges.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Clear stale Planning badges when refreshed task state shows execution has advanced.
|
||||
category: fix
|
||||
dev: Equal-clock complete snapshots clear stale lifecycle status without erasing newer planner activity.
|
||||
@@ -633,6 +633,43 @@ describe("AppModals", () => {
|
||||
expect(document.querySelector(".list-status-badge")).toHaveTextContent("Planning");
|
||||
expect(screen.getByTestId("task-detail-status-badge")).toHaveTextContent("Planning");
|
||||
});
|
||||
|
||||
const inProgressTask = {
|
||||
...parkedTask,
|
||||
column: "in-progress" as const,
|
||||
status: "planning",
|
||||
updatedAt: "2026-08-05T10:03:00.000Z",
|
||||
columnMovedAt: "2026-08-05T10:03:00.000Z",
|
||||
};
|
||||
act(() => {
|
||||
boardEvents?.["task:moved"]?.({ data: JSON.stringify({
|
||||
task: inProgressTask,
|
||||
from: "triage",
|
||||
to: "in-progress",
|
||||
}) });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.card[data-id="FN-8798"] .card-status-badge')).toHaveTextContent("Planning");
|
||||
expect(document.querySelector(".list-status-badge")).toHaveTextContent("Planning");
|
||||
expect(screen.getByTestId("task-detail-status-badge")).toHaveTextContent("Planning");
|
||||
});
|
||||
|
||||
vi.mocked(taskApi.fetchTasks).mockResolvedValueOnce([{
|
||||
...inProgressTask,
|
||||
status: null,
|
||||
}] as any);
|
||||
act(() => window.dispatchEvent(new Event("focus")));
|
||||
|
||||
await waitFor(() => expect(taskApi.fetchTasks).toHaveBeenCalledTimes(2));
|
||||
await waitFor(() => {
|
||||
const card = document.querySelector('.card[data-id="FN-8798"]');
|
||||
expect(card).toBeInTheDocument();
|
||||
expect(card?.querySelector(".card-status-badge")).not.toBeInTheDocument();
|
||||
expect(document.querySelector(".list-status-badge")).not.toHaveTextContent("Planning");
|
||||
expect(document.querySelector("#task-detail-modal-title")).toHaveTextContent("FN-8798");
|
||||
expect(screen.queryByTestId("task-detail-status-badge")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ModelOnboardingModal wiring", () => {
|
||||
|
||||
@@ -215,6 +215,71 @@ describe("task snapshot lifecycle freshness", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("clears a stale planning status from an equal-clock complete refresh", () => {
|
||||
const current = {
|
||||
...todo,
|
||||
column: "in-progress",
|
||||
status: "planning",
|
||||
updatedAt: "2026-08-05T10:02:00.000Z",
|
||||
recentAgentActivityAt: "2026-08-05T10:02:00.000Z",
|
||||
} as Task;
|
||||
const completeFetch = {
|
||||
...todo,
|
||||
column: "in-progress",
|
||||
status: null,
|
||||
updatedAt: current.updatedAt,
|
||||
} as Task;
|
||||
|
||||
expect(mergeTaskSnapshot(current, completeFetch, { fullSnapshot: true })).toMatchObject({
|
||||
column: "in-progress",
|
||||
status: null,
|
||||
recentAgentActivityAt: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves newer planner activity when an equal-clock refresh has unchanged status", () => {
|
||||
const current = {
|
||||
...todo,
|
||||
column: "triage",
|
||||
status: "needs-replan",
|
||||
updatedAt: "2026-08-05T10:02:00.000Z",
|
||||
recentAgentActivityAt: "2026-08-05T10:03:00.000Z",
|
||||
} as Task;
|
||||
const completeFetch = {
|
||||
...todo,
|
||||
column: "triage",
|
||||
status: "needs-replan",
|
||||
updatedAt: current.updatedAt,
|
||||
} as Task;
|
||||
|
||||
expect(mergeTaskSnapshot(current, completeFetch, { fullSnapshot: true })).toMatchObject({
|
||||
status: "needs-replan",
|
||||
recentAgentActivityAt: current.recentAgentActivityAt,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps column and status from the same lifecycle row", () => {
|
||||
const current = {
|
||||
...todo,
|
||||
column: "in-progress",
|
||||
status: "planning",
|
||||
updatedAt: "2026-08-05T10:02:00.000Z",
|
||||
columnMovedAt: "2026-08-05T10:02:00.000Z",
|
||||
} as Task;
|
||||
const staleCompleteFetch = {
|
||||
...todo,
|
||||
column: "triage",
|
||||
status: null,
|
||||
updatedAt: current.updatedAt,
|
||||
columnMovedAt: current.columnMovedAt,
|
||||
} as Task;
|
||||
|
||||
expect(mergeTaskSnapshot(current, staleCompleteFetch, { fullSnapshot: true })).toMatchObject({
|
||||
column: "in-progress",
|
||||
status: "planning",
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts a genuinely newer column transition", () => {
|
||||
const queued = { ...todo, column: "todo", status: "queued-overlap", updatedAt: "2026-08-05T10:02:00.000Z", columnMovedAt: "2026-08-05T10:02:00.000Z" };
|
||||
const executing = { ...todo, column: "in-progress", status: "executing", updatedAt: "2026-08-05T10:03:00.000Z", columnMovedAt: "2026-08-05T10:03:00.000Z" };
|
||||
|
||||
@@ -313,8 +313,15 @@ export function mergeTaskSnapshot<T extends Task>(
|
||||
|| (options.fullSnapshot === true && columnMovedAtCompare === 0 && updatedAtCompare > 0)
|
||||
// Older rows have no column-move clock. A newer task timestamp is still evidence for a real move.
|
||||
|| (!current.columnMovedAt && !incoming.columnMovedAt && updatedAtCompare > 0));
|
||||
// An equal-clock fetch may resolve a stale client-only status only when it describes the same
|
||||
// lifecycle row. Otherwise accepting its status while rejecting its column would tear the pair.
|
||||
const acceptsEqualClockStatus = acceptsEqualClockFields
|
||||
&& (incoming.column === undefined || incoming.column === current.column);
|
||||
const incomingUpdatesStatus = incoming.status !== undefined
|
||||
&& (current.status === undefined || updatedAtCompare > 0 || incomingMovesColumn);
|
||||
&& (current.status === undefined
|
||||
|| acceptsIncomingSnapshot
|
||||
|| acceptsEqualClockStatus
|
||||
|| incomingMovesColumn);
|
||||
|
||||
// The lifecycle fields are evidence-owned rather than object-spread-owned.
|
||||
merged.column = incomingMovesColumn ? incoming.column : current.column;
|
||||
@@ -325,13 +332,16 @@ export function mergeTaskSnapshot<T extends Task>(
|
||||
? carryAwaitingPlanning(current, incoming)
|
||||
: current.awaitingPlanning;
|
||||
/*
|
||||
FNXC:TaskStatusConsistency 2026-08-05-04:05:
|
||||
FNXC:TaskStatusConsistency 2026-08-07-06:10:
|
||||
`recentAgentActivityAt` is a client-only bridge from an agent-log event to the next task snapshot.
|
||||
Preserve it while a stale/equal payload is rejected so live Planning does not flash back to Queued,
|
||||
but clear it when a newer authoritative row arrives without the marker. Equal-clock sparse events
|
||||
may fill missing fields but cannot replace populated detail metadata; only complete fetches may do so.
|
||||
Preserve it while a stale payload is rejected so live Planning does not flash back to Queued. A
|
||||
complete equal-clock fetch clears it only when that same lifecycle row proves the status changed;
|
||||
otherwise an agent-log that arrived while the fetch was in flight remains newer evidence.
|
||||
*/
|
||||
merged.recentAgentActivityAt = acceptsIncomingSnapshot
|
||||
const equalClockStatusChanged = acceptsEqualClockStatus
|
||||
&& incoming.status !== undefined
|
||||
&& incoming.status !== current.status;
|
||||
merged.recentAgentActivityAt = acceptsIncomingSnapshot || equalClockStatusChanged
|
||||
? incoming.recentAgentActivityAt
|
||||
: current.recentAgentActivityAt;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user