fix(fusion): persist priority changes from PATCH /tasks/:id

The dashboard task-edit route destructured every editable body field
except priority, so changing priority via the task-detail modal was
silently dropped before reaching store.updateTask. Wire priority
through with isTaskPriority validation (null resets to default).

Without this fix the priority-aware triage/scheduler/merge ordering
shipped previously had no effect for tasks edited in the dashboard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-01 15:57:38 -07:00
parent c08a872169
commit e72eff47e9
3 changed files with 50 additions and 2 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Fix `PATCH /tasks/:id` silently dropping task priority updates. The route handler in the dashboard server was destructuring every editable field from the request body except `priority`, so changing a task's priority via the dashboard task-detail modal had no effect on disk. The handler now accepts `priority`, validates it against the allowed values (`urgent`, `high`, `normal`, `low`) — `null` resets to the default — and forwards it to `store.updateTask`. Combined with the priority-aware merge queue and sweep ordering shipped earlier, dashboard priority changes now actually shift triage, scheduling, and merge order.

View File

@@ -3188,6 +3188,40 @@ describe("PATCH /tasks/:id", () => {
expect(res.body.dependencies).toEqual(["FN-002"]);
});
it("forwards priority to store.updateTask", async () => {
const updatedTask = { ...FAKE_TASK_DETAIL, priority: "high" as const };
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(updatedTask);
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/KB-001", JSON.stringify({ priority: "high" }), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(store.updateTask).toHaveBeenCalledWith("KB-001", { priority: "high" });
expect(res.body.priority).toBe("high");
});
it("forwards priority=null to store.updateTask (resets to default)", async () => {
const updatedTask = { ...FAKE_TASK_DETAIL, priority: "normal" as const };
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(updatedTask);
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/KB-001", JSON.stringify({ priority: null }), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(store.updateTask).toHaveBeenCalledWith("KB-001", { priority: null });
});
it("rejects unknown priority values with 400", async () => {
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/KB-001", JSON.stringify({ priority: "medium" }), {
"Content-Type": "application/json",
});
expect(res.status).toBe(400);
expect(store.updateTask).not.toHaveBeenCalled();
});
it("returns 409 when changing nodeId on an in-progress task", async () => {
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue({
...FAKE_TASK_DETAIL,

View File

@@ -2,7 +2,9 @@ import { createReadStream } from "node:fs";
import type { TaskStore, Task, TaskDetail, Column } from "@fusion/core";
import {
COLUMNS,
TASK_PRIORITIES,
VALID_TRANSITIONS,
isTaskPriority,
resolveTitleSummarizerSettingsModel,
validateNodeOverrideChange,
} from "@fusion/core";
@@ -1319,7 +1321,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
router.patch("/tasks/:id", async (req, res) => {
try {
const { store: scopedStore } = await getProjectContext(req);
const { title, description, prompt, dependencies, enabledWorkflowSteps, modelProvider, modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, thinkingLevel, assigneeUserId, reviewLevel, executionMode, sourceIssue, nodeId } = req.body;
const { title, description, prompt, priority, dependencies, enabledWorkflowSteps, modelProvider, modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, thinkingLevel, assigneeUserId, reviewLevel, executionMode, sourceIssue, nodeId } = req.body;
const hasBodyField = (field: string) => Object.prototype.hasOwnProperty.call(req.body, field);
// Validate model fields are strings or undefined/null
@@ -1359,6 +1361,12 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
throw new Error(`executionMode must be one of: ${validExecutionModes.join(", ")}`);
}
// Validate priority if provided. `null` resets to the default (`normal`)
// via store.updateTask's null-handling.
if (priority !== undefined && priority !== null && !isTaskPriority(priority)) {
throw new Error(`priority must be one of: ${TASK_PRIORITIES.join(", ")}`);
}
if (enabledWorkflowSteps !== undefined) {
if (!Array.isArray(enabledWorkflowSteps) || !enabledWorkflowSteps.every((id: unknown) => typeof id === "string")) {
throw new Error("enabledWorkflowSteps must be an array of strings");
@@ -1429,6 +1437,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
if (title !== undefined) updates.title = title;
if (description !== undefined) updates.description = description;
if (prompt !== undefined) updates.prompt = prompt;
if (hasBodyField("priority")) updates.priority = priority;
if (dependencies !== undefined) updates.dependencies = dependencies;
if (enabledWorkflowSteps !== undefined) updates.enabledWorkflowSteps = enabledWorkflowSteps;
if (hasBodyField("modelProvider")) updates.modelProvider = validatedModelProvider;
@@ -1461,7 +1470,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
if (err instanceof ApiError) {
throw err;
}
const status = (err instanceof Error ? err.message : String(err)).includes("must be a string") || (err instanceof Error ? err.message : String(err)).includes("must be a non-empty string") || (err instanceof Error ? err.message : String(err)).includes("must be a string or null") || (err instanceof Error ? err.message : String(err)).includes("must be an array of strings") || (err instanceof Error ? err.message : String(err)).includes("thinkingLevel must be one of") || (err instanceof Error ? err.message : String(err)).includes("reviewLevel must be an integer") || (err instanceof Error ? err.message : String(err)).includes("executionMode must be one of") || (err instanceof Error ? err.message : String(err)).includes("sourceIssue") ? 400 : 500;
const status = (err instanceof Error ? err.message : String(err)).includes("must be a string") || (err instanceof Error ? err.message : String(err)).includes("must be a non-empty string") || (err instanceof Error ? err.message : String(err)).includes("must be a string or null") || (err instanceof Error ? err.message : String(err)).includes("must be an array of strings") || (err instanceof Error ? err.message : String(err)).includes("thinkingLevel must be one of") || (err instanceof Error ? err.message : String(err)).includes("reviewLevel must be an integer") || (err instanceof Error ? err.message : String(err)).includes("executionMode must be one of") || (err instanceof Error ? err.message : String(err)).includes("priority must be one of") || (err instanceof Error ? err.message : String(err)).includes("sourceIssue") ? 400 : 500;
throw new ApiError(status, err instanceof Error ? err.message : String(err));
}
});