diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eef67dbf3b..1d11ebe15b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -553,7 +553,16 @@ jobs: # Sharded like the Linux legs. The single-leg run reached 30 minutes on a # green suite and was killed in cleanup; four shards put each leg inside the # same budget the Linux shards already hold. - timeout-minutes: 15 + # + # 15 was that Linux budget, and on this leg it truncated the evidence rather + # than bounding a hang: shard 1/4 of run 32340498394 was CANCELLED at exactly + # 15m12s while still executing tests, so its result was neither pass nor fail + # and the composed-acceptance cases it carries could not be read at all. The + # other shards finished in 14-15 minutes, which is the wrong side of the + # margin. 25 leaves the outer bound in place — a wedged shard still dies — + # while making a completed shard the normal outcome. The crash-retry below can + # double a shard's work, and this ceiling has to cover that second attempt too. + timeout-minutes: 25 strategy: fail-fast: false matrix: diff --git a/tests/ci-workflows.test.ts b/tests/ci-workflows.test.ts index 8ba3e7d406..becd15dbdc 100644 --- a/tests/ci-workflows.test.ts +++ b/tests/ci-workflows.test.ts @@ -101,7 +101,9 @@ describe("GitHub Actions hardening", () => { expect(ci.jobs?.test?.["timeout-minutes"]).toBe(15); expect(ci.jobs?.gates?.["timeout-minutes"]).toBe(15); expect(ci.jobs?.["platform-macos"]?.["timeout-minutes"]).toBe(30); - expect(ci.jobs?.["platform-windows"]?.["timeout-minutes"]).toBe(15); + // Higher than the Linux shards on purpose: at 15 the Windows leg cancelled a + // shard mid-suite, which reports as neither pass nor fail (#2152). + expect(ci.jobs?.["platform-windows"]?.["timeout-minutes"]).toBe(25); expect(ci.jobs?.["keyring-smoke"]?.["timeout-minutes"]).toBe(8); expect(ci.jobs?.["npm-global-smoke"]?.["timeout-minutes"]).toBe(8); expect(ci.jobs?.ci?.["timeout-minutes"]).toBe(5); diff --git a/tests/claude-shell-hook.test.ts b/tests/claude-shell-hook.test.ts index 9a2b4cbd36..676827f874 100644 --- a/tests/claude-shell-hook.test.ts +++ b/tests/claude-shell-hook.test.ts @@ -83,13 +83,21 @@ describe("Claude Code shell-hook reconciliation", () => { expect(readFileSync(zshrcPath, "utf8").match(/opencodex claude-env hook/g)).toHaveLength(1); }); - test("does not treat a non-executable claude file as an installed CLI", () => { - writeFileSync(join(binDir, "claude"), "#!/bin/sh\nexit 0\n", { mode: 0o644 }); + // Windows has no execute permission bit: `accessSync(path, X_OK)` succeeds for any + // readable file, so a 0o644 fixture cannot express "present but not executable" there. + // The case asserts a POSIX permission semantic, and skipping it on a platform that + // cannot represent the precondition is honest; asserting it anyway measured the + // fixture, not the product (#2152). + test.skipIf(originalPlatform === "win32")( + "does not treat a non-executable claude file as an installed CLI", + () => { + writeFileSync(join(binDir, "claude"), "#!/bin/sh\nexit 0\n", { mode: 0o644 }); - expect(claudeCodeCliInstalled()).toBe(false); - expect(reconcileShellHook(true)).toMatchObject({ changed: false, state: "absent" }); - expect(existsSync(zshrcPath)).toBe(false); - }); + expect(claudeCodeCliInstalled()).toBe(false); + expect(reconcileShellHook(true)).toMatchObject({ changed: false, state: "absent" }); + expect(existsSync(zshrcPath)).toBe(false); + }, + ); test("removes the hook when system environment integration is inactive", () => { installClaudeCli(); diff --git a/tests/helpers/test-budget.ts b/tests/helpers/test-budget.ts index 94914ac3ed..5b827c7dc3 100644 --- a/tests/helpers/test-budget.ts +++ b/tests/helpers/test-budget.ts @@ -44,7 +44,22 @@ export const STORE_BUDGET_MS = 30_000; * assertion (durable spill is the product contract), so the wait is intrinsic; the * orphan-cleanup cap test measured ~34s on windows-latest against Bun's 5s default. */ -export const BULK_DURABLE_IO_BUDGET_MS = 90_000; +export const BULK_DURABLE_IO_BUDGET_MS = bulkDurableIoBudgetMs(); + +/** + * Windows needs a higher ceiling than the ~34s that sized this number, for the same + * reason `watchdogMs` carries a higher floor there: the leg runs four Bun pools on one + * runner, and every one of these writes is an individual fsync against a filesystem that + * is slower under that contention to begin with. The orphan-cleanup cap case ran 100.6s + * against the 90s budget on shard 4/4 (#2152) while doing exactly the work it claims — + * 521 durable writes — so the number was measuring runner contention, not a hang. + * + * 180s stays a bound rather than an absence of one, and it is gated on Windows so no + * other lane loses the shorter signal. + */ +function bulkDurableIoBudgetMs(): number { + return process.platform === "win32" ? 180_000 : 90_000; +} /** * A deadline *inside* a test, for an await that would otherwise hang forever.