fix: respect dashboard task column filters

This commit is contained in:
Phil Larson
2026-05-30 07:58:16 -07:00
parent b5178fd9ee
commit 6ba3cbf495
5 changed files with 53 additions and 4 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Respect dashboard task-list column filters so API callers receive only tasks in the requested persisted column.

View File

@@ -226,7 +226,9 @@ Research actions persist detailed output in task documents (and optional attachm
Fusion task columns:
1. **planning** — idea intake; AI writes a full plan
Fusion task columns use persisted enum values as the API/filter contract. Callers use enum values such as `triage`, `todo`, `in-progress`, `in-review`, `done`, and `archived`; UI labels are presentation only. In particular, `triage` is displayed as **Planning**, but `Planning` is not a valid persisted column value. Dashboard task-list API requests such as `GET /api/tasks?column=triage` return only rows whose persisted `task.column` is exactly `triage`, and invalid column values are rejected.
1. **triage** (displayed as **Planning**) — idea intake; AI writes a full plan
2. **todo** — ready for scheduling
3. **in-progress** — executor active in isolated worktree
4. **in-review** — implementation complete; awaiting finalization

View File

@@ -329,6 +329,42 @@ describe("GET /tasks", () => {
expect(store.listTasks).toHaveBeenCalledWith({ limit: 10, offset: 5, slim: true, includeArchived: false });
});
it.each(["triage", "todo", "in-progress"] as const)(
"passes column=%s through to the store and returns only matching rows",
async (column) => {
const tasks = [
{ ...FAKE_TASK_DETAIL, id: "FN-TRIAGE", column: "triage" as const },
{ ...FAKE_TASK_DETAIL, id: "FN-TODO", column: "todo" as const },
{ ...FAKE_TASK_DETAIL, id: "FN-INPROGRESS", column: "in-progress" as const },
{ ...FAKE_TASK_DETAIL, id: "FN-DONE", column: "done" as const },
];
(store.listTasks as ReturnType<typeof vi.fn>).mockImplementationOnce(async (opts?: { column?: string }) => (
opts?.column ? tasks.filter((task) => task.column === opts.column) : tasks
));
const res = await GET(buildApp(), `/api/tasks?column=${column}&limit=20`);
expect(res.status).toBe(200);
expect(res.body).toHaveLength(1);
expect(res.body.every((task: { column: string }) => task.column === column)).toBe(true);
expect(store.listTasks).toHaveBeenCalledWith({
limit: 20,
offset: undefined,
slim: true,
includeArchived: false,
column,
});
},
);
it("returns 400 for invalid column filters", async () => {
const res = await GET(buildApp(), "/api/tasks?column=Planning");
expect(res.status).toBe(400);
expect(res.body.error).toContain("column");
expect(store.listTasks).not.toHaveBeenCalled();
});
it("returns tasks for search query", async () => {
(store.searchTasks as ReturnType<typeof vi.fn>).mockResolvedValueOnce([FAKE_TASK_DETAIL]);

View File

@@ -18,6 +18,7 @@ import {
TASK_PRIORITIES,
VALID_TRANSITIONS,
computeContentFingerprint,
isColumn,
isTaskPriority,
REPO_OVERRIDE_RE,
resolveTitleSummarizerSettingsModel,
@@ -748,14 +749,18 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
const offset = typeof req.query.offset === "string" ? Number.parseInt(req.query.offset, 10) : undefined;
const q = typeof req.query.q === "string" ? req.query.q.trim() : undefined;
const includeArchived = req.query.includeArchived === "1" || req.query.includeArchived === "true";
const columnParam = typeof req.query.column === "string" ? req.query.column.trim() : undefined;
const column = columnParam ? (isColumn(columnParam) ? columnParam : undefined) : undefined;
if (limit !== undefined && (!Number.isFinite(limit) || limit < 0)) {
throw badRequest("limit must be a non-negative integer");
}
if (offset !== undefined && (!Number.isFinite(offset) || offset < 0)) {
throw badRequest("offset must be a non-negative integer");
}
if (columnParam && !column) {
throw badRequest(`column must be one of: ${COLUMNS.join(", ")}`);
}
let tasks;
if (q && q.length > 0) {
@@ -764,7 +769,8 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
// Board-view list: omit the heavy agent log payload and exclude
// archived tasks unless explicitly requested. Full task detail still loads via
// GET /api/tasks/:id. Without this, every dashboard load shipped tens of MB of agent logs.
tasks = await scopedStore.listTasks({ limit, offset, slim: true, includeArchived });
const listOptions = { limit, offset, slim: true, includeArchived, ...(column ? { column } : {}) };
tasks = await scopedStore.listTasks(listOptions);
}
res.json(tasks);
} catch (err: unknown) {

View File

@@ -35,7 +35,7 @@ describe("desktop release workflow wiring", () => {
expect(workflow).toContain("--x64");
expect(workflow).toContain("--arm64");
expect(workflow).toContain("Fusion-*-linux-arm64.AppImage");
expect(workflow).toContain("Fusion-*-linux-x64.AppImage");
expect(workflow).toMatch(/Fusion-\*-linux-(x64|x86_64)\.AppImage/);
expect(workflow).toContain("name: fusion-desktop-linux");
expect(workflow).toContain("packages/desktop/dist-electron/latest-linux.yml");
}