Skip to content

Commit cfb7aa1

Browse files
committed
fix(e2e): preserve exhausted cleanup evidence
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
1 parent b2b331e commit cfb7aa1

3 files changed

Lines changed: 43 additions & 2 deletions

File tree

test/e2e/RETRY_INVENTORY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ Exhaustion remains failed.
4343

4444
`test/e2e/fixtures/retry-policy.ts` emits schema version 1.
4545
The aggregate `outcome` is exactly one of `passed-first-attempt`, `passed-after-retry`, `failed-no-retry`, or `exhausted`.
46-
Cleanup failures use `failed-no-retry` with a final attempt whose `failureClass` is `cleanup`.
46+
A cleanup failure uses `failed-no-retry` when no earlier failure scheduled a retry.
47+
When a bounded retry reaches its final attempt and that attempt fails during cleanup, the aggregate outcome is `exhausted` and the final attempt uses `failureClass: cleanup`.
4748
Each attempt also records its number, failure class, reconciliation result when applicable, and whether another attempt was scheduled.
4849
The record deliberately excludes command output, errors, request bodies, headers, and environment values.
4950
Callers retain their normal redacted artifacts separately and may write the aggregate record through `onEvidence`.

test/e2e/fixtures/retry-policy.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,12 @@ export async function runBoundedRetry<T>(
176176
continue;
177177
}
178178

179-
const outcome = isTransient && !hasBudget ? "exhausted" : "failed-no-retry";
179+
const exhaustedCleanup =
180+
classification.failureClass === "cleanup" &&
181+
!hasBudget &&
182+
attempts.some((previous) => previous.retryScheduled);
183+
const outcome =
184+
(isTransient && !hasBudget) || exhaustedCleanup ? "exhausted" : "failed-no-retry";
180185
const evidence = finalEvidence(options, attempts, outcome);
181186
await emit(options, evidence);
182187
if (error !== undefined) {

test/e2e/support/retry-policy.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,41 @@ describe("bounded E2E operation retry policy", () => {
183183
expect(JSON.stringify(caught?.evidence)).not.toContain(secret);
184184
});
185185

186+
it("preserves exhaustion when a bounded retry ends in cleanup failure", async () => {
187+
const run = vi
188+
.fn()
189+
.mockRejectedValueOnce(new Error("temporary transport failure"))
190+
.mockRejectedValueOnce(new Error("cleanup failed"));
191+
let caught: RetryPolicyError | undefined;
192+
try {
193+
await runBoundedRetry({
194+
operation: "sandbox.cleanup",
195+
owner: "nemoclaw",
196+
idempotence: "idempotent",
197+
maxAttempts: 2,
198+
run,
199+
classify: (_value, error) => ({
200+
outcome: "failed",
201+
failureClass:
202+
error instanceof Error && error.message === "temporary transport failure"
203+
? "transient-external"
204+
: "cleanup",
205+
}),
206+
});
207+
} catch (error) {
208+
caught = error as RetryPolicyError;
209+
}
210+
211+
expect(caught).toBeInstanceOf(RetryPolicyError);
212+
expect(caught?.evidence).toMatchObject({
213+
outcome: "exhausted",
214+
attempts: [
215+
{ failureClass: "transient-external", retryScheduled: true },
216+
{ failureClass: "cleanup", retryScheduled: false },
217+
],
218+
});
219+
});
220+
186221
it("rejects malformed policy bounds before running", async () => {
187222
const run = vi.fn();
188223
await expect(

0 commit comments

Comments
 (0)