From b17ecd1946e7d342b54875c4bf89bc1698993d63 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 30 Oct 2026 06:30:26 -0500 Subject: [PATCH] fix(e2e): include verified transport recovery causes Co-Authored-By: Paperclip --- tests/runner-e2e/PRODUCT-JOURNEYS.md | 7 +++++-- .../onboarding-first-task.test.ts | 16 +++++++++++----- .../product-journeys/workspace-deferral.ts | 5 ++++- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/tests/runner-e2e/PRODUCT-JOURNEYS.md b/tests/runner-e2e/PRODUCT-JOURNEYS.md index c5cf56ff5ce..3d1f998169a 100644 --- a/tests/runner-e2e/PRODUCT-JOURNEYS.md +++ b/tests/runner-e2e/PRODUCT-JOURNEYS.md @@ -105,8 +105,11 @@ server proves that no provider work started. A scheduled replacement remains pending and consumes the run limit. It cannot satisfy completion; the child must still finish through native execution and pass the independent artifact checks. -A native run with a saved automatic same-run recovery remains pending within the -original campaign deadline. The authenticated run projection must identify an +A native run can remain pending for saved automatic same-run recovery with code +`native_session_interrupted`, `provider_transport_failed`, +`provider_request_timeout`, or `native_runner_process_exited`. +The wait stays within the original campaign deadline. +The authenticated run projection must identify a matching source code and an agent-owned retryable native interruption, a bounded attempt and retry schedule, and no successor. An overdue retry can still be reconnecting; it does not extend the deadline or authorize a new story attempt. Ordinary failures, terminal or diff --git a/tests/runner-e2e/product-journeys/onboarding-first-task.test.ts b/tests/runner-e2e/product-journeys/onboarding-first-task.test.ts index 9104bcee9c8..0efa6442d29 100644 --- a/tests/runner-e2e/product-journeys/onboarding-first-task.test.ts +++ b/tests/runner-e2e/product-journeys/onboarding-first-task.test.ts @@ -301,7 +301,8 @@ describe("Cloud canonical onboarding first task", () => { expect(result.artifact.originatingRunId).toBe("child-run"); expect(connectionDelay).toHaveBeenCalledOnce(); }); - it.each([false, true])("observes the same native recovery through success with workspace predecessor=%s", async predecessor => { + it.each(["native_session_interrupted", "provider_transport_failed", "provider_request_timeout", "native_runner_process_exited"] + .flatMap(cause => [false, true].map(predecessor => ({ cause, predecessor }))))("observes $cause recovery through success with workspace predecessor=$predecessor", async ({ cause, predecessor }) => { let now = Date.now(), stage = 0; const failedAt = now - 1000, retryAt = now + 30_000; vi.spyOn(Date, "now").mockImplementation(() => now); @@ -311,7 +312,7 @@ describe("Cloud canonical onboarding first task", () => { const child = state.runs.find(run => run.id === "child-run")!; if (predecessor) { state.runs.splice(state.runs.indexOf(child), 0, workspaceDeferral()); linkWorkspaceRetry(child); } if (stage < 2) { - withPendingNativeRecovery(child, failedAt, retryAt); + withPendingNativeRecovery(child, failedAt, retryAt, cause); child.execution.phase = stage === 0 ? "retry_scheduled" : "reconnecting"; child.scheduledRetryAt = new Date(failedAt - 60_000).toISOString(); } else if (stage === 2) Object.assign(child, { status: "running", nativePhase: "observed", finishedAt: null }); @@ -365,6 +366,11 @@ describe("Cloud canonical onboarding first task", () => { ["mismatched issue binding", (run: Row) => { run.contextSnapshot.issueId = "other-issue"; }], ["provider failure", (run: Row) => { run.errorCode = run.execution.cause = "native_provider_terminal_failed"; }], ["mismatched cause", (run: Row) => { run.execution.cause = "provider_transport_failed"; }], + ["unknown matching cause", (run: Row) => { run.errorCode = run.execution.cause = "unclassified_provider_error"; }], + ["ambiguous transport failure", (run: Row) => { + run.errorCode = run.execution.cause = "provider_transport_failed"; run.nativePhase = "terminal_failure"; + Object.assign(run.execution, { phase: "recovery_needed", recoveryOwner: "board", retryAt: null }); + }], ["generic recovery", (run: Row) => { run.execution.phase = "recovery_needed"; }], ["board owner", (run: Row) => { run.execution.recoveryOwner = "board"; }], ["missing schedule", (run: Row) => { run.execution.retryAt = null; }], @@ -526,10 +532,10 @@ function workspaceDeferral(): Row { } }; } -function withPendingNativeRecovery(run: Row, failedAt: number, retryAt: number) { - Object.assign(run, { status: "failed", nativePhase: "retryable_failure", errorCode: "native_session_interrupted", +function withPendingNativeRecovery(run: Row, failedAt: number, retryAt: number, cause = "native_session_interrupted") { + Object.assign(run, { status: "failed", nativePhase: "retryable_failure", errorCode: cause, finishedAt: new Date(failedAt).toISOString(), execution: { - phase: "retry_scheduled", cause: "native_session_interrupted", retryAt: new Date(retryAt).toISOString(), + phase: "retry_scheduled", cause, retryAt: new Date(retryAt).toISOString(), attempt: 1, maxAttempts: 3, recoveryOwner: "agent", successorRunId: null, } }); } diff --git a/tests/runner-e2e/product-journeys/workspace-deferral.ts b/tests/runner-e2e/product-journeys/workspace-deferral.ts index ba0a66b8461..9f004f7a7ad 100644 --- a/tests/runner-e2e/product-journeys/workspace-deferral.ts +++ b/tests/runner-e2e/product-journeys/workspace-deferral.ts @@ -1,6 +1,9 @@ type RecordValue = Record; const object = (value: unknown): RecordValue => value !== null && typeof value === "object" && !Array.isArray(value) ? value as RecordValue : {}; +const nativeRecoveryCauses = new Set([ + "native_session_interrupted", "provider_transport_failed", "provider_request_timeout", "native_runner_process_exited", +]); function hasProviderUsage(value: unknown): boolean { if (value == null) return false; @@ -20,7 +23,7 @@ export function isPendingNativeRecovery(run: RecordValue, deadlineAt: number): b const finishedAt = typeof run.finishedAt === "string" ? Date.parse(run.finishedAt) : NaN; const retryAt = typeof execution.retryAt === "string" ? Date.parse(execution.retryAt) : NaN; return run.runtimeMode === "native" && run.status === "failed" && run.nativePhase === "retryable_failure" && - run.errorCode === "native_session_interrupted" && execution.cause === "native_session_interrupted" && + typeof run.errorCode === "string" && nativeRecoveryCauses.has(run.errorCode) && execution.cause === run.errorCode && typeof run.nativeIssueId === "string" && run.nativeIssueId === object(run.contextSnapshot).issueId && (run.issueId == null || run.issueId === run.nativeIssueId) && (execution.phase === "retry_scheduled" || execution.phase === "reconnecting") && execution.recoveryOwner === "agent" &&