Skip to content

Commit 9a8f21d

Browse files
committed
fix(windows): resolve CI failures with file locking and import resolution
Fixes multiple Windows-specific CI failures related to file system operations and ESLint configuration. Changes: - Add retry logic with exponential backoff for file writes (EPERM/EBUSY errors) - Add retry logic for temp directory cleanup in test afterEach hooks - Add .mjs files to ESLint test file patterns for import resolution - Handle transient Windows file handle locks during test cleanup Technical details: - EditableJson.save() retries up to 3 times with 10/20/40ms backoff - Test cleanup retries up to 5 times with 50ms + 100ms delays - ESLint now ignores import resolution for test/**/*.mjs files - Fixes: json.test.ts EPERM errors and esm-imports.test.mjs lint failures
1 parent 7a2b6d6 commit 9a8f21d

File tree

3 files changed

+61
-5
lines changed

3 files changed

+61
-5
lines changed

.config/eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ const eslintConfig = [
346346
},
347347
{
348348
// Relax rules for test files - testing code has different conventions
349-
files: ['test/**/*.ts', 'test/**/*.mts'],
349+
files: ['test/**/*.ts', 'test/**/*.mts', 'test/**/*.mjs'],
350350
rules: {
351351
'n/no-missing-import': 'off',
352352
'import-x/no-unresolved': 'off',

src/json/edit.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,44 @@ function getFs() {
4141
return _fs as typeof import('node:fs')
4242
}
4343

44+
/**
45+
* Retry a file write operation with exponential backoff on Windows EPERM errors.
46+
* Windows can have transient file locking issues with temp directories.
47+
* @private
48+
*/
49+
async function retryWrite(
50+
filepath: string,
51+
content: string,
52+
retries = 3,
53+
baseDelay = 10,
54+
): Promise<void> {
55+
const { promises: fsPromises } = getFs()
56+
57+
for (let attempt = 0; attempt <= retries; attempt++) {
58+
try {
59+
// eslint-disable-next-line no-await-in-loop
60+
await fsPromises.writeFile(filepath, content)
61+
return
62+
} catch (err) {
63+
const isLastAttempt = attempt === retries
64+
const isEperm =
65+
err instanceof Error &&
66+
'code' in err &&
67+
(err.code === 'EPERM' || err.code === 'EBUSY')
68+
69+
// Only retry on Windows EPERM/EBUSY errors, and not on the last attempt
70+
if (!isEperm || isLastAttempt) {
71+
throw err
72+
}
73+
74+
// Exponential backoff: 10ms, 20ms, 40ms
75+
const delay = baseDelay * 2 ** attempt
76+
// eslint-disable-next-line no-await-in-loop
77+
await new Promise(resolve => setTimeout(resolve, delay))
78+
}
79+
}
80+
}
81+
4482
/**
4583
* Parse JSON content and extract formatting metadata.
4684
* @private
@@ -216,9 +254,8 @@ export function getEditableJsonClass<
216254
// Generate file content
217255
const fileContent = stringifyWithFormatting(sortedContent, formatting)
218256

219-
// Save to disk
220-
const { promises: fsPromises } = getFs()
221-
await fsPromises.writeFile(this.filename, fileContent)
257+
// Save to disk with retry logic for Windows file locking issues
258+
await retryWrite(this.filename, fileContent)
222259
this._readFileContent = fileContent
223260
this._readFileJson = parseJson(fileContent)
224261
return true

test/unit/json.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import { mkdtemp, readFile, writeFile } from 'node:fs/promises'
1717
import { tmpdir } from 'node:os'
1818
import { join } from 'node:path'
19+
import { setTimeout as sleep } from 'node:timers/promises'
1920

2021
import { safeDelete } from '@socketsecurity/lib/fs'
2122
import { getEditableJsonClass } from '@socketsecurity/lib/json/edit'
@@ -766,7 +767,25 @@ describe('json', () => {
766767

767768
afterEach(async () => {
768769
if (testDir) {
769-
await safeDelete(testDir)
770+
// On Windows, add retry logic for directory deletion due to file handle timing
771+
if (process.platform === 'win32') {
772+
let retries = 5
773+
while (retries > 0) {
774+
try {
775+
await sleep(50)
776+
await safeDelete(testDir)
777+
break
778+
} catch (err) {
779+
retries--
780+
if (retries === 0) {
781+
throw err
782+
}
783+
await sleep(100)
784+
}
785+
}
786+
} else {
787+
await safeDelete(testDir)
788+
}
770789
}
771790
})
772791

0 commit comments

Comments
 (0)