Skip to content

Commit 3e85766

Browse files
committed
test: draw transient codes from the full retryable set and cover exhaustion
From review: flaky and the new exhausted behavior throw any of the five retryable Windows codes (EPERM/EACCES dominate in practice, not just EBUSY), and the exhausted variant pins that a task burning the whole four-attempt budget rejects with the retryable error while its key's successors still run. https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
1 parent 96edddd commit 3e85766

1 file changed

Lines changed: 29 additions & 4 deletions

File tree

test/property/settings-write-queue.property.test.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,23 @@ import { describe, expect, it } from "vitest";
22
import * as fc from "fast-check";
33
import { withQueuedRetry } from "../../lib/codex-manager/settings-write-queue.js";
44

5-
type TaskBehavior = "ok" | "flaky" | "fatal";
5+
type TaskBehavior = "ok" | "flaky" | "fatal" | "exhausted";
6+
7+
const RETRYABLE_CODES = [
8+
"EBUSY",
9+
"EPERM",
10+
"EACCES",
11+
"EAGAIN",
12+
"ENOTEMPTY",
13+
] as const;
614

715
const arbSchedule = fc.array(
816
fc.record({
917
key: fc.integer({ min: 0, max: 2 }),
10-
behavior: fc.constantFrom<TaskBehavior>("ok", "flaky", "fatal"),
18+
behavior: fc.constantFrom<TaskBehavior>("ok", "flaky", "fatal", "exhausted"),
19+
// The transient code a flaky/exhausted task throws: the whole
20+
// retryable set matters on Windows, not just EBUSY.
21+
retryableCode: fc.constantFrom(...RETRYABLE_CODES),
1122
}),
1223
{ minLength: 1, maxLength: 12 },
1324
);
@@ -42,9 +53,13 @@ describe("withQueuedRetry serialization properties", () => {
4253
if (spec.behavior === "fatal") {
4354
throw errnoError("ENOSPC");
4455
}
56+
if (spec.behavior === "exhausted") {
57+
// Retryable on every attempt: burns the whole budget.
58+
throw errnoError(spec.retryableCode);
59+
}
4560
if (spec.behavior === "flaky" && !flakyFailed.has(taskIndex)) {
4661
flakyFailed.add(taskIndex);
47-
throw errnoError("EBUSY");
62+
throw errnoError(spec.retryableCode);
4863
}
4964
return `result-${taskIndex}`;
5065
},
@@ -60,7 +75,7 @@ describe("withQueuedRetry serialization properties", () => {
6075
// their own result; a failed predecessor never blocks successors.
6176
for (const result of settled) {
6277
const spec = schedule[result.taskIndex];
63-
if (spec.behavior === "fatal") {
78+
if (spec.behavior === "fatal" || spec.behavior === "exhausted") {
6479
expect(result.outcome).toBe("error");
6580
} else {
6681
expect(result).toMatchObject({
@@ -70,6 +85,16 @@ describe("withQueuedRetry serialization properties", () => {
7085
}
7186
}
7287

88+
// An exhausted task burns exactly the full retry budget.
89+
const allInvocations = [...invocationsByKey.values()].flat();
90+
for (let taskIndex = 0; taskIndex < schedule.length; taskIndex += 1) {
91+
if (schedule[taskIndex].behavior === "exhausted") {
92+
expect(
93+
allInvocations.filter((id) => id === taskIndex),
94+
).toHaveLength(4);
95+
}
96+
}
97+
7398
// Per key: invocations form contiguous groups (retries never
7499
// interleave with another task) and groups run in submission order.
75100
for (const [, invocations] of invocationsByKey) {

0 commit comments

Comments
 (0)