Skip to content

fix(FileSink): truncate existing file when using writer() - #25981

Closed
robobun wants to merge 1 commit into
mainfrom
claude/fix-filesink-truncate
Closed

robobun wants to merge 1 commit into
mainfrom
claude/fix-filesink-truncate

Conversation

@robobun

@robobun robobun commented Jan 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Fixes the FileSink.Options.flags() method to respect the truncate option which defaults to true
  • When truncate is true, the O_TRUNC flag is now included when opening the file, ensuring existing content is removed

Closes #25968

Test plan

  • Added regression test in test/regression/issue/25968.test.ts
  • Verified test fails with system Bun (outputs Shortcontent instead of Short)
  • Verified test passes with debug build

🤖 Generated with Claude Code

The FileSink's flags() method was ignoring the truncate option, which
defaults to true. This caused existing files to not be truncated when
writing via file.writer(), so if the new content was shorter than the
old content, the tail of the old content would remain.

Closes #25968

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

robobun commented Jan 12, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 1:50 AM PT - Jan 12th, 2026

Your commit c4c80d8 is building: #34545

@coderabbitai

coderabbitai Bot commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Fixes FileSink truncation behavior by conditionally applying the TRUNC flag based on the truncate setting. Previously, the flag combination was fixed and didn't account for the truncate option. Includes regression tests validating the fix for file overwrite scenarios.

Changes

Cohort / File(s) Summary
FileSink Implementation
src/bun.js/webcore/FileSink.zig
Modifies Options.flags() to conditionally OR TRUNC flag when this.truncate is true, ensuring existing file content is properly truncated during write operations.
Regression Tests
test/regression/issue/25968.test.ts
Adds two test cases validating FileSink truncation behavior when overwriting files: one with direct file overwrite and one using a spawned process.

Suggested reviewers

  • alii
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: fixing FileSink to truncate files when using writer(), which is the primary objective of this PR.
Description check ✅ Passed The PR description covers both required template sections: a clear summary of what the fix does and a test plan with specific verification steps.
Linked Issues check ✅ Passed The code changes directly address the issue #25968: FileSink.Options.flags() now includes the TRUNC flag when truncate is true, and regression tests validate the fix works correctly.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the FileSink truncation behavior: one implementation fix in FileSink.zig and regression tests, with no unrelated modifications.

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


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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @test/regression/issue/25968.test.ts:
- Around line 30-37: The spawned inline script calls writer.end() without
awaiting, which can let the read occur before the write completes; update the
script so the end is awaited (await writer.end()) after using file.writer() and
before calling file.text(), ensuring the write completes before reading the file
contents.
- Around line 13-15: The test has a race: writer.end() can return a promise and
must be awaited to ensure all buffered data is flushed before reading; update
the test to await the writer.end() call (e.g., await writer.end()) so that
subsequent file.text() reads the fully written contents, and ensure the
surrounding test function is async if not already.
📜 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 c4c80d8.

📒 Files selected for processing (2)
  • src/bun.js/webcore/FileSink.zig
  • test/regression/issue/25968.test.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.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/25968.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/25968.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/25968.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/25968.test.ts
**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • src/bun.js/webcore/FileSink.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/webcore/FileSink.zig
🧠 Learnings (16)
📚 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/25968.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/25968.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/**/*-fixture.ts : Test files that spawn Bun processes should end in `*-fixture.ts` to identify them as test fixtures rather than tests themselves.

Applied to files:

  • test/regression/issue/25968.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/25968.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 `normalizeBunSnapshot` to normalize snapshot output of tests

Applied to files:

  • test/regression/issue/25968.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/25968.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/25968.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/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/25968.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/25968.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/25968.test.ts
📚 Learning: 2025-11-06T00:58:23.965Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 24417
File: test/js/bun/spawn/spawn.test.ts:903-918
Timestamp: 2025-11-06T00:58:23.965Z
Learning: In Bun test files, `await using` with spawn() is appropriate for long-running processes that need guaranteed cleanup on scope exit or when explicitly testing disposal behavior. For short-lived processes that exit naturally (e.g., console.log scripts), the pattern `const proc = spawn(...); await proc.exited;` is standard and more common, as evidenced by 24 instances vs 4 `await using` instances in test/js/bun/spawn/spawn.test.ts.

Applied to files:

  • test/regression/issue/25968.test.ts
📚 Learning: 2025-11-08T04:06:33.198Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 24491
File: test/js/bun/transpiler/declare-global.test.ts:17-17
Timestamp: 2025-11-08T04:06:33.198Z
Learning: In Bun test files, `await using` with Bun.spawn() is the preferred pattern for spawned processes regardless of whether they are short-lived or long-running. Do not suggest replacing `await using proc = Bun.spawn(...)` with `const proc = Bun.spawn(...); await proc.exited;`.

Applied to files:

  • test/regression/issue/25968.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: When spawning Bun processes in tests, use `bunExe` and `bunEnv` from `harness` to ensure the same build of Bun is used and debug logging is silenced.

Applied to files:

  • test/regression/issue/25968.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: Use `-e` flag for single-file tests when spawning Bun processes with `Bun.spawn()`.

Applied to files:

  • test/regression/issue/25968.test.ts
📚 Learning: 2025-10-16T02:17:35.237Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 23710
File: src/analytics.zig:15-21
Timestamp: 2025-10-16T02:17:35.237Z
Learning: In src/analytics.zig and similar files using bun.EnvVar boolean environment variables: the new EnvVar API for boolean flags (e.g., bun.EnvVar.do_not_track.get(), bun.EnvVar.ci.get()) is designed to parse and return boolean values from environment variables, not just check for their presence. This is an intentional design change from the previous presence-based checks using bun.getenvZ().

Applied to files:

  • src/bun.js/webcore/FileSink.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/webcore/FileSink.zig
🧬 Code graph analysis (1)
test/regression/issue/25968.test.ts (1)
test/harness.ts (1)
  • tempDir (277-284)
🔇 Additional comments (2)
src/bun.js/webcore/FileSink.zig (1)

56-62: LGTM! The fix correctly addresses the truncation issue.

The implementation properly builds the flags incrementally, starting with the base flags (NONBLOCK | CLOEXEC | CREAT | WRONLY) and conditionally adding TRUNC when this.truncate is true. Since truncate defaults to true (line 52), this ensures existing file content is removed by default when using file.writer(), which matches the expected behavior reported in issue #25968.

test/regression/issue/25968.test.ts (1)

45-49: LGTM! Good test structure following coding guidelines.

The test correctly uses Promise.all to await all outputs, and the assertion order (checking stdout at line 48 before exit code at line 49) follows the guideline for better error messages on test failure.

Comment on lines +13 to +15
const writer = file.writer();
writer.write("Short");
writer.end();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential race condition: writer.end() should be awaited.

The writer.end() call may not complete synchronously—if there's pending data, it returns a promise. Without awaiting, file.text() on line 18 might read the file before the write is fully flushed, potentially causing flaky tests.

Proposed fix
   // Write shorter content using writer
   const writer = file.writer();
   writer.write("Short");
-  writer.end();
+  await writer.end();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const writer = file.writer();
writer.write("Short");
writer.end();
const writer = file.writer();
writer.write("Short");
await writer.end();
🤖 Prompt for AI Agents
In @test/regression/issue/25968.test.ts around lines 13 - 15, The test has a
race: writer.end() can return a promise and must be awaited to ensure all
buffered data is flushed before reading; update the test to await the
writer.end() call (e.g., await writer.end()) so that subsequent file.text()
reads the fully written contents, and ensure the surrounding test function is
async if not already.

Comment on lines +30 to +37
`
const file = Bun.file("${filePath.replace(/\\/g, "\\\\")}");
await Bun.write(file, "Long content that is longer");
const writer = file.writer();
writer.write("Short");
writer.end();
console.log(await file.text());
`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same issue in spawned script: writer.end() should be awaited.

The inline script also calls writer.end() without awaiting. To ensure the write is complete before reading, add await:

Proposed fix
 const file = Bun.file("${filePath.replace(/\\/g, "\\\\")}");
 await Bun.write(file, "Long content that is longer");
 const writer = file.writer();
 writer.write("Short");
-writer.end();
+await writer.end();
 console.log(await file.text());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`
const file = Bun.file("${filePath.replace(/\\/g, "\\\\")}");
await Bun.write(file, "Long content that is longer");
const writer = file.writer();
writer.write("Short");
writer.end();
console.log(await file.text());
`,
`
const file = Bun.file("${filePath.replace(/\\/g, "\\\\")}");
await Bun.write(file, "Long content that is longer");
const writer = file.writer();
writer.write("Short");
await writer.end();
console.log(await file.text());
`,
🤖 Prompt for AI Agents
In @test/regression/issue/25968.test.ts around lines 30 - 37, The spawned inline
script calls writer.end() without awaiting, which can let the read occur before
the write completes; update the script so the end is awaited (await
writer.end()) after using file.writer() and before calling file.text(), ensuring
the write completes before reading the file contents.

@github-actions

Copy link
Copy Markdown
Contributor

Closing this PR because it has been inactive for more than 90 days.

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.

FileSink does not truncate existing file

1 participant