mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -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)
|
||||
|
||||
@@ -514,6 +514,40 @@ async def test_update_returns_none_for_missing(task_setup: dict) -> None:
|
||||
assert await svc.update(uuid4(), title="x") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_acceptance_criteria_restamps_ids_1to1(
|
||||
task_setup: dict,
|
||||
) -> None:
|
||||
# fe-pm delegate-loop root cause: TaskService.update() rewrote
|
||||
# acceptance_criteria without touching acceptance_criteria_ids, leaving a
|
||||
# stale/mismatched id list. A criterion whose TEXT is unchanged must keep
|
||||
# its id (children/findings reference it by id or exact text); a new one
|
||||
# mints a fresh id; a dropped one drops its id.
|
||||
_N = 3
|
||||
svc = task_setup["svc"]
|
||||
task = await svc.create(_req(task_setup, acceptance_criteria=["a", "b", "c"]))
|
||||
old_ids = list(task.acceptance_criteria_ids)
|
||||
updated = await svc.update(task.id, acceptance_criteria=["a", "c", "d"])
|
||||
assert updated is not None
|
||||
assert len(updated.acceptance_criteria_ids) == _N
|
||||
assert updated.acceptance_criteria_ids[0] == old_ids[0] # "a" unchanged
|
||||
assert updated.acceptance_criteria_ids[1] == old_ids[2] # "c" unchanged
|
||||
assert updated.acceptance_criteria_ids[2] not in old_ids # "d" is new
|
||||
assert len(set(updated.acceptance_criteria_ids)) == _N
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_without_acceptance_criteria_leaves_ids_untouched(
|
||||
task_setup: dict,
|
||||
) -> None:
|
||||
svc = task_setup["svc"]
|
||||
task = await svc.create(_req(task_setup, acceptance_criteria=["a", "b"]))
|
||||
old_ids = list(task.acceptance_criteria_ids)
|
||||
updated = await svc.update(task.id, title="renamed")
|
||||
assert updated is not None
|
||||
assert list(updated.acceptance_criteria_ids) == old_ids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_progress(task_setup: dict) -> None:
|
||||
svc = task_setup["svc"]
|
||||
|
||||
@@ -1694,6 +1694,46 @@ async def test_production_replay_root_with_child_and_root_owned_coverage(
|
||||
assert await svc.uncovered_parent_acceptance_criteria(parent.id) == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parent_ac_edit_preserves_ids_children_still_resolve(
|
||||
task_setup: dict, db_session: AsyncSession
|
||||
) -> None:
|
||||
"""A board-redraft-style parent AC edit that drops one criterion and adds
|
||||
another must keep the id of every criterion whose TEXT is unchanged, so a
|
||||
child's existing parent_ac_refs -- by id OR by exact text -- still
|
||||
resolves after the rewrite instead of orphaning."""
|
||||
svc = task_setup["svc"]
|
||||
parent = await svc.create(
|
||||
_req(task_setup, acceptance_criteria=["crit a", "crit b", "crit x"])
|
||||
)
|
||||
id_a, _id_b, id_x = parent.acceptance_criteria_ids
|
||||
child_by_id = await svc.create(
|
||||
_req(task_setup, parent_task_id=parent.id, parent_ac_refs=[id_a])
|
||||
)
|
||||
child_by_id.status = TaskStatus.COMPLETED
|
||||
child_by_text = await svc.create(
|
||||
_req(task_setup, parent_task_id=parent.id, parent_ac_refs=["crit x"])
|
||||
)
|
||||
child_by_text.status = TaskStatus.COMPLETED
|
||||
await db_session.flush()
|
||||
|
||||
# "crit b" dropped, "crit new" added, "crit a"/"crit x" kept (reordered).
|
||||
await svc.update(
|
||||
parent.id,
|
||||
acceptance_criteria=["crit x", "crit a", "crit new"],
|
||||
)
|
||||
await db_session.flush()
|
||||
|
||||
refreshed = await svc.get(parent.id)
|
||||
assert refreshed is not None
|
||||
assert set(refreshed.acceptance_criteria_ids[:2]) == {id_a, id_x}
|
||||
assert refreshed.acceptance_criteria_ids[2] not in {id_a, id_x}
|
||||
|
||||
# Both the id-based and text-based refs still resolve -- only the
|
||||
# brand-new criterion is uncovered.
|
||||
assert await svc.uncovered_parent_acceptance_criteria(parent.id) == ["crit new"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _unblock_dependents
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user