From 32f4fd25bd68a152e6bd034ac18b8cddc0af8084 Mon Sep 17 00:00:00 2001 From: kitos Date: Wed, 15 Jul 2026 11:41:29 +0200 Subject: [PATCH] fix(tests): surface cross-validation and disputed tests in the review queue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- frontend/src/pages/TestsPage.tsx | 73 ++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/TestsPage.tsx b/frontend/src/pages/TestsPage.tsx index 0767275..b02a27f 100644 --- a/frontend/src/pages/TestsPage.tsx +++ b/frontend/src/pages/TestsPage.tsx @@ -365,9 +365,18 @@ export default function TestsPage() { // The auto load-balancer always assigns a reviewer immediately, so // "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. - 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) { - 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; @@ -391,9 +400,24 @@ export default function TestsPage() { const inState = searchFiltered.filter((t) => t.state === reviewState); const available = 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]); + // ── 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 ───────────────────────────────────────────── const formatDate = (dateStr: string | null | undefined) => { if (!dateStr) return "-"; @@ -637,6 +661,32 @@ export default function TestsPage() { )} + {/* ── Disputed tests — most urgent, always on top when present ──── */} + {isReviewLead && disputedTests.length > 0 && ( +
+
+
+ +

Disputed

+ + {disputedTests.length} + +
+ Leads disagree on the outcome — needs resolution +
+ +
+ )} + {/* ── Tests Table / Two-Queue View ─────────────────────────────── */} {techRole ? ( /* Two-section queue for red_tech and blue_tech */ @@ -699,6 +749,23 @@ export default function TestsPage() { + + {/* 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". */} +
+
+
+

Awaiting My Validation

+ + {pendingMyValidation.length} + +
+ Cross-validation — both leads vote independently +
+ +
) : ( /* Normal single-table view for admin, viewer, and leads viewing "My Tasks" */