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:
@@ -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> = {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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 ────────────────────── */
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user