fix(tasks): reconcile acceptance_criteria_ids at the update chokepoint (#682)

Every post-create rewrite of acceptance_criteria (task PATCH route,
prompter update_live_draft / _patch_batch_child / update_live_batch)
routes through TaskService.update()'s generic field loop, which
overwrote the criteria without touching acceptance_criteria_ids —
leaving ids mismatched or empty, and an empty id list silently
disabled the parent-coverage gate entirely.

- New pure _reconcile_ac_ids: one id per new criterion; text-unchanged
  criteria keep their id (children and findings reference criteria by
  id or exact text — a blanket re-mint would orphan every live
  reference), new/reworded text mints fresh, dropped criteria drop
  theirs. create() now stamps through the same helper (explicitly
  supplied ids still win).
- update() derives acceptance_criteria_ids whenever acceptance_criteria
  is rewritten without an explicit id list.
- The parent-coverage gate self-heals a criteria-bearing row whose ids
  are empty/out-of-length (re-stamp in place) instead of returning
  early and silently waiving coverage for the whole subtree.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-24 14:18:58 +02:00
committed by GitHub
co-authored by Renn F
parent c4ba351ae0
commit 3516d925fe
5 changed files with 250 additions and 18 deletions
@@ -207,6 +207,22 @@ async def test_update_live_draft_main_pm_updates_and_hands_off(
assert task.team == Team.MAIN_PM
@pytest.mark.asyncio
async def test_update_live_draft_reconciles_acceptance_criteria_ids(
redraft_setup: dict,
) -> None:
# The wiping path: update_live_draft patches acceptance_criteria via the
# generic TaskService.update(), which used to leave acceptance_criteria_ids
# stale/mismatched against the revised criteria (the fe-pm delegate-loop
# incident). Must come out 1:1 with the new criteria after a redraft.
_N = 2
task = redraft_setup["mk"](True)
await redraft_setup["db"].flush()
await redraft_setup["svc"].update_live_draft(task.id, _DRAFT, route="main_pm")
assert len(task.acceptance_criteria_ids) == len(task.acceptance_criteria) == _N
assert len(set(task.acceptance_criteria_ids)) == _N
@pytest.mark.asyncio
async def test_update_live_draft_reboard_resets_flag(redraft_setup: dict) -> None:
task = redraft_setup["mk"](True)