From e06f1de543cc469c56234a719fdf87e0c1f13eef Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 16 Sep 2026 10:58:41 +0800 Subject: [PATCH] test(provision): retry scratch-dir removal after a killed child on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `test (windows-latest, unit)` on main `2bae6708` failed in `provision-runner-signals.test.ts` — "normal close confirms the whole GROUP": the assertions passed, then `afterEach` `rmSync('.tmp-group-close')` threw `EPERM`. The directory is the cwd of the spawned leader and grandchild. On Windows `TerminateProcess` returns before the dead process releases its handles, so an immediate `rmSync` races that release. `removeProcessScratchDir` passes `maxRetries: 10, retryDelay: 100`, which makes Node retry `EPERM`/`EBUSY`/`ENOTEMPTY` with linear backoff. The three describes that spawn a real wrangler stand-in inside a scratch cwd (`.tmp-timeout-kill`, `.tmp-signal-teardown`, `.tmp-group-close`) use it in both `beforeEach` and `afterEach`. Test bodies are unchanged. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SStP281NuirMSouULEE2pW --- packages/void/test/unit/provision.cases.ts | 23 ++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/void/test/unit/provision.cases.ts b/packages/void/test/unit/provision.cases.ts index d37a1b447..800bfb8ce 100644 --- a/packages/void/test/unit/provision.cases.ts +++ b/packages/void/test/unit/provision.cases.ts @@ -40,6 +40,17 @@ import { getProvisionTestShard, type ProvisionTestShard } from './provision-test const activeTestShard = getProvisionTestShard(); +/** + * Remove a scratch directory that a spawned wrangler stand-in used as its cwd. + * On Windows `TerminateProcess` returns before the dead process releases its + * handles, so an immediate `rmSync` in `afterEach` fails with `EPERM` even + * though the test already confirmed the process dead. Node retries + * `EPERM`/`EBUSY`/`ENOTEMPTY` with linear backoff when `maxRetries` is set. + */ +function removeProcessScratchDir(dir: string): void { + rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }); +} + function shardForDescribe(name: string): ProvisionTestShard { if (name.startsWith('normalizeRunWranglerResult')) { return 'runner-signals'; @@ -2483,11 +2494,11 @@ describe('normalizeRunWranglerResult — signal / spawn-error handling', () => { describe('createDefaultRunWrangler — hard timeout kills the REAL process', () => { const TIMEOUT_TMP = join(import.meta.dirname, '.tmp-timeout-kill'); beforeEach(() => { - rmSync(TIMEOUT_TMP, { recursive: true, force: true }); + removeProcessScratchDir(TIMEOUT_TMP); mkdirSync(TIMEOUT_TMP, { recursive: true }); }); afterEach(() => { - rmSync(TIMEOUT_TMP, { recursive: true, force: true }); + removeProcessScratchDir(TIMEOUT_TMP); }); /** Poll `process.kill(pid, 0)` until the pid is dead (ESRCH) or the deadline passes. */ @@ -5602,11 +5613,11 @@ const WINDOWS_NO_SIGNALS = 'signal delivery to another process is POSIX-only (se describe('createDefaultRunWrangler — parent-signal forwarding awaits confirmed teardown', () => { const SIGNAL_TEARDOWN_TMP = join(import.meta.dirname, '.tmp-signal-teardown'); beforeEach(() => { - rmSync(SIGNAL_TEARDOWN_TMP, { recursive: true, force: true }); + removeProcessScratchDir(SIGNAL_TEARDOWN_TMP); mkdirSync(SIGNAL_TEARDOWN_TMP, { recursive: true }); }); afterEach(() => { - rmSync(SIGNAL_TEARDOWN_TMP, { recursive: true, force: true }); + removeProcessScratchDir(SIGNAL_TEARDOWN_TMP); }); it('SIGTERM to the parent: the child group is confirmed dead BEFORE the parent re-raises and exits', async (ctx) => { @@ -5695,11 +5706,11 @@ describe('createDefaultRunWrangler — parent-signal forwarding awaits confirmed describe('createDefaultRunWrangler — normal close confirms the whole GROUP', () => { const GROUP_CLOSE_TMP = join(import.meta.dirname, '.tmp-group-close'); beforeEach(() => { - rmSync(GROUP_CLOSE_TMP, { recursive: true, force: true }); + removeProcessScratchDir(GROUP_CLOSE_TMP); mkdirSync(GROUP_CLOSE_TMP, { recursive: true }); }); afterEach(() => { - rmSync(GROUP_CLOSE_TMP, { recursive: true, force: true }); + removeProcessScratchDir(GROUP_CLOSE_TMP); }); it('an exit-0 leader that leaves a SAME-GROUP grandchild alive: the runner resolves only after the group is dead', async () => {