@@ -6,7 +6,7 @@ import path from 'node:path';
66import assert from 'node:assert' ;
77import process from 'node:process' ;
88import { describe , it , beforeEach , afterEach } from 'node:test' ;
9- import { writeFileSync , mkdirSync , appendFileSync } from 'node:fs' ;
9+ import { writeFileSync , mkdirSync , appendFileSync , rmSync } from 'node:fs' ;
1010import { createInterface } from 'node:readline' ;
1111import { setTimeout } from 'node:timers/promises' ;
1212import { once } from 'node:events' ;
@@ -44,6 +44,19 @@ describe('watch mode file watcher', () => {
4444 } ) ;
4545 }
4646
47+ function replaceAndWaitForChanges ( watcher , file ) {
48+ return new Promise ( ( resolve ) => {
49+ const interval = setInterval ( ( ) => {
50+ rmSync ( file , { force : true } ) ;
51+ writeFileSync ( file , `replace ${ counter ++ } ` ) ;
52+ } , 100 ) ;
53+ watcher . once ( 'changed' , ( ) => {
54+ clearInterval ( interval ) ;
55+ resolve ( ) ;
56+ } ) ;
57+ } ) ;
58+ }
59+
4760 it ( 'should watch changed files' , async ( ) => {
4861 const file = tmpdir . resolve ( 'file1' ) ;
4962 writeFileSync ( file , 'written' ) ;
@@ -52,6 +65,19 @@ describe('watch mode file watcher', () => {
5265 assert . strictEqual ( changesCount , 1 ) ;
5366 } ) ;
5467
68+ it ( 'should keep detecting files replaced via unlink and create' , async ( ) => {
69+ // Regression test for https://github.com/nodejs/node/issues/51621: a watch
70+ // bound to the file inode stops firing after the first replacement, so the
71+ // second `replaceAndWaitForChanges` call would hang on the buggy behavior.
72+ const file = tmpdir . resolve ( 'replaced.js' ) ;
73+ writeFileSync ( file , 'written' ) ;
74+ watcher . filterFile ( file ) ;
75+ await replaceAndWaitForChanges ( watcher , file ) ;
76+ await replaceAndWaitForChanges ( watcher , file ) ;
77+ await replaceAndWaitForChanges ( watcher , file ) ;
78+ assert . ok ( changesCount >= 3 , `expected at least 3 changes, got ${ changesCount } ` ) ;
79+ } ) ;
80+
5581 it ( 'should watch changed files with same prefix path string' , async ( ) => {
5682 mkdirSync ( tmpdir . resolve ( 'subdir' ) ) ;
5783 mkdirSync ( tmpdir . resolve ( 'sub' ) ) ;
@@ -204,10 +230,9 @@ describe('watch mode file watcher', () => {
204230 const child = spawn ( process . execPath , [ file ] , { stdio : [ 'pipe' , 'pipe' , 'pipe' , 'ipc' ] , encoding : 'utf8' } ) ;
205231 watcher . watchChildProcessModules ( child ) ;
206232 await once ( child , 'exit' ) ;
207- let expected = [ file , tmpdir . resolve ( 'file' ) ] ;
208- if ( supportsRecursiveWatching ) {
209- expected = expected . map ( ( file ) => path . dirname ( file ) ) ;
210- }
233+ // The parent directory is watched on every platform so that files replaced
234+ // via unlink+create or rename are still detected.
235+ const expected = [ file , tmpdir . resolve ( 'file' ) ] . map ( ( file ) => path . dirname ( file ) ) ;
211236 assert . deepStrictEqual ( watcher . watchedPaths , expected ) ;
212237 } ) ;
213238} ) ;
0 commit comments