Skip to content

test: propagate the client's environment to forked test processes - #621

Merged
oyvindberg merged 1 commit into
masterfrom
env-var-test
Jul 26, 2026
Merged

test: propagate the client's environment to forked test processes#621
oyvindberg merged 1 commit into
masterfrom
env-var-test

Conversation

@oyvindberg

Copy link
Copy Markdown
Owner

FOO=bar bleep test could not reach a test. Tests are forked by the BSP daemon, which is long-lived and shared across workspaces, so its environment belongs to whichever shell cold-started it — possibly days ago in another directory.

Approach

The daemon does not mutate its own environment. That would race between concurrent requests and leak one workspace's values into another's test run.

Instead the client sends its environment on the BSP request (TestOptions.env, on the existing bleep-test-options dataKind) and the daemon applies it as an overlay on the forked child only. The hole was already there — JvmPool.spawnJvm applies an env map to the child's ProcessBuilder, and JvmKey already hashes it so a differing environment forces a distinct fork rather than reusing a poisoned one. Every call site simply passed Map.empty.

Scope

Applied on every platform: JVM, Scala.js, Scala Native, Kotlin/JS, Kotlin/Native.

The four non-JVM runners already accepted an env map and applied it to their ProcessBuilder; because nothing ever passed one, they did not honour platform.jvmEnvironment either. The same omission existed in all three bleep run paths (Run.scala:101,132,164) and in the BSP run handler, which additionally ignored the standard RunParams.environmentVariables outright. All fixed.

Precedence

Weakest first: NO_COLOR=1 → the client's shell → platform.jvmEnvironment.

The build deliberately outranks the ambient shell. The environment is forwarded wholesale, so letting it win would let a stray AWS_REGION in someone's profile silently override a region the build states on purpose — failing on exactly one machine. Variables the build does not mention pass through untouched.

CLASSPATH is never forwarded (JvmPool uses it as the long-classpath channel on Windows; a forwarded copy would replace the test classpath). PWD/OLDPWD/_ are dropped as stale shell bookkeeping. PATH and JAVA_HOME are forwarded so tests that shell out find the same tools.

Wire compatibility

The TestOptions codec is now hand-written and absent-tolerant. A derived codec does not fill in case-class defaults — verified rather than assumed:

Left(DecodingFailure at .b: Missing required field)

So adding env would have made every older client's request fail to decode, and the failure path in handleTest degrades to TestOptions.empty — silently dropping that run's --only/--exclude/--jvm-opt and running the wrong tests without saying so. Every field now reads through getOrElse, so the next addition stays non-breaking too. (The pre-existing flamegraph = false default was never actually tolerant either.)

Tests

Every new test has a negative control, so a runner that ignored env entirely could not pass a one-sided assertion. The end-to-end test also asserts the probe variable is absent from the harness JVM — otherwise the fork would just inherit it and the test would prove nothing.

  • TestEnvPropagationIT (5) — end-to-end through the real BSP server, including precedence
  • ScalaJsTestIntegrationTest (+2), KotlinTestRunnerEnvTest (4) — real Node / real binaries
  • TestOptionsCodecTest (6), ClientEnvTest (6)

Full build compiles (25 projects); bleep-bsp-tests 629 passed / 0 failed.

Also

  • schema.json was missing jvmEnvironment entirely — added
  • New docs/usage/environment-variables.mdx, wired into the sidebar
  • Deleted ReactiveTestRunner (dead, no callers, and the second reader of jvmEnvironment that would have drifted). Verified with a clean rebuild of bleep-core.

Not covered

Scala Native's runTestsViaAdapter receives the environment (previously Map.empty) but is untested here: the toolchain artifacts were never fetched in this worktree, and the repo's own test for that path has been ignored as a "known RPC protocol issue" since #496. I also did not verify whether scala-native's withEnvVars replaces or merges the environment.

🤖 Generated with Claude Code

`FOO=bar bleep test` could not reach a test. Tests are forked by the BSP
daemon, which is long-lived and shared across workspaces, so its
environment belongs to whichever shell cold-started it — possibly days
ago in another directory.

The daemon does not mutate its own environment to fix this. That would
race between concurrent requests and leak one workspace's values into
another's test run. Instead the client sends its environment on the BSP
request (`TestOptions.env`, on the existing `bleep-test-options`
dataKind) and the daemon applies it as an overlay on the forked child
only. The hole was already there: `JvmPool.spawnJvm` applies an env map
to the child's ProcessBuilder and `JvmKey` already hashes it, so a
differing environment forces a distinct fork rather than reusing a
poisoned one. Every call site simply passed `Map.empty`.

Applied on every platform — JVM, Scala.js, Scala Native, Kotlin/JS,
Kotlin/Native. The four non-JVM runners already accepted an env map and
applied it to their ProcessBuilder; because nothing ever passed one they
did not honour `platform.jvmEnvironment` either. Same omission in all
three `bleep run` paths and in the BSP run handler, which additionally
ignored the standard `RunParams.environmentVariables` outright.

Precedence, weakest first: NO_COLOR=1, then the client's shell, then
`platform.jvmEnvironment`. The build deliberately outranks the ambient
shell: the environment is forwarded wholesale, so letting it win would
let a stray AWS_REGION in someone's profile silently override a region
the build states on purpose, failing on exactly one machine. Variables
the build does not mention pass through untouched.

CLASSPATH is never forwarded — JvmPool uses it as the long-classpath
channel on Windows and a forwarded copy would replace the test
classpath. PWD/OLDPWD/_ are dropped as stale shell bookkeeping. PATH and
JAVA_HOME are forwarded so tests that shell out find the same tools.

The TestOptions codec is now hand-written and absent-tolerant. A derived
codec does not fill in case-class defaults (verified: `Missing required
field`), so adding `env` would make every older client's request fail to
decode — and the failure path degrades to `TestOptions.empty`, silently
dropping that run's --only/--exclude/--jvm-opt and running the wrong
tests. Every field now reads through getOrElse.

Also: add the missing `jvmEnvironment` to schema.json, document the
behaviour, and delete `ReactiveTestRunner` (dead, no callers, and the
second reader of jvmEnvironment that would have drifted).

Not covered: Scala Native's TestAdapter path receives the environment
but is untested here — the repo's own test for it has been ignored as a
"known RPC protocol issue" since #496.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@oyvindberg
oyvindberg merged commit 64c4e24 into master Jul 26, 2026
10 checks passed
@oyvindberg
oyvindberg deleted the env-var-test branch July 26, 2026 14:53
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.

1 participant