fix(kanban): demote ready children when a parent is reopened

This commit is contained in:
Drexuxux 2026-05-18 20:17:21 -07:00 committed by Teknium
parent 9281599b6f
commit 917e51858d
2 changed files with 72 additions and 0 deletions

View file

@ -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 = ?, "
@ -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)

View file

@ -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",