feat(tests): let managers escalate and directly resolve disputed tests
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled

Request Discussion only ever nudged the peer lead, and the one-time
manager notification when a dispute starts had no real follow-up
action attached to it — a manager could be told a dispute existed but
had no way to actually do anything about it. Adds:

- POST /tests/{id}/escalate-to-manager — either lead can explicitly
  ask managers to step in, distinct from the passive initial notice.
- POST /tests/{id}/manager-resolve-dispute — a manager decides the
  final outcome (validated/rejected) directly, bypassing both leads'
  votes entirely, reusing the same dual-validation event dispatch so
  Jira/notifications behave like any other resolution.

Both leads are notified of the manager's final call, win or lose.
This commit is contained in:
kitos
2026-07-15 13:03:01 +02:00
parent 32f4fd25bd
commit 3a01facd46
9 changed files with 444 additions and 1 deletions
+21
View File
@@ -672,6 +672,27 @@ class TestEntity:
self._events.append(DomainEvent("dispute_resolved_to_rework", {"target": target}))
def resolve_dispute_by_manager(self, outcome: str) -> None:
"""A manager makes the final call on a disputed test, ending the standoff.
Unlike ``resolve_dispute_reject`` (a lead flipping their own vote),
this bypasses both leads entirely — the manager's decision is final
and terminal, going straight to ``validated`` or the normal
``rejected`` dead-end (which any lead can later reopen via the
standard reopen-to-draft flow, same as any other rejection).
Args:
outcome (str): ``"validated"`` or ``"rejected"``.
"""
if outcome not in ("validated", "rejected"):
raise InvalidOperationError("outcome must be 'validated' or 'rejected'")
target_state = TestState.validated if outcome == "validated" else TestState.rejected
self._transition(target_state)
self._events.append(
DomainEvent("dual_validation_approved" if outcome == "validated" else "dual_validation_rejected")
)
# -- Private -------------------------------------------------------
def _auto_resume(self) -> int:
+74
View File
@@ -65,6 +65,7 @@ from app.schemas.test import (
TestClassificationUpdate,
TestCreate,
TestHold,
TestManagerResolveDispute,
TestOut,
TestRedReview,
TestRedUpdate,
@@ -152,6 +153,7 @@ from app.services.test_workflow_service import (
validate_as_red_lead as wf_validate_red,
validate_as_blue_lead as wf_validate_blue,
resolve_dispute as wf_resolve_dispute,
resolve_dispute_by_manager as wf_resolve_dispute_by_manager,
reopen_test as wf_reopen,
handle_remediation_completed as wf_handle_remediation,
get_retest_chain as wf_get_retest_chain,
@@ -1192,6 +1194,78 @@ def resolve_dispute(
return test
# ---------------------------------------------------------------------------
# POST /tests/{id}/escalate-to-manager — disputed: notify managers to step in
# ---------------------------------------------------------------------------
@router.post("/{test_id}/escalate-to-manager")
def escalate_to_manager(
test_id: uuid.UUID,
db: Session = Depends(get_db),
current_user: User = Depends(require_any_role_strict("red_lead", "blue_lead")),
):
"""Either lead on a disputed test can escalate it for a manager to decide.
Unlike the automatic manager notification sent when a test first becomes
disputed, this is an explicit, repeatable nudge — a lead pulls this when
peer discussion (Request Discussion) isn't going anywhere.
"""
from app.services.notification_service import notify_role_with_email
test = crud_get_test_or_raise(db, test_id)
if test.state.value != "disputed":
from app.domain.errors import BusinessRuleViolation
raise BusinessRuleViolation("Test is not in disputed state")
try:
notify_role_with_email(
db,
role="manager",
type="validation_disputed",
title="Dispute escalated — needs your decision",
message=(
f'{current_user.username} escalated test "{test.name}" — the leads could not '
f'resolve their disagreement. Please review and decide the final outcome.'
),
entity_type="test",
entity_id=test.id,
)
except Exception as e:
import logging
logging.getLogger(__name__).warning("Failed to notify managers of escalation: %s", e)
log_action(
db, user_id=current_user.id, action="escalate_dispute_to_manager",
entity_type="test", entity_id=test.id, details={"test_name": test.name},
)
db.commit()
return {"status": "escalated", "message": "Managers have been notified"}
# ---------------------------------------------------------------------------
# POST /tests/{id}/manager-resolve-dispute — manager makes the final call
# ---------------------------------------------------------------------------
@router.post("/{test_id}/manager-resolve-dispute", response_model=TestOut)
def manager_resolve_dispute(
test_id: uuid.UUID,
payload: TestManagerResolveDispute,
db: Session = Depends(get_db),
current_user: User = Depends(require_any_role_strict("manager")),
) -> TestOut:
"""A manager decides the final outcome of a disputed test directly."""
test = crud_get_test_with_technique(db, test_id)
with UnitOfWork(db) as uow:
test = wf_resolve_dispute_by_manager(db, test, current_user, payload.outcome, notes=payload.notes)
uow.commit()
db.refresh(test)
return test
# ---------------------------------------------------------------------------
# POST /tests/{id}/reopen — rejected → draft
# ---------------------------------------------------------------------------
+7
View File
@@ -185,6 +185,13 @@ class TestResolveDispute(BaseModel):
notes: str | None = None
class TestManagerResolveDispute(BaseModel):
"""Payload sent by a manager making the final call on a disputed test."""
outcome: str # "validated" | "rejected"
notes: str | None = None
# ── Red Lead review gate (pre-Blue-Team) ────────────────────────────
@@ -1341,6 +1341,50 @@ def resolve_dispute(db: Session, test: Test, user: User, target_team: str, notes
return test
def resolve_dispute_by_manager(db: Session, test: Test, user: User, outcome: str, notes: str | None = None) -> Test:
"""A manager makes the final call on a disputed test — validated or rejected.
Unlike ``resolve_dispute`` (only the approving lead flipping their own
vote), this is a manager override that ends the standoff directly,
without either lead having to concede. Reuses the same dual-validation
event dispatch as a normal in_review resolution so Jira/notifications
behave identically to an ordinary validated/rejected outcome.
"""
if test.state != TestState.disputed:
raise InvalidOperationError("Test is not in disputed state")
entity = TestEntity.from_orm(test)
entity.resolve_dispute_by_manager(outcome)
entity.apply_to(test)
db.flush()
log_action(
db, user_id=user.id, action="manager_resolve_dispute",
entity_type="test", entity_id=test.id,
details={"outcome": outcome, "notes": notes, "test_name": test.name},
)
_dispatch_dual_validation_effects(db, test, entity, actor=user)
# Let both leads know the manager made the final call, not just whichever
# side "won" — the losing side needs to know just as much.
for lead_id in filter(None, {test.red_validated_by, test.blue_validated_by}):
try:
create_notification(
db, user_id=lead_id, type="manager_dispute_resolution",
title=f"Manager resolved disputed test as {outcome}",
message=(
f'{user.username} resolved the dispute on "{test.name}" as {outcome}.'
+ (f" Notes: {notes}" if notes else "")
),
entity_type="test", entity_id=test.id,
)
except Exception as e:
logger.warning("Notification failed for test %s: %s", test.id, e, exc_info=True)
return test
# Define function handle_remediation_completed
def handle_remediation_completed(db: Session, test: Test, user: User) -> Test | None:
"""Create a re-test when remediation is completed.
@@ -112,3 +112,96 @@ def test_resolve_dispute_routes_to_red_queue(
assert body["state"] == "red_executing"
assert body["red_validation_status"] is None
assert body["blue_validation_status"] is None
def test_escalate_to_manager_notifies_managers(
client, db, api, auth_headers, red_tech_headers, red_lead_headers,
blue_tech_headers, blue_lead_headers, manager_headers, manager_user, technique,
):
test_id = _reach_disputed(client, db, api, auth_headers, red_tech_headers, red_lead_headers,
blue_tech_headers, blue_lead_headers, technique)
resp = api("post", f"/api/v1/tests/{test_id}/escalate-to-manager", red_lead_headers)
assert resp.status_code == 200, resp.text
assert resp.json()["status"] == "escalated"
notifications = api("get", "/api/v1/notifications", manager_headers).json()
assert any("escalated" in (n.get("title") or "").lower() for n in notifications)
def test_escalate_to_manager_requires_disputed_state(
client, db, api, auth_headers, red_tech_headers, red_lead_headers, technique,
):
resp = api(
"post", "/api/v1/tests", red_lead_headers,
json={"technique_id": technique, "name": "Not disputed"},
)
test_id = resp.json()["id"]
resp = api("post", f"/api/v1/tests/{test_id}/escalate-to-manager", red_lead_headers)
assert resp.status_code == 400
def test_manager_resolve_dispute_as_validated(
client, db, api, auth_headers, red_tech_headers, red_lead_headers,
blue_tech_headers, blue_lead_headers, manager_headers, technique,
):
test_id = _reach_disputed(client, db, api, auth_headers, red_tech_headers, red_lead_headers,
blue_tech_headers, blue_lead_headers, technique)
resp = api(
"post", f"/api/v1/tests/{test_id}/manager-resolve-dispute", manager_headers,
json={"outcome": "validated", "notes": "Detection was sufficient after all"},
)
assert resp.status_code == 200, resp.text
assert resp.json()["state"] == "validated"
red_notifs = api("get", "/api/v1/notifications", red_lead_headers).json()
assert any(n["type"] == "manager_dispute_resolution" for n in red_notifs)
blue_notifs = api("get", "/api/v1/notifications", blue_lead_headers).json()
assert any(n["type"] == "manager_dispute_resolution" for n in blue_notifs)
def test_manager_resolve_dispute_as_rejected(
client, db, api, auth_headers, red_tech_headers, red_lead_headers,
blue_tech_headers, blue_lead_headers, manager_headers, technique,
):
test_id = _reach_disputed(client, db, api, auth_headers, red_tech_headers, red_lead_headers,
blue_tech_headers, blue_lead_headers, technique)
resp = api(
"post", f"/api/v1/tests/{test_id}/manager-resolve-dispute", manager_headers,
json={"outcome": "rejected"},
)
assert resp.status_code == 200, resp.text
assert resp.json()["state"] == "rejected"
def test_manager_resolve_dispute_forbidden_for_lead(
client, db, api, auth_headers, red_tech_headers, red_lead_headers,
blue_tech_headers, blue_lead_headers, technique,
):
test_id = _reach_disputed(client, db, api, auth_headers, red_tech_headers, red_lead_headers,
blue_tech_headers, blue_lead_headers, technique)
resp = api(
"post", f"/api/v1/tests/{test_id}/manager-resolve-dispute", red_lead_headers,
json={"outcome": "validated"},
)
assert resp.status_code == 403
def test_manager_resolve_dispute_requires_disputed_state(
client, db, api, red_lead_headers, manager_headers, technique,
):
resp = api(
"post", "/api/v1/tests", red_lead_headers,
json={"technique_id": technique, "name": "Not disputed either"},
)
test_id = resp.json()["id"]
resp = api(
"post", f"/api/v1/tests/{test_id}/manager-resolve-dispute", manager_headers,
json={"outcome": "validated"},
)
assert resp.status_code == 400