Repository navigation
Conversation
The color init path used a single flag for both streams, so a tty on stderr could enable ANSI escapes on a piped stdout. Evaluate each stream independently. FORCE_COLOR and NO_COLOR still override both. Test: console-no-color-pipe — exercises piped, FORCE_COLOR, and NO_COLOR paths. Note: both-streams-piped in spawnSync does not reproduce the original bug (requires one tty + one pipe), so the test validates the correct behavior but not the exact trigger.
WalkthroughThis PR changes console output ANSI color enabling from unified cross-stream logic to independent per-stream detection. When neither ChangesConsole ANSI Color Per-Stream Behavior
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/js/bun/console/console-no-color-pipe.test.ts`:
- Around line 21-65: The tests invoking spawnSync (calls to spawnSync with cmd
[bunExe(), import.meta.dir + "/console-no-color-pipe.ts"] in the three it
blocks) currently only assert stdout/stderr contents and can yield false
positives if the subprocess failed; update each test to also assert the
subprocess exit code is 0 by checking the spawnSync result's exitCode === 0
after the stdout/stderr content assertions (preserve the order: check output
contents first, then assert exitCode) so failures give useful output and ensure
the child process succeeded; reference the spawnSync results (stdout, stderr,
exitCode) and the cleanEnv(...) usage when adding the assertions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 694f9b40-921c-489d-a193-c2ac992a64e9
📒 Files selected for processing (3)
src/bun_core/output.zigtest/js/bun/console/console-no-color-pipe.test.tstest/js/bun/console/console-no-color-pipe.ts
| const { stdout, stderr } = spawnSync({ | ||
| cmd: [bunExe(), import.meta.dir + "/console-no-color-pipe.ts"], | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| env: cleanEnv(), | ||
| }); | ||
|
|
||
| const out = stdout.toString(); | ||
| const err = stderr.toString(); | ||
|
|
||
| // Neither piped stream should contain escape codes | ||
| expect(out).not.toContain("\x1b["); | ||
| expect(err).not.toContain("\x1b["); | ||
| // Verify actual data is present | ||
| expect(out).toContain("Map"); | ||
| expect(err).toContain("Map"); | ||
| }); | ||
|
|
||
| it("should emit ANSI escape codes on both streams when FORCE_COLOR is set", () => { | ||
| const { stdout, stderr } = spawnSync({ | ||
| cmd: [bunExe(), import.meta.dir + "/console-no-color-pipe.ts"], | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| env: cleanEnv({ FORCE_COLOR: "1" }), | ||
| }); | ||
|
|
||
| const out = stdout.toString(); | ||
| const err = stderr.toString(); | ||
| expect(out).toContain("\x1b["); | ||
| expect(err).toContain("\x1b["); | ||
| }); | ||
|
|
||
| it("should not emit ANSI escape codes when NO_COLOR is set", () => { | ||
| const { stdout, stderr } = spawnSync({ | ||
| cmd: [bunExe(), import.meta.dir + "/console-no-color-pipe.ts"], | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| env: cleanEnv({ NO_COLOR: "1" }), | ||
| }); | ||
|
|
||
| const out = stdout.toString(); | ||
| const err = stderr.toString(); | ||
| expect(out).not.toContain("\x1b["); | ||
| expect(err).not.toContain("\x1b["); | ||
| }); |
There was a problem hiding this comment.
Add explicit subprocess exit-code assertions to prevent false positives.
All three spawnSync tests should assert exitCode === 0 (after stdout/stderr content assertions). Without it, a failing subprocess can still satisfy some current predicates.
Suggested patch
- const { stdout, stderr } = spawnSync({
+ const { stdout, stderr, exitCode } = spawnSync({
cmd: [bunExe(), import.meta.dir + "/console-no-color-pipe.ts"],
stdout: "pipe",
stderr: "pipe",
env: cleanEnv(),
});
@@
expect(out).toContain("Map");
expect(err).toContain("Map");
+ expect(exitCode).toBe(0);
});
it("should emit ANSI escape codes on both streams when FORCE_COLOR is set", () => {
- const { stdout, stderr } = spawnSync({
+ const { stdout, stderr, exitCode } = spawnSync({
cmd: [bunExe(), import.meta.dir + "/console-no-color-pipe.ts"],
stdout: "pipe",
stderr: "pipe",
env: cleanEnv({ FORCE_COLOR: "1" }),
});
@@
const err = stderr.toString();
expect(out).toContain("\x1b[");
expect(err).toContain("\x1b[");
+ expect(exitCode).toBe(0);
});
it("should not emit ANSI escape codes when NO_COLOR is set", () => {
- const { stdout, stderr } = spawnSync({
+ const { stdout, stderr, exitCode } = spawnSync({
cmd: [bunExe(), import.meta.dir + "/console-no-color-pipe.ts"],
stdout: "pipe",
stderr: "pipe",
env: cleanEnv({ NO_COLOR: "1" }),
});
@@
const err = stderr.toString();
expect(out).not.toContain("\x1b[");
expect(err).not.toContain("\x1b[");
+ expect(exitCode).toBe(0);
});📝 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.
| const { stdout, stderr } = spawnSync({ | |
| cmd: [bunExe(), import.meta.dir + "/console-no-color-pipe.ts"], | |
| stdout: "pipe", | |
| stderr: "pipe", | |
| env: cleanEnv(), | |
| }); | |
| const out = stdout.toString(); | |
| const err = stderr.toString(); | |
| // Neither piped stream should contain escape codes | |
| expect(out).not.toContain("\x1b["); | |
| expect(err).not.toContain("\x1b["); | |
| // Verify actual data is present | |
| expect(out).toContain("Map"); | |
| expect(err).toContain("Map"); | |
| }); | |
| it("should emit ANSI escape codes on both streams when FORCE_COLOR is set", () => { | |
| const { stdout, stderr } = spawnSync({ | |
| cmd: [bunExe(), import.meta.dir + "/console-no-color-pipe.ts"], | |
| stdout: "pipe", | |
| stderr: "pipe", | |
| env: cleanEnv({ FORCE_COLOR: "1" }), | |
| }); | |
| const out = stdout.toString(); | |
| const err = stderr.toString(); | |
| expect(out).toContain("\x1b["); | |
| expect(err).toContain("\x1b["); | |
| }); | |
| it("should not emit ANSI escape codes when NO_COLOR is set", () => { | |
| const { stdout, stderr } = spawnSync({ | |
| cmd: [bunExe(), import.meta.dir + "/console-no-color-pipe.ts"], | |
| stdout: "pipe", | |
| stderr: "pipe", | |
| env: cleanEnv({ NO_COLOR: "1" }), | |
| }); | |
| const out = stdout.toString(); | |
| const err = stderr.toString(); | |
| expect(out).not.toContain("\x1b["); | |
| expect(err).not.toContain("\x1b["); | |
| }); | |
| const { stdout, stderr, exitCode } = spawnSync({ | |
| cmd: [bunExe(), import.meta.dir + "/console-no-color-pipe.ts"], | |
| stdout: "pipe", | |
| stderr: "pipe", | |
| env: cleanEnv(), | |
| }); | |
| const out = stdout.toString(); | |
| const err = stderr.toString(); | |
| // Neither piped stream should contain escape codes | |
| expect(out).not.toContain("\x1b["); | |
| expect(err).not.toContain("\x1b["); | |
| // Verify actual data is present | |
| expect(out).toContain("Map"); | |
| expect(err).toContain("Map"); | |
| expect(exitCode).toBe(0); | |
| }); | |
| it("should emit ANSI escape codes on both streams when FORCE_COLOR is set", () => { | |
| const { stdout, stderr, exitCode } = spawnSync({ | |
| cmd: [bunExe(), import.meta.dir + "/console-no-color-pipe.ts"], | |
| stdout: "pipe", | |
| stderr: "pipe", | |
| env: cleanEnv({ FORCE_COLOR: "1" }), | |
| }); | |
| const out = stdout.toString(); | |
| const err = stderr.toString(); | |
| expect(out).toContain("\x1b["); | |
| expect(err).toContain("\x1b["); | |
| expect(exitCode).toBe(0); | |
| }); | |
| it("should not emit ANSI escape codes when NO_COLOR is set", () => { | |
| const { stdout, stderr, exitCode } = spawnSync({ | |
| cmd: [bunExe(), import.meta.dir + "/console-no-color-pipe.ts"], | |
| stdout: "pipe", | |
| stderr: "pipe", | |
| env: cleanEnv({ NO_COLOR: "1" }), | |
| }); | |
| const out = stdout.toString(); | |
| const err = stderr.toString(); | |
| expect(out).not.toContain("\x1b["); | |
| expect(err).not.toContain("\x1b["); | |
| expect(exitCode).toBe(0); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test/js/bun/console/console-no-color-pipe.test.ts` around lines 21 - 65, The
tests invoking spawnSync (calls to spawnSync with cmd [bunExe(), import.meta.dir
+ "/console-no-color-pipe.ts"] in the three it blocks) currently only assert
stdout/stderr contents and can yield false positives if the subprocess failed;
update each test to also assert the subprocess exit code is 0 by checking the
spawnSync result's exitCode === 0 after the stdout/stderr content assertions
(preserve the order: check output contents first, then assert exitCode) so
failures give useful output and ensure the child process succeeded; reference
the spawnSync results (stdout, stderr, exitCode) and the cleanEnv(...) usage
when adding the assertions.
|
Closing as stale: this PR predates the Rust rewrite. Every If the underlying change is still wanted, it will need to be redone against the current Rust/C++ tree. Apologies for the churn, and thank you for the contribution. |
What does this PR do?
The color-init path in
output.zigused a single boolean for both streams: ifisColorTerminal()was true and either stream was a tty, both got ANSI escapes enabled. This meant piping stdout while stderr remained a tty produced colored output on the piped stream.Now each stream is evaluated independently:
enable_ansi_colors_stdout = is_stdout_tty,enable_ansi_colors_stderr = is_stderr_tty.FORCE_COLORandNO_COLORstill override both.How did you verify your code works?
Added
console-no-color-pipe.test.tswhich spawns bun with both streams piped and verifies no ANSI escapes appear, plusFORCE_COLORandNO_COLORoverride tests.Note on test coverage: The test validates correct behavior when both streams are piped (both should be plain), but it cannot reproduce the exact original bug scenario (one tty + one pipe) because
spawnSyncpipes both. The Zig fix is the minimal correct change — theisColorTerminal()check was only needed as a gate for the shared flag, which no longer exists.