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
6 changes: 5 additions & 1 deletion src/app/admin/reviews/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,11 @@ export default function ReviewsPage() {
const results = await Promise.allSettled(
selectedReviews.map((r) => fn(r.ref_id))
)
const failures = results.filter((r) => r.status === "rejected").length
// A failed approve action answers HTTP 200 with an Error envelope, so a
// fulfilled promise is not enough — check the envelope status too.
const failures = results.filter(
(r) => r.status === "rejected" || r.value.status !== "Success"
).length
setBulkRunning(null)
if (failures > 0) {
setBulkError(
Expand Down
8 changes: 4 additions & 4 deletions src/components/admin/review-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -707,8 +707,8 @@ export function ReviewRow({
setInlineError(null)
try {
const res = await approveReview(review.ref_id, override)
if (res.error_message || res.status === "failed") {
setInlineError(res.error_message ?? "Approval failed")
if (res.status !== "Success") {
setInlineError(res.error_message ?? res.message ?? "Approval failed")
onCountRefresh?.()
return
}
Expand Down Expand Up @@ -744,12 +744,12 @@ export function ReviewRow({
? { from: effectiveFrom, to: canonicalId }
: undefined
const res = await approveReview(review.ref_id, override)
if (res.error_message || res.status === "failed") {
if (res.status !== "Success") {
// Keep the row and its error visible instead of silently refetching it
// out of the pending list (it is now 'failed', not pending) — otherwise
// a failed merge just vanishes and looks like it succeeded. Still refresh
// the pending badge so the count reflects that it left the queue.
setInlineError(res.error_message ?? "Approval failed")
setInlineError(res.error_message ?? res.message ?? "Approval failed")
onCountRefresh?.()
return
}
Expand Down
12 changes: 6 additions & 6 deletions src/lib/__tests__/reviews.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ describe("ReviewRow", () => {

it("calls approveReview with correct ref_id after confirmation", async () => {
const user = userEvent.setup()
mockApproveReview.mockResolvedValue({ status: "approved" })
mockApproveReview.mockResolvedValue({ status: "Success" })
const onRefresh = vi.fn()

const { getByText } = render(
Expand All @@ -223,10 +223,10 @@ describe("ReviewRow", () => {
await waitFor(() => expect(onRefresh).toHaveBeenCalled())
})

it("shows inline error when approve returns failed status", async () => {
it("shows inline error when approve returns an Error envelope", async () => {
const user = userEvent.setup()
mockApproveReview.mockResolvedValue({
status: "failed",
status: "Error",
error_message: "no handler registered for action: supersede",
})

Expand All @@ -246,7 +246,7 @@ describe("ReviewRow", () => {

it("calls dismissReview with reason after entering text", async () => {
const user = userEvent.setup()
mockDismissReview.mockResolvedValue({ status: "dismissed" })
mockDismissReview.mockResolvedValue({ status: "Success" })
const onRefresh = vi.fn()

const { getByText, getByPlaceholderText } = render(
Expand All @@ -266,7 +266,7 @@ describe("ReviewRow", () => {

it("calls dismissReview without reason when textarea left empty", async () => {
const user = userEvent.setup()
mockDismissReview.mockResolvedValue({ status: "dismissed" })
mockDismissReview.mockResolvedValue({ status: "Success" })
const onRefresh = vi.fn()

const { getByText } = render(
Expand Down Expand Up @@ -1061,7 +1061,7 @@ describe("ReviewRow merge_nodes interactive controls", () => {

beforeEach(() => {
mockApproveReview.mockReset()
mockApproveReview.mockResolvedValue({ status: "approved" })
mockApproveReview.mockResolvedValue({ status: "Success" })
})

it("shows all sources checked by default on expand", async () => {
Expand Down
31 changes: 21 additions & 10 deletions src/lib/graph-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1145,15 +1145,26 @@ export async function getSchemaProposal(
)
}

/**
* Edges-style envelope shared by the review decision endpoints: `status` is
* the operation outcome ("Success"/"Error"), while the updated review under
* `review` carries the lifecycle state (approved/failed/dismissed). A failed
* approve action still answers HTTP 200 with `status: "Error"`.
*/
export interface ReviewDecisionResponse {
status: string
message?: string
status_messages?: string[]
error_message?: string
promotion_summary?: PromotionSummary | null
review?: Review
}

export async function approveReview(
refId: string,
overridePayload?: ReviewOverridePayload,
signal?: AbortSignal
): Promise<{
status: string
error_message?: string
promotion_summary?: PromotionSummary | null
}> {
): Promise<ReviewDecisionResponse> {
if (isMocksEnabled()) {
const store = getMockReviewsStore()
const review = store.find((r) => r.ref_id === refId)
Expand All @@ -1162,10 +1173,10 @@ export async function approveReview(
review.decided_at = new Date().toISOString()
review.decided_by = "mock-admin"
}
return { status: "approved" }
return { status: "Success", review }
}
const body = overridePayload ? { override_payload: overridePayload } : {}
return api.post<{ status: string; error_message?: string }>(
return api.post<ReviewDecisionResponse>(
`/v2/reviews/${refId}/approve`,
body,
undefined,
Expand All @@ -1177,7 +1188,7 @@ export async function dismissReview(
refId: string,
reason?: string,
signal?: AbortSignal
): Promise<{ status: string }> {
): Promise<ReviewDecisionResponse> {
if (isMocksEnabled()) {
const store = getMockReviewsStore()
const review = store.find((r) => r.ref_id === refId)
Expand All @@ -1187,9 +1198,9 @@ export async function dismissReview(
review.decided_by = "mock-admin"
if (reason) review.dismissal_reason = reason
}
return { status: "dismissed" }
return { status: "Success", review }
}
return api.post<{ status: string }>(
return api.post<ReviewDecisionResponse>(
`/v2/reviews/${refId}/dismiss`,
{ reason },
undefined,
Expand Down
Loading