Skip to content

test(e2e): drive the Lemonade recovery scenario with a planted runtime - #351

Draft
rominf wants to merge 3 commits into
mainfrom
chore/remove-e2e-test-hooks
Draft

test(e2e): drive the Lemonade recovery scenario with a planted runtime#351
rominf wants to merge 3 commits into
mainfrom
chore/remove-e2e-test-hooks

Conversation

@rominf

@rominf rominf commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Depends on #342 — this rewrites the contract test #342 adds, so it stays in draft until that merges. Closes #349.

Problem

e2e-test-hooks is a cargo feature that compiles two scripted-failure seams into rocm, and it exists to serve exactly one scenario: @id:serve-lemonade-preparation-recovery (serve-18).

  • engines/lemonade/src/lib.rs — makes the llama.cpp backend install fail on demand
  • apps/rocm/src/main.rs — waives rocm serve's no-GPU pre-flight, so the scenario can reach the install phase on a GPU-less host

The cost is spread across the repo: 2 feature declarations, 3 cfg sites, 10 --features lines across nightly.yml and e2e-selfhosted.yml, xtask's own build, and 3 contract tests.

Two consequences follow from the feature existing at all.

Binaries under test are not the binaries shipped. Every lane that pre-builds with the feature installs a binary that differs from a release build. #342 fixed this for the Windows lifecycle lane; it was still true on Linux, where the release build happens inside cargo xtask e2e — with the feature — so the Linux lifecycle lane packaged and installed a hook-carrying binary through the real installer.

The contract tests encoded the asymmetry instead of removing it. One test asserted the Windows lane must be hook-free, two asserted self-hosted and nightly must be hook-ful, and ci.yml's Linux lane was asserted by neither — the same blind spot that hid the duplicate-build drift #342 fixed, one lane over.

Fix

Delete the feature and drive the failure from a planted runtime instead.

tests/e2e-cucumber/src/bin/fake-lemonade.rs is a fixture binary copied into an isolated runtime directory under both lemonade and lemond (it takes its role from argv[0]), alongside a hand-written install manifest. It reproduces the three calls the engine makes: status exits 0, backends prints a table reporting llamacpp:rocm as installable, and backends install exits non-zero.

The CLI then fails where a genuinely broken install fails — at a non-zero child process — and rocm stays byte-identical to what ships.

A bin target of e2e-cucumber rather than an xtask-built artifact, so the suite reaches it through CARGO_BIN_EXE_fake-lemonade with no path to keep in step. It has to be a real executable, not a script: the engine spawns it by exact path and Windows CreateProcess needs a PE image.

Why this doesn't weaken verification

resolve_runtime accepts the planted manifest because it checks only that the manifest parses and that lemond is a file. No checksum, version, or signature is involved on this path — the SHA-pinned archive download lives in prepare_embeddable, which the serve path does not go through. Nothing here bypasses a check the product performs.

Approaches ruled out are recorded in #349 (mocking the request — the boundary is a subprocess, not HTTP; prewarming — ephemeral on hosted lanes, already installed on prewarmed ones; dropping the network; mocking verify_sha256 — a fail-open seam inside supply-chain verification).

The tradeoff, explicitly

Seam #2 was what let serve-18 run without a GPU. Without it, rocm serve reaches its GPU-required pre-flight first, so the scenario is retagged @requires-gpu and moves off the blocking every-PR lane onto a gated one.

That is a real loss of coverage cadence and it is the reviewer's call, not a cleanup side effect. The counter-argument is that what it covered on the every-PR lane was a code path that only existed in test builds.

To limit what that move gives up, a unit test in engines/lemonade pins the coupling a GPU-less lane would otherwise silently lose: the fixture's backends table must still parse into a llamacpp:rocm install attempt. If a parser change stopped accepting it, the scenario would quietly stop reaching the failure it asserts and only a GPU lane would notice. That test runs everywhere.

Contract tests

assert_prebuilt_e2e_lanes_enable_test_hooks is repurposed, not deleted: "match the feature xtask uses" becomes "match the invocation xtask uses", which is the invariant that actually prevents the duplicate release build. It asserts whole-line, so a trailing --features … cannot satisfy it — verified by re-adding the flag and watching it fail.

#342's !contains("e2e-test-hooks") assertion is dropped; its reuse assertions stay.

Test plan

Verified locally:

  • The fixture's four behaviours, driven directly: backends table, status → 0, backends install → non-zero, lemond stays alive until killed.
  • The new lemonade unit test, and the reworked contract tests (24/24). Re-adding --features rocm/e2e-test-hooks to a lane makes nightly_prebuilt_e2e_lanes_match_xtask_build fail.
  • cargo check --workspace --all-targets, plus cargo check -p e2e-cucumber --test e2e explicitly (the e2e target is test = false, so --all-targets skips it).
  • cargo clippy --workspace --all-targets -- -D warnings and cargo fmt --check clean.
  • cargo test --workspace --all-targets: two failures in rocm-dash-tui (chatgpt_oauth_client_*, "cannot create token cache dir: Permission denied"). Confirmed pre-existing — they fail identically on origin/main in this environment, and this diff does not touch that crate.

Not verified locally, stated per AGENTS.md §8: serve-18 itself. This host has no usable AMD GPU, so rocm serve stops at the pre-flight — which is exactly why the scenario is now @requires-gpu. I confirmed the CLI reaches and reports that pre-flight against a planted runtime, but the retry-count and recovery-guidance assertions past it need a GPU host. The self-hosted GPU lane is what will exercise them, and this PR should not merge until that lane has run it green.

The Windows lane compiled the whole release graph twice, adding 3-4
minutes to the one job that alone determines when CI goes green.

The Build step builds rocm and rocmd plainly, but the lifecycle E2E step
then runs `cargo xtask e2e`, which builds them for itself with
`--features rocm/e2e-test-hooks`. That is a different feature resolution,
so cargo recompiles rocm, rocmd, rocm-core, reqwest, rustls, ring and the
rest rather than reusing any of it. The cache is not at fault: it
restores with a full match on every run.

Export ROCM_CLI_BINARY / ROCM_CLI_ROCMD_BINARY so xtask skips its build
and uses what is already there. Measured on four consecutive runs, the
second release build took 2m58 to 4m22.

The binaries stay hook-free, unlike the prebuilt lanes in
e2e-selfhosted.yml and nightly.yml. Those run the full suite, whose
scripted failure seams are compiled out without the feature.
E2E_ONLY_LIFECYCLE restricts this lane to @lifecycle scenarios, none of
which use a seam, and the lane packages and installs through the real
installer -- so it should ship exactly what a release ships.

A contract test pins the invariant. The existing test enforcing the
opposite rule covers only e2e-selfhosted.yml and nightly.yml, which is
why this drift went unnoticed in ci.yml.

Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
…_DIR

Review follow-up on the prebuilt-binary reuse change.

The exported binary paths hard-coded `$PWD\target\`, which a future
job-level CARGO_TARGET_DIR would silently invalidate — the guard would
then fail on a path nothing builds to. Derive $targetDir the same way the
PowerShell prebuilt lanes in nightly.yml and e2e-selfhosted.yml already
do, which retires the caveat comment that documented the limitation.
`xtask e2e` resolves a relative override against the invocation dir
(resolve_caller_path), so dropping `$PWD\` changes nothing at runtime.

Also:

- Test-Path -LiteralPath, so a target dir containing PowerShell wildcard
  metacharacters cannot make the guard match the wrong path.
- Pin the full release-path assignment in the contract test. Asserting
  only that the variable names appear would pass for `target\debug\`,
  defeating the reuse the test is named for — the same pin
  assert_prebuilt_e2e_lanes_export_rocmd applies to the other lanes.
- Give the contract test a descriptive fallback instead of an empty
  string, so a lane that stops invoking `cargo xtask e2e` fails with a
  message that says so rather than an assertion against "".

Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
Delete the `e2e-test-hooks` cargo feature and both scripted-failure seams
it gated. They existed to serve one scenario,
`@id:serve-lemonade-preparation-recovery`, and cost the repo a
CI-wide asymmetry: every lane running the full suite pre-built `rocm`
with the feature, so the binary under test was not the binary released.
The Linux `ci.yml` lifecycle lane packaged and installed a hook-carrying
build through the real installer, and the contract tests encoded the
split rather than removing it — one lane asserted hook-free, two
asserted hook-ful, and Linux was covered by neither.

The scenario needs Lemonade's llama.cpp backend install to fail twice so
it can pin the retry count and the terminal recovery guidance. It now
gets that from a planted runtime instead of a compiled-in branch: a
fixture binary copied to `lemonade`/`lemond` in an isolated runtime dir,
plus a hand-written install manifest. `resolve_runtime` accepts it
because it checks only that the manifest parses and that `lemond` is a
file — no checksum, version or signature is involved on this path, so
nothing here weakens a verification the product performs. The CLI then
fails where a genuinely broken install fails, at a non-zero child
process, and `rocm` stays byte-identical to what ships.

Seam #2 waived serve's no-GPU pre-flight so the scenario could reach the
install phase on a GPU-less host. Without it the pre-flight is reached
first, so the scenario is retagged `@requires-gpu`. That is the real
cost of this change: it moves off the blocking every-PR lane onto a
gated one. Tracked in the issue this closes.

Because the scenario no longer runs on a GPU-less lane, a unit test pins
the coupling that lane would otherwise silently lose: the fixture's
`backends` table must still parse into a `llamacpp:rocm` install
attempt.

The two prebuilt-lane contract tests are repurposed rather than deleted.
"Match the feature xtask uses" becomes "match the invocation xtask uses",
which is the invariant that actually prevents the duplicate release
build, asserted whole-line so a trailing flag cannot satisfy it.

Closes #349.

Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
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.

Remove the e2e-test-hooks feature

1 participant