A spilled background job and one that genuinely exited 3 were the same
report. `Job::to_info` copied `result.code` and nothing else, so
`did_spill` and `original_code` never reached `JobInfo`, and `failed:3`
was the whole answer either way. An embedder reading `jobs --json` could
not ask whether the 3 was the command's own.
`failed:3` stays. It is the loud signal GH #212 installed, and changing
it would trade one ambiguity for a compatibility break. The two facts
ride alongside it instead: `JobInfo` is `#[non_exhaustive]` with a
builder, so the fields are additive, and `jobs --json` serializes
`JobInfo` directly so they appear with no rendering change.
The test that proves it goes through `Job::to_info` rather than building
a `JobInfo` by hand, which is how the gap stayed invisible — every
existing `JobInfo` test constructs the struct itself, so a field the
converter forgets is a field no test misses. It runs two jobs under one
output limit, `seq 1 5000` (spills, remapped from 0) and a function
returning 3 (its own code), and separates them on the new fields alone.
Second: the test-only `BackendDispatcher` drained an external's stdout
into its own ring with no tee into the background job's stream, so no
test driven through that dispatcher could observe stream routing — a
hole directly under the #446/#448/#449 streaming work, in the twin that
exists so tests can reach exactly this. It now tees the way
`spawn::spawn_process` does, under the same
first-or-only-stage rule.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A spilled background job and one that genuinely exited 3 were the same report.
Job::to_infocopiedresult.codeand nothing else, sodid_spillandoriginal_codenever reachedJobInfo, andfailed:3was the whole answer either way. An embedder readingjobs --jsonhad no way to ask whether the 3 was the command's own.failed:3stays as the status string — it is the loud signal that GH #212 installed, and replacing it would trade one ambiguity for a compatibility break. The two facts ride alongside it instead.JobInfois#[non_exhaustive]with a builder, so the fields are additive, andjobs --jsonserializesJobInfodirectly, so they appear with no rendering change:The test that proves it goes through
Job::to_inforather than building aJobInfoby hand — which is how the gap stayed invisible, since every existingJobInfotest constructs the struct itself and so cannot miss a field the converter forgets. It runs two jobs under one output limit and separates them on the new fields alone.Second: the test-only
BackendDispatcherdrained an external's stdout into its own ring with no tee into the background job's stream, so no test driven through that dispatcher could observe stream routing at all — a hole directly under the streaming work in #446/#448/#449, in the twin that exists so tests can reach exactly that. It now tees the way the production spawn site does, under the same only-or-last-stage rule, with a test that fails when the tee is removed.Gates:
cargo test --allclean,cargo clippy --all --all-targetszero warnings.🤖 Generated with Claude Code