fix(tests): fix veto bug preventing disputed state, add manager notification and dispute-resolution routing

This commit is contained in:
kitos
2026-07-06 12:40:05 +02:00
parent 388c9773ab
commit f53e124c50
7 changed files with 448 additions and 8 deletions
+61 -4
View File
@@ -83,7 +83,10 @@ VALID_TRANSITIONS: dict[TestState, list[TestState]] = {
TestState.blue_evaluating: [TestState.blue_review],
TestState.blue_review: [TestState.in_review, TestState.blue_evaluating],
TestState.in_review: [TestState.validated, TestState.rejected, TestState.disputed],
TestState.disputed: [TestState.validated, TestState.rejected],
TestState.disputed: [
TestState.validated, TestState.rejected,
TestState.red_executing, TestState.blue_evaluating,
],
TestState.rejected: [TestState.draft],
TestState.validated: [],
}
@@ -627,6 +630,44 @@ class TestEntity:
# Call self._events.append()
self._events.append(DomainEvent("test_reopened"))
def resolve_dispute_reject(self, target: str) -> None:
"""Resolve a ``disputed`` test by routing rework to the team at fault.
Called when the lead who originally approved flips their vote to
agree with the rejection. Rather than the generic terminal
``rejected`` state (which forces a full draft restart for both
teams), the flipping lead identifies WHICH side's work needs
redoing — the test goes straight back to that team's active queue
so the other team's work is left untouched.
Args:
target (str): ``"red"`` or ``"blue"`` — which team must redo work.
Returns:
None
"""
target_state = TestState.red_executing if target == "red" else TestState.blue_evaluating
self._transition(target_state)
# Both leads must re-vote once the test reaches in_review again —
# clear decisions but keep notes (context for the team doing rework).
self.red_validation_status = None
self.red_validated_by = None
self.red_validated_at = None
self.blue_validation_status = None
self.blue_validated_by = None
self.blue_validated_at = None
self.paused_at = None
if target == "red":
self.red_started_at = datetime.utcnow()
self.red_paused_seconds = 0
else:
self.blue_started_at = datetime.utcnow()
self.blue_paused_seconds = 0
self._events.append(DomainEvent("dispute_resolved_to_rework", {"target": target}))
# -- Private -------------------------------------------------------
def _auto_resume(self) -> int:
@@ -694,7 +735,14 @@ class TestEntity:
# Define function _check_dual_validation
def _check_dual_validation(self) -> None:
"""Advance the test state once both leads have voted."""
"""Advance the test state once enough leads have voted.
A genuine conflict (one lead's *recorded* decision disagreeing with
the other's) routes to ``disputed`` — there are two opinions to
reconcile. A lone rejection while the other side hasn't voted yet
isn't a conflict (nothing to disagree with), so it still vetoes
straight to ``rejected`` without waiting for the second vote.
"""
r, b = self.red_validation_status, self.blue_validation_status
if r == "approved" and b == "approved":
@@ -702,7 +750,16 @@ class TestEntity:
# Call self._events.append()
self._events.append(DomainEvent("dual_validation_approved"))
elif r == "rejected" or b == "rejected":
# Any rejection is a veto — one lead can reject without waiting for the other
elif r == "rejected" and b == "rejected":
self.state = TestState.rejected
self._events.append(DomainEvent("dual_validation_rejected"))
elif (r == "approved" and b == "rejected") or (r == "rejected" and b == "approved"):
self.state = TestState.disputed
self._events.append(DomainEvent("dual_validation_disputed"))
elif r == "rejected" or b == "rejected":
# One side rejected while the other hasn't voted yet — no
# disagreement to dispute yet, just a straightforward veto.
self.state = TestState.rejected
self._events.append(DomainEvent("dual_validation_rejected"))