diff --git a/python/robomp/web/src/work-items.test.ts b/python/robomp/web/src/work-items.test.ts index aaa1034f4..60177c0bb 100644 --- a/python/robomp/web/src/work-items.test.ts +++ b/python/robomp/web/src/work-items.test.ts @@ -614,4 +614,103 @@ describe("buildWorkItems", () => { expect(items[0].latestEvent?.delivery_id).toBe("live-older-3"); }); + test("suppresses an orphan failed recent event superseded by a newer done recent event for the same absent issue", () => { + const items = buildWorkItems( + status({ + recent_events: [ + recentEvent({ + delivery_id: "done-newer-orphan", + issue_key: "owner/repo#30", + state: "done", + received_at: "2026-06-17T00:06:00Z", + last_error: null, + }), + recentEvent({ + delivery_id: "failed-older-orphan", + issue_key: "owner/repo#30", + received_at: "2026-06-17T00:04:00Z", + last_error: "superseded orphan failure", + }), + ], + }), + ); + expect(items.some((item) => item.deliveryId === "failed-older-orphan")).toBe(false); + expect(items).toEqual([]); + }); + + test("suppresses an orphan failed recent event when a newer queued event appears later in the list", () => { + const items = buildWorkItems( + status({ + recent_events: [ + recentEvent({ + delivery_id: "failed-older-out-of-order", + issue_key: "owner/repo#32", + received_at: "2026-06-17T00:04:00Z", + last_error: "older failure", + }), + recentEvent({ + delivery_id: "queued-newer-out-of-order", + issue_key: "owner/repo#32", + state: "queued", + received_at: "2026-06-17T00:06:00Z", + last_error: null, + }), + ], + }), + ); + expect(items).toEqual([]); + }); + + test("suppresses a same-timestamp orphan failure when a non-failed recent event exists", () => { + const items = buildWorkItems( + status({ + recent_events: [ + recentEvent({ + delivery_id: "failed-same-time", + issue_key: "owner/repo#33", + received_at: "2026-06-17T00:06:00Z", + last_error: "ambiguous failure", + }), + recentEvent({ + delivery_id: "done-same-time", + issue_key: "owner/repo#33", + state: "done", + received_at: "2026-06-17T00:06:00Z", + last_error: null, + }), + ], + }), + ); + expect(items).toEqual([]); + }); + + test("renders an orphan failed recent event that is the newest for an absent issue", () => { + const items = buildWorkItems( + status({ + recent_events: [ + recentEvent({ + delivery_id: "failed-newest-orphan", + issue_key: "owner/repo#31", + received_at: "2026-06-17T00:06:00Z", + last_error: "current orphan failure", + }), + recentEvent({ + delivery_id: "done-older-orphan", + issue_key: "owner/repo#31", + state: "done", + received_at: "2026-06-17T00:04:00Z", + last_error: null, + }), + ], + }), + ); + expect(items).toHaveLength(1); + expect(items[0]).toMatchObject({ + key: "owner/repo#31", + deliveryId: "failed-newest-orphan", + bucket: "failed", + error: "current orphan failure", + }); + }); + }); diff --git a/python/robomp/web/src/work-items.ts b/python/robomp/web/src/work-items.ts index e2851ff2f..be9645342 100644 --- a/python/robomp/web/src/work-items.ts +++ b/python/robomp/web/src/work-items.ts @@ -142,6 +142,19 @@ export function buildWorkItems(status: StatusResponse): WorkItem[] { items.push(orphanLiveItem(key, null, true)); } + const newestRecentByKey = new Map(); + for (const event of status.recent_events) { + if (!event.issue_key) continue; + const current = newestRecentByKey.get(event.issue_key); + const eventTs = parseTs(event.received_at); + const currentTs = current ? parseTs(current.received_at) : 0; + const nonFailedBreaksTie = + current != null && eventTs === currentTs && current.state === "failed" && event.state !== "failed"; + if (!current || eventTs > currentTs || nonFailedBreaksTie) { + newestRecentByKey.set(event.issue_key, event); + } + } + for (const event of status.recent_events) { if (event.state !== "failed" || !event.delivery_id || seen.has(event.delivery_id)) continue; if (event.issue_key) { @@ -149,6 +162,13 @@ export function buildWorkItems(status: StatusResponse): WorkItem[] { if (latest && (latest.delivery_id !== event.delivery_id || latest.state !== "failed")) { continue; } + // For issues outside the capped `status.issues` window, fall back to the + // newest recent event for this issue_key. If a newer (or non-failed) + // delivery exists for the same issue, this older failed orphan is stale. + const newestRecent = newestRecentByKey.get(event.issue_key); + if (newestRecent && (newestRecent.delivery_id !== event.delivery_id || newestRecent.state !== "failed")) { + continue; + } if (seen.has(event.issue_key)) continue; }