mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(gateway): roll-up AC-verification gate (guardrails spec 4/4)
A parent could complete / submit_up / escalate_to_ceo once its subtasks were merely terminal — never checking whether the parent's acceptance criteria were actually satisfied. That's how PR #175's half-built umbrella sailed to CEO approval (escalate_to_ceo had no subtask/AC check at all). - TaskService.uncovered_parent_acceptance_criteria(parent): parent ACs not covered by a COMPLETED child (via parent_ac_refs). Safe-by-construction — returns [] unless a child declares coverage, so it is INERT for tasks decomposed before coverage tracking and activates only once a PM maps children to parent criteria. Cancelled children do not count. - _parent_acs_covered_envelope wired into all four roll-up gates: cell_pm_complete, main_pm_complete, submit_up, and escalate_to_ceo (the weakest — previously only journal:decision). isinstance guard keeps it inert under partial mocks. - 4 new tests; 57 task + 89 gateway tests green. Pairs with spec 2 (coverage at decompose-time forces the linkage this enforces).
This commit is contained in:
@@ -621,6 +621,57 @@ async def test_create_generates_ac_ids_and_carries_parent_ac_refs() -> None:
|
||||
assert list(task.parent_ac_refs) == ["parent-ac-1", "parent-ac-2"]
|
||||
|
||||
|
||||
def _svc_with_children(parent: object, child_rows: list[tuple]) -> TaskService:
|
||||
"""TaskService whose get() returns `parent` and whose execute() yields the
|
||||
(status, parent_ac_refs) child rows the coverage primitive selects."""
|
||||
rows = MagicMock()
|
||||
rows.all.return_value = child_rows
|
||||
svc = TaskService(MagicMock(execute=AsyncMock(return_value=rows)))
|
||||
_bind(svc, "get", AsyncMock(return_value=parent))
|
||||
return svc
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uncovered_parent_acs_inert_without_declared_coverage() -> None:
|
||||
# No child declares parent_ac_refs -> coverage tracking inactive -> the gate
|
||||
# is inert (legacy/in-flight tasks are never blocked).
|
||||
parent = _build_task(
|
||||
acceptance_criteria=["a", "b"], acceptance_criteria_ids=["id-a", "id-b"]
|
||||
)
|
||||
svc = _svc_with_children(
|
||||
parent, [(TaskStatus.COMPLETED, []), (TaskStatus.COMPLETED, [])]
|
||||
)
|
||||
assert await svc.uncovered_parent_acceptance_criteria(parent.id) == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uncovered_parent_acs_flags_unsatisfied_and_ignores_cancelled() -> None:
|
||||
parent = _build_task(
|
||||
acceptance_criteria=["crit a", "crit b", "crit c"],
|
||||
acceptance_criteria_ids=["id-a", "id-b", "id-c"],
|
||||
)
|
||||
svc = _svc_with_children(
|
||||
parent,
|
||||
[
|
||||
(TaskStatus.COMPLETED, ["id-a"]), # covers crit a
|
||||
(TaskStatus.CANCELLED, ["id-b"]), # cancelled -> does NOT cover crit b
|
||||
],
|
||||
)
|
||||
assert await svc.uncovered_parent_acceptance_criteria(parent.id) == [
|
||||
"crit b",
|
||||
"crit c",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uncovered_parent_acs_empty_when_all_covered() -> None:
|
||||
parent = _build_task(
|
||||
acceptance_criteria=["a", "b"], acceptance_criteria_ids=["id-a", "id-b"]
|
||||
)
|
||||
svc = _svc_with_children(parent, [(TaskStatus.COMPLETED, ["id-a", "id-b"])])
|
||||
assert await svc.uncovered_parent_acceptance_criteria(parent.id) == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unblock_with_branch_resumes_in_progress() -> None:
|
||||
# A task claimed (has a branch) before it blocked resumes in_progress.
|
||||
|
||||
Reference in New Issue
Block a user