From 88c4f1561a9a97856d132439000632ded869715b Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Thu, 20 Aug 2026 16:21:09 +0900 Subject: [PATCH] fix(ci): stop the Windows leg from truncating and mismeasuring its own shards Run 32340498394 dispatched the Windows leg at the release head and produced three results that were not defects in this repository: 1. shard 1/4 was CANCELLED at 15m12s while still executing tests. That is neither a pass nor a fail, and it silently removed the composed-acceptance cases from the evidence. The other shards finished at 14-15 minutes, so 15 was inside the noise band rather than above it. Raised to 25, which still kills a wedged shard and now also covers the second attempt the crash retry is allowed to make. 2. `Responses previous_response_id state > orphan cleanup obeys scan and cleanup caps` ran 100.6s against a 90s budget on shard 4/4 while doing exactly the work it claims: 521 individually fsync'd durable writes. The number was sized from a ~34s windows-latest measurement and was measuring runner contention, not a hang. BULK_DURABLE_IO_BUDGET_MS now carries a Windows-only 180s ceiling, the same shape as the watchdogMs floor. 3. `Claude Code shell-hook reconciliation > does not treat a non-executable claude file as an installed CLI` writes mode 0o644 and expects claudeCodeCliInstalled() to be false. Windows has no execute-permission bit, so accessSync(path, X_OK) succeeds for any readable file and the fixture cannot express its own precondition. It now skips on win32, as several neighbouring symlink cases already do. Refs #2152. --- .github/workflows/ci.yml | 11 ++++++++++- tests/ci-workflows.test.ts | 4 +++- tests/claude-shell-hook.test.ts | 20 ++++++++++++++------ tests/helpers/test-budget.ts | 17 ++++++++++++++++- 4 files changed, 43 insertions(+), 9 deletions(-) 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.