mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 06:04:11 +01:00
fix(e2e): include verified transport recovery causes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
bc4b3564cd
commit
b17ecd1946
3 files changed
+20
-8
No files matched your search
@@ -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
|
||||
|
||||
@@ -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,
|
||||
} });
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
type RecordValue = Record<string, unknown>;
|
||||
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" &&
|
||||
|
||||
Reference in new issue
Block a user