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 <noreply@runfusion.ai>
This commit is contained in:
Fusion Agent
2026-08-20 06:21:27 +00:00
parent 0b71e9f55e
commit c82e0cdeee
2 changed files with 25 additions and 7 deletions

View File

@@ -68,7 +68,12 @@ function createApp(store: TaskStore) {
return app;
}
function createStore(root: string, task: Task, events: string[], publish: (id: string, intake: string) => Promise<Task>) {
function createStore(
root: string,
task: Task,
events: string[],
publish: (this: TaskStore, id: string, intake: string) => Promise<Task>,
) {
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" });

View File

@@ -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<Task>;
}).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 {