Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions src/components/admin/review-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,14 @@ export function ReviewRow({
}
)

// A human review takes as long as it takes — showing its run as "Sending…"
// for that whole time reads as though the dispatch is still going. Once the
// run exists, the only fact the operator needs is that it was sent, so an
// in-flight run and a completed one look the same here. Polling continues
// regardless: onCompleted is what surfaces the workflow's results.
const humanReviewSent =
humanReviewStatus === "COMPLETED" || humanReviewInFlight

// ── Merge-specific interactive state ────────────────────────────────────────
const [checkedSources, setCheckedSources] = useState<Set<string>>(new Set())
const [canonicalId, setCanonicalId] = useState<string>("")
Expand Down Expand Up @@ -778,38 +786,36 @@ export function ReviewRow({
<button
type="button"
data-testid="send-for-human-review-btn"
disabled={humanReviewInFlight || humanReviewTriggering}
disabled={humanReviewSent || humanReviewTriggering}
onClick={(e) => {
e.stopPropagation()
if (humanReviewStatus === "COMPLETED") return
if (humanReviewSent) return
triggerHumanReview(() => triggerMergeWorkflow(review.ref_id))
}}
title={
humanReviewInFlight
? "Human review in progress…"
: humanReviewStatus === "COMPLETED"
? "Human review completed"
: humanReviewStatus === "FAILED"
? "Review failed — click to retry"
: "Send for human review"
humanReviewSent
? "Sent for human review"
: humanReviewStatus === "FAILED"
? "Review failed — click to retry"
: "Send for human review"
}
className={cn(
"inline-flex shrink-0 items-center gap-1 whitespace-nowrap rounded border px-2 py-0.5 text-[11px] font-medium transition-all",
humanReviewInFlight || humanReviewTriggering
humanReviewTriggering
? "cursor-not-allowed border-sky-500/40 bg-sky-500/10 text-sky-300"
: humanReviewStatus === "COMPLETED"
: humanReviewSent
? "cursor-default border-emerald-500/40 bg-emerald-500/10 text-emerald-300"
: humanReviewStatus === "FAILED"
? "border-red-500/40 bg-red-500/5 text-red-400 hover:border-red-500/70 hover:bg-red-500/15"
: "border-sky-500/30 bg-sky-500/5 text-sky-400 hover:border-sky-500/60 hover:bg-sky-500/10"
)}
>
{humanReviewInFlight || humanReviewTriggering ? (
{humanReviewTriggering ? (
<>
<Loader2 className="h-3 w-3 animate-spin" />
Sending…
</>
) : humanReviewStatus === "COMPLETED" ? (
) : humanReviewSent ? (
<>
<CheckCircle2 className="h-3 w-3" />
Sent
Expand Down
13 changes: 8 additions & 5 deletions src/lib/__tests__/reviews.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1307,9 +1307,9 @@ describe("ReviewRow — Send for Human Review button", () => {

await waitFor(() => {
expect(btn.textContent).toMatch(/Sent/i)
// COMPLETED: not disabled by the HTML attribute, but click is a no-op
// (disabled only when humanReviewInFlight || humanReviewTriggering)
expect(btn).not.toHaveProperty("disabled", true)
// Once sent, the button is genuinely disabled rather than merely
// click-guarded — nothing is left to send.
expect(btn).toHaveProperty("disabled", true)
})
})

Expand Down Expand Up @@ -1347,7 +1347,7 @@ describe("ReviewRow — Send for Human Review button", () => {
})
})

it("hydrates to RUNNING and shows Sending… (button disabled while in-flight)", async () => {
it("hydrates to RUNNING and shows Sent (not Sending — the dispatch is done)", async () => {
mockGetLatestStakworkRun.mockResolvedValueOnce({
ref_id: "mock-run-789",
job_type: "node_merge_review",
Expand All @@ -1362,8 +1362,11 @@ describe("ReviewRow — Send for Human Review button", () => {
)
const btn = await findByTestId("send-for-human-review-btn")

// A human review runs for as long as a human takes; "Sending…" would imply
// the dispatch is still in progress. Once a run exists it is simply sent.
await waitFor(() => {
expect(btn.textContent).toMatch(/Sending/i)
expect(btn.textContent).toMatch(/Sent/i)
expect(btn.textContent).not.toMatch(/Sending/i)
expect(btn).toHaveProperty("disabled", true)
})
})
Expand Down
Loading