@@ -6,28 +6,35 @@ const _originalFsPromisesAccess = actualFsPromises.access
66
77jest . mock ( "fs/promises" , ( ) => {
88 const actual = jest . requireActual ( "fs/promises" )
9- return {
10- // Explicitly mock functions used by the SUT and tests, defaulting to actual implementations
11- writeFile : jest . fn ( actual . writeFile ) ,
12- readFile : jest . fn ( actual . readFile ) ,
13- rename : jest . fn ( actual . rename ) ,
14- unlink : jest . fn ( actual . unlink ) ,
15- access : jest . fn ( actual . access ) ,
16- mkdtemp : jest . fn ( actual . mkdtemp ) ,
17- rm : jest . fn ( actual . rm ) ,
18- readdir : jest . fn ( actual . readdir ) ,
19- // Ensure all functions from 'fs/promises' that might be called are explicitly mocked
20- // or ensure that the SUT and tests only call functions defined here.
21- // For any function not listed, calls like fs.someOtherFunc would be undefined.
22- }
9+ // Start with all actual implementations.
10+ const mockedFs = { ...actual }
11+
12+ // Selectively wrap functions with jest.fn() if they are spied on
13+ // or have their implementations changed in tests.
14+ // This ensures that other fs.promises functions used by the SUT
15+ // (like proper-lockfile's internals) will use their actual implementations.
16+ mockedFs . writeFile = jest . fn ( actual . writeFile )
17+ mockedFs . readFile = jest . fn ( actual . readFile )
18+ mockedFs . rename = jest . fn ( actual . rename )
19+ mockedFs . unlink = jest . fn ( actual . unlink )
20+ mockedFs . access = jest . fn ( actual . access )
21+ mockedFs . mkdtemp = jest . fn ( actual . mkdtemp )
22+ mockedFs . rm = jest . fn ( actual . rm )
23+ mockedFs . readdir = jest . fn ( actual . readdir )
24+ // fs.stat and fs.lstat will be available via { ...actual }
25+
26+ return mockedFs
2327} )
2428
2529import * as fs from "fs/promises" // This will now be the mocked version
2630import * as path from "path"
2731import * as os from "os"
28- import { safeWriteJson , activeLocks } from "../safeWriteJson"
32+ // import * as lockfile from 'proper-lockfile' // No longer directly used in tests
33+ import { safeWriteJson } from "../safeWriteJson"
2934
3035describe ( "safeWriteJson" , ( ) => {
36+ jest . useRealTimers ( ) // Use real timers for this test suite
37+
3138 let tempTestDir : string = ""
3239 let currentTestFilePath = ""
3340
@@ -36,15 +43,15 @@ describe("safeWriteJson", () => {
3643 const tempDirPrefix = path . join ( os . tmpdir ( ) , "safeWriteJson-test-" )
3744 tempTestDir = await fs . mkdtemp ( tempDirPrefix )
3845 currentTestFilePath = path . join ( tempTestDir , "test-data.json" )
39- activeLocks . clear ( )
46+ // Individual tests will now handle creation of currentTestFilePath if needed.
4047 } )
4148
4249 afterEach ( async ( ) => {
4350 if ( tempTestDir ) {
4451 await fs . rm ( tempTestDir , { recursive : true , force : true } )
4552 tempTestDir = ""
4653 }
47- activeLocks . clear ( )
54+ // activeLocks is no longer used
4855
4956 // Explicitly reset mock implementations to default (actual) behavior
5057 // This helps prevent state leakage between tests if spy.mockRestore() isn't fully effective
@@ -102,6 +109,13 @@ describe("safeWriteJson", () => {
102109
103110 // Failure Scenarios
104111 test ( "should handle failure when writing to tempNewFilePath" , async ( ) => {
112+ // Ensure the target file does not exist for this test.
113+ try {
114+ await fs . unlink ( currentTestFilePath )
115+ } catch ( e : any ) {
116+ if ( e . code !== "ENOENT" ) throw e
117+ }
118+
105119 const data = { message : "This should not be written" }
106120 const writeFileSpy = jest . spyOn ( fs , "writeFile" )
107121 // Make the first call to writeFile (for tempNewFilePath) fail
@@ -261,6 +275,13 @@ describe("safeWriteJson", () => {
261275 } )
262276
263277 test ( "should handle failure when renaming tempNewFilePath to filePath (filePath does not exist)" , async ( ) => {
278+ // Ensure the target file does not exist for this test.
279+ try {
280+ await fs . unlink ( currentTestFilePath )
281+ } catch ( e : any ) {
282+ if ( e . code !== "ENOENT" ) throw e
283+ }
284+
264285 const data = { message : "This should not be written" }
265286 const renameSpy = jest . spyOn ( fs , "rename" )
266287 // The rename from tempNew to target fails
@@ -285,18 +306,30 @@ describe("safeWriteJson", () => {
285306 renameSpy . mockRestore ( )
286307 } )
287308
288- test ( "should throw an error if a lock is already held for the filePath" , async ( ) => {
309+ test ( "should throw an error if an inter-process lock is already held for the filePath" , async ( ) => {
310+ jest . resetModules ( ) // Clear module cache to ensure fresh imports for this test
311+
289312 const data = { message : "test lock" }
290- // Manually acquire lock for testing purposes
291- activeLocks . add ( path . resolve ( currentTestFilePath ) )
313+ // Ensure the resource file exists.
314+ await fs . writeFile ( currentTestFilePath , "{}" , "utf8" )
292315
293- await expect ( safeWriteJson ( currentTestFilePath , data ) ) . rejects . toThrow (
294- `File operation already in progress for this path: ${ path . resolve ( currentTestFilePath ) } ` ,
295- )
316+ // Temporarily mock proper-lockfile for this test only
317+ jest . doMock ( "proper-lockfile" , ( ) => ( {
318+ ...jest . requireActual ( "proper-lockfile" ) ,
319+ lock : jest . fn ( ) . mockRejectedValueOnce ( new Error ( "Failed to get lock." ) ) ,
320+ } ) )
321+
322+ // Re-require safeWriteJson so it picks up the mocked proper-lockfile
323+ const { safeWriteJson : safeWriteJsonWithMockedLock } =
324+ require ( "../safeWriteJson" ) as typeof import ( "../safeWriteJson" )
296325
297- // Ensure lock is still there (safeWriteJson shouldn't release if it didn't acquire)
298- expect ( activeLocks . has ( path . resolve ( currentTestFilePath ) ) ) . toBe ( true )
299- activeLocks . delete ( path . resolve ( currentTestFilePath ) ) // Manual cleanup for this test
326+ try {
327+ await expect ( safeWriteJsonWithMockedLock ( currentTestFilePath , data ) ) . rejects . toThrow (
328+ / F a i l e d t o g e t l o c k .| L o c k f i l e i s a l r e a d y b e i n g h e l d / i,
329+ )
330+ } finally {
331+ jest . unmock ( "proper-lockfile" ) // Ensure the mock is removed after this test
332+ }
300333 } )
301334 test ( "should release lock even if an error occurs mid-operation" , async ( ) => {
302335 const data = { message : "test lock release on error" }
@@ -306,7 +339,9 @@ describe("safeWriteJson", () => {
306339
307340 await expect ( safeWriteJson ( currentTestFilePath , data ) ) . rejects . toThrow ( "Simulated FS Error during writeFile" )
308341
309- expect ( activeLocks . has ( path . resolve ( currentTestFilePath ) ) ) . toBe ( false ) // Lock should be released
342+ // Lock should be released, meaning the .lock file should not exist
343+ const lockPath = `${ path . resolve ( currentTestFilePath ) } .lock`
344+ await expect ( fs . access ( lockPath ) ) . rejects . toThrow ( expect . objectContaining ( { code : "ENOENT" } ) )
310345
311346 writeFileSpy . mockRestore ( )
312347 } )
@@ -321,7 +356,10 @@ describe("safeWriteJson", () => {
321356
322357 await expect ( safeWriteJson ( currentTestFilePath , data ) ) . rejects . toThrow ( "Simulated EACCES Error" )
323358
324- expect ( activeLocks . has ( path . resolve ( currentTestFilePath ) ) ) . toBe ( false ) // Lock should be released
359+ // Lock should be released, meaning the .lock file should not exist
360+ const lockPath = `${ path . resolve ( currentTestFilePath ) } .lock`
361+ await expect ( fs . access ( lockPath ) ) . rejects . toThrow ( expect . objectContaining ( { code : "ENOENT" } ) )
362+
325363 const tempFiles = await listTempFiles ( tempTestDir , "test-data.json" )
326364 // .new file might have been created before access check, should be cleaned up
327365 expect ( tempFiles . filter ( ( f : string ) => f . includes ( ".new_" ) ) . length ) . toBe ( 0 )
0 commit comments