mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[chore] logical-gaps: lifecycle-enforcement validators + status-class fixes (5 gaps)
enforcement/task_lifecycle.py:
- drop the spurious VERIFYING->awaiting_documentation legacy edge. The
canonical exit is submit_qa -> awaiting_qa -> (qa_pass) ->
awaiting_documentation; the direct edge bypassed the entire QA review hop
(ungated — no role gate existed for it).
- is_waiting_state: add awaiting_pr_review. The PR-review gate parks the PM on
the reviewer; it is a waiting state. The hard-coded set was never updated
when AWAITING_PR_REVIEW was added to the enum, so the gate status was
miscategorized as active.
foundation/_validate_lifecycle.py:
- _check_status_enum_coverage: replace the tautology (STATUS_GRAPH keys every
Status by construction) with a real bidirectional check — every non-terminal
Status is the source of a transition (catches orphan states), and every
source/target referenced is a real Status member (catches stray-string
targets).
- _check_terminal_exits: split the {COMPLETED, CANCELLED} reachability into a
COMPLETED-path requirement + a cancel-exit requirement. The cancel fan-out
made the old check structurally trivial — a status whose sole exit was cancel
passed with no real forward completion path.
- _check_status_enum_parity (new, registered): cross-check spec.Status against
models.base.TaskStatus at import so the ORM column type and the lifecycle
map cannot drift (TaskType had this guard; Status did not).
tests: verifying->awaiting_documentation rejected, self-fail preserved,
awaiting_pr_review is waiting, mutually-disjoint classification invariant,
status enum parity, stray-string-target / orphan-source / cancel-only-exit
validator rejections.
This commit is contained in:
@@ -17,6 +17,7 @@ from roboco.enforcement.task_lifecycle import (
|
||||
validate_task_transition,
|
||||
)
|
||||
from roboco.exceptions import TaskLifecycleError
|
||||
from roboco.foundation.policy.lifecycle import Status
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# validate_task_transition
|
||||
@@ -231,3 +232,46 @@ def test_validate_cancel_from_awaiting_ceo_raises_for_pm() -> None:
|
||||
validate_task_transition("awaiting_ceo_approval", "cancelled", "cell_pm")
|
||||
# CEO is allowed — no raise.
|
||||
assert validate_task_transition("awaiting_ceo_approval", "cancelled", "ceo") is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# VERIFYING must go through the QA hop (awaiting_qa), not straight to docs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_verifying_to_awaiting_documentation_is_rejected() -> None:
|
||||
"""VERIFYING is the dev self-verification state; the canonical exit is
|
||||
submit_qa -> awaiting_qa -> (qa_pass) -> awaiting_documentation. A spurious
|
||||
verifying->awaiting_documentation edge bypassed the entire QA review hop."""
|
||||
with pytest.raises(TaskLifecycleError):
|
||||
validate_task_transition("verifying", "awaiting_documentation", "qa")
|
||||
|
||||
|
||||
def test_verifying_self_fail_to_needs_revision_still_allowed() -> None:
|
||||
"""The legitimate self-fail out of verifying (QA/PM only) is preserved."""
|
||||
assert validate_task_transition("verifying", "needs_revision", "qa") is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# is_waiting_state must cover the in-path PR-review gate
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_is_waiting_state_includes_awaiting_pr_review() -> None:
|
||||
"""The PR-review gate parks the PM on the reviewer; it is a waiting state,
|
||||
not an active one (the predicates were never updated when AWAITING_PR_REVIEW
|
||||
was added to the enum)."""
|
||||
assert is_waiting_state("awaiting_pr_review") is True
|
||||
assert is_active_state("awaiting_pr_review") is False
|
||||
|
||||
|
||||
def test_status_classification_is_mutually_disjoint() -> None:
|
||||
"""No status may be classified as both active and waiting — the structural
|
||||
invariant that catches miscategorization (the awaiting_pr_review leak was
|
||||
an instance of a status falling into no category)."""
|
||||
active = {s.value for s in Status if is_active_state(s.value)}
|
||||
waiting = {s.value for s in Status if is_waiting_state(s.value)}
|
||||
terminal = {s.value for s in Status if is_terminal_state(s.value)}
|
||||
assert active & waiting == set()
|
||||
assert active & terminal == set()
|
||||
assert waiting & terminal == set()
|
||||
|
||||
Reference in New Issue
Block a user