Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions plugins/kanban/dashboard/plugin_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,10 @@ def _set_status_direct(
return False

was_running = prev["status"] == "running"
reopening_satisfied_parent = (
prev["status"] in {"done", "archived"}
and new_status not in {"done", "archived"}
)

cur = conn.execute(
"UPDATE tasks SET status = ?, "
Expand All @@ -745,6 +749,37 @@ def _set_status_direct(
"VALUES (?, ?, 'status', ?, ?)",
(task_id, run_id, json.dumps({"status": new_status}), int(time.time())),
)
if reopening_satisfied_parent:
# A parent leaving done/archived invalidates any direct child that
# was sitting in ready solely because that parent used to satisfy
# the dependency gate. Demote those children immediately so the
# dashboard does not keep advertising stale-ready work.
for row in conn.execute(
"SELECT child_id FROM task_links WHERE parent_id = ? ORDER BY child_id",
(task_id,),
).fetchall():
child_id = row["child_id"]
demoted = conn.execute(
"UPDATE tasks SET status = 'todo' "
"WHERE id = ? AND status = 'ready'",
(child_id,),
)
if demoted.rowcount == 1:
conn.execute(
"INSERT INTO task_events (task_id, kind, payload, created_at) "
"VALUES (?, 'status', ?, ?)",
(
child_id,
json.dumps(
{
"status": "todo",
"reason": "parent_reopened",
"parent": task_id,
}
),
int(time.time()),
),
)
# If we re-opened something, children may have gone stale.
if new_status in {"done", "ready"}:
kanban_db.recompute_ready(conn)
Expand Down
37 changes: 37 additions & 0 deletions tests/plugins/test_kanban_dashboard_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,43 @@ def test_patch_drag_drop_move_todo_to_ready(client):
assert child_after["status"] == "ready"


def test_reopening_parent_demotes_ready_child(client):
"""Reopening a completed parent must invalidate ready children immediately.

The dispatcher re-checks parent completion on claim, but the dashboard
should not keep showing a stale child as ready after an operator drags
its parent back out of done for more work.
"""
parent = client.post("/api/plugins/kanban/tasks", json={"title": "p"}).json()["task"]
child = client.post(
"/api/plugins/kanban/tasks",
json={"title": "c", "parents": [parent["id"]]},
).json()["task"]
assert child["status"] == "todo"

r = client.patch(
f"/api/plugins/kanban/tasks/{parent['id']}",
json={"status": "done"},
)
assert r.status_code == 200

child_after_done = client.get(
f"/api/plugins/kanban/tasks/{child['id']}"
).json()["task"]
assert child_after_done["status"] == "ready"

r = client.patch(
f"/api/plugins/kanban/tasks/{parent['id']}",
json={"status": "todo"},
)
assert r.status_code == 200

child_after_reopen = client.get(
f"/api/plugins/kanban/tasks/{child['id']}"
).json()["task"]
assert child_after_reopen["status"] == "todo"


def test_patch_reassign(client):
t = client.post(
"/api/plugins/kanban/tasks",
Expand Down
Loading