fix(robomp): harden lifecycle contract review cases

This commit is contained in:
lycaon
2026-06-17 22:21:45 -06:00
parent a20f6d46ce
commit 1961b830ba
4 changed files with 71 additions and 13 deletions
+3 -1
View File
@@ -8,9 +8,11 @@ export type IssueState =
| "new"
| "reproducing"
| "fixing"
| "reviewing"
| "opened"
| "merged"
| "closed"
| "needs_info"
| "abandoned";
export interface RuntimeInfo {
@@ -37,7 +39,7 @@ export interface IssueRow {
number: number;
branch: string | null;
pr_number: number | null;
state: IssueState | string;
state: IssueState;
classification: string | null;
updated_at: string;
latest_event: LatestEvent | null;
+44
View File
@@ -470,8 +470,15 @@ describe("buildWorkItems", () => {
expect(stageOrdinal("nonsense")).toBe(0);
expect(stageOrdinal("new")).toBe(0);
expect(stageOrdinal("reproducing")).toBe(1);
// needs_info is set by mark_unable_to_reproduce during reproduction and
// cleared back to reproducing by repro_record in host_tools; it stays in
// the reproduction phase (ordinal 1), not advanced past fixing.
expect(stageOrdinal("needs_info")).toBe(1);
expect(stageOrdinal("fixing")).toBe(2);
// reviewing is PR review work (tasks.py sets it on PR-opened issues); it
// lives on the PR step, same stage as opened (ordinal 3).
expect(stageOrdinal("opened")).toBe(3);
expect(stageOrdinal("reviewing")).toBe(3);
expect(stageOrdinal("merged")).toBe(4);
});
@@ -570,4 +577,41 @@ describe("buildWorkItems", () => {
expect(items[0].latestEvent?.state).toBe("running");
});
test("live running event outranks a newer queued latest_event for the same issue", () => {
const items = buildWorkItems(
status({
issues: [
issue({
key: "owner/repo#22",
number: 22,
latest_event: latestEvent({
delivery_id: "queued-newer",
state: "queued",
received_at: "2026-06-17T00:09:00Z",
}),
}),
],
running_events: [
runningEvent({
delivery_id: "live-older-3",
issue_key: "owner/repo#22",
received_at: "2026-06-17T00:01:00Z",
started_at: "2026-06-17T00:02:00Z",
}),
],
}),
);
expect(items).toHaveLength(1);
expect(items[0]).toMatchObject({
key: "owner/repo#22",
bucket: "running",
deliveryId: "live-older-3",
inflightOnly: false,
error: null,
});
expect(items[0].live?.delivery_id).toBe("live-older-3");
expect(items[0].latestEvent?.state).toBe("running");
expect(items[0].latestEvent?.delivery_id).toBe("live-older-3");
});
});
+6 -4
View File
@@ -38,8 +38,10 @@ export const SIMPLE_CLASSIFICATIONS: ReadonlySet<string> = new Set([
const STATE_ORDINAL: Record<string, number> = {
new: 0,
reproducing: 1,
needs_info: 1,
fixing: 2,
opened: 3,
reviewing: 3,
merged: 4,
closed: 4,
abandoned: 4,
@@ -78,10 +80,10 @@ export function buildWorkItems(status: StatusResponse): WorkItem[] {
const latest = issue.latest_event;
// A matching live running_events entry is authoritative over the issue's
// own latest_event, which may be a newer failed/done row that the live run
// has not yet superseded. Render the live run so the card stays running,
// cancel-capable (deliveryId from the live delivery), and free of the
// stale failure. Non-live rows keep latest_event authority below.
// own latest_event. That summary row can be a newer queued retry/comment,
// or a failed/done row the live run has not superseded yet. Render the live
// run so the card stays running, cancel-capable (deliveryId from the live
// delivery), and free of stale latest-event state.
if (live) {
items.push({
key,