From e215558ba705dac6fa4d8521761e3d7841db1af0 Mon Sep 17 00:00:00 2001 From: xxxigm <54813621+xxxigm@users.noreply.github.com> Date: Mon, 18 May 2026 21:02:44 -0700 Subject: [PATCH] test(kanban-dashboard): pin enriched 409 detail and inline error wiring (#26744) - Existing ``test_patch_drag_drop_move_todo_to_ready`` now asserts the enriched 409 detail names the blocking parent (id, quoted title, and current status), so the dashboard always has something actionable to render. - New bundle-assertion test ``test_dashboard_surfaces_ready_blocked_error_inline`` pins the frontend wiring: the ``parseApiErrorMessage`` helper exists, the drag/drop banner runs through it, and the drawer maintains a visible ``patchErr`` state that's cleared between PATCHes and tasks. --- tests/plugins/test_kanban_dashboard_plugin.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/plugins/test_kanban_dashboard_plugin.py b/tests/plugins/test_kanban_dashboard_plugin.py index fbb111ff78..708f9084d9 100644 --- a/tests/plugins/test_kanban_dashboard_plugin.py +++ b/tests/plugins/test_kanban_dashboard_plugin.py @@ -341,6 +341,18 @@ def test_patch_drag_drop_move_todo_to_ready(client): ) assert r.status_code == 409 + # The 409 detail must name the blocking parent so the dashboard can + # render an actionable toast instead of a silent no-op (#26744). + detail = r.json()["detail"] + assert "Cannot move to 'ready'" in detail + assert parent["id"] in detail + assert "'p'" in detail + assert "status=" in detail + # Whatever non-``done`` status the parent currently has must show up + # so the operator knows what to fix. + assert f"status={parent['status']}" in detail + assert parent["status"] != "done" + # Complete the parent. r = client.patch( f"/api/plugins/kanban/tasks/{parent['id']}", @@ -918,6 +930,34 @@ def test_dashboard_done_actions_prompt_for_completion_summary(): assert "body: JSON.stringify(finalPatch)" in bundle +def test_dashboard_surfaces_ready_blocked_error_inline(): + """Regression for #26744: failed status transitions must be surfaced + inline, not swallowed. The drag/drop banner and the drawer's action + row each render the parsed API ``detail`` so operators see *why* + their click did nothing. + """ + repo_root = Path(__file__).resolve().parents[2] + bundle = ( + repo_root / "plugins" / "kanban" / "dashboard" / "dist" / "index.js" + ).read_text() + + # Helper that strips ``"409: {\"detail\":\"…\"}"`` down to the + # human-readable message before it lands in any banner. + assert "function parseApiErrorMessage(err)" in bundle + assert "parsed.detail" in bundle + + # Drag/drop banner now uses the parsed message instead of raw + # ``err.message`` so it no longer leaks HTTP plumbing. + assert "setError(tx(t, \"moveFailed\", \"Move failed: \") + parseApiErrorMessage(err))" in bundle + + # Drawer action row has its own visible error surface and clears it + # on success/refresh so stale failures don't follow the operator + # around. + assert "const [patchErr, setPatchErr] = useState(null);" in bundle + assert "setPatchErr(parseApiErrorMessage(e))" in bundle + assert "setPatchErr(null)" in bundle + + def test_dashboard_dependency_selects_use_value_change_handler(): """Regression for the dependency selects in the task drawer: the add-parent / add-child dropdowns must wire through the shared