Skip to content

fix(fs.watch): emit 'change' events for files in watched directories on Linux#26009

Merged
Jarred-Sumner merged 1 commit into
mainfrom
claude/fix-fs-watch-change-events
Jan 15, 2026
Merged

fix(fs.watch): emit 'change' events for files in watched directories on Linux#26009
Jarred-Sumner merged 1 commit into
mainfrom
claude/fix-fs-watch-change-events

Conversation

@robobun

@robobun robobun commented Jan 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

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

  • Added regression test test/regression/issue/3657.test.ts
  • Verified test fails with system Bun (before fix)
  • Verified test passes with debug build (after fix)
  • Verified manual reproduction from issue now works correctly

🤖 Generated with Claude Code

…on Linux

Fixes #3657

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.

The root cause 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').

This fix:
- 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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@robobun

robobun commented Jan 12, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:18 AM PT - Jan 12th, 2026

❌ Your commit ea5617eb has 4 failures in Build #34588 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 26009

That installs a local version of the PR into your bun-26009 executable, so you can run:

bun-26009 --bun

@coderabbitai

coderabbitai Bot commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Changes to the file watching system introduce a create event flag to the Op struct and refactor event merging logic. Platform-specific watchers are updated to track CREATE and MODIFY events, and event dispatch logic distinguishes between rename (creation/move) and change (modification) events. A regression test validates the fix for issue #3657.

Changes

Cohort / File(s) Summary
Core watching infrastructure
src/Watcher.zig, src/watcher/INotifyWatcher.zig
Added create: bool field to Op struct with adjusted padding layout. Refactored WatchEvent.merge to delegate to Op.merge. Extended inotify mask to include IN.MODIFY for directories. Updated watchEventFromInotifyEvent to populate the new create field from IN.CREATE events.
Event type handling
src/bun.js/node/path_watcher.zig
Replaced fixed event type assignment with conditional logic: emits "rename" for create/delete/rename/move_to events, and "change" for write/modify scenarios.
Regression testing
test/regression/issue/3657.test.ts
Added test suite validating fs.watch behavior on directories. Verifies multiple events are emitted when files are created and subsequently modified (rename followed by change events).
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main fix: enabling 'change' events for watched directories on Linux, which is the primary objective of this changeset.
Description check ✅ Passed The description provides comprehensive context including root cause analysis, changes made, and verification steps, though it could explicitly mention all modified files.
Linked Issues check ✅ Passed Changes implement the exact requirements from #3657: adding IN.MODIFY to watch events, tracking file creation, and distinguishing rename vs. change events.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing #3657: INotifyWatcher additions for modify events, Op struct enhancement for create tracking, path_watcher logic refinement, and regression test.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between beccd01 and ea5617e.

📒 Files selected for processing (4)
  • src/Watcher.zig
  • src/bun.js/node/path_watcher.zig
  • src/watcher/INotifyWatcher.zig
  • test/regression/issue/3657.test.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, be careful with allocators and use defer for cleanup

Files:

  • src/bun.js/node/path_watcher.zig
  • src/Watcher.zig
  • src/watcher/INotifyWatcher.zig
src/**/*.zig

📄 CodeRabbit inference engine (src/CLAUDE.md)

src/**/*.zig: Use the # prefix for private fields in Zig structs, e.g., struct { #foo: u32 };
Use Decl literals in Zig, e.g., const decl: Decl = .{ .binding = 0, .value = 0 };
Place @import statements at the bottom of the file in Zig (auto formatter will handle positioning)
Never use @import() inline inside functions in Zig; always place imports at the bottom of the file or containing struct

Files:

  • src/bun.js/node/path_watcher.zig
  • src/Watcher.zig
  • src/watcher/INotifyWatcher.zig
**/*.test.ts?(x)

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.test.ts?(x): Never use bun test directly - always use bun bd test to run tests with debug build changes
For single-file tests, prefer -e flag over tempDir
For multi-file tests, prefer tempDir and Bun.spawn over single-file tests
Use normalizeBunSnapshot to normalize snapshot output of tests
Never write tests that check for 'panic', 'uncaught exception', or similar strings in test output
Use tempDir from harness to create temporary directories - do not use tmpdirSync or fs.mkdtempSync
When spawning processes in tests, expect stdout before expecting exit code for more useful error messages on test failure
Do not write flaky tests - do not use setTimeout in tests; instead await the condition to be met
Verify tests fail with USE_SYSTEM_BUN=1 bun test <file> and pass with bun bd test <file> - tests are invalid if they pass with USE_SYSTEM_BUN=1
Test files must end with .test.ts or .test.tsx
Avoid shell commands like find or grep in tests - use Bun's Glob and built-in tools instead

Files:

  • test/regression/issue/3657.test.ts
test/regression/issue/*.test.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Place regression tests for specific GitHub issues in test/regression/issue/${issueNumber}.test.ts with real issue numbers only

Files:

  • test/regression/issue/3657.test.ts
test/**/*.test.ts?(x)

📄 CodeRabbit inference engine (CLAUDE.md)

Always use port: 0 in tests - do not hardcode ports or use custom random port number functions

Files:

  • test/regression/issue/3657.test.ts
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}: Use bun bd test <...test file> to run tests with compiled code changes. Do not use bun test as it will not include your changes.
Use bun:test for files ending in *.test.{ts,js,jsx,tsx,mjs,cjs}. For test files without .test extension in test/js/node/test/{parallel,sequential}/*.js, use bun bd <file> instead of bun bd test <file> since they expect exit code 0.
Do not set a timeout on tests. Bun already has timeouts built-in.

Files:

  • test/regression/issue/3657.test.ts
🧠 Learnings (16)
📚 Learning: 2025-10-17T20:50:58.644Z
Learnt from: taylordotfish
Repo: oven-sh/bun PR: 23755
File: src/bun.js/api/bun/socket/Handlers.zig:154-159
Timestamp: 2025-10-17T20:50:58.644Z
Learning: In Bun socket configuration error messages (src/bun.js/api/bun/socket/Handlers.zig), use the user-facing JavaScript names "data" and "drain" instead of internal field names "onData" and "onWritable", as these are the names users see in the API according to SocketConfig.bindv2.ts.

Applied to files:

  • src/bun.js/node/path_watcher.zig
📚 Learning: 2026-01-05T16:32:07.551Z
Learnt from: alii
Repo: oven-sh/bun PR: 25474
File: src/bun.js/event_loop/Sigusr1Handler.zig:0-0
Timestamp: 2026-01-05T16:32:07.551Z
Learning: In Zig codebases (e.g., Bun), treat std.posix.sigaction as returning void and do not perform runtime error handling for its failure. The Zig standard library views sigaction failures as programmer errors (unreachable) because they only occur with invalid signals like SIGKILL/SIGSTOP. Apply this pattern across Zig files that call sigaction (e.g., crash_handler.zig, main.zig, filter_run.zig, process.zig) and ensure failures are not handled as recoverable errors; prefer reaching an explicit unreachable/compile-time assumption when such failures are detected.

Applied to files:

  • src/bun.js/node/path_watcher.zig
  • src/Watcher.zig
  • src/watcher/INotifyWatcher.zig
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to test/regression/issue/*.test.ts : Place regression tests for specific GitHub issues in `test/regression/issue/${issueNumber}.test.ts` with real issue numbers only

Applied to files:

  • test/regression/issue/3657.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Organize regression tests for specific issues in `/test/regression/issue/${issueNumber}.test.ts`. Do not place regression tests in the regression directory if there is no associated issue number.

Applied to files:

  • test/regression/issue/3657.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : For multi-file tests, prefer `tempDir` and `Bun.spawn` over single-file tests

Applied to files:

  • test/regression/issue/3657.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : For single-file tests, prefer `-e` flag over `tempDir`

Applied to files:

  • test/regression/issue/3657.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Do not write flaky tests - do not use `setTimeout` in tests; instead await the condition to be met

Applied to files:

  • test/regression/issue/3657.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Use `tempDir` from `harness` to create temporary directories - do not use `tmpdirSync` or `fs.mkdtempSync`

Applied to files:

  • test/regression/issue/3657.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Avoid shell commands like `find` or `grep` in tests - use Bun's Glob and built-in tools instead

Applied to files:

  • test/regression/issue/3657.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : When spawning processes in tests, expect stdout before expecting exit code for more useful error messages on test failure

Applied to files:

  • test/regression/issue/3657.test.ts
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/test/v8/v8.test.ts : Add corresponding test cases to test/v8/v8.test.ts using checkSameOutput() function to compare Node.js and Bun output

Applied to files:

  • test/regression/issue/3657.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun bd test <...test file>` to run tests with compiled code changes. Do not use `bun test` as it will not include your changes.

Applied to files:

  • test/regression/issue/3657.test.ts
📚 Learning: 2025-10-26T01:32:04.844Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 24082
File: test/cli/test/coverage.test.ts:60-112
Timestamp: 2025-10-26T01:32:04.844Z
Learning: In the Bun repository test files (test/cli/test/*.test.ts), when spawning Bun CLI commands with Bun.spawnSync for testing, prefer using stdio: ["inherit", "inherit", "inherit"] to inherit stdio streams rather than piping them.

Applied to files:

  • test/regression/issue/3657.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun:test` for files ending in `*.test.{ts,js,jsx,tsx,mjs,cjs}`. For test files without .test extension in test/js/node/test/{parallel,sequential}/*.js, use `bun bd <file>` instead of `bun bd test <file>` since they expect exit code 0.

Applied to files:

  • test/regression/issue/3657.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Verify tests fail with `USE_SYSTEM_BUN=1 bun test <file>` and pass with `bun bd test <file>` - tests are invalid if they pass with USE_SYSTEM_BUN=1

Applied to files:

  • test/regression/issue/3657.test.ts
📚 Learning: 2025-10-19T02:44:46.354Z
Learnt from: theshadow27
Repo: oven-sh/bun PR: 23798
File: packages/bun-otel/context-propagation.test.ts:1-1
Timestamp: 2025-10-19T02:44:46.354Z
Learning: In the Bun repository, standalone packages under packages/ (e.g., bun-vscode, bun-inspector-protocol, bun-plugin-yaml, bun-plugin-svelte, bun-debug-adapter-protocol, bun-otel) co-locate their tests with package source code using *.test.ts files. This follows standard npm/monorepo patterns. The test/ directory hierarchy (test/js/bun/, test/cli/, test/js/node/) is reserved for testing Bun's core runtime APIs and built-in functionality, not standalone packages.

Applied to files:

  • test/regression/issue/3657.test.ts
🧬 Code graph analysis (1)
test/regression/issue/3657.test.ts (2)
test/harness.ts (1)
  • tempDirWithFiles (259-266)
src/js/node/fs.ts (1)
  • eventType (68-88)
🔇 Additional comments (7)
src/bun.js/node/path_watcher.zig (1)

251-253: LGTM! Correct event categorization for directory events.

The logic properly distinguishes between structural changes (create, delete, rename, move_to) that emit 'rename' events and content modifications (pure writes) that emit 'change' events. This aligns with the PR objective to fix issue #3657.

src/watcher/INotifyWatcher.zig (2)

80-80: LGTM! Core fix for the issue.

Adding IN.MODIFY to watch_dir_mask is the essential fix—without this, inotify wasn't subscribed to file modification events for watched directories. This enables fs.watch to receive 'change' events for files created after the watch started.


360-371: LGTM! Proper mapping of IN.CREATE to the new create flag.

The create field correctly tracks IN.CREATE events, enabling the event dispatch logic in path_watcher.zig to distinguish file creation (emits 'rename') from file modification (emits 'change').

test/regression/issue/3657.test.ts (2)

28-29: Consider potential flakiness with fixed sleep delays.

Using fixed Bun.sleep(100) delays between operations could be flaky on slow CI systems or under heavy load. The inotify coalescing behavior (as seen in INotifyWatcher.zig with coalesce_interval) may also affect event delivery timing.

The promise-based waiting for event counts is good, but if the watcher initialization sleep is insufficient, the first writeFileSync might execute before the watcher is fully ready.

If flakiness is observed, consider polling for watcher readiness or increasing the initial delay.


1-111: Well-structured regression test for issue #3657.

The test correctly:

  • Uses tempDirWithFiles from harness (per coding guidelines)
  • Places the file at test/regression/issue/3657.test.ts with the real issue number
  • Uses AbortSignal.timeout for cleanup to prevent hangs
  • Validates both creation (rename) and modification (change) events
  • Uses flexible assertions (toBeGreaterThanOrEqual) to handle event coalescing variability
src/Watcher.zig (2)

167-170: LGTM! Clean refactor to centralize merge logic.

Delegating to Op.merge instead of inlining the flag-merging logic improves maintainability—the merge semantics are now defined in one place within the Op struct.


172-190: LGTM! Correct addition of create flag to Op.

The packed struct layout remains valid: 6 boolean flags + 2-bit padding = 8 bits. The create field follows the established pattern, and Op.merge correctly propagates it using logical OR.


Comment @coderabbitai help to get the list of available commands and usage tips.

@Jarred-Sumner Jarred-Sumner merged commit 11aedbe into main Jan 15, 2026
53 of 56 checks passed
@Jarred-Sumner Jarred-Sumner deleted the claude/fix-fs-watch-change-events branch January 15, 2026 00:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

new file is not being watched in fs.watch

2 participants