feat(FN-4990): complete Step 3 — guard out-of-order done updates

Fusion-Task-Id: FN-4990
Fusion-Task-Lineage: 167a6011-d246-45bd-ad7a-1f2fe8b99323
This commit is contained in:
Fusion (runfusion.ai)
2026-05-18 02:23:28 -07:00
committed by gsxdsm
parent 7bcffa5a58
commit dca221829d
2 changed files with 79 additions and 2 deletions

View File

@@ -0,0 +1,57 @@
import { beforeAll, beforeEach, afterEach, afterAll, describe, expect, it } from "vitest";
import { createSharedTaskStoreTestHarness } from "./store-test-helpers.js";
describe("TaskStore.updateStep step-order guard", () => {
const harness = createSharedTaskStoreTestHarness();
beforeAll(harness.beforeAll);
beforeEach(harness.beforeEach);
afterEach(harness.afterEach);
afterAll(harness.afterAll);
it("no-ops out-of-order done updates when an earlier step is pending", async () => {
const store = harness.store();
const task = await harness.createTaskWithSteps();
await store.updateStep(task.id, 0, "done");
const updated = await store.updateStep(task.id, 2, "done");
expect(updated.steps[2].status).toBe("pending");
expect(updated.log.some((entry) => entry.action.includes("Ignored out-of-order done for step 2"))).toBe(true);
});
it("allows done when prior steps are skipped", async () => {
const store = harness.store();
const task = await harness.createTaskWithSteps();
await store.updateStep(task.id, 0, "done");
await store.updateStep(task.id, 1, "skipped");
const updated = await store.updateStep(task.id, 2, "done");
expect(updated.steps[2].status).toBe("done");
expect(updated.currentStep).toBe(3);
});
it("allows done when prior steps are done and advances currentStep", async () => {
const store = harness.store();
const task = await harness.createTaskWithSteps();
await store.updateStep(task.id, 0, "done");
await store.updateStep(task.id, 1, "done");
const updated = await store.updateStep(task.id, 2, "done");
expect(updated.steps[2].status).toBe("done");
expect(updated.currentStep).toBe(3);
});
it("keeps done→in-progress regression guard behavior", async () => {
const store = harness.store();
const task = await harness.createTaskWithSteps();
await store.updateStep(task.id, 0, "done");
const updated = await store.updateStep(task.id, 0, "in-progress");
expect(updated.steps[0].status).toBe("done");
expect(updated.log.some((entry) => entry.action.includes("Ignored done→in-progress regression"))).toBe(true);
});
});

View File

@@ -5190,14 +5190,34 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
return task;
}
if (status === "done") {
for (let i = 0; i < stepIndex; i++) {
const priorStatus = task.steps[i].status;
if (priorStatus === "pending" || priorStatus === "in-progress") {
const ts = new Date().toISOString();
task.updatedAt = ts;
task.log.push({
timestamp: ts,
action:
`Ignored out-of-order ${status} for step ${stepIndex} (${task.steps[stepIndex].name}) — ` +
`earlier step ${i} (${task.steps[i].name}) is still ${priorStatus}`,
});
await this.atomicWriteTaskJson(dir, task);
if (this.isWatching) this.taskCache.set(id, { ...task });
this.emit("task:updated", task);
return task;
}
}
}
task.steps[stepIndex].status = status;
task.updatedAt = new Date().toISOString();
// Advance currentStep to first non-done step
// Advance currentStep to first non-done/non-skipped step
if (status === "done") {
while (
task.currentStep < task.steps.length &&
task.steps[task.currentStep].status === "done"
(task.steps[task.currentStep].status === "done" || task.steps[task.currentStep].status === "skipped")
) {
task.currentStep++;
}