From c82e0cdeeebdf408aba66a025a3ae0b01ec6baa2 Mon Sep 17 00:00:00 2001 From: Fusion Agent Date: Thu, 20 Aug 2026 06:21:27 +0000 Subject: [PATCH] FN-072: preserve the TaskStore receiver during reset publication Ensure confirmed task resets invoke publication with the scoped TaskStore receiver.\n\n- Call resetTaskPublication as a bound store method so async state remains available.\n- Strengthen the lifecycle test to verify receiver identity and publication arguments.\n\nFiles changed:\n .../src/__tests__/task-reset-lifecycle.test.ts | 18 +++++++++++++++---\n .../src/routes/register-task-workflow-routes.ts | 14 ++++++++++----\n 2 files changed, 25 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-072 Fusion-Task-Lineage: 4d75f824-7176-4030-bbaa-c5ce76aab45f Co-authored-by: Fusion --- .../src/__tests__/task-reset-lifecycle.test.ts | 18 +++++++++++++++--- .../routes/register-task-workflow-routes.ts | 14 ++++++++++---- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/dashboard/src/__tests__/task-reset-lifecycle.test.ts b/packages/dashboard/src/__tests__/task-reset-lifecycle.test.ts index 746dfb6efe..c1a58210ec 100644 --- a/packages/dashboard/src/__tests__/task-reset-lifecycle.test.ts +++ b/packages/dashboard/src/__tests__/task-reset-lifecycle.test.ts @@ -68,7 +68,12 @@ function createApp(store: TaskStore) { return app; } -function createStore(root: string, task: Task, events: string[], publish: (id: string, intake: string) => Promise) { +function createStore( + root: string, + task: Task, + events: string[], + publish: (this: TaskStore, id: string, intake: string) => Promise, +) { return { getRootDir: vi.fn().mockReturnValue(root), getSettings: vi.fn().mockResolvedValue({ worktreesDir: ".worktrees" }), @@ -89,7 +94,7 @@ function createStore(root: string, task: Task, events: string[], publish: (id: s describe("POST /tasks/:id/reset", () => { afterEach(() => vi.restoreAllMocks()); - it("fences cancellation, removes worktree and plan, then publishes Planning atomically", async () => { + it("preserves the TaskStore receiver while publishing the confirmed reset", async () => { const root = await mkdtemp(join(tmpdir(), "fusion-reset-route-")); const worktree = join(root, ".worktrees", "fn-400"); const taskDir = join(root, ".fusion", "tasks", "FN-400"); @@ -100,7 +105,12 @@ describe("POST /tasks/:id/reset", () => { const task = taskFixture(worktree); vi.mocked(getRegisteredWorktreeBranches).mockResolvedValue([{ branch: task.branch!, worktreePath: worktree }]); const reset = { ...task, column: "triage", status: "needs-replan", worktree: undefined, branch: undefined, steps: task.steps.map((step) => ({ ...step, status: "pending" as const })) }; - const store = createStore(root, task, events, async () => { + let store!: TaskStore; + store = createStore(root, task, events, async function (this: TaskStore, id, intake) { + void (this as unknown as { asyncLayer: unknown }).asyncLayer; + expect(this).toBe(store); + expect(id).toBe("FN-400"); + expect(intake).toBe("triage"); events.push("published"); return reset; }); @@ -111,6 +121,8 @@ describe("POST /tasks/:id/reset", () => { try { const res = await performRequest(createApp(store), "POST", "/api/tasks/FN-400/reset", JSON.stringify({ confirm: true }), { "content-type": "application/json" }); expect(res.status).toBe(200); + expect(vi.mocked(store.resetTaskPublication)).toHaveBeenCalledWith("FN-400", "triage"); + expect(vi.mocked(store.resetTaskPublication).mock.contexts).toEqual([store]); expect(events).toEqual(["cancelled", "published"]); await expect(readFile(join(taskDir, "PROMPT.md"), "utf8")).rejects.toMatchObject({ code: "ENOENT" }); await expect(readFile(worktree, "utf8")).rejects.toMatchObject({ code: "ENOENT" }); diff --git a/packages/dashboard/src/routes/register-task-workflow-routes.ts b/packages/dashboard/src/routes/register-task-workflow-routes.ts index b1c45e46a2..4e916f5c47 100644 --- a/packages/dashboard/src/routes/register-task-workflow-routes.ts +++ b/packages/dashboard/src/routes/register-task-workflow-routes.ts @@ -3765,13 +3765,19 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork throw conflict(`Reset incomplete; runtime finalization failed: ${error instanceof Error ? error.message : String(error)}`); } - const publish = (scopedStore as TaskStore & { + const storeWithPublisher = scopedStore as TaskStore & { resetTaskPublication?: (taskId: string, intake: string) => Promise; - }).resetTaskPublication; - if (typeof publish !== "function") { + }; + if (typeof storeWithPublisher.resetTaskPublication !== "function") { throw new Error("Atomic task reset publication is unavailable"); } - return publish(req.params.id, intakeColumn); + /* + FNXC:TaskReset 2026-08-20-05:53: + Reset publication is a TaskStore instance method whose PostgreSQL implementation reads + `this.asyncLayer`. Invoke it through the scoped store so the atomic publisher retains its + project-scoped receiver after cleanup and runtime finalization. + */ + return storeWithPublisher.resetTaskPublication(req.params.id, intakeColumn); } finally { if (reservation?.state === "held") { try {