From 31150049176739ca5d0a0ff3fab8ef2e2e7f8ff0 Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 17 Oct 2026 06:13:26 +0000 Subject: [PATCH] fix(windows-seed): record slow Windows files in full instead of at the 6-minute deadline The first Windows recording on eca247e99 (run 39008249142) killed test/gstack-doctor.test.ts at the 360 s per-file deadline and would have seeded it at 360 s, although it takes about 484 s on Windows. --record-durations now honors an explicit --wall-timeout as its per-file deadline, and the windows-record-durations job passes --wall-timeout 1200 (inside its 30-minute job limit). The shard job's 15-minute timeout and the shard count are unchanged. The recording job also uploads its full per-file logs, so a file that fails while recording (browse/test/cookie-import-native-job.test.ts did in that run) can be diagnosed. Intentional pin change: test/windows-native-workflows.test.ts now expects the --wall-timeout 1200 recording command and the log upload step. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- .github/workflows/windows-free-tests.yml | 12 +++++++++++- scripts/test-free-shards.ts | 14 +++++++++++--- test/test-free-shards.test.ts | 7 +++++++ test/windows-native-workflows.test.ts | 5 ++++- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/.github/workflows/windows-free-tests.yml b/.github/workflows/windows-free-tests.yml index 824ccfc48..5d7775a52 100644 --- a/.github/workflows/windows-free-tests.yml +++ b/.github/workflows/windows-free-tests.yml @@ -235,11 +235,21 @@ jobs: env: TEMP: ${{ runner.temp }} TMP: ${{ runner.temp }} - run: bun run test:windows --record-durations + run: bun run test:windows --record-durations --wall-timeout 1200 - name: Name the recording as the Windows seed if: always() shell: bash run: cp scripts/free-test-durations.json "$RUNNER_TEMP/free-test-durations-windows.json" + - name: Upload full recording logs + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + with: + name: windows-record-durations-logs + path: | + .context/free-test-logs/gstack-free-test-*.log + ${{ runner.temp }}/gstack-free-test-*.log + include-hidden-files: true + if-no-files-found: ignore - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 if: always() with: diff --git a/scripts/test-free-shards.ts b/scripts/test-free-shards.ts index 8b53a9f20..7e73c048a 100755 --- a/scripts/test-free-shards.ts +++ b/scripts/test-free-shards.ts @@ -1707,13 +1707,21 @@ export function exitCodeFor(status: FreeShardStatus): number { return status === 'timed-out' ? 124 : 1; } +/** Per-file deadline for `--record-durations`: an explicit --wall-timeout, else the one-file default. */ +export function recordingWallTimeoutMs(options: Pick): number { + return options.wallTimeoutExplicit ? options.wallTimeoutMs : wallTimeoutForShard(1); +} + /** * `--record-durations`: time every file in its own child (exact per-file wall, * immune to bun's stream buffering) and write the committed seed atomically. * Uses the same isolated, strictly classified children as a normal run. * Run against an immutable checkout; never time while editing its inputs. + * An explicit --wall-timeout sets the per-file deadline, so a file slower than + * the default deadline is timed in full instead of being killed and recorded + * at the deadline. */ -async function recordFreeTestDurations(files: string[], jobs: number): Promise { +async function recordFreeTestDurations(files: string[], jobs: number, wallTimeoutMs: number): Promise { const durations: Record = {}; const failed: string[] = []; const expectedCount = files.length; @@ -1722,7 +1730,7 @@ async function recordFreeTestDurations(files: string[], jobs: number): Promise !(file in TREE_MUTATING)), files.filter(file => file in TREE_MUTATING), jobs, async (file, index) => { const outcome = await runFreeShard([file], index + 1, files.length, { - wallTimeoutMs: wallTimeoutForShard(1), quiet: true, homeGuard: sharedFreeHome, + wallTimeoutMs, quiet: true, homeGuard: sharedFreeHome, }); durations[normalizeRelativePath(file)] = outcome.elapsedMs; if (outcome.status !== 'passed') failed.push(file); @@ -1907,7 +1915,7 @@ async function main(): Promise { } if (options.recordDurations) { - return recordFreeTestDurations(files, fullSuiteJobs()); + return recordFreeTestDurations(files, fullSuiteJobs(), recordingWallTimeoutMs(options)); } if (options.attributeHome) return attributeFreeHomeWriters(files.filter(file => !(file in TREE_MUTATING)), files.filter(file => file in TREE_MUTATING), fullSuiteJobs(), (file, index, homeGuard) => runFreeShard([file], index + 1, files.length, { wallTimeoutMs: wallTimeoutForShard(1), quiet: true, log: () => {}, homeGuard })); diff --git a/test/test-free-shards.test.ts b/test/test-free-shards.test.ts index 2ef9d8fbc..16272b9f8 100644 --- a/test/test-free-shards.test.ts +++ b/test/test-free-shards.test.ts @@ -18,6 +18,8 @@ import { DEFAULT_WALL_TIMEOUT_MS, PER_FILE_WALL_MS, wallTimeoutForShard, + recordingWallTimeoutMs, + parseCliOptions, KNOWN_WINDOWS_INCOMPATIBLE, TEST_ROOTS, TREE_MUTATING, @@ -940,6 +942,11 @@ describe('test-free-shards: wall-timeout scaling', () => { test('an explicit base above the scaled value wins', () => { expect(wallTimeoutForShard(10, 10 * 60_000)).toBe(10 * 60_000); }); + + test('--record-durations times each file under the one-file default unless --wall-timeout is explicit', () => { + expect(recordingWallTimeoutMs(parseCliOptions(['--record-durations']))).toBe(wallTimeoutForShard(1)); + expect(recordingWallTimeoutMs(parseCliOptions(['--record-durations', '--wall-timeout', '1200']))).toBe(1_200_000); + }); }); diff --git a/test/windows-native-workflows.test.ts b/test/windows-native-workflows.test.ts index 0023e3228..838d7c528 100644 --- a/test/windows-native-workflows.test.ts +++ b/test/windows-native-workflows.test.ts @@ -62,11 +62,14 @@ test('a record_durations dispatch times each Windows-safe file alone and uploads expect(job.if).toBe('${{ inputs.record_durations }}'); expect(job['runs-on']).toBe('windows-latest'); const record = job.steps.find((step: any) => step.name === 'Time every Windows-safe file alone'); - expect(record.run).toBe('bun run test:windows --record-durations'); + expect(record.run).toBe('bun run test:windows --record-durations --wall-timeout 1200'); expect(record.env.GSTACK_FREE_TEST_DURATIONS).toBeUndefined(); const rename = job.steps.find((step: any) => step.name === 'Name the recording as the Windows seed'); expect(rename.if).toBe('always()'); expect(rename.run).toBe('cp scripts/free-test-durations.json "$RUNNER_TEMP/free-test-durations-windows.json"'); + const logs = job.steps.find((step: any) => step.name === 'Upload full recording logs'); + expect(logs.if).toBe('always()'); + expect(logs.with['include-hidden-files']).toBe(true); expect(job.steps.at(-1).with.name).toBe('free-test-durations-windows'); });