---
name: cargo-builds-package-bins-for-integration-tests
description: Cargo builds EVERY bin target of a package whenever it builds ANY integration test of that package — so same-package fixture bins are already guaranteed and env!(CARGO_BIN_EXE_*) adds no build edge; the real gap is cross-package consumers and UNIT tests.
metadata: 
  node_type: memory
  type: project
  originSessionId: 7a3c43bb-f0d9-471d-88ad-ac02f7759292
  modified: 2026-08-05T00:56:36.552Z
---

Measured on spt-core 2026-08-03, cargo 1.93.0, and it falsified a claim I had put in
the infra register:

**Cargo builds every `[[bin]]` target of a package whenever it builds ANY integration
test (`tests/*.rs`) of that package.** That is what makes `CARGO_BIN_EXE_<name>`
resolvable in the first place. Probe: deleted all four `translate_proof_fixture`
artifacts, then built `--test attach_wedge_e2e` — an unmodified test that never names
the fixture. The plain exe returned at a fresh mtime; the hash-suffixed harness exes
stayed absent (so it came back as a *bin dependency of the integration-test build*,
not as a `--bins` harness compile). doyle replicated on `gh_fixture` / `--test
json_emit` / a different pool, same result.

**Consequence — split the population by "is the BUILD guaranteed", NOT by "could the
dependency be expressed":**

| consumer | build guaranteed? |
|---|---|
| integration test, SAME package | YES, automatically. No hazard. |
| **unit** test, same package | NO — unit tests get no `CARGO_BIN_EXE_*` at all |
| any test, CROSS package | NO |

So converting same-package sites to `env!("CARGO_BIN_EXE_<fixture>")` is a **clarity
and robustness** change (the path cargo actually emitted, instead of string-joining
`EXE_SUFFIX` onto a `CARGO_BIN_EXE_spt` directory anchor) — **not a build fix**. In
spt-core the genuine hazard population is 33 cross-package sites plus one unit-test
member (`crates/spt/src/cli.rs`), which is why `.github/workflows/ci.yml:105-106`
hand-prebuilds `translate_proof_fixture`.

Remedy options for the cross-package half, measured the same day:
- **Artifact deps** (`artifact = "bin"` in dev-dependencies) is the only shape that
  expresses the edge to cargo, but `artifact = …` **requires `-Z bindeps`** — refused
  on stable 1.93.0 (exit 101), works on nightly. Under **nextest the CLI flag is not
  enough** (nextest's inner `cargo metadata` dies, exit 102); it needs
  `[unstable] bindeps = true` in `.cargo/config.toml`. Artifacts land in
  `target/debug/build/<pkg>/<hash>/out/` via `CARGO_BIN_FILE_*`, not `target/<profile>/`.
- **In-test `cargo build`** guarantees the file at run time but expresses nothing to
  cargo, and **serialises**: 4 of 4 concurrent invocations hit "Blocking waiting for
  file lock" on the build directory. Warm cost ~0.4s; cold cost unmeasured.

**Why:** the register's own CI-member paragraph already stated the general rule (bins
are built for integration tests and benches, not unit tests) and both doyle and I drew
the opposite consequence one section later. A general rule you wrote does not protect
you from contradicting it nearby — see [[audit-the-boring-claims]].

⚠ **THE CONSUMER POPULATION IS BIGGER THAN A `sibling_bin` GREP FINDS — there are
THREE resolution syntaxes, and counts built on two of them undercount.** Measured
2026-08-03 after a checker returned a clean green over a gap known to be real:
1. `sibling_bin("<name>")` — 29→24 local resolver copies.
2. `env!("CARGO_BIN_EXE_<name>")`.
3. **A bare bin-name string joined onto a resolved directory** — `crates/spt/src/cli.rs`
   (`current_exe()` → `deps/` → parent, then `format!("translate_proof_fixture{EXE_SUFFIX}")`),
   and the SHARED resolver `crates/spt-term/tests/support/fixture_bin.rs`, whose
   cross-package consumers (`capture-player` ×3, `console-mode-probe` ×1 in
   spt-term/ and spt-daemon/ tests) appear in NO `sibling_bin` count.
   That resolver takes the required build command as its second argument
   (`"cargo build -p mock-adapter --bin capture-player"`), so at those sites the
   needed prebuild is already declared machine-readably in the source.
A raw string-literal scan returns ~133 hits, but `mock-shell` (121) and `mock-session`
(6) double as adapter/kind names — that number is an over-matching upper bound, not a
site count, and the triage is unfinished.

**THE GAP FIRED IN A REAL ARM RUN — the recipe line for any `-p <pkg>` repeat run
(measured 2026-08-04, doyle ruled it be filed where the arm recipe lives).** A loaded
arm run as `cargo nextest run -p spt-daemon` from a lane worktree with a FRESH `target/`
reds `spt-daemon::attach_resize_capture` ×3 (`the_captured_attach_and_resize_ends_on_the_
authoritative_screen`, `the_field_client_model_reproduces_the_captured_scrap`,
`the_oracle_detects_a_seeded_geometry_mismatch`) — **10 of 10 runs, deterministic.**
The panic names its own fix: `prebuild the fixture binary: cargo build -p mock-adapter
--bin capture-player`. `-p spt-daemon` never builds it (cross-package, row 3 above), and
a fresh lane target has no copy — main's pool did, which is why the same recipe can pass
in one tree and red in another. **Before any `-p <pkg>` arm: build the workspace, or add
`-p mock-adapter`.** Second fired member, 2026-08-04 (hertz's F1 red-arm rig, fresh 997bd1c
worktree): `cargo nextest run --no-run -p spt --test engine_room_bringup_e2e` never builds
`mock-session` (mock-adapter's bin), so `sibling_bin("mock-session")` pointed at a nonexistent
exe and the leg red at the PRECONDITION assert (engine_room_bringup_e2e.rs:1150, spawned=false)
— correctly discarded as rig red, fix `cargo build -p mock-adapter --bin mock-session` (2.79s). Two readings this rules out: 10/10 deterministic is a PRECONDITION
signature, never a race — do not spend a triage on it; and golden being green on those
rows at the same sha is not a contradiction, CI builds the workspace. Third consequence,
which must ride any citation of such an arm: the neighbourhood was ONE PACKAGE, smaller
than golden's population, so the arm's verdict carries that scope sentence with it.

⚠⚠ **SECOND FIRING, AND THE FAILURE FACE IS WORSE THAN capture-player's — IT DOES NOT NAME ITSELF.**
2026-08-04, mine, F1 red-arm re-run. `cargo nextest run -p spt --test engine_room_bringup_e2e` in a
lane worktree with a fresh `target/`: `sibling_bin("mock-session")` (row 1 + row 3 — the name is
resolved by string-join, and the bin belongs to **mock-adapter**, cross-package) resolved to an exe
the narrowed build never produced. **The rig did not say "missing binary".** It registered a harness
pointing at a nonexistent path, nothing spawned, and the test died 186s later on a PRECONDITION:

    assertion `left == right` failed: PRECONDITION: the offline arm must actually reach the offline
    row ... setup: ... warm bring-up spawned=false
      left: None   right: Some("offline")

That reads as a product or lifecycle defect. capture-player's panic prints its own
`cargo build -p mock-adapter --bin capture-player` recipe; **this one prints a story about the
subject under test.** Cost: one 186s run plus a triage, on a shared box, inside a lent window.
- **The remedy was already written in this very file** ("Before any `-p <pkg>` arm: build the
  workspace, or add `-p mock-adapter`") and I did not apply it — [[rule-failed-its-author-needs-a-mechanism]].
  The mechanism: I narrowed the build to make the timed leg honest (compile out of the measured leg),
  and narrowing is exactly what breaks row 3. **The hygiene move and the hazard are the same move.**
- **Corroboration it is a class, not my one-off:** todlando reports the identical trap on this box for
  `translate_proof_fixture`, where `--bins` does not build it either. `golden.yml:921` carries a
  dedicated `cargo build -p mock-adapter --bin mock-session` step — CI already pays this toll
  explicitly, which is why golden never sees the face a local narrow run does.
- **How to apply, concretely:** when a real-daemon e2e fails at a PRECONDITION with a
  "spawned=false" / "no ready marker" shape, `ls target/<profile>/` for the fixture bins BEFORE
  believing the precondition's own story. Absence of the exe is cheaper to check than any hypothesis
  about the product, and it is the first thing to rule out, not the last.

**How to apply:** before claiming an `env!` reference creates a build edge, ask which
of the three rows above the consumer is in — and before trusting any COUNT of consumers,
check it covers all three syntaxes ([[verdict-from-probe-competence]]). Probe design that settled it:
[[probe-an-unmodified-subject-needs-no-baseline]]. Related: [[spt-bin-lane-fixture-bins]],
[[workspace-bin-name-collision]], [[gate-int-tests-with-nextest-not-bare-cargo-test]].
