From 0bbabbaf1ce608322a056371f9d0a87e95796798 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 27 Aug 2026 11:45:08 -0500 Subject: [PATCH] test(e2e): capture API evidence on failures --- tests/runner-e2e/runner.spec.ts | 89 +++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/runner-e2e/runner.spec.ts b/tests/runner-e2e/runner.spec.ts index 04fa5d9f015..1ed7944f557 100644 --- a/tests/runner-e2e/runner.spec.ts +++ b/tests/runner-e2e/runner.spec.ts @@ -312,6 +312,76 @@ for (const execution of executions) { screenshots.push({ id, label, file }); }; + const captureFailureApiState = async () => { + if (!fixtures || !issue) return; + const capture = async (operation: () => Promise) => + operation().catch((error) => ({ + evidenceCaptureError: + error instanceof Error ? error.message : String(error), + })); + const [currentIssue, listedRuns, comments, interactions] = + await Promise.all([ + capture(() => api.get(`/api/issues/${issue!.id}`)), + capture(() => + api.get( + `/api/companies/${fixtures!.company.id}/heartbeat-runs?agentId=${fixtures!.agent.id}&limit=20`, + ), + ), + capture(() => + api.get( + `/api/issues/${issue!.id}/comments?order=asc`, + ), + ), + capture(() => + api.get( + `/api/issues/${issue!.id}/interactions`, + ), + ), + ]); + const taskRuns = Array.isArray(listedRuns) + ? matchingRuns( + listedRuns, + "id" in currentIssue ? currentIssue : issue, + ) + : []; + const detailedRuns = await Promise.all( + taskRuns.map((candidate) => + capture(() => + api.get(`/api/heartbeat-runs/${candidate.id}`), + ).then((value) => ("id" in value ? value : candidate)), + ), + ); + if (detailedRuns.length > 0) selectedRuns = detailedRuns; + const runEvidence = await Promise.all( + detailedRuns.map(async (candidate) => ({ + runId: candidate.id, + log: await capture(() => + api.get( + `/api/heartbeat-runs/${candidate.id}/log?limitBytes=1048576`, + ), + ), + events: await capture(() => + api.get( + `/api/heartbeat-runs/${candidate.id}/events?limit=1000`, + ), + ), + })), + ); + await writeSanitizedJson( + snapshotsDir, + "api-state.json", + { + capturePhase: "failure", + issue: currentIssue, + runs: detailedRuns, + comments, + interactions, + runEvidence, + }, + secrets, + ); + }; + page.on("console", (message) => { if (["error", "warning"].includes(message.type())) { consoleDiagnostics.push({ @@ -890,6 +960,25 @@ for (const execution of executions) { } } catch (error) { primaryError = error; + try { + await captureFailureApiState(); + } catch (captureError) { + const failureClass = classifyFailure(captureError); + if (failureClass === "secret_leak") { + failureClassOverride = "secret_leak"; + primaryError = new AggregateError( + [primaryError, captureError], + `Failure evidence contained unsafe data: ${captureError instanceof Error ? captureError.message : String(captureError)}`, + ); + } else { + networkDiagnostics.push({ + evidenceCaptureError: + captureError instanceof Error + ? captureError.message + : String(captureError), + }); + } + } if (!page.isClosed()) { const failureScreenshot = path.join(privateDir, "failure.png"); await page