feat(tests): disputed state + fix timestamps on reopen
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:
kitos
2026-06-03 12:21:47 +02:00
parent 2de95a3082
commit 61e6037e97
11 changed files with 166 additions and 21 deletions

View File

@@ -26,6 +26,7 @@ const stateBadge: Record<TestState, string> = {
in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30",
validated: "bg-green-900/50 text-green-400 border-green-500/30",
rejected: "bg-red-900/50 text-red-400 border-red-500/30",
disputed: "bg-amber-900/50 text-amber-400 border-amber-500/30",
};
const SEVERITY_COLORS: Record<string, string> = {

View File

@@ -8,6 +8,8 @@ import {
Loader2,
Shield,
ShieldCheck,
AlertTriangle,
MessageSquare,
} from "lucide-react";
import type { Test, TestState, User } from "../../types/models";
import LiveTimer from "./LiveTimer";
@@ -40,6 +42,7 @@ const STATE_BADGE: Record<TestState, string> = {
in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30",
validated: "bg-green-900/50 text-green-400 border-green-500/30",
rejected: "bg-red-900/50 text-red-400 border-red-500/30",
disputed: "bg-amber-900/50 text-amber-400 border-amber-500/30",
};
// ── Props ──────────────────────────────────────────────────────────
@@ -212,6 +215,30 @@ export default function TestDetailHeader({
);
}
// Disputed: the lead who approved can change their vote to rejected
if (test.state === "disputed") {
const canChangeVote =
(role === "red_lead" && test.red_validation_status === "approved") ||
(role === "blue_lead" && test.blue_validation_status === "approved") ||
role === "admin";
if (canChangeVote) {
const side = role === "red_lead" ? "red" : role === "blue_lead" ? "blue" : null;
if (side || role === "admin") {
buttons.push(
<button
key="change-vote"
onClick={() => onOpenValidateModal(side ?? (test.red_validation_status === "approved" ? "red" : "blue"))}
className="flex items-center gap-1.5 rounded-lg bg-amber-600 px-4 py-2 text-sm font-medium text-white hover:bg-amber-500 transition-colors"
>
<XCircle className="h-4 w-4" />
Change Vote to Rejected
</button>,
);
}
}
}
// Leads/admin on rejected -> Reopen
if (
test.state === "rejected" &&
@@ -238,7 +265,7 @@ export default function TestDetailHeader({
// ── Dual validation indicators ───────────────────────────────────
const renderValidationIndicators = () => {
if (test.state !== "in_review" && test.state !== "validated" && test.state !== "rejected") {
if (!["in_review", "validated", "rejected", "disputed"].includes(test.state)) {
return null;
}
@@ -347,6 +374,31 @@ export default function TestDetailHeader({
</div>
</div>
{/* Disputed conflict banner */}
{test.state === "disputed" && (
<div className="mt-3 flex items-start gap-3 rounded-xl border border-amber-500/30 bg-amber-500/5 p-4">
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-amber-400" />
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-amber-300">
Validation Conflict Leads Disagree
</p>
<p className="mt-0.5 text-xs text-amber-400/80">
One lead approved and the other rejected this test.
{test.red_validation_status === "rejected" && test.red_validation_notes && (
<> Red Lead's reason: <span className="italic">"{test.red_validation_notes}"</span></>
)}
{test.blue_validation_status === "rejected" && test.blue_validation_notes && (
<> Blue Lead's reason: <span className="italic">"{test.blue_validation_notes}"</span></>
)}
</p>
<p className="mt-1.5 text-[10px] text-amber-400/60 flex items-center gap-1">
<MessageSquare className="h-3 w-3" />
The lead who approved should review the rejection reason and either change their vote or discuss with the other lead to resolve the disagreement.
</p>
</div>
</div>
)}
{/* Progress bar */}
{test.state !== "rejected" && (
<div className="pt-2">

View File

@@ -55,6 +55,7 @@ const testStateColors: Record<string, string> = {
in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30",
validated: "bg-green-900/50 text-green-400 border-green-500/30",
rejected: "bg-red-900/50 text-red-400 border-red-500/30",
disputed: "bg-amber-900/50 text-amber-400 border-amber-500/30",
};
export default function CampaignDetailPage() {

View File

@@ -54,6 +54,7 @@ const testStateBadgeColors: Record<string, string> = {
in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30",
validated: "bg-green-900/50 text-green-400 border-green-500/30",
rejected: "bg-red-900/50 text-red-400 border-red-500/30",
disputed: "bg-amber-900/50 text-amber-400 border-amber-500/30",
};
const testStateLabels: Record<string, string> = {

View File

@@ -45,6 +45,7 @@ const testStateBadgeColors: Record<TestState, string> = {
in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30",
validated: "bg-green-900/50 text-green-400 border-green-500/30",
rejected: "bg-red-900/50 text-red-400 border-red-500/30",
disputed: "bg-amber-900/50 text-amber-400 border-amber-500/30",
};
const testResultBadgeColors: Record<TestResult, string> = {

View File

@@ -32,6 +32,7 @@ const testStateBadgeColors: Record<TestState, string> = {
in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30",
validated: "bg-green-900/50 text-green-400 border-green-500/30",
rejected: "bg-red-900/50 text-red-400 border-red-500/30",
disputed: "bg-amber-900/50 text-amber-400 border-amber-500/30",
};
const testStateLabels: Record<TestState, string> = {
@@ -41,6 +42,7 @@ const testStateLabels: Record<TestState, string> = {
in_review: "In Review",
validated: "Validated",
rejected: "Rejected",
disputed: "Disputed",
};
const ALL_STATES: TestState[] = [
@@ -50,6 +52,7 @@ const ALL_STATES: TestState[] = [
"in_review",
"validated",
"rejected",
"disputed",
];
/* ── Helper: which team "owns" the current state ────────────────────── */

View File

@@ -43,7 +43,8 @@ export type TestState =
| "blue_evaluating"
| "in_review"
| "validated"
| "rejected";
| "rejected"
| "disputed"; // one lead approved, the other rejected — needs discussion
export type TestResult = "detected" | "not_detected" | "partially_detected";