From e8ce7b83fa206dbbe007af7f3e3f7c576d72aef8 Mon Sep 17 00:00:00 2001 From: kronexoi <201800237+kronexoi@users.noreply.github.com> Date: Mon, 18 May 2026 20:38:26 -0700 Subject: [PATCH] fix(kanban): reject direct running transitions in dashboard bulk updates Salvages #24050 by @kronexoi. The single-task PATCH already rejects direct status='running' since it bypasses the dispatcher/claim invariant, but the bulk-update endpoint still accepted it. Aligns bulk with single by emitting an error result row for any 'running' entry. --- plugins/kanban/dashboard/plugin_api.py | 12 ++++++++- tests/plugins/test_kanban_dashboard_plugin.py | 25 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/plugins/kanban/dashboard/plugin_api.py b/plugins/kanban/dashboard/plugin_api.py index 5ed0e6144d..f13755dfb7 100644 --- a/plugins/kanban/dashboard/plugin_api.py +++ b/plugins/kanban/dashboard/plugin_api.py @@ -913,7 +913,17 @@ def bulk_update(payload: BulkTaskBody, board: Optional[str] = Query(None)): ok = kanban_db.unblock_task(conn, tid) else: ok = _set_status_direct(conn, tid, "ready") - elif s in {"todo", "running", "triage"}: + elif s == "running": + entry.update( + ok=False, + error=( + "Cannot set status to 'running' directly; " + "use the dispatcher/claim path" + ), + ) + results.append(entry) + continue + elif s in {"todo", "triage"}: ok = _set_status_direct(conn, tid, s) else: entry.update(ok=False, error=f"unknown status {s!r}") diff --git a/tests/plugins/test_kanban_dashboard_plugin.py b/tests/plugins/test_kanban_dashboard_plugin.py index 50da5071e3..fbb111ff78 100644 --- a/tests/plugins/test_kanban_dashboard_plugin.py +++ b/tests/plugins/test_kanban_dashboard_plugin.py @@ -880,6 +880,31 @@ def test_bulk_status_done_forwards_completion_summary(client): conn.close() +def test_bulk_status_running_rejected(client): + """Bulk updates must match single-task PATCH: direct 'running' is invalid.""" + t = client.post("/api/plugins/kanban/tasks", json={"title": "x"}).json()["task"] + + r = client.post( + "/api/plugins/kanban/tasks/bulk", + json={"ids": [t["id"]], "status": "running"}, + ) + + assert r.status_code == 200 + results = r.json()["results"] + assert len(results) == 1 + assert results[0]["id"] == t["id"] + assert results[0]["ok"] is False + assert "running" in results[0]["error"] + + board = client.get("/api/plugins/kanban/board").json() + statuses = { + tt["id"]: col["name"] + for col in board["columns"] + for tt in col["tasks"] + } + assert statuses.get(t["id"]) != "running" + + def test_dashboard_done_actions_prompt_for_completion_summary(): repo_root = Path(__file__).resolve().parents[2] bundle = (