Skip to content

[Bug]: NO_COLOR set via the env config option is overridden by the injected FORCE_COLOR #1767

Description

@chenjiahan

Version

System:
    OS: macOS 26.5.2
    CPU: (16) arm64 Apple M3 Max
    Memory: 6.09 GB / 64.00 GB
    Shell: 5.9 - /bin/zsh
  npmPackages:
    @rstest/core: 0.11.11 => 0.11.11

Details

Setting NO_COLOR through the env config option does not disable colored output in worker processes, while setting process.env.NO_COLOR at the top level of the config file does.

The env value itself is delivered correctly — the difference is that it does not suppress the FORCE_COLOR=1 that Rstest injects into workers, and in Node.js FORCE_COLOR overrides NO_COLOR.

getForceColorEnv() decides whether to inject FORCE_COLOR based on whether the parent process has color env vars set:

const userSetColorEnv =
  options?.userSetColorEnv ??
  (process.env.FORCE_COLOR !== undefined || process.env.NO_COLOR !== undefined);
if (userSetColorEnv) return {};

Because env is resolved config data rather than a mutation of the parent's process.env, this check never sees it. Rstest concludes the user has expressed no preference, detects color support (CI is one trigger via picocolors.isColorSupported), and injects FORCE_COLOR=1 alongside the user's NO_COLOR=1.

Observed behavior, all under CI=true:

Config worker NO_COLOR worker FORCE_COLOR styleText('yellow', 'X')
env: { NO_COLOR: '1' } "1" "1" "\u001b[33mX\u001b[39m"
process.env.NO_COLOR = '1' "1" undefined "X"
NO_COLOR=1 in parent env "1" undefined "X"
env: { NO_COLOR: '1', FORCE_COLOR: '0' } "1" "0" "X"

The docs describe env as "Custom environment variables available on process.env during tests", so NO_COLOR: '1' set this way reads as a request to disable color. Instead, workers receive a contradictory pair (NO_COLOR=1 + FORCE_COLOR=1), and Node.js emits a warning that NO_COLOR is ignored.

This is only observable when the parent detects color support, which makes it easy to miss locally: piping output makes stdout a non-TTY, so the same config passes. It surfaces in CI, on Windows (platform === 'win32' is a trigger), and in interactive terminals.

The impact lands on code under test rather than on Rstest's own output. In rsbuild-plugin-check-syntax, the plugin colors error output via node:util's styleText, and snapshots captured plain text. Migrating process.env.NO_COLOR = 'true' to env: { NO_COLOR: 'true' } — a translation that looks equivalent — broke two snapshots in CI.

I understand injecting FORCE_COLOR=1 is intentional (#1081) so diff output keeps ANSI in workers that use piped stdio, and that #946 established "don't intervene when the user set color env vars". This report is narrower: the escape hatch for that rule is only reachable via process.env, not via the env config option that the docs point users to.

Possible directions, in case they're useful:

  1. Include the resolved env config when computing userSetColorEnv, so env: { NO_COLOR } reaches the same escape hatch.
  2. Skip injecting FORCE_COLOR when the resolved env already sets NO_COLOR, avoiding the contradictory pair.
  3. If the current behavior is intended, document that color env vars must be set on process.env rather than through env.

A dedicated option (e.g. color: false on define.test / defineConfig) would also remove the need for users to reason about which variable wins. Today the working config-level workaround is FORCE_COLOR: '0', which relies on Node's value parsing — per force-color.org any non-empty value forces color on, so '0' disabling color is Node-specific rather than guaranteed by the informal standard.

Reproduce Steps

  1. Create a project with @rstest/core@0.11.11:
{
  "name": "rstest-color-repro",
  "private": true,
  "type": "module",
  "devDependencies": { "@rstest/core": "0.11.11" }
}
import { defineConfig } from '@rstest/core';

export default defineConfig({
  env: {
    NO_COLOR: '1',
  },
});
import { styleText } from 'node:util';
import { expect, test } from '@rstest/core';

test('color env inside worker', () => {
  console.log(
    'WORKER NO_COLOR=', JSON.stringify(process.env.NO_COLOR),
    '| FORCE_COLOR=', JSON.stringify(process.env.FORCE_COLOR),
    '| styleText=', JSON.stringify(styleText('yellow', 'X')),
  );
  expect(true).toBe(true);
});
  1. Run CI=true npx rstest run test/color.test.ts.

Actual output — FORCE_COLOR is injected and the text is colored despite NO_COLOR:

WORKER NO_COLOR= "1" | FORCE_COLOR= "1" | styleText= "\u001b[33mX\u001b[39m"
  1. Replace the config with the process.env form and run the same command:
import { defineConfig } from '@rstest/core';

process.env.NO_COLOR = '1';

export default defineConfig({});
WORKER NO_COLOR= "1" | FORCE_COLOR= undefined | styleText= "X"

Note that without CI=true both forms print "X", because piped stdout is not a TTY and no color support is detected.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      Sponsor
      SponsoredKunjungi sekarang
      Promo