diff --git a/tests/restart-chaos/observer.test.ts b/tests/restart-chaos/observer.test.ts index 4e6510fe111..2e0cba8b33f 100644 --- a/tests/restart-chaos/observer.test.ts +++ b/tests/restart-chaos/observer.test.ts @@ -111,10 +111,14 @@ describe("TaskObserver", () => { it("clicks the board Retry on a blocked subtask, as a user would, and keeps watching the parent", async () => { let childStatus = "blocked"; const base = fakeApi({ "parentIds=": () => [{ id: "c1", parentId: "i1", status: "done" }, { id: "c2", parentId: "i1", status: childStatus }], - "/documents/": () => null, "/recovery-actions": () => ({ active: { id: "a1" }, actions: [] }), "/api/issues/c2": () => ({ id: "c2", status: childStatus, + "/documents/": () => null, "/recovery-actions": () => ({ active: { id: "a1", status: "active", ownerType: "board" }, actions: [] }), "/api/issues/c2": () => ({ id: "c2", status: childStatus, interruption: { canRetry: childStatus === "blocked", observedRunId: "child-run" } }) }, "blocked"); const childRetries: string[] = []; - const api = { ...base.api, get: base.api.get.bind(base.api), + const api = { ...base.api, async get(route: string) { + if (route === "/api/issues/i1") return { id: "i1", status: "blocked", stalledDependencies: childStatus === "blocked" + ? [{ stalledIssueId: "c2", relation: "child", recoveryAction: { id: "a1", status: "active", ownerType: "board" } }] : [] }; + return base.api.get(route); + }, async post(route: string, body: unknown) { if (route === "/api/issues/c2/retry") { childRetries.push(route); childStatus = "in_progress"; return { accepted: true }; } return (base.api as unknown as { post(route: string, body: unknown): Promise }).post(route, body); @@ -214,6 +218,27 @@ describe("TaskObserver", () => { expect(artifacts.stop?.runsStartedBeforeContinue).toEqual(["r2"]); }); + it.each([ + ["agent-owned interrupted session", { id: "a-agent", status: "cancelled", ownerType: "agent" }], + ["system reconciliation resolved as cancelled", { id: "a-system", status: "resolved", ownerType: "system", outcome: "cancelled" }], + ])("records no stall for a blocked subtask the product does not list as stalled (%s), but still clicks its Retry", async (_name, action) => { + let childStatus = "blocked"; + const base = fakeApi({ "parentIds=": () => [{ id: "c1", parentId: "i1", status: "done" }, { id: "c2", parentId: "i1", status: childStatus }], + "/documents/": () => null, "/recovery-actions": () => ({ active: null, actions: [action] }), + "/api/issues/i1": route => route === "/api/issues/i1" ? { id: "i1", status: "blocked", stalledDependencies: [] } : null, + "/api/issues/c2": () => ({ id: "c2", status: childStatus, interruption: { canRetry: childStatus === "blocked", observedRunId: "child-run" } }) }, "blocked"); + const childRetries: string[] = []; + const api = { ...base.api, get: base.api.get.bind(base.api), async post(route: string, body: unknown) { + if (route === "/api/issues/c2/retry") { childRetries.push(route); childStatus = "in_progress"; return { accepted: true }; } + return (base.api as unknown as { post(route: string, body: unknown): Promise }).post(route, body); + } } as unknown as ChaosApi; + const observer = new TaskObserver({ ...plan, workload: "M1", followUpAfterMs: null }, api, lab, new Timeline(null), ready); + await observer.launch(); + await observer.poll(observer.record.createdAt! + 1_000); + expect(observer.record.childStalls ?? []).toEqual([]); + expect(childRetries).toEqual(["/api/issues/c2/retry"]); + }); + it("closes an open task as unfinished at interrupt, so the wave grades it as a failure", async () => { const { api } = fakeApi({}, "in_progress"); const timeline = new Timeline(null); diff --git a/tests/restart-chaos/observer.ts b/tests/restart-chaos/observer.ts index 251a2b9bf90..54155f1f253 100644 --- a/tests/restart-chaos/observer.ts +++ b/tests/restart-chaos/observer.ts @@ -587,11 +587,13 @@ export class TaskObserver { const rows = await this.api.get(`/api/companies/${this.lab.companyId}/issues?view=index&parentIds=${encodeURIComponent(this.record.issueId)}&limit=20`) .catch(() => null); const blocked = (Array.isArray(rows) ? rows : []).filter(row => row.parentId === this.record.issueId && row.status === "blocked").map(row => String(row.id)); - for (const childIssueId of blocked) { - const recovery = await this.api.get(`/api/issues/${childIssueId}/recovery-actions`).catch(() => null); - const actions = Array.isArray(recovery?.actions) ? recovery.actions as Json[] : []; - const latest = (recovery?.active as Json | null | undefined) ?? actions[0] ?? null; - const recoveryActionId = typeof latest?.id === "string" ? latest.id : null; + const parent = await this.api.get(`/api/issues/${this.record.issueId}`).catch(() => null); + const stalled = Array.isArray(parent?.stalledDependencies) ? parent.stalledDependencies as Json[] : []; + for (const dependency of stalled) { + if (dependency.relation !== "child" || typeof dependency.stalledIssueId !== "string") continue; + const action = (dependency.recoveryAction ?? null) as Json | null; + const childIssueId = dependency.stalledIssueId; + const recoveryActionId = typeof action?.id === "string" ? action.id : null; const stalls = this.record.childStalls ??= []; if (!stalls.some(stall => stall.childIssueId === childIssueId && stall.recoveryActionId === recoveryActionId)) { stalls.push({ childIssueId, recoveryActionId });