feat(tests): disputed state + fix timestamps on reopen
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
1. New 'disputed' state — one lead approved, the other rejected:
- Both approved → validated (unchanged)
- Both rejected → rejected (unchanged)
- One approves + one rejects → disputed (new)
- DB: ALTER TYPE teststate ADD VALUE 'disputed'
- Notification sent to the approving lead explaining the conflict
with the rejection notes
2. Disputed UI in TestDetailHeader:
- Amber banner showing conflict + rejection reason from notes
- 'Change Vote to Rejected' button for the lead who approved
- Validation indicators shown for disputed state too
3. Fix timestamps on reopen (rejected → draft):
- Keep red_started_at, blue_started_at etc. as historical record
- Only clear paused_at defensively
- Timestamps naturally update when test is re-executed
4. disputed badge (amber) added to all badge color maps
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -314,21 +314,21 @@ class TestEntity:
|
||||
def check_dual_validation(self) -> None:
|
||||
"""Evaluate both leads' votes and advance state if appropriate.
|
||||
|
||||
- Both **approved** -> ``validated``
|
||||
- Either **rejected** -> ``rejected``
|
||||
- Otherwise no change (waiting for the other lead).
|
||||
Rules (v2 — consensus required):
|
||||
- Both **approved** -> ``validated``
|
||||
- Both **rejected** -> ``rejected``
|
||||
- One approved + one rejected -> ``disputed`` (conflict, needs discussion)
|
||||
- Otherwise (one or both still pending) -> no change
|
||||
|
||||
Called automatically by :meth:`validate_red` and :meth:`validate_blue`.
|
||||
Also available as a standalone entry point for backward compatibility
|
||||
when validation fields are set externally.
|
||||
"""
|
||||
self._check_dual_validation()
|
||||
|
||||
def _assert_in_review(self, side: str) -> None:
|
||||
if self.state != TestState.in_review:
|
||||
if self.state not in (TestState.in_review, TestState.disputed):
|
||||
raise InvalidOperationError(
|
||||
f"Cannot validate {side} side while test is in "
|
||||
f"'{self.state.value}' state (must be in_review)"
|
||||
f"'{self.state.value}' state (must be in_review or disputed)"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -339,11 +339,19 @@ class TestEntity:
|
||||
)
|
||||
|
||||
def _check_dual_validation(self) -> None:
|
||||
"""If both leads have voted, advance to validated or rejected."""
|
||||
"""Advance the test state once both leads have voted."""
|
||||
r, b = self.red_validation_status, self.blue_validation_status
|
||||
if r == "rejected" or b == "rejected":
|
||||
self.state = TestState.rejected
|
||||
self._events.append(DomainEvent("dual_validation_rejected"))
|
||||
elif r == "approved" and b == "approved":
|
||||
|
||||
if r == "approved" and b == "approved":
|
||||
self.state = TestState.validated
|
||||
self._events.append(DomainEvent("dual_validation_approved"))
|
||||
|
||||
elif r == "rejected" and b == "rejected":
|
||||
# Full consensus to reject
|
||||
self.state = TestState.rejected
|
||||
self._events.append(DomainEvent("dual_validation_rejected"))
|
||||
|
||||
elif (r == "approved" and b == "rejected") or (r == "rejected" and b == "approved"):
|
||||
# Conflict: one approves, one rejects → needs discussion
|
||||
self.state = TestState.disputed
|
||||
self._events.append(DomainEvent("dual_validation_disputed"))
|
||||
|
||||
Reference in New Issue
Block a user