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:
- Include the resolved
env config when computing userSetColorEnv, so env: { NO_COLOR } reaches the same escape hatch.
- Skip injecting
FORCE_COLOR when the resolved env already sets NO_COLOR, avoiding the contradictory pair.
- 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
- 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);
});
- 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"
- 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.
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.11Details
Setting
NO_COLORthrough theenvconfig option does not disable colored output in worker processes, while settingprocess.env.NO_COLORat the top level of the config file does.The
envvalue itself is delivered correctly — the difference is that it does not suppress theFORCE_COLOR=1that Rstest injects into workers, and in Node.jsFORCE_COLORoverridesNO_COLOR.getForceColorEnv()decides whether to injectFORCE_COLORbased on whether the parent process has color env vars set:Because
envis resolved config data rather than a mutation of the parent'sprocess.env, this check never sees it. Rstest concludes the user has expressed no preference, detects color support (CIis one trigger viapicocolors.isColorSupported), and injectsFORCE_COLOR=1alongside the user'sNO_COLOR=1.Observed behavior, all under
CI=true:NO_COLORFORCE_COLORstyleText('yellow', 'X')env: { NO_COLOR: '1' }"1""1""\u001b[33mX\u001b[39m"process.env.NO_COLOR = '1'"1"undefined"X"NO_COLOR=1in parent env"1"undefined"X"env: { NO_COLOR: '1', FORCE_COLOR: '0' }"1""0""X"The docs describe
envas "Custom environment variables available onprocess.envduring tests", soNO_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 thatNO_COLORis 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 vianode:util'sstyleText, and snapshots captured plain text. Migratingprocess.env.NO_COLOR = 'true'toenv: { NO_COLOR: 'true' }— a translation that looks equivalent — broke two snapshots in CI.I understand injecting
FORCE_COLOR=1is 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 viaprocess.env, not via theenvconfig option that the docs point users to.Possible directions, in case they're useful:
envconfig when computinguserSetColorEnv, soenv: { NO_COLOR }reaches the same escape hatch.FORCE_COLORwhen the resolvedenvalready setsNO_COLOR, avoiding the contradictory pair.process.envrather than throughenv.A dedicated option (e.g.
color: falseondefine.test/defineConfig) would also remove the need for users to reason about which variable wins. Today the working config-level workaround isFORCE_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
@rstest/core@0.11.11:{ "name": "rstest-color-repro", "private": true, "type": "module", "devDependencies": { "@rstest/core": "0.11.11" } }CI=true npx rstest run test/color.test.ts.Actual output —
FORCE_COLORis injected and the text is colored despiteNO_COLOR:process.envform and run the same command:Note that without
CI=trueboth forms print"X", because piped stdout is not a TTY and no color support is detected.