Skip to content

fix(test): silence engine stderr in test builds — the anyOf 'flake' was a display artifact (#444) - #456

Merged
justrach merged 1 commit into
release/0.0.242from
fix/444-anyof-flake
Aug 6, 2026
Merged

fix(test): silence engine stderr in test builds — the anyOf 'flake' was a display artifact (#444)#456
justrach merged 1 commit into
release/0.0.242from
fix/444-anyof-flake

Conversation

@justrach

@justrach justrach commented Aug 6, 2026

Copy link
Copy Markdown
Owner

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:1164 is explicit that "no matter the result, we want to display error/warning messages": a Run step whose result_stderr is non-empty goes to printErrorMessages regardless of outcome. That renders the failure tree, dumps the captured stderr, and prints failed command: — which is recorded unconditionally at Maker/Step/Run.zig:2157, before the child's term is even known. The odd +- run test w line is printStepFailure emitting 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-built Agent has no writer and agent_output.say falls through to the tick-gate stderr path.

Why it looked "first run after compile"

Maker/Step/Run.zig:243 sets has_side_effects = false for .zig_test, so an unchanged binary is a manifest cache hit that never executes the tests at all:

=== IMMEDIATE re-run, identical args ===
test success
+- run test cached          real 0.15

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 through writeLine, gated on a comptime emit_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.
  • New tick_gate.workerPrint; the four raw [workflow]/[diversity] prints in workflow.zig, workflow_pipeline.zig and brief_diversity.zig now 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 one assertClean call that is meant to fail. A real offender still prints, and the assertion itself is unchanged.
  • Guard test asserts !emit_to_stderr at both comptime and runtime while driving the real workerLine path, so removing the gate fails the suite.

Verification

zig build test0 bytes of stderr (was 2250), exit 0. Suite 1043 → 1044, exactly +1, count-diffed by stashing src/. scripts/eval-tier1.sh green 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.

#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>
@justrach
justrach changed the base branch from main to release/0.0.242 August 6, 2026 12:32
@justrach
justrach merged commit 4504104 into release/0.0.242 Aug 6, 2026
6 checks passed
@justrach
justrach deleted the fix/444-anyof-flake branch August 6, 2026 12:32
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.

Flaky: 'tool fake has a JSON Schema anyOf' test failure on first-run-after-compile (3x today, seed-independent)

1 participant