fix(tests): fix veto bug preventing disputed state, add manager notification and dispute-resolution routing
This commit is contained in:
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user