DEV Community

Deva
Deva

Posted on

Your subprocess test is running against production state

The comment in the test said "in memory fallback." That was wrong. The test was running the real warmup eval command, no , dry run, against the live state.json.

I have a warm up phase in my X engine that seeds a dwell anchor before the engine graduates to full publishing. The subprocess tests for warmup eval included a note claiming the editable install would use some kind of ephemeral state. It did not. config.STATE_PATH resolved to the real file on disk because that is what editable installs do: they point at the source directory. The "in memory fallback" comment was wishful thinking left over from an earlier version of the code.

Why did this go unnoticed for so long? Because the no op path in warmup eval wrote nothing. A test that exits 0 without touching state looks perfectly clean even when it is aimed straight at production. The moment I added dwell anchor seeding in the previous commit, the test started writing to state.json during every test run. I noticed because warm up state in production was advancing on its own.

The fix is six lines and should have been there from the start. I added X_ENGINE_STATE_PATH as an env override in config:

STATE_PATH = Path(os.environ.get("X_ENGINE_STATE_PATH", DEFAULT_STATE_PATH))
Enter fullscreen mode Exit fullscreen mode

Then pointed all three warmup eval subprocess tests at a tmp_path fixture:

env = {**os.environ, "X_ENGINE_STATE_PATH": str(tmp_path / "state.json")}
result = subprocess.run(cmd, env=env, capture_output=True)
Enter fullscreen mode Exit fullscreen mode

Verification was running the full 141 test suite and confirming state.json is byte identical before and after. It is.

The tradeoff here is worth naming. An env override for the state path means config is now partially mutable at runtime, which adds surface area. The cleaner architecture is proper dependency injection: pass the state path into every function that needs it rather than read a global. I would agree with that if you pushed me. But this engine runs as a long lived launchd process where the state path is fixed at startup and never changes in normal operation. Threading a new parameter through the entire call stack to satisfy test isolation is a bigger change than it sounds, and the env override gets there without touching production behavior at all.

What I would do differently: write the env override on day one whenever a subprocess test spawns your own CLI. The pattern is obvious in hindsight. Any test that shells out to your binary is running the full binary against whatever paths config resolves to at that moment. If those paths are not overridable, you have two honest choices: mock at a lower level, or accept that you are writing integration tests against the live system. Neither is inherently wrong, but it has to be a deliberate choice, not an accident discovered six commits later.

The thing that kept this survivable was that the no op path is the common case during warm up: most ticks do nothing because the phase gates are closed. Bugs that hide behind benign behavior until something real changes underneath them are the hard ones. The dwell anchor seeding commit was what flushed this out, which is annoying in the moment but exactly how it should work. New production behavior forced the test to actually exercise the code path, and the lie in the comment became visible.

Top comments (0)