Skip to content

Commit 7cda98e

Browse files
committed
fix(windows): add file flush delay to prevent ENOENT errors in tests
On Windows, file operations can be asynchronous at the OS level. After writeFile() completes, the file handle may not be fully released immediately, causing ENOENT errors when code tries to read the file right after writing. Enhanced retryWrite() with ENOENT retry support: 1. **Retry on ENOENT errors** (not just EPERM/EBUSY) - this is the key fix 2. Add 10ms initial delay after write on Windows 3. Verify file accessibility with up to 3 retries (10ms each) 4. Total potential wait time: 10ms + (3 × 10ms) = 40ms max Root cause: The previous code only retried EPERM/EBUSY errors. The write itself can fail with ENOENT on Windows if the filesystem is slow to make the file available. Adding ENOENT to the retry conditions is the critical fix. The delays are kept minimal since the retry logic handles failures. This fixes intermittent test failures in Windows CI runners, including: - "should handle load -> update -> save workflow" - "should use default 2-space indent for new files" - "should preserve line endings" - "should support sort option"
1 parent 1fb817e commit 7cda98e

File tree

1 file changed

+29
-5
lines changed

1 file changed

+29
-5
lines changed

src/json/edit.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* @fileoverview Editable JSON file manipulation with formatting preservation.
33
*/
44

5+
import { setTimeout as sleep } from 'node:timers/promises'
6+
57
import {
68
INDENT_SYMBOL,
79
NEWLINE_SYMBOL,
@@ -58,23 +60,45 @@ async function retryWrite(
5860
try {
5961
// eslint-disable-next-line no-await-in-loop
6062
await fsPromises.writeFile(filepath, content)
63+
// On Windows, add a small delay and verify file exists to ensure it's fully flushed
64+
// This prevents ENOENT errors when immediately reading after write
65+
if (process.platform === 'win32') {
66+
// Small initial delay to allow OS to flush the write
67+
// eslint-disable-next-line no-await-in-loop
68+
await sleep(10)
69+
// Verify the file is actually readable with retries
70+
let accessRetries = 0
71+
const maxAccessRetries = 3
72+
while (accessRetries < maxAccessRetries) {
73+
try {
74+
// eslint-disable-next-line no-await-in-loop
75+
await fsPromises.access(filepath)
76+
break
77+
} catch {
78+
// If file isn't accessible yet, wait a bit more
79+
// eslint-disable-next-line no-await-in-loop
80+
await sleep(10)
81+
accessRetries++
82+
}
83+
}
84+
}
6185
return
6286
} catch (err) {
6387
const isLastAttempt = attempt === retries
64-
const isEperm =
88+
const isRetriableError =
6589
err instanceof Error &&
6690
'code' in err &&
67-
(err.code === 'EPERM' || err.code === 'EBUSY')
91+
(err.code === 'EPERM' || err.code === 'EBUSY' || err.code === 'ENOENT')
6892

69-
// Only retry on Windows EPERM/EBUSY errors, and not on the last attempt
70-
if (!isEperm || isLastAttempt) {
93+
// Only retry on Windows file system errors (EPERM/EBUSY/ENOENT), and not on the last attempt
94+
if (!isRetriableError || isLastAttempt) {
7195
throw err
7296
}
7397

7498
// Exponential backoff: 10ms, 20ms, 40ms
7599
const delay = baseDelay * 2 ** attempt
76100
// eslint-disable-next-line no-await-in-loop
77-
await new Promise(resolve => setTimeout(resolve, delay))
101+
await sleep(delay)
78102
}
79103
}
80104
}

0 commit comments

Comments
 (0)