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:
5
.changeset/task-priority-patch-route-fix.md
Normal file
5
.changeset/task-priority-patch-route-fix.md
Normal 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.
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user