From a8b689f0c2541ce71afbe9052ddc5d50c1abd71d Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 4 May 2026 04:46:26 -0700 Subject: [PATCH] test(kanban): regression for status=running rejection at dashboard PATCH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reporter of #19535 explicitly asked for a regression test — covers it here so a future refactor of _set_status_direct can't silently re-enable the direct ready/todo -> running bypass. Asserts both: (a) HTTP 400 with 'running' in the detail message, and (b) the task's status is unchanged after the rejected PATCH (pre-request status preserved, no partial mutation). --- tests/plugins/test_kanban_dashboard_plugin.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/plugins/test_kanban_dashboard_plugin.py b/tests/plugins/test_kanban_dashboard_plugin.py index 4bbc621f1a..0055fc80f0 100644 --- a/tests/plugins/test_kanban_dashboard_plugin.py +++ b/tests/plugins/test_kanban_dashboard_plugin.py @@ -253,6 +253,33 @@ def test_patch_invalid_status(client): assert r.status_code == 400 +def test_patch_status_running_rejected(client): + """Dashboard PATCH cannot transition a task directly to 'running'. + + The only legitimate path into 'running' is through the dispatcher's + ``claim_task`` — which atomically creates a ``task_runs`` row, + claim_lock, expiry, and worker-PID metadata. Allowing a direct set + creates orphaned 'running' tasks with no run row or claim, which + violate the board's run-history invariants. See issue #19535. + """ + t = client.post("/api/plugins/kanban/tasks", json={"title": "x"}).json()["task"] + r = client.patch( + f"/api/plugins/kanban/tasks/{t['id']}", + json={"status": "running"}, + ) + assert r.status_code == 400 + assert "running" in r.json()["detail"] + # Task's status should still be its pre-request value — the direct-set + # was rejected before any mutation. + 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" + + # --------------------------------------------------------------------------- # Comments + Links # ---------------------------------------------------------------------------