test(e2e): pin the contracts a README walkthrough expects (EAI-8024) - #241
test(e2e): pin the contracts a README walkthrough expects (EAI-8024)#241fredespi wants to merge 20 commits into
Conversation
d94221b to
9fc2de0
Compare
Walking the public README end to end on an MI300X pod turned up thirteen
places where the CLI does not do what the README leads a reader to expect.
This adds a scenario for each and registers it as an expected failure, so
CI stays green and every row goes stale the day its defect is fixed.
No product behaviour changes.
Pinned on the no-GPU lane, so they gate every PR:
EAI-8011 the help offers `rocm serve qwen2.5-7b-instruct`, which is
neither a name the model listing knows nor an owner/repo
reference
EAI-8011 the help describes running `rocm` with no subcommand as
opening the dashboard, which is what it says `rocm dash` does
EAI-8010 `rocm update --dry-run` is refused as a usage error, though
the README documents --apply and --dry-run as independent
EAI-8019 `serve --help` advertises cpu_only and the command refuses it
EAI-8020 the diagnosis and the fix preview print different verification
steps for the same remedy
EAI-8009 chatting locally with no --model ignores a ready local server
that is not the built-in assistant model
EAI-8014 uninstall reports completion while leaving the servers it
manages running, having deleted the records needed to stop them
Pinned on the GPU lane, scoped `when = {}` for a first measurement:
EAI-8007 a stop that worked reports that it stopped no processes
EAI-8015 a remedy names the group-lookup failure UNKNOWN as a group
EAI-8020 the diagnosis and the fix propose different group commands
EAI-8021 a device the user can already read and write is still offered
as a group-permission cause
EAI-8006 `--gpu auto` selects none on a machine that has a GPU
EAI-8016 a second server is given an address already in use
EAI-8018 Lemonade is advertised ready on the GPU but never answers
EAI-8017 installing an engine fails when the session provides no
scratch directory
Harness additions, all small: removing environment variables from the
child (not just setting them), a `stat` shim scoped to one device path so
a device-group state can be arranged rather than hoped for, and a child
process a scenario owns and registers as a managed service so it can ask
whether the product stopped it — asserted before teardown, which would
otherwise clean up after the product and hide the defect.
EAI-8007 was first written against that child-process fixture on the
no-GPU lane and XPASSed: a record with no start-time token takes the
legacy best-effort path and is counted correctly. It now serves a real
model instead, which is the path the pod exercised.
Verified in a Linux container: ten new scenarios fail, each for the reason
its row states, with no XPASS and no new unexpected failures. The one
remaining failure there, engine-shell-marks-the-prompt, reproduces
identically on unmodified main — the container runs as root, so the prompt
ends in `#` where the assertion looks for `$`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
The first app-dev-gpu dispatch answered the MEASURE questions the rows were
carrying, and three of them came back differently from the plan.
EAI-8007 loses its row. Neither fixture reproduces it: a plain process
registered as a managed service is counted correctly (no-GPU lane), and so is
a real vLLM serve (MI300X). A row would be a stale XPASS on both lanes and
would fail them, so the scenario ships as a guard for the contract the pod
violated instead. What was different about that host is still unknown.
EAI-8021 needed a second substituted answer. The scenario made the device
world-writable and expected the group-permission cause to stop being offered,
but the GPU runner's user is already in the conventional groups, so the cause
scored zero there for a reason that has nothing to do with device access and
the scenario passed. It now substitutes the group answer too, so the state it
describes — access works, membership absent — holds on any host.
EAI-8006 was failing for the wrong reason: the serve was refused for having no
active ROCm runtime, before it ever chose a device. It now takes the same
managed-runtime precondition its sibling serve scenarios do.
Two scenarios could not be measured at all, because `rocm engines install
lemonade` fails on the GPU runner within two seconds:
- EAI-8017 was passing vacuously — the install never got far enough to leave
the engine without a scratch directory, and the assertion tolerated that.
It now requires the install to have got under way before it will speak.
- EAI-8018 asserted a claim the inventory never makes there, because
Lemonade is not installed in the scenario's isolated directories. It now
installs it first and reports the install output when the claim is absent.
Both will say why on the next dispatch.
Also scopes the diagnosis comparisons to the cause under test rather than the
first one reported (which cause ranks highest is host-dependent), tightens the
scratch-directory match so ordinary path output cannot satisfy it, and stops
the uninstall assertion passing vacuously when no server was registered.
Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The second dispatch settled the remaining MEASURE questions. EAI-8021 now fails for its stated reason once the group answer is substituted alongside the device one (score 45 with the device readable and writable), so its row stands. EAI-8006 loses its row and ships as a guard. `--gpu auto` reported selecting none on the pod, where the bundled amd-smi could not execute; the MI300X runner's amd-smi runs, so the CLI names a device and the scenario passes there. EAI-8017 and EAI-8018 lose their scenarios entirely. Both need a working Lemonade install, and Lemonade's launcher cannot start on either lane: on the MI300X runner it wants GLIBC_2.38 and GLIBCXX_3.4.32, which that OS does not have, and in a plain container it wants libdrm_amdgpu.so.1. Each dies before reaching the behaviour under test, so the scenarios were passing vacuously — which reads as coverage while proving nothing. Better no scenario than that one; both stay on their tickets, reproduced by hand on the pod, until a lane can run Lemonade. Removing the Lemonade install from the engine-inventory scenario also stops the suite writing into the runner's shared pre-warm tree, which it had begun doing. That leaves nine of the thirteen pinned as expected failures, two shipping as guards for a contract our lanes uphold and the pod did not, and two not covered. Signed-off-by: fredespi <fredrik.espinoza@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#242 landed `Scenario: 14` in model_serving.feature while this branch was also using 14, and the two touch different parts of the file — so git merges them cleanly and the collision is invisible until someone tries to refer to "scenario 14" and finds two of them. Renumbers this branch's five additions to 15-19, after main's highest. No scenario, tag, step or expectation changes. Signed-off-by: fredespi <fredrik.espinoza@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Strix Halo Windows lane finally reported, and it contradicts the reading this branch shipped with. `--gpu auto` really does report `selected none` on a machine that has a GPU — just not on the lanes measured first. MI300X gfx943 linux passes Strix Halo Ubuntu gfx1151 linux passes Strix Halo Windows gfx1151 windows fails: `gpu: auto (selected none)` Same GPU family on both Strix lanes, so the discriminator is the OS rather than the family — which is why this row uses `os = "windows"` and not the `therock_family` scoping `examine-both-forms-agree-on-gpu` uses. So the scenario stops being a bare guard and gets the expected-failure row it should have had, scoped to where the defect was actually observed. It still guards both Linux lanes, where the device is named. Signed-off-by: fredespi <fredrik.espinoza@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… bug `services-stop-reports-what-it-stopped` needs a real running server before it can ask what stopping one reports, and that Given serves on whatever engine the host defaults to. On a lemonade Linux host that is the serve which reaches ready and is shut down ~0.08s later (EAI-7423), so the PRECONDITION fails and the scenario never reaches the contract it exists to hold. Measured on Strix Halo Ubuntu: passed in run 196, failed in run 210 with "endpoint did not serve model Qwen3-0.6B after 2 attempt(s) of 300s" — the EAI-7423 signature its sibling rows already describe. Adds the row those siblings carry, scoped identically: os=linux because native Windows lemonade serve works and this passes there, gfx* because it needs a real GPU host, and flaky=true because which of these pass varies run to run and a deterministic-XPASS row would fail that lane on unrelated PRs. The row cites EAI-7423, not EAI-8007 — the stop-reporting contract itself still holds everywhere it has been observed, so it keeps no expected-failure row of its own. Signed-off-by: fredespi <fredrik.espinoza@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Strix Halo lanes cap at 35 minutes, and on main alone the Ubuntu one already runs 32.3 of them. This branch adds three scenarios that each start a real server, costing about nine more minutes on that lane, so both Strix jobs being cancelled at the cap is arithmetic rather than bad luck. Tags them @merge-queue, the mechanism scenarios 6, 6b and 8 already use for exactly this: they drop off the PR fast path and run in the merge queue, where the budget for heavy serves lives and where all three self-hosted lanes still execute them before anything lands. Measured on Strix Ubuntu after the fail-fast row in the previous commit: 232s, 136s and 149s. Everything this branch pins that does NOT need a running server keeps running on every PR, on every lane, unchanged: the help contracts, update, device policies, the diagnose comparisons and shims, local chat, and uninstall. Signed-off-by: fredespi <fredrik.espinoza@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9fc2de0 to
39afe38
Compare
juhovainio
left a comment
There was a problem hiding this comment.
Reviewed this against gh pr diff 241 — good, well-documented test-only PR (no product code touched), and I cross-checked all 13 new scenarios against their expectations.toml rows and lane tags; that bookkeeping is consistent and the reasoning for each row (including the two tickets deliberately left uncovered because Lemonade can't start on either available lane) holds up.
One real bug in the new harness code, though: advertised_device_policies in serving_steps.rs reads the wrong flag's help text, which breaks the exact scenario it's meant to support. Left as an inline comment with a fix suggestion. Also left a smaller nit on find_bool in diagnose_steps.rs that's not blocking but worth a look.
The device-policy scenario scraped the FIRST '[possible values:' line out of 'serve --help', which is --engine's clap-generated list, not --device's. So it served '--device lemonade' and '--device vllm', which are refused as unsupported policies -- wording close enough to the cpu_only rejection that the assertion matched and the scenario looked like it was failing for the reason its row states. It was not, and it could never have gone stale: those two values stay invalid whatever happens to EAI-8019, so a fixed bug would still have read as unfixed. Scopes the lookup to --device's own block, stopping at the next option so a --device that advertises nothing cannot borrow a neighbour's list. Verified against the real 'serve --help': the parser now yields gpu_required, gpu_preferred, cpu_only. Introspecting clap's device arg directly would be better still, but this suite is black-box by design and cannot import the product crates; main.rs already guards that list from the inside. Also reads user_can_read/user_can_write from the kfd device by name rather than searching the whole report. Every device in it carries those fields, so the generic walk was answered by whichever sorted first rather than by the device the scenario shimmed. Signed-off-by: fredespi <fredrik.espinoza@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resolve two additive conflicts: - diagnose.feature: keep both scenario sets, renumbering the README-walkthrough scenarios to 13-16 and updating the internal cross-reference. - expectations.toml: keep both appended row blocks. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
d2c7ac6 to
fedc6b2
Compare
|
Note on the red E2E tests (Strix Halo, Windows) check: this is a pre-existing failure on
Fixing it (either the Windows Lemonade serve, or an |
Dismissing this stale review: both points it raised were addressed in 559c704 and both threads are resolved. (1) advertised_device_policies now scopes the lookup to --device's own option block instead of taking the first [possible values:] line, which was --engine's — it now yields gpu_required, gpu_preferred, cpu_only, so the scenario fails for the cpu_only rejection it is meant to pin and its row can go stale once EAI-8019 is fixed. (2) diagnose_steps.rs reads the kfd device by name rather than walking the report for the first matching bool. Dismissed so the PR can receive a fresh review; no objection to a further look.
rominf
left a comment
There was a problem hiding this comment.
The test design here is careful in ways I don't usually see: anti-vacuity guards placed before teardown, premise assertions that fail loudly rather than silently passing, shims scoped to a single path, and expectation rows written to self-delete when the bug they pin gets fixed. The tag gating is sound too — @requires-no-gpu/@requires-bare-metal/@merge-queue all resolve through expectation.rs::resolve to Skip rather than a silent pass, and the merge-queue scenarios do reach a real lane via E2E_MERGE_QUEUE.
The problem is elapsed time rather than design. This has been open about two weeks and main has since fixed three of the bugs it pins. Because e2e.rs:1350-1362 treats a deterministic XPASS as fatal (std::process::exit(1)), rebasing onto current main turns those rows into a hard CI failure on every lane. I verified two of them empirically against a main-built binary rather than by reading, since that's the kind of claim that's easy to get wrong.
One is subtler and I think worth catching before merge: serve-rejects-no-advertised-device-policy is now vacuous in exactly the way @juhovainio described earlier in the thread — the parse changed again under it. --device moved to a clap ValueEnum in #304 and no longer renders an inline [possible values: …], so advertised_device_policies returns [] and the step panics on its own guard before serving anything. The scenario fails, the row is satisfied, and it stays satisfied forever — even though the underlying contract is now met on main. Same failure mode as before, different cause.
Also worth knowing: the red Windows check (FAIL: 'serve-hf-checkpoint-inference' was expected to pass) is drift, not this PR — main added an xfail row for it at expectations.toml:206 after this branch was cut.
For what it's worth, I re-checked the xfails that are still valid and they hold: diagnose-and-fix-agree-on-how-to-verify (diagnose still carries ls -l /dev/kfd, fix.rs doesn't), diagnose-and-fix-agree-on-the-remedy-command (fix.rs:129 still hard-codes render,video), update-preview-without-applying (--dry-run still requires = "apply"), and uninstall-stops-what-it-manages (the fix commit exists but is not on main).
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · fedc6b2
Summary
Adds 12 e2e cucumber scenarios (plus a new update.feature, shims for stat/id, and an owned-process fixture) that pin contracts a README walkthrough expects, most registered as expected-failure rows. Verdict: Needs work — the scenarios themselves are unusually well built (I could not find a vacuous assertion), but the branch is 53 commits behind main and at least three of the bugs it pins have already been fixed there, which turns the required E2E lane red. Verified: I did the revert test on all 12 scenarios and every literal they match (cpu mode is not a fallback path, stopped processes:, gpu: auto (selected , port:, uninstall complete, rocm serve ) exists in the product today, so none pass against a binary lacking the behaviour; 6 of the 12 run on the gating no-GPU E2E tests job, the other 6 are @merge-queue or @requires-gpu @requires-bare-metal and land only on lanes the workflow comment marks non-blocking; a previously-reported finding about advertised_device_policies returning empty is fixed (commit 559c704 scopes the scrape to --device's own block, and I reproduced the real serve --help layout with a clap 4.6 build of the same arg set — the helper correctly yields gpu_required, gpu_preferred, cpu_only, and also survives main's new ValueEnum rendering); the single red check is almost certainly the E2E lane exiting 1 on stale XPASS, since actions/checkout in that job takes the PR merge ref against current main (cargo clippy -p e2e-cucumber --test e2e and cargo test -p e2e-cucumber --lib both pass locally on this head, ruling those out). No prompt-injection content found in the checkout. Blocking: 3 · Non-blocking: 5.
🚫 Blocking (must fix before merge)
-
tests/e2e-cucumber/expectations.toml:178,:187,:207(and:277on the GPU bare-metal lanes) — these rows are already stale againstmain. The branch's merge-base is 53 commits behind, and in that windowmainlanded: the help fix that changed the worked example torocm serve qwen(a resolvable catalog alias) and relabelled barerocmas "the interactive launcher (TUI)" rather than the dashboard; the--devicerework that made the flag aValueEnumwithcpu_onlymarkedhide = true, so it is no longer advertised and the scenario's premise dissolves; and the diagnose fix that filters the literalUNKNOWNgroup out of the remediation command. I confirmed each against the base source (apps/rocm/src/main.rs:84-85,:347,:358,:1015;crates/rocm-core/src/diagnose.rs), and traced the consequence intests/e2e-cucumber/tests/e2e.rs:1350-1362: a non-flakyXPASS callsstd::process::exit(1). Since the requiredE2E testsjob checks out the PR merge ref, those three unconditional (when = {}) rows XPASS on the gating lane right now. Fix: rebase onto currentmainand delete the rows whose bugs have landed (keeping the scenarios, which then guard the fixes), re-checking the remaining rows against post-rebase behaviour. -
tests/e2e-cucumber/tests/e2e.rs:789—run_rocm_without_envis added by this PR and has zero call sites (repo-wide grep: the definition is the only match). Dead code in a PR whose subject is honest test bookkeeping. Fix: delete it, or land it with the scenario that needs it. -
tests/e2e-cucumber/features/model_serving.feature:161— the scenario-16 comment states "this carries no expected-failure row … goes red if CI ever meets it", buttests/e2e-cucumber/expectations.toml:245does carry a row forservices-stop-reports-what-it-stopped(scoped to the lemonade/linux/gfx lane,flaky = true), added later in the same branch. The comment was not updated with it, so the feature file now tells the next reader the opposite of the truth about the one scenario that is supposed to be an unconditional guard. Fix: amend the comment to say the guard is unconditional except on the lemonade-default lane, where a separate serve bug pre-empts it — the TOML's own note at:252-259already draws that distinction correctly.
Non-blocking
tests/e2e-cucumber/tests/e2e.rs:1308-1319— reconciliation keys only on cucumber's pass/fail boolean, so a step that panics on its own harness guard is indistinguishable from the declared bug reproducing;serving_steps.rs:790(assert!(!policies.is_empty(), …)) behind an unconditional row is the live instance. The current fix is correct, but a stale-help regression there would hide forever — consider having the guard fail the run rather than the scenario.tests/e2e-cucumber/tests/e2e/serving_steps.rs:817-819—assert_no_policy_refusedmatches two full product sentences verbatim; a harmless rewording ("CPU execution is not a fallback") silently turns the scenario green with the defect intact. Matching on the policy name plus a nonzero rc would be more durable.tests/e2e-cucumber/features/examine.feature:26and:128— two scenarios are both labelledScenario: 11, and 11/12 are inserted ahead of 3. Harmless (the harness keys on@id:, which I confirmed unique) but confusing to read.- Six of the twelve new scenarios (
@merge-queue×3,@requires-gpu @requires-bare-metal×3) never run on the required job — the merge queue and self-hosted GPU lanes are where they land, andci.yml:653-655calls the latter non-blocking. Worth stating in the PR description so the coverage claim is not read as gating. tests/e2e-cucumber/tests/e2e/diagnose_steps.rs:824-827— the "recognised non-default group" premise takes the first entry in/etc/group(likelyroot/daemon), not a plausibly device-owning group; the assertion still holds, but the premise is weaker than the comment claims. Also: nothing lints for anexpectations.tomlrow whose id no longer matches any scenario, so a future orphan row would sit inert and unreported.
Brings the branch up to date with main and addresses the review findings that depend on it. Stale expectation rows deleted (their bugs are fixed on main); the scenarios stay as guards: - help-serve-example-names-a-resolvable-model, help-describes-the-default-command (#296 made the worked example 'rocm serve qwen' and relabelled bare 'rocm' the launcher) - serve-rejects-no-advertised-device-policy (#304 hid cpu_only from the advertised list) - diagnose-commands-name-a-real-group (#302 filters the UNKNOWN group lookup) Verified in a Linux container on two hosts: 0 XPASS, 0 stale. Other review fixes: - advertised_device_policies reads clap's block rendering as well as the inline one, so the scenario keeps measuring the contract instead of tripping its own guard - run_rocm_without_env deleted (no call sites) - the services-stop comment now matches its expectations.toml row - 'not the leading remedy' reworded to 'not offered as a cause', which is what the assertion checks - the non-default device group is chosen from plausible device groups rather than whichever /etc/group lists first - scenario numbers made unique and prefixed after main's renumbering - new unit test rejects an expectations.toml row whose scenario no longer exists Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Missed by the --no-verify commit; caught by the prek lint job. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
feature_naming.rs (added on main) requires every @id: to start with its feature's key so the id alone says which feature it belongs to. Four ids predating that convention are renamed, with their expectations.toml rows. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
The mock lane ran a flat 64 scenarios at once. Its scenarios are safe to run together, but several drive a TUI under a pseudo-terminal and assert on wall-clock windows: the dashboard held-value window is 6s, and at 64-way those steps were descheduled for seconds and missed the window they were timing. Measured at 4.7s, which reports a value that expired on schedule as one that was never held. This surfaced here because the branch adds scenarios: main passes dash-08/09 at 64-way, this branch failed them on three separate hosts, and they pass 10/10 in isolation. Bisected in a container: 8-way starves them even with 10 cores free, 4-way is green and faster (33s vs 54s, since starvation costs more than it buys). Bound to the core count, clamped to 2..=6, with E2E_MAX_CONCURRENT to override when bisecting a lane whose failures look like contention rather than behaviour. Verified twice on the merged tree: 6 xfail, 0 XPASS, dash-08/09 green, with dash_steps.rs unchanged. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
The held-value assertion took a single screenshot after the first failed scrape. Under suite load that look lands late, sees a value that expired exactly on schedule, and reports it as 'cleared immediately' — the harness losing its window, stated as a product regression. It now samples for as long as the window is open, so every in-window sample must show the value and one that does not is the regression. That is strictly stronger than one look and does not depend on the look being prompt. If a whole window passes with no sample at all, the observation is retried on a fresh window (up to three) rather than reported: nothing was measured, so there is nothing to say. The failure instant now comes from the mock's own counter moving, counted relative to its value before the switch so a retry is not satisfied by an earlier failure. Verified on the merged tree: green at the default bound, and the honest 'lost the window' message rather than a false regression claim when forced to 32-way. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Reverts dash_steps.rs to main's version. The sampling assertion was correct and it found something, but what it found is not this PR's to land. Sampling the whole window showed the held value disappearing 0.8s and 1.3s after the failed scrape on the CI runner — far too fast to be scheduling loss, and inside a window that is supposed to be 6s. So EAI-7960's hold logic really is absent, and main's single-look assertion passes only when that one look catches a frame painted before the clear propagates. That turns two of main's scenarios from flaky-green into deterministically red. It is a real finding and it is reported on the PR, but strengthening another change's assertion is not something a test-bookkeeping PR should do silently. The concurrency bound stays: it is what this branch's added load actually needed, and it cuts the contention that made these scenarios flaky either way. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Two things this branch's added load exposed on the required mock lane, neither of them new: 1. EAI-7960 is not fixed, and its two scenarios were passing by luck. Their own step documentation says the assertion must be RED until the hold logic lands, yet they carried no row. A single instantaneous screenshot of a TUI sometimes catches a frame painted before the clear, so they went green about as often as not. Sampling the whole window instead (tried, then reverted as out of scope here) measured the held value disappearing 0.80s and 1.27s after the failed scrape, against a window meant to be at least 6s. Declared flaky = true: the bug is constant, the observation of it is not. 2. The mock lane ran a flat 64 scenarios at once. Several drive a TUI under a pseudo-terminal and assert on wall-clock behaviour, and at 64-way on a 2-4 vCPU runner those steps were descheduled for seconds - measured at 4.7s against a 6s window - so they reported a starved reading as a product verdict. Bounded to the core count, clamped 2..=6, with E2E_MAX_CONCURRENT to override. No slower: starvation cost more than the concurrency bought (33s vs 54s when bisecting). dash_steps.rs is left exactly as main has it. Verified on the merged tree at the default bound and at a forced 32-way: 6 xfail, 2 flaky XPASS, 0 stale, no unexpected failures beyond the two container-only gaps that fail on clean main in the same image. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
serve-21 was deleting the shared CI download cache. Its comment claimed the cache directory is the scenario's own temporary one; that is true only on the mock lane. isolate_env points ROCM_CLI_CACHE_DIR at the persistent E2E_SHARED_CACHE_DIR whenever CI provides one, which every GPU lane does, and the scenario is @requires-os:linux so it runs there. uninstall then removed it, leaving every later scenario and every later run to re-download. Adds --keep-cache, which costs the scenario nothing: what it asserts lives under the data dir, which is still removed. E2E_MAX_CONCURRENT, added earlier this round, could override the GPU lanes' serialization. That 1 is a correctness requirement, not a tuning choice - two multi-GiB installs racing one runtimes tree, two serves racing one port and one GPU. Now applied after the override rather than inside its fallback. groups_in_command returned an empty list for any spelling other than a bare -G, and two empty lists compare equal: a reworded usermod would have made the EAI-8020 comparison pass while measuring nothing, and turned that row stale. Reads -G, --groups and bundled -aG, and both callers now reject an empty parse as a harness read failure. assert_named_groups_exist also guards the machine's group list, which machine_group_names swallows into an empty vec on a read error. Also: the EAI-7960 rows now say plainly that flaky = true costs them the XPASS deletion signal, so they are removed from the ticket rather than from this lane; the stop step requires exactly one service record instead of taking whichever read_dir yields first; the examine-14 guard reads the whole help paragraph rather than one wrapped line; and the held-address step records why it is safe only under serialization. Verified: clippy clean, 114 lib + 4 naming tests pass, mock lane 6 xfail, 2 flaky XPASS, 0 stale. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
assert_prompt_marked waited for the marker to appear anywhere on screen, then read the screen once and required it on a prompt line. The banner the CLI prints before handing over to bash already contains the marker, so the wait returned before bash had painted its prompt and the single read that followed landed in the gap, failing a shell that was simply still starting. Poll the real condition instead. The assertion is unchanged - a shell whose prompt is never marked still fails, at the timeout rather than instantly. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
|
All blocking findings from @siloteemu's review are addressed, and @rominf's points are covered by the same work. CI is green on BlockingStale rows / branch behind main. Merged current
Non-blocking
Two things this PR gained that were not in the original reviewTwo rows added for EAI-7960: the dashboard clears generation throughput on a failing scrape instead of holding it for the validity window. Sampling on CI put the held value gone after 804 ms and 1.27 s, well inside the window, so the hold logic is genuinely absent and main's single-look assertion passes only when it reads a stale frame. Both rows are Suite concurrency is now derived from Ready for re-review. |
|
🔴 Automated review · pr-review-watcher · a8803fe SummaryTest-only PR adding e2e cucumber scenarios (chat, diagnose, examine, model-serving, update) that pin CLI contracts a README walkthrough expects, plus the xfail rows for the ones the product does not yet honour — Approve. Verified: read the cited production source directly and confirmed every load-bearing claim the new comments make — Prior objection (a guard that could not fail for the defect its comment named, because the scenario runs against a machine with nothing installed): FIXED. 🚫 Blocking (must fix before merge)None. Non-blocking
|
All three blocking findings are fixed at de41663, so withdrawing this change request. Verified rather than taken on trust: the branch is now level with main and four stale rows were deleted with their scenarios kept as real guards (you found one more than I did); the dead helper is gone repo-wide; and the feature comment now matches the expectations file including the lane scope. The new concurrency work also checks out — deriving serialization from the hazards rather than the GPU probe alone, and placing the force-to-1 outside the override so neither the override nor the clamp floor can raise a serialized lane, is the correct ordering. Non-blocking notes are in the review comment; the one worth acting on is that the two new flaky dashboard rows gate nothing on the required lane, so the measured evidence for a non-flaky row should live on the ticket rather than only in a code comment.
…iable is_running() answered false on any try_wait() error. Its only caller asserts the product stopped the process, so that answered 'the product did its job' for a wait the harness could not perform - failing open on the defect the scenario exists to catch. Child caches the status once reaped, so a repeated call returns Ok(Some(_)) and no benign case is left to absorb; panic instead. update-02 only checked the preview was not refused as a usage error. Correct for the bug it pins, but once that row goes it becomes a permanent guard that a binary which accepted the preview and then performed the update would still pass. Assert the preview installed nothing, read back through the CLI's own report against this scenario's empty isolated data dir. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
|
Thanks — and thank you for re-verifying the three prior findings against the head rather than taking the response at face value. Two of the five non-blocking points were actionable and are now fixed in Fixed
Answered, not changedThe two Rows scoped
VerificationLinux container, on the pushed tree: Two mock-lane failures in that run — |
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · fac85e0
Summary
Test-only PR pinning the README-walkthrough contracts as cucumber scenarios and expectation rows. This round adds two remediation commits on the baseline I already looked at. Fix 1 holds up under adversarial checking — the sole caller is confirmed, there is no Drop path into it, EINTR is retried inside std and the status is cached after reap, so no benign error arm remains; cucumber's per-step catch_unwind plus intact Drop cleanup make panicking the right call here. The engine-shell poll change is sound and its assertion unchanged. The three items you answered without changing are all reasonably answered. Fix 2 is correctly isolated and dormant, but it is not discriminating for the defect its comment names — details below. Blocking: 1 · Non-blocking: 4.
🚫 Blocking (must fix before merge)
-
tests/e2e-cucumber/tests/e2e/update_steps.rs:100-126(step) withtests/e2e-cucumber/features/update.feature:30— the new guard cannot fail for the defect its comment claims to catch. The comment says it would catch "a binary that accepted the preview and then performed the update". On this scenario's fixture that outcome is unreachable regardless of whether the product is correct:rocm update --applyroutes throughapply_runtime_update→select_runtime_update_source(apps/rocm/src/main.rs:15824,15939-15942), which bails with "no managed runtimes are registered" on an empty registry — update upgrades an existing managed runtime, it never installs a first one. update-02 has noGiven, never callsuse_shared_runtimes(), so it always runs against an emptydata/runtimes. "managed runtimes: none" is therefore guaranteed by the fixture, not by the product's dry-run honesty. And if the fix being anticipated simply dropsrequires = "apply"fromdry_run(apps/rocm/src/main.rs:308-309),--dry-runalone falls into the report-onlyelsebranch (main.rs:1806-1889) and cannot install either.So round 1's finding — a guard that stops discriminating once the bug it pins is fixed — is answered here with a second guard that never discriminated at all, and a durable comment asserting otherwise will mislead the next maintainer. Either:
- (a) keep the assertion, but replace the comment's claim with what it actually guards (the readback path stays clean on an empty machine) and note that the real preview-does-not-mutate contract needs a managed-runtime fixture; or
- (b) move the guard onto a scenario that opts into a managed runtime, where a performed update is observable.
(a) is comment-only and closes the misleading claim at zero risk. I would take that and leave (b) for whenever a managed-runtime fixture exists.
Non-blocking
tests/e2e-cucumber/tests/e2e.rs:145-152— fix 1's comment explains whyfalsewas wrong but not whypanic!is safe. One clause — never reached fromDrop, and cucumber catches a step panic per scenario — would stop the next reader re-litigating abort-during-unwind. That was my first concern too, and it took a while to discharge.tests/e2e-cucumber/expectations.toml:365-380— the dash-08/09 note prescribes sampling as the durable fix without recording that sampling was tried this round, measured the hold vanishing 0.8 s / 1.3 s into a 6 s window, and was reverted in 88f46e3 as out of scope. A reader of the file alone loses that evidence; one sentence pointing at the finding would carry it.tests/e2e-cucumber/tests/e2e.rs:1426-1427— accepted. It now applies the1after theE2E_MAX_CONCURRENToverride, so the override can no longer raise a serialized lane. Fail-open would need a GPU lane whose probe reads false and neither shared dir exported; the documented exclusion ofshared_uv_cache_diris consistent, since uv is content-addressed and self-locking.tests/e2e-cucumber/tests/e2e/engines_steps.rs:111-127— the hand-rolled deadline loop matches four sibling loops in this tree, so it is convention rather than a novel pattern. Await_for_screen_where(predicate, timeout)onTuiSessionwould DRY all five if the pattern grows again.
Verification notes: one cheap targeted check was run (cargo fmt --check -p e2e-cucumber, clean); per policy the full and e2e suites were not re-run locally, since CI covers hardware this environment does not have. CI at this head was 23 success, 1 skipped, 1 pending, 0 failures at fetch time — still in flight, so not yet evidence of success, and no outcome is attributed to any named job.
On the round-1 item you pushed back on: your reading was right and stronger than mine. A helper reporting "not running" when the check itself errored means a failure-to-observe reads as "the product did its job", for exactly the defect the scenario exists to catch. Removing the arm rather than narrowing it is the correct call.
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · fac85e0
Summary
Test-only PR pinning the README-walkthrough contracts as cucumber scenarios and expectation rows. This round adds two remediation commits on the baseline I already looked at. Fix 1 holds up under adversarial checking — the sole caller is confirmed, there is no Drop path into it, EINTR is retried inside std and the status is cached after reap, so no benign error arm remains; cucumber's per-step catch_unwind plus intact Drop cleanup make panicking the right call here. The engine-shell poll change is sound and its assertion unchanged. The three items you answered without changing are all reasonably answered. Fix 2 is correctly isolated and dormant, but it is not discriminating for the defect its comment names — details below. Blocking: 1 · Non-blocking: 4.
🚫 Blocking (must fix before merge)
-
tests/e2e-cucumber/tests/e2e/update_steps.rs:100-126(step) withtests/e2e-cucumber/features/update.feature:30— the new guard cannot fail for the defect its comment claims to catch. The comment says it would catch "a binary that accepted the preview and then performed the update". On this scenario's fixture that outcome is unreachable regardless of whether the product is correct:rocm update --applyroutes throughapply_runtime_update→select_runtime_update_source(apps/rocm/src/main.rs:15824,15939-15942), which bails with "no managed runtimes are registered" on an empty registry — update upgrades an existing managed runtime, it never installs a first one. update-02 has noGiven, never callsuse_shared_runtimes(), so it always runs against an emptydata/runtimes. "managed runtimes: none" is therefore guaranteed by the fixture, not by the product's dry-run honesty. And if the fix being anticipated simply dropsrequires = "apply"fromdry_run(apps/rocm/src/main.rs:308-309),--dry-runalone falls into the report-onlyelsebranch (main.rs:1806-1889) and cannot install either.So round 1's finding — a guard that stops discriminating once the bug it pins is fixed — is answered here with a second guard that never discriminated at all, and a durable comment asserting otherwise will mislead the next maintainer. Either:
- (a) keep the assertion, but replace the comment's claim with what it actually guards (the readback path stays clean on an empty machine) and note that the real preview-does-not-mutate contract needs a managed-runtime fixture; or
- (b) move the guard onto a scenario that opts into a managed runtime, where a performed update is observable.
(a) is comment-only and closes the misleading claim at zero risk. I would take that and leave (b) for whenever a managed-runtime fixture exists.
Non-blocking
tests/e2e-cucumber/tests/e2e.rs:145-152— fix 1's comment explains whyfalsewas wrong but not whypanic!is safe. One clause — never reached fromDrop, and cucumber catches a step panic per scenario — would stop the next reader re-litigating abort-during-unwind. That was my first concern too, and it took a while to discharge.tests/e2e-cucumber/expectations.toml:365-380— the dash-08/09 note prescribes sampling as the durable fix without recording that sampling was tried this round, measured the hold vanishing 0.8 s / 1.3 s into a 6 s window, and was reverted in 88f46e3 as out of scope. A reader of the file alone loses that evidence; one sentence pointing at the finding would carry it.tests/e2e-cucumber/tests/e2e.rs:1426-1427— accepted. It now applies the1after theE2E_MAX_CONCURRENToverride, so the override can no longer raise a serialized lane. Fail-open would need a GPU lane whose probe reads false and neither shared dir exported; the documented exclusion ofshared_uv_cache_diris consistent, since uv is content-addressed and self-locking.tests/e2e-cucumber/tests/e2e/engines_steps.rs:111-127— the hand-rolled deadline loop matches four sibling loops in this tree, so it is convention rather than a novel pattern. Await_for_screen_where(predicate, timeout)onTuiSessionwould DRY all five if the pattern grows again.
Verification notes: one cheap targeted check was run (cargo fmt --check -p e2e-cucumber, clean); per policy the full and e2e suites were not re-run locally, since CI covers hardware this environment does not have. CI at this head was 23 success, 1 skipped, 1 pending, 0 failures at fetch time — still in flight, so not yet evidence of success, and no outcome is attributed to any named job.
On the round-1 item you pushed back on: your reading was right and stronger than mine. A helper reporting "not running" when the check itself errored means a failure-to-observe reads as "the product did its job", for exactly the defect the scenario exists to catch. Removing the arm rather than narrowing it is the correct call.
Duplicate — this review was submitted twice by our tooling. The identical review below it is the live one; nothing here is withdrawn.
The step added last round claimed to guard against a preview that performed the update. It cannot: on an empty registry `--apply` bails in select_runtime_update_source (main.rs:15939) because update upgrades a managed runtime and never installs a first one, so 'manages none' is guaranteed by the fixture rather than by the product. A comment asserting otherwise would mislead the next reader. Renamed to say what it does hold - the readback path stays clean - and record why the real non-mutation contract is not proven here: it needs a managed runtime, and with one present the report resolves the latest version per runtime (therock.rs:810) and reaches the TheRock index, which the mock lane has no network for. Tracked on EAI-8010. Also note why panicking in is_running is safe (no Drop path, cucumber catches per step), and cite the reverted sampling commit in the EAI-7960 note. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
|
Blocking finding accepted — and it's the right catch. I verified it rather than taking it on trust: I took (a), with one addition you didn't ask for: the Gherkin line overclaimed too. On why (b) is not taken, with a sharper reason than "no fixture exists": a managed runtime would make the scenario network-dependent. With a manifest present, Non-blocking
VerificationLinux container on the pushed tree: Two failures in that run, One process note: this review arrived twice, as |
Withdrawing this change request: the objection is resolved at a8803fe. The step that could not fail for the defect its comment named has been rescoped to the readback path it can actually pin, and the comment now states plainly which contract it does not prove and why. The companion is_running fix is sound as well. Full round posted as a comment. You were right to push back on the wording; the underlying objection held and you addressed it directly.
Walking the public README end to end on an MI300X pod turned up thirteen places
where the CLI does not do what the README leads a reader to expect. This adds
end-to-end coverage for them. It is test-only — no product behaviour changes.
Each scenario asserts the behaviour the README promises and is registered as an
expected failure in
expectations.toml, citing its ticket. CI stays green, andthe day a defect is fixed its row becomes a stale XPASS and deletes itself —
except for rows marked
flaky, which tolerate either outcome and so must bedeleted from their ticket rather than by the lane.
Pinned as expected failures
rocm update --dry-runis refused as a usage error, though the README documents--applyand--dry-runas independentFour became guards during review
mainmoved while this was open and fixed the bugs behind four rows: the twoEAI-8011 help defects, EAI-8019 (
--deviceis now aValueEnumwithcpu_onlyhidden, so it is no longer advertised) and EAI-8015 (the
UNKNOWNsentinel isfiltered before the remedy is built). Leaving those rows would have XPASSed and
failed the required lane. Each row is deleted and its scenario kept, so it now
guards the fix instead of pinning the bug. Verified by running a binary built
from the merged tree, not by reading the diff.
Two rows added for EAI-7960
dash-gen-tps-held-after-scrape-failureanddash-gen-tps-expiry-boundarypina dashboard defect: generation throughput is cleared on a failing scrape instead
of being held for its validity window. Measured on CI, the held value is gone
after 804 ms and 1.27 s, well inside the window — so the hold logic is genuinely
absent, and main's single-look assertion passes only when it happens to read a
stale frame. Both rows are
flaky = truebecause the outcome depends on whichframe the reader lands on; delete them when EAI-7960 closes, tracked from the
ticket rather than from this lane.
Which lanes actually gate
Six of the new scenarios run on the required no-GPU
E2E testsjob. The rest are@merge-queueor@requires-gpu @requires-bare-metaland land on the merge queueand self-hosted GPU lanes, the latter marked non-blocking in
ci.yml. Thecoverage above should be read with that split in mind.
Suite concurrency
The mock lane now derives its scenario concurrency from
available_parallelism()clamped to 2..=6, overridable withE2E_MAX_CONCURRENT,and is forced to 1 whenever a GPU or a shared cache/runtimes directory is in play
— those lanes cannot safely run two installs or two serves at once, so the bound
is applied after the override rather than inside it. A 64-way default was
starving scenarios into their own timeouts: an 8-way run starved on a 10-core
host while a 4-way run was both green and faster (33 s vs 54 s).
Two ship as guards, with no expected-failure row
EAI-8007 (a stop that worked reporting it stopped nothing) and EAI-8006
(
--gpu autoselecting none) were reproduced on the pod but hold correctly onevery lane available here — EAI-8007 against both a plain registered process and
a real vLLM serve, EAI-8006 because the runner's
amd-smiexecutes where thepod's bundled one could not. A row would be a stale XPASS and would fail the
lane, so their scenarios ship as guards for the contract instead. Both tickets
keep the pod observation.
Two are not covered, deliberately
EAI-8017 and EAI-8018 both need a working Lemonade install, and Lemonade's
launcher cannot start on either lane: on the MI300X runner it wants GLIBC_2.38
and GLIBCXX_3.4.32, which that OS does not have, and in a plain container it
wants
libdrm_amdgpu.so.1. Each dies before reaching the behaviour under test,so scenarios for them passed while proving nothing. They were removed rather
than shipped as false coverage, and stay on their tickets.
Harness additions
Confined to the test crate: removing environment variables from the child (the
existing helper only sets them),
statandidshims scoped to one device pathso a device-group state can be arranged rather than hoped for, and a child
process a scenario owns and registers as a managed service so it can ask whether
the product stopped it — asserted before teardown, which would otherwise clean up
after the product and hide the defect.
Two caveats worth stating. A substituted
statanswer proves how the CLI handlesan unnameable group, not that a given host produces one; the provenance for that
is the pod. And the device-group scenarios need a real device node, so they run
on the GPU lane only.
Test plan
unexpected failures.
e2e-selfhosted, no name filter, so expectations resolvenormally): 64 scenarios, 16 xfail, 0 XPASS, 0 unexpected failures.
row states, rather than because a step is miswired.
cargo clippy --workspace --all-targets -D warnings, the workspace tests, andcargo test -p e2e-cucumber --libpass in a Linux container. So doescargo clippy -p e2e-cucumber --test e2e --all-features -- -D warnings, whichthe local container gate skips but CI runs.
every_expectation_row_names_a_scenario_that_exists, fails therun on an
expectations.tomlrow whose id matches no scenario — nothing caughtthat orphan case before.
Four rows were corrected from what the GPU dispatches measured rather than left
as first written, which is why two became guards and two scenarios were dropped.