From 9c678b2d907d3ba905cbabec48e0227597da76a6 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 11 Oct 2026 00:06:23 -0500 Subject: [PATCH] Wait for signoff fixture runs before stage decisions --- tests/e2e/signoff-policy.spec.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/e2e/signoff-policy.spec.ts b/tests/e2e/signoff-policy.spec.ts index 471589cf3c..37c0056417 100644 --- a/tests/e2e/signoff-policy.spec.ts +++ b/tests/e2e/signoff-policy.spec.ts @@ -270,6 +270,29 @@ async function getIssueRunLockState(board: APIRequestContext, issueId: string): }; } +async function waitForFixtureRun( + board: APIRequestContext, + agentId: string, + issueId: string, + runId: string, +) { + // Negative authorization cases deliberately invoke a non-participant. Leave + // their API rejection intact instead of waiting for a run they cannot own. + if ((await getIssueRunLockState(board, issueId)).assigneeAgentId !== agentId) return; + const waitBudgetMs = wakeupWaitBudgetMs(); + const deadline = Date.now() + waitBudgetMs; + let status: string | null = null; + do { + const run = await fetchVerifiedRun(board, runId, agentId, issueId); + expect(run, "Fixture run must belong to the acting agent and issue").not.toBeNull(); + status = run!.status; + if (status === "succeeded") return; + expect(["queued", "running"], `Fixture run ${runId} ended as ${status}`).toContain(status); + await new Promise((resolve) => setTimeout(resolve, WAKEUP_POLL_INTERVAL_MS)); + } while (Date.now() < deadline); + throw new Error(`Fixture run ${runId} did not succeed within ${waitBudgetMs}ms (last status: ${status})`); +} + async function retryAgentPatchWithCurrentLockOnConflict( board: APIRequestContext, agent: AgentAuth, @@ -325,6 +348,9 @@ async function agentPatch( }: { maxAttempts?: number; backoffMs?: number; maxBackoffMs?: number } = {}, ) { const runId = await invokeHeartbeat(board, agent.agentId, issueId); + // The fixture process only prints "done". Let its startup and cleanup settle + // before a simulated decision reassigns the issue and cancels that process. + await waitForFixtureRun(board, agent.agentId, issueId, runId); const patchWith = (patchRunId: string) => agent.request.patch(`${BASE_URL}/api/issues/${issueId}`, { headers: { "X-Paperclip-Run-Id": patchRunId }, @@ -353,6 +379,7 @@ async function agentCheckoutAndPatch( patchData: Record, ) { const runId = await invokeHeartbeat(board, agent.agentId, issueId); + await waitForFixtureRun(board, agent.agentId, issueId, runId); const directPatchRes = await agent.request.patch(`${BASE_URL}/api/issues/${issueId}`, { headers: { "X-Paperclip-Run-Id": runId }, data: patchData,