From 3ecd87e06b270ed8cc5d74fe8cb596e8cb7acc68 Mon Sep 17 00:00:00 2001 From: tahayusab Date: Thu, 10 Sep 2026 02:38:07 +0300 Subject: [PATCH] fix(tests): retry EBUSY when removing test DB dirs on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows the OS keeps the SQLite WAL/SHM siblings locked for a while after the last handle closes (asynchronous release, sometimes extended by real-time antivirus scans of freshly written files). fs.rm then fails with EBUSY and flaky teardown failures appear across the server test suite. Retrying with exponential backoff (15s budget) is safe — every handle in the process is already closed — and on POSIX the first attempt always succeeds, so CI is unaffected. --- src/__tests__/helpers/createTestDb.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/__tests__/helpers/createTestDb.ts b/src/__tests__/helpers/createTestDb.ts index 996b5860d..9f8be1e9b 100644 --- a/src/__tests__/helpers/createTestDb.ts +++ b/src/__tests__/helpers/createTestDb.ts @@ -54,7 +54,32 @@ export async function createTestDb(): Promise { // Close before unlinking: the file, and its WAL/SHM siblings, stay // locked on Windows while the handle is open. await db.close() - await fs.rm(path.dirname(tmpFile), { recursive: true, force: true }) + await rmWithRetry(path.dirname(tmpFile)) }, } } + +/** + * Remove a test DB directory, retrying EBUSY with exponential backoff. + * + * On Windows the OS can keep the WAL/SHM siblings locked for a while after + * the last SQLite handle is closed — the release is asynchronous, and + * real-time antivirus scanners may hold a freshly written file for seconds + * (observed up to ~5s on CI workstations). Every handle in the process is + * already closed, so retrying is safe and turns a flaky teardown into a + * clean one; on POSIX the first attempt always succeeds. + */ +async function rmWithRetry(dir: string): Promise { + const deadline = Date.now() + 15_000 + let waitMs = 100 + for (;;) { + try { + await fs.rm(dir, { recursive: true, force: true }) + return + } catch (err) { + if ((err as NodeJS.ErrnoException)?.code !== 'EBUSY' || Date.now() + waitMs > deadline) throw err + await new Promise((resolve) => setTimeout(resolve, waitMs)) + waitMs = Math.min(waitMs * 2, 1000) + } + } +}