fix(tests): surface cross-validation and disputed tests in the review queue
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
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
Cross-validation (in_review) tests awaiting a lead's vote were only reachable via the 'My Tasks' toggle, so the default review queue view never showed them despite being exactly the kind of work a lead expects to find there. Added an 'Awaiting My Validation' section to the review queue, and a standalone 'Disputed' section — always fetched independently of other filters, shown above everything else, and only rendered when non-empty — since disputed tests need the most urgent attention.
This commit is contained in:
@@ -365,9 +365,18 @@ export default function TestsPage() {
|
|||||||
// The auto load-balancer always assigns a reviewer immediately, so
|
// The auto load-balancer always assigns a reviewer immediately, so
|
||||||
// "available" here means "assigned to another lead" — visible so a
|
// "available" here means "assigned to another lead" — visible so a
|
||||||
// lead can pick up a peer's review if they can't get to it.
|
// lead can pick up a peer's review if they can't get to it.
|
||||||
const { availableReviews, myAssignedReviews } = useMemo(() => {
|
//
|
||||||
|
// Also includes cross-validation (in_review) tests still awaiting this
|
||||||
|
// lead's vote — previously only surfaced by toggling "My Tasks", which
|
||||||
|
// meant a lead landing on this page by default never saw them here at
|
||||||
|
// all despite this being "the review queue" as far as they're concerned.
|
||||||
|
const { availableReviews, myAssignedReviews, pendingMyValidation } = useMemo(() => {
|
||||||
if (!isReviewLead || !user || !allTests || showMyTasks) {
|
if (!isReviewLead || !user || !allTests || showMyTasks) {
|
||||||
return { availableReviews: [] as typeof tests, myAssignedReviews: [] as typeof tests };
|
return {
|
||||||
|
availableReviews: [] as typeof tests,
|
||||||
|
myAssignedReviews: [] as typeof tests,
|
||||||
|
pendingMyValidation: [] as typeof tests,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let searchFiltered = allTests;
|
let searchFiltered = allTests;
|
||||||
@@ -391,9 +400,24 @@ export default function TestsPage() {
|
|||||||
const inState = searchFiltered.filter((t) => t.state === reviewState);
|
const inState = searchFiltered.filter((t) => t.state === reviewState);
|
||||||
const available = inState.filter((t) => t[reviewerField] !== user.id);
|
const available = inState.filter((t) => t[reviewerField] !== user.id);
|
||||||
const mine = inState.filter((t) => t[reviewerField] === user.id);
|
const mine = inState.filter((t) => t[reviewerField] === user.id);
|
||||||
return { availableReviews: available, myAssignedReviews: mine };
|
|
||||||
|
const validationField = user.role === "red_lead" ? "red_validation_status" : "blue_validation_status";
|
||||||
|
const pendingValidation = searchFiltered.filter(
|
||||||
|
(t) => t.state === "in_review" && !t[validationField]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { availableReviews: available, myAssignedReviews: mine, pendingMyValidation: pendingValidation };
|
||||||
}, [isReviewLead, user, allTests, searchText, platformFilter, showMyTasks]);
|
}, [isReviewLead, user, allTests, searchText, platformFilter, showMyTasks]);
|
||||||
|
|
||||||
|
// ── Disputed tests — always fetched independently of the my-tasks
|
||||||
|
// toggle/state filter so this alert never silently disappears just
|
||||||
|
// because the lead is looking at a different view. ──────────────────
|
||||||
|
const { data: disputedTests = [] } = useQuery({
|
||||||
|
queryKey: ["tests", "disputed-queue"],
|
||||||
|
queryFn: () => getTests({ state: "disputed", limit: 200 }),
|
||||||
|
enabled: isReviewLead,
|
||||||
|
});
|
||||||
|
|
||||||
// ── Formatting helpers ─────────────────────────────────────────────
|
// ── Formatting helpers ─────────────────────────────────────────────
|
||||||
const formatDate = (dateStr: string | null | undefined) => {
|
const formatDate = (dateStr: string | null | undefined) => {
|
||||||
if (!dateStr) return "-";
|
if (!dateStr) return "-";
|
||||||
@@ -637,6 +661,32 @@ export default function TestsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ── Disputed tests — most urgent, always on top when present ──── */}
|
||||||
|
{isReviewLead && disputedTests.length > 0 && (
|
||||||
|
<div className="rounded-xl border border-amber-500/40 bg-amber-500/5 p-6">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-amber-400" />
|
||||||
|
<h2 className="text-lg font-semibold text-amber-400">Disputed</h2>
|
||||||
|
<span className="rounded-full border border-amber-500/30 bg-amber-500/10 px-2 py-0.5 text-xs font-medium text-amber-400">
|
||||||
|
{disputedTests.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-amber-400/70">Leads disagree on the outcome — needs resolution</span>
|
||||||
|
</div>
|
||||||
|
<TestTable
|
||||||
|
tests={disputedTests}
|
||||||
|
columns={mainTableColumns}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDir={sortDir}
|
||||||
|
handleSort={handleSort}
|
||||||
|
navigate={navigate}
|
||||||
|
formatDate={formatDate}
|
||||||
|
emptyMessage=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ── Tests Table / Two-Queue View ─────────────────────────────── */}
|
{/* ── Tests Table / Two-Queue View ─────────────────────────────── */}
|
||||||
{techRole ? (
|
{techRole ? (
|
||||||
/* Two-section queue for red_tech and blue_tech */
|
/* Two-section queue for red_tech and blue_tech */
|
||||||
@@ -699,6 +749,23 @@ export default function TestsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<TestTable tests={myAssignedReviews} columns={mainTableColumns} sortKey={sortKey} sortDir={sortDir} handleSort={handleSort} navigate={navigate} formatDate={formatDate} emptyMessage="No reviews currently assigned to you." />
|
<TestTable tests={myAssignedReviews} columns={mainTableColumns} sortKey={sortKey} sortDir={sortDir} handleSort={handleSort} navigate={navigate} formatDate={formatDate} emptyMessage="No reviews currently assigned to you." />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Awaiting My Validation — cross-validation stage (in_review),
|
||||||
|
distinct from the red_review/blue_review gate above: both
|
||||||
|
leads act independently here, so there's no "assigned to
|
||||||
|
someone else" bucket, just "have I voted yet or not". */}
|
||||||
|
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className="text-lg font-semibold text-white">Awaiting My Validation</h2>
|
||||||
|
<span className="rounded-full border border-blue-500/30 bg-blue-500/10 px-2 py-0.5 text-xs font-medium text-blue-400">
|
||||||
|
{pendingMyValidation.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-500">Cross-validation — both leads vote independently</span>
|
||||||
|
</div>
|
||||||
|
<TestTable tests={pendingMyValidation} columns={mainTableColumns} sortKey={sortKey} sortDir={sortDir} handleSort={handleSort} navigate={navigate} formatDate={formatDate} emptyMessage="Nothing awaiting your validation right now." />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Normal single-table view for admin, viewer, and leads viewing "My Tasks" */
|
/* Normal single-table view for admin, viewer, and leads viewing "My Tasks" */
|
||||||
|
|||||||
Reference in New Issue
Block a user