Skip to content

Fix per-stream ANSI color detection when stdout/stderr differ - #30430

Closed
kimjune01 wants to merge 1 commit into
oven-sh:mainfrom
kimjune01:fix-console-ansi-escape-pipe
Closed

kimjune01 wants to merge 1 commit into
oven-sh:mainfrom
kimjune01:fix-console-ansi-escape-pipe

Conversation

@kimjune01

Copy link
Copy Markdown

What does this PR do?

The color-init path in output.zig used a single boolean for both streams: if isColorTerminal() 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_COLOR and NO_COLOR still override both.

How did you verify your code works?

Added console-no-color-pipe.test.ts which spawns bun with both streams piped and verifies no ANSI escapes appear, plus FORCE_COLOR and NO_COLOR override 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 spawnSync pipes both. The Zig fix is the minimal correct change — the isColorTerminal() check was only needed as a gate for the shared flag, which no longer exists.

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.

@claude claude 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.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR changes console output ANSI color enabling from unified cross-stream logic to independent per-stream detection. When neither FORCE_COLOR nor NO_COLOR is set, each stream now enables colors only if it is a TTY, rather than allowing either stream's TTY status to affect both. New tests validate the behavior with environment variable overrides and piped streams.

Changes

Console ANSI Color Per-Stream Behavior

Layer / File(s) Summary
Per-Stream ANSI Color Enabling
src/bun_core/output.zig
Source.setInit now enables ANSI colors independently for stdout and stderr based on whether each specific stream is a TTY, replacing unified logic where either TTY could enable colors for both streams.
Test Fixture
test/js/bun/console/console-no-color-pipe.ts
New fixture that logs a Map object to both stdout and stderr, with comments asserting ANSI-free output when streams are piped.
Test Helper and Cases
test/js/bun/console/console-no-color-pipe.test.ts
Test suite with cleanEnv helper to manage environment state and three test cases: clean environment expects no ANSI codes; FORCE_COLOR=1 expects ANSI codes in both streams; NO_COLOR=1 expects no ANSI codes.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing per-stream ANSI color detection when stdout/stderr differ, which is the core issue addressed in the PR.
Description check ✅ Passed The description is complete and covers both required sections: 'What does this PR do?' explains the bug and fix clearly, and 'How did you verify your code works?' describes the tests and acknowledges test limitations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

❤️ Share

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6d0d86b and d2107da.

📒 Files selected for processing (3)
  • src/bun_core/output.zig
  • test/js/bun/console/console-no-color-pipe.test.ts
  • test/js/bun/console/console-no-color-pipe.ts

Comment on lines +21 to +65
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[");
});

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 | ⚡ Quick win

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);
   });
As per coding guidelines: “When spawning processes in tests, check stdout expectations before exit code expectations to provide more useful error messages on test failure”.
📝 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 { 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.

@robobun

robobun commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Closing as stale: this PR predates the Rust rewrite. Every src/ file it modifies has since been removed or relocated on main (Zig sources deleted; src/bun.js/ reorganized into src/jsc/), so it can no longer merge.

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.

@robobun robobun closed this Jun 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants