mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
## Summary - Fixes #3657 - `fs.watch` on directory doesn't emit `change` events for files created after watch starts When watching a directory with `fs.watch`, files created after the watch was established would only emit a 'rename' event on creation, but subsequent modifications would not emit 'change' events. ## Root Cause The issue was twofold: 1. `watch_dir_mask` in INotifyWatcher.zig was missing `IN.MODIFY`, so the inotify system call was not subscribed to file modification events for watched directories. 2. When directory events were processed in path_watcher.zig, all events were hardcoded to emit 'rename' instead of properly distinguishing between file creation/deletion ('rename') and file modification ('change'). ## Changes - Adds `IN.MODIFY` to `watch_dir_mask` to receive modification events - Adds a `create` flag to `WatchEvent.Op` to track `IN.CREATE` events - Updates directory event processing to emit 'change' for pure write events and 'rename' for create/delete/move events ## Test plan - [x] Added regression test `test/regression/issue/3657.test.ts` - [x] Verified test fails with system Bun (before fix) - [x] Verified test passes with debug build (after fix) - [x] Verified manual reproduction from issue now works correctly 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
// https://github.com/oven-sh/bun/issues/3657
|
|
// fs.watch on a directory should emit 'change' events for files created after the watch is established
|
|
|
|
import { describe, expect, test } from "bun:test";
|
|
import { isLinux, tempDirWithFiles } from "harness";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
describe.skipIf(!isLinux)("GitHub Issue #3657", () => {
|
|
test("fs.watch on directory emits 'change' events for files created after watch starts", async () => {
|
|
const testDir = tempDirWithFiles("issue-3657", {});
|
|
const testFile = path.join(testDir, "test.txt");
|
|
|
|
const events: Array<{ eventType: string; filename: string | null }> = [];
|
|
let resolver: () => void;
|
|
const promise = new Promise<void>(resolve => {
|
|
resolver = resolve;
|
|
});
|
|
|
|
const watcher = fs.watch(testDir, { signal: AbortSignal.timeout(5000) }, (eventType, filename) => {
|
|
events.push({ eventType, filename: filename as string | null });
|
|
// We expect at least 2 events: one rename (create) and one change (modify)
|
|
if (events.length >= 2) {
|
|
resolver();
|
|
}
|
|
});
|
|
|
|
// Give the watcher time to initialize
|
|
await Bun.sleep(100);
|
|
|
|
// Create the file - should emit 'rename' event
|
|
fs.writeFileSync(testFile, "hello");
|
|
|
|
// Wait a bit for the event to be processed
|
|
await Bun.sleep(100);
|
|
|
|
// Modify the file - should emit 'change' event
|
|
fs.appendFileSync(testFile, " world");
|
|
|
|
try {
|
|
await promise;
|
|
} finally {
|
|
watcher.close();
|
|
}
|
|
|
|
// Verify we got at least one event for "test.txt"
|
|
const testFileEvents = events.filter(e => e.filename === "test.txt");
|
|
expect(testFileEvents.length).toBeGreaterThanOrEqual(2);
|
|
|
|
// Verify we got a 'rename' event (file creation)
|
|
const renameEvents = testFileEvents.filter(e => e.eventType === "rename");
|
|
expect(renameEvents.length).toBeGreaterThanOrEqual(1);
|
|
|
|
// Verify we got a 'change' event (file modification)
|
|
const changeEvents = testFileEvents.filter(e => e.eventType === "change");
|
|
expect(changeEvents.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
test("fs.watch emits multiple 'change' events for repeated modifications", async () => {
|
|
const testDir = tempDirWithFiles("issue-3657-multi", {});
|
|
const testFile = path.join(testDir, "multi.txt");
|
|
|
|
const events: Array<{ eventType: string; filename: string | null }> = [];
|
|
let resolver: () => void;
|
|
const promise = new Promise<void>(resolve => {
|
|
resolver = resolve;
|
|
});
|
|
|
|
const watcher = fs.watch(testDir, { signal: AbortSignal.timeout(5000) }, (eventType, filename) => {
|
|
events.push({ eventType, filename: filename as string | null });
|
|
// We expect 1 rename (create) + 3 change events = 4 total
|
|
if (events.length >= 4) {
|
|
resolver();
|
|
}
|
|
});
|
|
|
|
// Give the watcher time to initialize
|
|
await Bun.sleep(100);
|
|
|
|
// Create the file - should emit 'rename' event
|
|
fs.writeFileSync(testFile, "line1\n");
|
|
await Bun.sleep(100);
|
|
|
|
// Multiple modifications - should emit 'change' events
|
|
fs.appendFileSync(testFile, "line2\n");
|
|
await Bun.sleep(100);
|
|
|
|
fs.appendFileSync(testFile, "line3\n");
|
|
await Bun.sleep(100);
|
|
|
|
fs.appendFileSync(testFile, "line4\n");
|
|
|
|
try {
|
|
await promise;
|
|
} finally {
|
|
watcher.close();
|
|
}
|
|
|
|
// Verify we got events for "multi.txt"
|
|
const testFileEvents = events.filter(e => e.filename === "multi.txt");
|
|
expect(testFileEvents.length).toBeGreaterThanOrEqual(4);
|
|
|
|
// Verify we got a 'rename' event (file creation)
|
|
const renameEvents = testFileEvents.filter(e => e.eventType === "rename");
|
|
expect(renameEvents.length).toBeGreaterThanOrEqual(1);
|
|
|
|
// Verify we got multiple 'change' events (file modifications)
|
|
const changeEvents = testFileEvents.filter(e => e.eventType === "change");
|
|
expect(changeEvents.length).toBeGreaterThanOrEqual(3);
|
|
});
|
|
});
|