feat(tests): add red_review/blue_review states to test state machine
This commit is contained in:
@@ -23,7 +23,11 @@ from app.domain.test_entity import (
|
||||
VALID_TRANSITIONS,
|
||||
DomainEvent,
|
||||
)
|
||||
from app.domain.errors import BusinessRuleViolation, InvalidStateTransition
|
||||
from app.domain.errors import (
|
||||
BusinessRuleViolation,
|
||||
InvalidOperationError,
|
||||
InvalidStateTransition,
|
||||
)
|
||||
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────
|
||||
@@ -97,9 +101,15 @@ def test_validated_is_terminal():
|
||||
("draft", "red_executing", True),
|
||||
("draft", "validated", False),
|
||||
("draft", "blue_evaluating", False),
|
||||
("red_executing", "blue_evaluating", True),
|
||||
("red_executing", "red_review", True),
|
||||
("red_executing", "blue_evaluating", False),
|
||||
("red_executing", "draft", False),
|
||||
("blue_evaluating", "in_review", True),
|
||||
("red_review", "blue_evaluating", True),
|
||||
("red_review", "red_executing", True),
|
||||
("blue_evaluating", "blue_review", True),
|
||||
("blue_evaluating", "in_review", False),
|
||||
("blue_review", "in_review", True),
|
||||
("blue_review", "blue_evaluating", True),
|
||||
("in_review", "validated", True),
|
||||
("in_review", "rejected", True),
|
||||
("in_review", "draft", False),
|
||||
@@ -187,10 +197,9 @@ def test_start_execution_from_wrong_state():
|
||||
def test_submit_red_evidence():
|
||||
e = _entity("red_executing", red_started_at=datetime.utcnow())
|
||||
total_paused = e.submit_red_evidence()
|
||||
assert e.state == TestState.blue_evaluating
|
||||
assert e.state == TestState.red_review
|
||||
assert total_paused == 0
|
||||
assert e.blue_started_at is not None
|
||||
assert e.blue_paused_seconds == 0
|
||||
assert e.blue_started_at is None
|
||||
|
||||
|
||||
def test_submit_red_evidence_auto_resumes():
|
||||
@@ -201,13 +210,55 @@ def test_submit_red_evidence_auto_resumes():
|
||||
assert total_paused >= 40
|
||||
|
||||
|
||||
# ── 5b. Lifecycle: red_review gate ───────────────────────────────────
|
||||
|
||||
|
||||
def test_submit_red_evidence_now_goes_to_red_review():
|
||||
e = _entity("red_executing", red_started_at=datetime.utcnow())
|
||||
total_paused = e.submit_red_evidence()
|
||||
assert e.state == TestState.red_review
|
||||
assert total_paused == 0
|
||||
# blue timer must NOT start yet — that's approve_red_review's job
|
||||
assert e.blue_started_at is None
|
||||
|
||||
|
||||
def test_approve_red_review_moves_to_blue_evaluating():
|
||||
e = _entity("red_review")
|
||||
e.approve_red_review()
|
||||
assert e.state == TestState.blue_evaluating
|
||||
assert e.blue_started_at is not None
|
||||
assert e.blue_paused_seconds == 0
|
||||
assert any(ev.name == "red_review_approved" for ev in e.events)
|
||||
|
||||
|
||||
def test_approve_red_review_wrong_state():
|
||||
e = _entity("red_executing")
|
||||
with pytest.raises(InvalidStateTransition):
|
||||
e.approve_red_review()
|
||||
|
||||
|
||||
def test_reopen_red_review_moves_to_red_executing():
|
||||
e = _entity("red_review", red_paused_seconds=50)
|
||||
e.reopen_red_review()
|
||||
assert e.state == TestState.red_executing
|
||||
assert e.red_started_at is not None
|
||||
assert e.red_paused_seconds == 0
|
||||
assert any(ev.name == "red_review_reopened" for ev in e.events)
|
||||
|
||||
|
||||
def test_reopen_red_review_wrong_state():
|
||||
e = _entity("blue_evaluating")
|
||||
with pytest.raises(InvalidStateTransition):
|
||||
e.reopen_red_review()
|
||||
|
||||
|
||||
# ── 6. Lifecycle: submit_blue_evidence ───────────────────────────────
|
||||
|
||||
|
||||
def test_submit_blue_evidence():
|
||||
e = _entity("blue_evaluating", blue_started_at=datetime.utcnow())
|
||||
total_paused = e.submit_blue_evidence()
|
||||
assert e.state == TestState.in_review
|
||||
assert e.state == TestState.blue_review
|
||||
assert total_paused == 0
|
||||
|
||||
|
||||
@@ -219,6 +270,62 @@ def test_submit_blue_evidence_auto_resumes():
|
||||
assert total_paused >= 25
|
||||
|
||||
|
||||
# ── 6b. Lifecycle: blue_review gate ──────────────────────────────────
|
||||
|
||||
|
||||
def test_approve_blue_review_moves_to_in_review():
|
||||
e = _entity("blue_review")
|
||||
e.approve_blue_review()
|
||||
assert e.state == TestState.in_review
|
||||
assert any(ev.name == "blue_review_approved" for ev in e.events)
|
||||
|
||||
|
||||
def test_approve_blue_review_wrong_state():
|
||||
e = _entity("blue_evaluating")
|
||||
with pytest.raises(InvalidStateTransition):
|
||||
e.approve_blue_review()
|
||||
|
||||
|
||||
def test_reopen_blue_review_moves_to_blue_evaluating():
|
||||
e = _entity("blue_review", blue_paused_seconds=30)
|
||||
e.reopen_blue_review()
|
||||
assert e.state == TestState.blue_evaluating
|
||||
assert e.blue_started_at is not None
|
||||
assert e.blue_paused_seconds == 0
|
||||
assert any(ev.name == "blue_review_reopened" for ev in e.events)
|
||||
|
||||
|
||||
def test_reopen_blue_review_wrong_state():
|
||||
e = _entity("in_review")
|
||||
with pytest.raises(InvalidStateTransition):
|
||||
e.reopen_blue_review()
|
||||
|
||||
|
||||
def test_flag_blue_review_gap_moves_to_in_review():
|
||||
e = _entity("blue_review")
|
||||
e.flag_blue_review_gap()
|
||||
assert e.state == TestState.in_review
|
||||
assert any(ev.name == "blue_review_gap_flagged" for ev in e.events)
|
||||
|
||||
|
||||
def test_flag_blue_review_gap_wrong_state():
|
||||
e = _entity("blue_evaluating")
|
||||
with pytest.raises(InvalidStateTransition):
|
||||
e.flag_blue_review_gap()
|
||||
|
||||
|
||||
def test_validate_red_not_reachable_from_red_review():
|
||||
e = _entity("red_review")
|
||||
with pytest.raises(InvalidOperationError, match="must be in_review"):
|
||||
e.validate_red("approved", by=uuid.uuid4())
|
||||
|
||||
|
||||
def test_validate_blue_not_reachable_from_blue_review():
|
||||
e = _entity("blue_review")
|
||||
with pytest.raises(InvalidOperationError, match="must be in_review"):
|
||||
e.validate_blue("approved", by=uuid.uuid4())
|
||||
|
||||
|
||||
# ── 7. pause_timer / resume_timer ────────────────────────────────────
|
||||
|
||||
|
||||
@@ -408,9 +515,15 @@ def test_full_lifecycle_happy_path():
|
||||
assert e.state == TestState.red_executing
|
||||
|
||||
e.submit_red_evidence()
|
||||
assert e.state == TestState.red_review
|
||||
|
||||
e.approve_red_review()
|
||||
assert e.state == TestState.blue_evaluating
|
||||
|
||||
e.submit_blue_evidence()
|
||||
assert e.state == TestState.blue_review
|
||||
|
||||
e.approve_blue_review()
|
||||
assert e.state == TestState.in_review
|
||||
|
||||
e.validate_red("approved", by=uid_red)
|
||||
@@ -428,7 +541,9 @@ def test_full_lifecycle_rejection_reopen():
|
||||
e = _entity("draft")
|
||||
e.start_execution()
|
||||
e.submit_red_evidence()
|
||||
e.approve_red_review()
|
||||
e.submit_blue_evidence()
|
||||
e.approve_blue_review()
|
||||
e.validate_red("rejected", by=uuid.uuid4())
|
||||
assert e.state == TestState.rejected
|
||||
|
||||
|
||||
Reference in New Issue
Block a user