fix(test): silence engine stderr in test builds — the anyOf 'flake' was a display artifact (#444) - #456
Merged
Merged
Conversation
#444 is not a flake and nothing was ever failing. `zig build test` exits 0 on every one of these runs; what it also does is print a step-failure tree and a red `failed command: .../test --listen=-`, and the line people read as the cause — `tool 'fake' has a JSON Schema 'anyOf'` — is a passing negative control's expected diagnostic that merely sat in the same buffer. The mechanism is in the build runner (zig 0.17.0-dev.813, lib/compiler/Maker.zig:1164): a Run step whose `result_stderr` is non-empty is handed to `printErrorMessages` REGARDLESS of its result, which renders the failure tree, dumps the captured stderr, and — because the failed command is recorded unconditionally before the child's term is known (Maker/Step/Run.zig:2157) — prints `failed command:`. `printStepFailure` even emits a bare `" w\n"` placeholder for this case, which is the `+- run test w` line. Exit status stays 0 throughout. So any stderr at all from a PASSING suite produces the whole alarming block. This suite emitted twelve lines of it: the negative control's `std.debug.print`, and eleven subagent activity lines — `[test]`, `[workflow]`, `[diversity]` — which reach stderr because a test-built Agent has no writer and `say()` falls through to the tick-gate path. Why it looked correlated with the first run after a fresh compile: a zig_test Run step sets `has_side_effects = false`, so `zig build test` with an unchanged binary is a manifest cache hit that does not execute the tests at all (`+- run test cached`, 0.15s). Only the first run after a compile ever runs the suite, so only it can produce stderr. The "warm re-runs passed" observation was vacuous — those runs ran nothing. And seed-independence follows for free: there was no randomness involved, only presence or absence of a cache hit. Reproduced 9/9 byte-identical (2250 bytes, exit 0) on consecutive forced-recompile runs. The fix makes the trigger structurally impossible rather than quieter: - tick_gate gains `writeLine`, gated on `emit_to_stderr = !builtin.is_test`, a comptime constant. Every stderr write on the worker-line path now goes through it, so a test binary cannot reach one. The gate's accounting is untouched: offers are still held, drops still counted, released lines still counted. - The four raw `std.debug.print(" [workflow]/[diversity] ...")` call sites now use the new `tick_gate.workerPrint`. They were always this module's business — its own doc names them — and bypassing it meant they neither honoured the line-boundary gate nor could be elided. - tool_schema_tests mutes the offender diagnostic for the ONE call meant to fail. A real offender still prints, where stderr is exactly what you want. Guarded by a new test asserting `!emit_to_stderr` at both comptime and runtime while driving the real `workerLine` path, so removing the gate fails the suite. `zig build test` now emits zero bytes; suite 1043 -> 1044, tier 1 green. Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #444, by refuting its premise. This was not a flake, not seed-independent by luck, and not cross-test interference. Nothing was ever failing.
What actually happens
The Zig build runner prints a step-failure report for a successful step.
lib/compiler/Maker.zig:1164is explicit that "no matter the result, we want to display error/warning messages": a Run step whoseresult_stderris non-empty goes toprintErrorMessagesregardless of outcome. That renders the failure tree, dumps the captured stderr, and printsfailed command:— which is recorded unconditionally atMaker/Step/Run.zig:2157, before the child's term is even known. The odd+- run test wline isprintStepFailureemitting a literal" w\n"placeholder, an unfinished string in this dev toolchain.So any stderr from a passing suite renders the entire alarming block, exit code 0 included.
This suite emitted 12 such lines: the negative control's own
std.debug.print, plus 11 subagent activity lines that reach stderr because a test-builtAgenthas no writer andagent_output.sayfalls through to the tick-gate stderr path.Why it looked "first run after compile"
Maker/Step/Run.zig:243setshas_side_effects = falsefor.zig_test, so an unchanged binary is a manifest cache hit that never executes the tests at all:Only the first run after a compile ever runs the suite, so only it can produce stderr. "Warm re-runs passed" was vacuous — those runs ran nothing. Seed-independence follows for free: there was never any randomness involved, only cache-hit versus cache-miss.
Reproduced 9 consecutive times by forcing recompiles, 2250 bytes of output, byte-identical, exit 0 every time. The cold-cache loop the issue suggested was never needed.
Both standing suspicions are refuted
The workflow/diversity process-spawn race and the cross-test stderr interleaving theories are both wrong. Those tests appeared in the output only because they log on the happy path.
The fix
tick_gate.zig: every stderr write on the worker-line path goes throughwriteLine, gated on a comptimeemit_to_stderr = !builtin.is_test. A test binary cannot reach a stderr write through this module. Gate accounting (holds, drops, released-line counts) is untouched.tick_gate.workerPrint; the four raw[workflow]/[diversity]prints inworkflow.zig,workflow_pipeline.zigandbrief_diversity.zignow route through it. They were always this module's business, and bypassing it meant they were also ignoring the line-boundary gate.tool_schema_tests.zig: the diagnostic is muted for the oneassertCleancall that is meant to fail. A real offender still prints, and the assertion itself is unchanged.!emit_to_stderrat both comptime and runtime while driving the realworkerLinepath, so removing the gate fails the suite.Verification
zig build test→ 0 bytes of stderr (was 2250), exit 0. Suite 1043 → 1044, exactly +1, count-diffed by stashingsrc/.scripts/eval-tier1.shgreen in 91s. All touched files well under the 600-line cap.Unproven
I did not prove the historical "failures" were only ever this. Every occurrence produced here exits 0. If anyone ever saw a genuinely nonzero exit, that is a separate bug, not seen across 9 forced-recompile runs and roughly 5 cold runs. Given the output is visually indistinguishable from a real failure, the strong read is that the reported failures were misread output — but that is inference, not proof.
Note that
" w\n"is specific to Zig 0.17.0-dev.813 and a newer toolchain may render it differently. The fix does not depend on that: zero stderr means the block never renders at all.