Skip to content

test: add BDD E2E tests with cucumber-rs#69

Open
fredespi wants to merge 51 commits into
mainfrom
test/add-e2e-robot-framework
Open

test: add BDD E2E tests with cucumber-rs#69
fredespi wants to merge 51 commits into
mainfrom
test/add-e2e-robot-framework

Conversation

@fredespi

@fredespi fredespi commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a behavioral end-to-end test layer using cucumber-rs — standard Gherkin .feature files backed by Rust step functions. Tests exercise the full user journey (install → examine → configure → serve → detect → chat) to catch cross-component integration failures that unit tests and smoke scripts miss.

Why cucumber-rs over Robot Framework / plain Rust tests

This PR started with Robot Framework, then switched to cucumber-rs after team discussion. The reasoning:

  • BDD earns its keep regardless of audience. The value isn't non-engineers reading scenarios — it's tests that describe user-facing behavior independently of implementation. "Short model names are expanded to their full name" tells you what's broken without reading code. assert_cmd/insta couples to output format and breaks on cosmetic changes.
  • Already proven. The 6 bugs at component seams were found by thinking in user journeys, not by reading code. That's what BDD forces you to do.
  • One toolchain. Rust step functions compile with the project. No Python dependency, no separate CI setup. Addresses the concern from EAI-7164 about consolidating on Rust.
  • Not two stacks. The xtask acceptance path handles install/upgrade lifecycle (imperative, infrastructure-level). Cucumber handles user journey scenarios (declarative, behavioral). Different concerns, one toolchain.
  • Real Gherkin. cucumber-rs uses standard Gherkin with Scenario Outline, Background, Rule, and @tags — not Robot Framework's cosmetic prefix stripping. The .feature file is the executable test, not a rewrite of a separate spec.
  • Reporting included. HTML report (comparable to Robot Framework's), JUnit XML for GitHub Actions, and Cucumber JSON — all generated automatically.

What's included

  • 20 BDD scenarios across 4 features: examine, runtime setup, model serving, chat/detection
  • Gherkin .feature files — the spec is the test
  • Rust step functions — organized by feature area (chat_steps.rs, examine_steps.rs, serving_steps.rs, runtime_steps.rs)
  • Mock OpenAI server — axum-based, OS-assigned ports (no port conflicts)
  • HTML report — summary, statistics by tag/feature, expandable scenario details with step-by-step results
  • Environment isolation — each scenario gets isolated config/data/cache directories
  • CI job on hosted runners (every PR)

Expected-failure pattern

Scenarios that exercise known bugs are tagged @expected-failure-EAI-XXXX. On CI without GPU hardware, these scenarios fail and produce clear failure messages. On GPU hardware, they exercise the actual bug. When a bug is fixed, the test passes — signaling the tag can be removed.

Tag Scenarios Bug
@expected-failure-EAI-7218 model_serving 5 PyTorch engine dependency resolution
@expected-failure-EAI-7219 model_serving 1, 2, 6 Short model names not expanded
@expected-failure-EAI-7220 model_serving 3, 4; chat 2, 4 Service registry / default port not in detection
@expected-failure-EAI-7221 chat 6 TUI hardcodes model name instead of querying endpoint
@expected-failure-EAI-7223 chat 5 vLLM rejects tool-calling requests

Each Jira ticket has a comment linking back to its test scenario(s).

Running tests

# All scenarios:
bash tests/e2e-cucumber/run.sh

# Via cargo directly:
ROCM_CLI_BINARY=./target/release/rocm cargo test -p e2e-cucumber --test e2e

# Filter by name:
cargo test -p e2e-cucumber --test e2e -- -n "short model"

# Skip known bugs:
cargo test -p e2e-cucumber --test e2e -- -t "not @expected-failure-EAI-7219"

Test plan

  • All 20 scenarios compile and run
  • Mock-tier scenarios pass locally
  • Expected-failure scenarios fail with clear messages
  • HTML report generated with summary, statistics, and step details
  • JUnit XML generated for GitHub Actions
  • CI job passes on this PR

@fredespi fredespi force-pushed the test/add-e2e-robot-framework branch from 9c0732e to a4cc8eb Compare June 30, 2026 11:59
@fredespi fredespi marked this pull request as draft June 30, 2026 12:41
@fredespi fredespi force-pushed the test/add-e2e-robot-framework branch from 68b4c94 to cd83918 Compare June 30, 2026 13:37
@fredespi fredespi marked this pull request as ready for review June 30, 2026 14:28
@fredespi fredespi changed the title test: add Robot Framework E2E test framework test: add BDD E2E tests with cucumber-rs Jul 1, 2026
@fredespi fredespi force-pushed the test/add-e2e-robot-framework branch from 5caff76 to ffab5e2 Compare July 1, 2026 12:50

@rominf rominf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to see a real behavioral layer going in — the feature files read very cleanly. A few things before this is ready, one of which is functional and important.

🔴 Blocking — the suite can't currently fail CI

tests/e2e.rs uses the builder form E2eWorld::cucumber()....run(...).await; and discards the result. In cucumber 0.23 that path records failures into the writers but does not set a non-zero exit code — only run_and_exit() (or World::run()) does. So cargo test -p e2e-cucumber --test e2e exits 0 even when scenarios fail, and the e2e job goes green regardless of results. That undercuts the PR's premise ("as bugs are fixed, more tests pass") — right now the job wouldn't report either way.

Fix: capture the returned writer and std::process::exit(1) on failed_steps() + parsing_errors() + failed_hooks() > 0 (after the HTML report is generated so the artifact still uploads), or switch to run_and_exit().

Related, same theme:

  • @expected-failure-* tags are inert in CI. They only take effect when a human passes -t "not @expected-failure-*"; neither the e2e nor e2e-gpu job passes any filter, so tagged scenarios run like any other. Once the exit-code issue above is fixed, every known-bug scenario would turn the job red — there's no xfail/skip mechanism wired up. Needs a real design (dedicated non-blocking job for the tagged set, or genuine skip-on-tag handling).
  • A couple of scenarios don't exercise rocm. e.g. "Chat requests use the model name reported by the endpoint" — the assertion checks what the test's own helper sent to the mock (it reads the model from /models and echoes it back); the rocm binary is never invoked, so it can't fail for a product reason. The mock-based services list scenarios assert the registry contains a mock that's never registered with the CLI, so they fail today for the wrong reason and won't flip to passing when the underlying bug is fixed. Worth routing these through the actual binary or dropping them.

🟠 Fit the project

  • run.sh → an xtask subcommand. We already standardize dev/CI tasks in xtask (signing, manifest, tpn, powershell-lint…). A bash wrapper is off-convention and bash-only in a repo that also builds/tests on Windows, and the build step is now duplicated between run.sh and the CI job. A cargo xtask e2e [args] used by both would be cross-platform and single-source.
  • src/report.rs (≈400 lines of hand-built HTML) — do we need it? The crate already emits JUnit XML (rendered natively in the GitHub Actions checks UI) and Cucumber JSON (consumable by standard HTML reporters). The custom generator duplicates that, and it currently miscounts undefined/ambiguous steps as passed and prints a placeholder -xx-xx date. I'd lean toward dropping it in favor of the two standard outputs; if we want an HTML artifact in-tree, a compile-checked template (maud/askama) beats format! soup.

🟡 Should-fix

  • Per-scenario isolation isn't isolated (e2e.rs): the temp root is keyed only on PID, so all concurrent scenarios share one config/data/cache tree, and the first scenario's Drop remove_dir_alls it out from under the others (risking a fall-back to the real ~/.rocm). Add a per-World unique suffix.
  • reqwest::blocking inside async steps (serving_steps.rs) panics under the Tokio runtime ("cannot start a runtime from within a runtime"). GPU-tier only, but it hard-panics instead of waiting — use an async poll loop.
  • The new crate gets no clippy (excluded from both the clippy job and the pre-commit hook). Add cargo clippy -p e2e-cucumber --all-targets -- -D warnings to the e2e job.
  • .feature files aren't in the heavy path filter, so a feature-only change skips the e2e job. Add **/*.feature.

🔵 Nit

  • @expected-failure-EAI-XXXX puts internal Jira IDs in files contributors read/type. Non-blocking and I know it's under discussion — flagging only so it's tracked; GitHub issue numbers would keep the same test↔bug link.

@fredespi fredespi force-pushed the test/add-e2e-robot-framework branch from e3a2c13 to 6741054 Compare July 8, 2026 15:08
@fredespi

fredespi commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review — all points addressed. Rebased on main and pushed as one commit (6741054). Summary of what changed:

🔴 Blocking

  • Suite can now fail CI. The runner captures the summarized writer and process::exit(1) on failed_steps + parsing_errors + hook_errors (after the HTML report is generated so the artifact still uploads). Verified: the blocking selection exits 0 when green and 1 when a scenario fails.
  • @expected-failure tags are now real. Cucumber tag filters are exact-match (no globbing), so each known-bug scenario carries a bare @expected-failure (for filtering) plus its @expected-failure-EAI-NNNN (for traceability). CI now runs three selections:
    • e2e (blocking): not @gpu and not @expected-failure
    • e2e-known-bugs (non-blocking): @expected-failure and not @gpu
    • e2e-gpu (non-blocking): @gpu
  • Scenarios now exercise rocm. Mock-based scenarios plant a managed-service record (plain JSON matching the on-disk schema, no crate import) into the isolated services dir, so rocm services list and the local chat provider actually discover the mock. The scenarios that were mislabeled EAI-7220 are retagged/untagged — EAI-7220 is a TUI-only wrong-port bug unrelated to services list, and its real surface can't be driven by the non-interactive CLI (noted below). The mock-only "chat model name" scenario that only tested the helper was dropped.

🟠 Fit the project

  • run.shcargo xtask e2e. New xtask subcommand builds the release binary + runs the suite (forwarding cucumber args), used by both CI and local dev. Cross-platform, single-source; run.sh removed.
  • report.rs kept, bugs fixed. Rather than drop it, fixed the two concrete issues: undefined/ambiguous steps now count as failures (not passes), and the -xx-xx placeholder is replaced with a real UTC timestamp. Happy to revisit dropping it in favor of the JUnit/JSON outputs as a follow-up if you'd prefer.

🟡 Should-fix

  • Per-scenario isolation: temp root now has a per-World unique suffix (was PID-only, so scenarios shared one tree and Drop raced).
  • reqwest::blocking → async poll loop in the serving steps (dropped the now-unused blocking feature).
  • Clippy on the crate: added cargo clippy -p e2e-cucumber --all-targets -- -D warnings to the e2e job; fixed the warnings it surfaced.
  • .feature in the heavy path filter so feature-only changes trigger the job.

🔵 Nit

  • Kept the EAI-XXXX IDs for now since it's under discussion — happy to switch to GitHub issue numbers if we land on that.

Also, while rebasing

  • Engine set: main now supports only lemonade/vllm (Limit serving engines to Lemonade and vLLM #79), so I updated the engines-list assertion, dropped the PyTorch scenario (EAI-7218 is Won't Fix and the engine is gone), and compare expansion across the supported engines.
  • Restored the mock/gpu split that the cucumber rewrite had lost: hardware-dependent scenarios (install sdk, real serve, GPU detect) are tagged @gpu so the mock-tier job stays green on ubuntu-latest.

The full workspace test suite and the mock-tier e2e selection pass on Linux.

Comment thread tests/e2e-cucumber/tests/e2e.rs Fixed
Comment thread tests/e2e-cucumber/tests/e2e.rs Fixed
Comment thread tests/e2e-cucumber/tests/e2e.rs Fixed
@fredespi fredespi force-pushed the test/add-e2e-robot-framework branch from 6741054 to 1ea8909 Compare July 8, 2026 16:47
@fredespi fredespi closed this Jul 9, 2026
@fredespi fredespi deleted the test/add-e2e-robot-framework branch July 9, 2026 13:34
@fredespi fredespi restored the test/add-e2e-robot-framework branch July 9, 2026 13:35
@fredespi fredespi reopened this Jul 9, 2026
Comment thread tests/e2e-cucumber/tests/e2e.rs Fixed
Comment thread tests/e2e-cucumber/tests/e2e.rs Fixed
@fredespi fredespi force-pushed the test/add-e2e-robot-framework branch from a6216e8 to 55b3aec Compare July 9, 2026 16:54
fredespi added 12 commits July 10, 2026 16:12
Behavioral end-to-end suite exercising the real `rocm` binary through its CLI
and HTTP endpoints (black-box, no crate imports). Gherkin .feature files backed
by Rust step functions, across chat, model serving, examine, and runtime setup.

The suite gates CI: the runner exits non-zero on any failed step, parsing
error, or hook error. Three selections via `cargo xtask e2e`: a blocking
mock-tier job (`not @gpu and not @expected-failure`), a non-blocking known-bugs
job (`@expected-failure`), and a non-blocking GPU job (`@gpu`) on self-hosted
hardware. Known-bug scenarios carry a bare `@expected-failure` tag plus an
`@expected-failure-EAI-NNNN` tag for traceability.

Mock-based scenarios plant a managed-service record (plain JSON matching the
on-disk schema) so `rocm services list` and the local chat provider discover
the mock, exercising the real binary rather than the test's own helper. The
HTML report is rendered with maud (compile-checked, auto-escaped markup); its
stats/status/date logic is unit-tested.

Adds a `cargo xtask e2e` subcommand as the cross-platform entry point.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
…setup

CodeQL flags std::env::temp_dir() as a tainted path source in the E2E World
setup. This is test-only code and every path segment joined onto the OS temp
dir is program-controlled (pid, an atomic counter, fixed names), so there is no
attacker-controlled traversal. Add inline codeql[rust/path-injection]
suppressions with justification.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Replaces the manual std::env::temp_dir() + create_dir_all + Drop cleanup with a
tempfile::TempDir. Each World gets a unique isolated config/data/cache root that
auto-removes on drop. This also resolves the CodeQL rust/path-injection findings:
the OS temp-dir lookup now happens inside the tempfile crate rather than our
source, so there is no tainted env value flowing into a path in this code.
(Inline codeql[...] suppressions were ineffective under this repo's CodeQL
setup; breaking the taint flow is the reliable fix.)

tempfile was already in the dependency tree; now a direct dev-facing dep.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Clippy's single_match fires on the ROCM_CLI_BINARY match; rewrite as
if-let/else. No behavior change.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Three fixes so the E2E suite passes the checks it only started exercising once
clippy went green (the heavy jobs were previously skipped behind the lint gate):

- Exclude the cucumber `e2e` integration test from the default test set
  (`test = false`). It uses a custom harness (`harness = false`) and is meant to
  run only via `cargo xtask e2e` on Linux. Nextest could not `--list` the custom
  harness (breaking "Test (affected crates)"), and the Windows job's
  `cargo test --all-targets` ran every scenario unfiltered with no mock, doing
  real installs/GPU probes (10/17 failures). `xtask e2e`'s explicit `--test e2e`
  still runs it.

- Add xfail inversion for the known-bugs job. `cargo xtask e2e --expect-failures`
  sets E2E_EXPECT_FAILURES; the harness then treats an @expected-failure scenario
  failing as the expected (green) outcome and fails only on XPASS (a known bug
  now passes -> drop its tag), an untagged failure, or a parse/hook error. The
  job drops continue-on-error so it genuinely gates instead of showing a
  permanently-red, ignored check. Adds report::evaluate_xfail + unit tests.

- Harden the cross-engine expansion check. It used unwrap_or_default(), so two
  engines that both errored before printing "resolved model:" compared "" == ""
  and passed vacuously; now a missing line fails loudly, mirroring the
  full-model-name step.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
The GPU job ran `-t "@gpu"` with normal (non-inverted) semantics, so it swept
up both hardware scenarios expected to pass and `@gpu @expected-failure`
known-bug scenarios. Any unfixed known bug made the whole job red, so a red
result couldn't distinguish a real GPU regression from a known bug still
reproducing — and a fixed known bug (XPASS) went unnoticed.

Mirror the mock tier's split:
- e2e-gpu now runs `@gpu and not @expected-failure` (expect-pass only), so a
  red here always means a real GPU regression.
- e2e-gpu-known-bugs runs `@gpu and @expected-failure` under `--expect-failures`
  (xfail inversion): green while each known GPU bug still fails, red only on
  XPASS (bug fixed -> drop the tag), an untagged failure, or a parse/hook error.

Both remain non-blocking (continue-on-error) given the ephemeral GPU runner.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Adds GPU e2e coverage on the two Strix Halo (gfx1151) self-hosted runners
alongside the existing amd-gpu runner, each with the same expect-pass /
known-bugs split:

- e2e-gpu-strix-ubuntu(-known-bugs): Linux gfx1151, a second AMD arch for the
  @gpu scenarios. Targeted by [self-hosted, linux, strix-halo].
- e2e-gpu-strix-windows(-known-bugs): first real Windows GPU coverage — the
  existing windows-build-and-test runs on GitHub-hosted windows-latest with no
  GPU, so @gpu scenarios have never run on Windows hardware. Targeted by
  [self-hosted, windows, strix-halo], runs via pwsh.

Label selectors route each job to exactly one runner (strix-halo vs amd-gpu,
and linux vs windows), so there is no cross-routing. All non-blocking
(continue-on-error) while the new hardware is proven out.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Each E2E job (4 environments x expect-pass/known-bugs = 8) uploaded its own
report artifact, with no single view across platforms and no platform label on
any report. Add a consolidated report published once per CI run.

- Extract report generation into a lean `e2e-report` crate (only maud + serde),
  moved from e2e-cucumber's report.rs. This lets xtask reuse it without pulling
  the harness's cucumber/axum/reqwest/tokio tree. e2e-cucumber re-exports it as
  `report` so existing call sites are unchanged; its 10 unit tests move along.

- Add generate_consolidated() + consolidated_summary_markdown() to e2e-report:
  a platform x tier matrix (total/pass/fail/skip/xfail/status) plus collapsible
  per-platform details, with known-bugs tiers evaluated under xfail inversion
  (green while bugs reproduce; a platform is flagged on XPASS or regression).

- Add `cargo xtask e2e-report --artifacts-dir <d> --html-out <f>`: auto-discovers
  platforms by scanning artifact subdirs for report.json, derives labels from
  the artifact names, writes the merged HTML, and prints the summary matrix to
  stdout for $GITHUB_STEP_SUMMARY.

- Add the e2e-report CI job: downloads all `*-report` artifacts (glob, so new
  platforms auto-appear in the report), runs the aggregator into the step
  summary, and uploads one e2e-consolidated-report HTML. `if: always()` so a
  non-blocking/failing platform still shows up.

Adding a platform needs no aggregator change beyond appending its job names to
the report job's `needs:` (ordering only; Actions has no wildcard needs).

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
…ngine + readiness

Make serve-dependent scenarios fail only for the behavior they test, and add
coverage for engine selection and readiness.

- Isolate serve preconditions: the shared GPU serve setup now uses the canonical
  model id and an explicit engine, so scenarios that test inference/chat no
  longer fail on the vLLM alias-resolution bug. The `qwen2.5` alias now appears
  only in the two scenarios that test alias resolution.

- Make the serve readiness timeout configurable via E2E_SERVE_TIMEOUT_SECS
  (default 600s) for real-hardware first-serve on a cold cache.

- Strengthen the auto-engine assertion to parse the chosen engine and require it
  to be one of the supported backends (lemonade/vllm), catching selection of a
  removed engine.

- Add explicit lemonade serve+inference coverage (GGUF model), tagged
  expected-failure (EAI-7052: lemonade falls back to its Vulkan backend on
  Instinct and inference stalls until it uses system ROCm).

- Add a scenario asserting a service the CLI reports ready can immediately serve
  inference (readiness contract), and a rocm-core unit test pinning that the
  /v1/models signal alone does not imply inference-readiness (EAI-7333).

- Add a scenario and rocm-core unit tests asserting vLLM is the default serving
  engine on Instinct data-center GPUs (gfx*-dcgpu) for vLLM-capable models.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
send_chat used a reqwest client with no timeout, so a hung backend (e.g. a
model that never returns a completion) blocked the HTTP call indefinitely and
let a scenario run until the CI job limit. Build the client with a 10s timeout
(override via E2E_INFERENCE_TIMEOUT_SECS) so a stalled inference fails promptly
instead of stretching the job to tens of minutes.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
The self-hosted Strix Halo Ubuntu runner has a full, non-persistent root disk,
so setup-rust-toolchain's rustup bootstrap fails writing rustup-init to /tmp
(curl exit 23). Point CARGO_HOME, RUSTUP_HOME, and TMPDIR at the persistent
actions-runner directory and pre-create the temp dir, so the toolchain installs
successfully and is reused by later jobs.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Add a workflow_dispatch trigger with platform and tier inputs so the E2E jobs
can be run on demand against this ref (dispatch skips build-and-test for a fast
loop; each E2E job builds its own binary via `cargo xtask e2e`, so the skip is
safe). Every E2E job's guard is extended to honor the inputs on dispatch while
preserving the exact push/PR behavior otherwise, and the consolidated report
runs on dispatch too.

Also bootstrap Rust on the Strix Halo Windows runner with a PowerShell step:
setup-rust-toolchain runs an internal bash script the runner lacks, so install
rustup via win.rustup.rs (--default-toolchain none; rust-toolchain.toml pins the
version). Idempotent, so it no-ops once the runner is provisioned.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
@fredespi fredespi force-pushed the test/add-e2e-robot-framework branch from 96b8bbb to 0d5645e Compare July 10, 2026 14:16
fredespi added 18 commits July 11, 2026 15:55
…ed serve port

On the single serial Strix Windows runner a serve leaked from a prior scenario
kept answering on the shared port 11435; because scenarios run in isolated data
dirs, the next scenario's rocm has no record of that managed service and can't
stop it. The old readiness check only required /v1/models to return 200, so the
inference scenario proceeded against the WRONG model (got Qwen3-0.6B-Q4_0.gguf
from the lemonade scenario while serving Qwen2.5-1.5B), failing "the response
identifies the correct model".

Make serve readiness model-aware: wait until /v1/models actually lists the model
this scenario served (distinctive substring) before proceeding, so a stale
endpoint no longer satisfies readiness. wait_for_endpoint keeps its old
any-200 behaviour for the mock scenarios.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
…e-envs

Sharing <data>/runtimes across scenarios regressed the app-dev GPU tier (4/7 →
1/7): the runtimes registry is state the suite asserts on — "Installing the SDK"
requires `a machine with no CLI-managed runtimes`, which a shared registry
populated by other scenarios breaks, cascading into serve failures. Engine envs
are similarly state-adjacent.

Narrow the sharing to genuinely state-free, content-addressed caches only: HF
model weights (HF_HOME) and the pip cache. Drop the runtimes symlink and
ROCM_CLI_ENGINE_ENVS_ROOT. Runtimes re-install per scenario into the isolated
data root (on the nvme via TMPDIR on Strix), trading some dedup for correctness.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Two robustness fixes exposed by an offline self-hosted runner:

- Concurrency: a workflow_dispatch run whose job is queued on an offline runner
  cannot be cancelled by GitHub, so under the shared per-ref concurrency group it
  deadlocked — blocking every later dispatch from starting. Give manual dispatches
  a unique group (github.run_id); push/PR/merge_group keep the shared per-ref group
  so new commits still supersede in-flight runs.

- timeout-minutes: 15 on all E2E jobs, so a hung/slow job self-fails at the 15-min
  ceiling instead of running for hours — also enforces the per-platform time budget.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
The app-dev GPU expect-pass tier timed out at 15 min because a lemonade Vulkan
llama-server leaked by an earlier run (whose Drop teardown never ran after a
kill/timeout) kept spinning on the GPU at ~96% CPU, starving this run's vLLM
serves. The suite itself never serves that model, confirming it was an orphan.

Add a best-effort pre-job step to every self-hosted GPU job that kills stray E2E
serve processes (scoped to /tmp/rocm-e2e-*, e2e-target, e2e-shared — never the
runner or /workload manual processes) and clears leftover temp roots, so each job
starts with a free GPU. bash on Linux, PowerShell on Windows.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
GPU expect-pass timed out because scenarios share port 11435 on one serial box
with per-scenario isolated data dirs: a prior scenario's managed serve detaches
and its record lives in a different dir, so the next scenario's rocm can't stop
it. Multiple vLLM servers then accumulated, oversubscribing GPU memory until the
15-min timeout.

Before each GPU serve, ensure the shared port is free: if something still answers
on 11435, kill the listener (fuser/lsof on Linux, Get-NetTCPConnection on Windows)
and wait for it to close. Best-effort — the serve can still replace it — so it
never hard-fails a scenario. Removes the cross-scenario server pile-up.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
The first port-free pass only killed a server that already answered /v1/models,
so a prior scenario's vLLM still LOADING (holding the port + 0.80 GPU mem but not
yet HTTP-ready) slipped through; the next serve then started a second vLLM,
overcommitted GPU memory, and the collision crashed a server — the following chat
POST failed with "error sending request". Kill by port unconditionally (catches
the starting server) and wait on a raw TCP connect for the socket to close.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
…ss serve wait

Even with vLLM accumulation fixed, the app-dev GPU expect-pass still exceeded the
15-min cap: the CLI auto-starts its built-in lemonade assistant (Qwen3-4B) on a
Vulkan llama-server (port 8001) that pins a GPU core (EAI-7052) and starves the
vLLM serve under test, so serve readiness never arrives within 600s.

- ensure_serve_port_free now also frees the assistant port 8001 (no scenario
  needs the built-in assistant), and the GPU reclaim step kills vulkan/llama-server.
- app-dev expect-pass gets E2E_SERVE_TIMEOUT_SECS=300 so a starved serve fails the
  scenario with a real error well under the 15-min job cap instead of being cancelled.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Scenarios 5, 8, and the inference half of 6 assert that a vLLM service which
reports ready can serve inference. On the self-hosted MI300X CI runner the
service reports ready (/v1/models 200) but POST /v1/chat/completions is refused —
the readiness signal is a false positive (EAI-7333). This reproduces identically
on the pre-change baseline (run 29104869493) and in an expect-pass-only run (no
GPU contention), so it is a genuine standing product bug, not a harness/flake.

Tag 5 and 8 @expected-failure-EAI-7333; split 6 so engine-selection + reachable
endpoint stay expect-pass (they pass) and the inference assertion moves to a new
known-bug scenario 6b. The expect-pass GPU tier now contains only scenarios that
actually pass; the inference bug is tracked honestly in the known-bugs tier until
EAI-7333 is fixed.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
…-bugs (EAI-7333)

Serving Qwen2.5-1.5B without --engine does not reach a ready /v1/models endpoint
within 300s on the MI300X CI runner — the same readiness gap as scenarios 5/6b/8
(EAI-7333). The engine-SELECTION contract stays covered as expect-pass by scenario
9 (serves 0.5B, checks only that vLLM is selected, no readiness wait). This leaves
the expect-pass GPU tier with only reliably-passing scenarios: examine 3/4,
serving 9, runtime 1.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
…ROCm install

Scenario 4 ("distinguishes CLI-managed from pre-existing ROCm") had a no-op Given
step that silently depended on the box already having a non-CLI ROCm install. That
holds on the MI300X runner (ambient /opt/rocm) but not on the Strix Halo Windows
runner (iGPU, display driver only), where `rocm examine` correctly reports "No ROCm
installs saved yet" and the scenario failed.

Plant a fake pre-existing ROCm install in the scenario's isolated tree
(legacy-rocm/.info/version) and export it via ROCM_PATH, which detect_legacy_rocm_summary
honors. examine then reports detected_unmanaged and emits the "rocm install sdk"
adopt guidance on every platform, independent of any ambient system ROCm. No
assertion change and no product change; this also hardens the previously-incidental
MI300X pass.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Replace the global @expected-failure / E2E_EXPECT_FAILURES tag model with a
per-(scenario × host) resolution to expected-pass / expected-fail (xfail) /
not-applicable (skip), derived from a host capability probe plus a declarative
xfail matrix, and render a reconciled (scenario × platform) grid in the report.

Harness (tests/e2e-cucumber):
- capability.rs: probe `rocm examine --json` + `engines list` once; derive the
  effective serve engine (re-implements the product family/OS rule, drift-guard
  tests). Distinguishes engine "adapter present" from "can start here".
- expectation.rs: resolve(tags, capability, matrix) -> pass/xfail/skip.
- expectations.toml: EAI-7333 conditions use effective_engine = "vllm", fixing
  the Strix-Windows XPASS from run #543.
- features: stable @id per scenario; @gpu -> @requires-gpu; @requires-engine pins.
- harness: filter_run skips N/A; per-scenario reconciliation; platform.json sidecar.

Report (crates/e2e-report):
- parse platform.json; reconcile actual vs expected by @id into a CellOutcome;
  render the (scenario × platform) grid in markdown + HTML with a Needs-attention
  list; add scenario_results_by_id().

Verified: 19 e2e-cucumber + 24 e2e-report lib tests pass; mock run resolves
8 pass / 2 xfail / 11 skip, exit 0; grid renders from real artifacts.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Collapse nested `if let`/`if` into let-chains (Rust 1.96), make label()/is_empty()
const, use Self in the Expectation::label match, and shorten two doc first
paragraphs. No behaviour change; 19 e2e-cucumber + 24 e2e-report lib tests still
pass, both crates clippy-clean under -D warnings.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
With the harness resolving pass/xfail/skip per scenario, the expect-pass vs
known-bugs tier split is redundant — membership is decided at runtime, not by a
tag filter. Collapse the 8 E2E jobs (4 platforms × 2 tiers) to 4 (one per
platform), keeping the mock(hosted, blocking) vs GPU(self-hosted, non-blocking)
boundary.

- xtask: drop the `--expect-failures` flag and the E2E_EXPECT_FAILURES env; each
  job just runs `cargo xtask e2e` (no tag filter). Ad-hoc `-- <args>` forwarding
  stays for local use.
- ci.yml: remove the four `*-known-bugs` jobs and all `-t "@gpu and ..."` /
  `--expect-failures` args; e2e-report `needs:` shrinks 8→4; drop the now-obsolete
  `tier` workflow_dispatch input (keep `platform`). Per-scenario serve timeouts
  come from expectations.toml; the job-level E2E_SERVE_TIMEOUT_SECS stays as the
  outer bound.

Verified: xtask/e2e-report/e2e-cucumber build clippy-clean under -D warnings;
43 lib tests pass; `cargo xtask e2e` runs end-to-end (8 pass / 2 xfail / 11 skip,
exit 0); ci.yml parses to exactly 4 E2E jobs + consolidator with no stale refs.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Full platform=all run #544 exposed two probe bugs:

1. The capability probe parsed `examine --json`, whose `Examination` struct
   reported has_amd_gpu:false / no gfx on a real MI300X — so the probe derived
   platform_slug=mock and effective_serve_engine=lemonade on GPU boxes, making
   every @requires-gpu scenario resolve to skip and the box masquerade as mock.
   Parse the HUMAN `examine` text instead (os:/detected_gfx_target:/wsl:), the
   same signal the scenarios themselves trust — GPU is present iff a real
   detected_gfx_target is reported.

2. Strix Halo Ubuntu and Windows share gfx1151, so both derived slug
   "strix-halo" and collided into a single report-grid column (one overwrote the
   other). Append the OS for multi-OS families → "strix-halo-linux" /
   "strix-halo-windows", giving each its own column.

Tests updated to the text parser and per-OS slugs; 19 lib tests pass, clippy
clean under -D warnings; Mac probe still resolves mock/lemonade.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Follow-up to 99d5890: the probe now parses human `examine` output, not
`--json`; update the HostCapability field docs and module doc accordingly. No
code change.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Full run #545 cancelled the MI300X job at the 15-min cap: with tiers collapsed,
one job runs every serve scenario, and the EAI-7333 known-bug serves each waited
the full 300s for a readiness that never comes. A cancelled job writes no
platform.json, so the grid lost that column entirely.

- Wire the per-scenario `serve_timeout_secs` from expectations.toml: the `before`
  hook resolves it (matching conditions on this host) onto the World, and the
  serve steps prefer it over the global default. So an xfail serve that a bug
  keeps from becoming ready fails fast (90s) instead of burning 300s, while real
  expect-pass serves keep the full window. `Expectations::serve_timeout_for` +
  unit test.
- Raise the three non-blocking GPU jobs' timeout-minutes 15 → 25 as a safety net
  so a stuck serve can never cancel a job before it writes its report. Mock stays
  15 (fast + blocking).

20 e2e-cucumber lib tests pass; clippy clean under -D warnings; ci.yml parses.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Two Strix-Windows "failures" from run #545 were test bugs, not product bugs:

1. serve-lemonade-inference: lemonade inference WORKS on Windows (serve + chat +
   reply all pass) — only the final model-name assertion failed, because lemonade
   reports the concrete GGUF artifact (`Qwen3-0.6B-Q4_0.gguf`) while the scenario
   served the catalog name (`Qwen3-0.6B-GGUF`). Match on a normalized base
   (strip `.gguf`, the `-gguf` catalog marker, and quantization suffixes) so the
   two reduce to the same id; vLLM's exact-id echo still matches.

2. runtime-adopt-preexisting-rejected: the step adopts `/opt/rocm` with
   `--python /usr/bin/python3` — Unix paths that on Windows resolve to a bogus
   `C:/usr/bin/python3`, so the CLI errors on the missing path before emitting
   the install-type guidance the assertion checks. The scenario's premise is
   Linux-only. Add a general `@requires-os:<os>` capability gate (mirrors
   @requires-gpu/@requires-engine) and tag the scenario `@requires-os:linux`, so
   it runs on Linux hosts and skips on Windows instead of failing spuriously.

21 e2e-cucumber lib tests pass (incl. requires-os gate); clippy clean under
-D warnings.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
…ts report

A full MI300X run measured ~28min and was cancelled at the 25min cap, so it
never wrote platform.json → no grid column. The dominant cost is `install sdk`
re-running per scenario (isolated per-scenario data dirs) plus vLLM cold-starts,
not the serve-readiness waits the per-scenario timeout already bounds. Raise the
three non-blocking GPU jobs to 35min so they complete and produce their report;
mock stays 15min (blocking, fast). Sharing the runtime across scenarios to cut
the redundant installs is tracked as a follow-up.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
fn shared_cache_dir() -> Option<PathBuf> {
let dir = std::env::var_os("E2E_SHARED_CACHE_DIR")?;
let dir = PathBuf::from(dir);
std::fs::create_dir_all(&dir).ok()?;
fredespi added 11 commits July 13, 2026 10:13
The command-coverage table showed which commands WERE exercised but had no notion
of the full surface, so it couldn't report what's missing. Add a curated
KNOWN_COMMAND_SURFACE (the `rocm <base>` catalog from the CLI's --help tree) as
the denominator, compute covered/total, and render a "CLI surface coverage:
N/M (P%)" header plus a <details> fold listing every uncovered command. Matching
normalizes the recorded signatures' --engine / (default engine) suffixes to the
base command. New command_base + command_coverage_summary with unit tests.

26 e2e-report tests pass; clippy clean under -D warnings; verified against a real
artifact (5/43 mock-only, 38 uncovered listed).

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
(cherry picked from commit 0fc67d1)
The black-box cucumber suite drives the `rocm` CLI and can't exercise the
interactive `dash` TUI, leaving that user journey uncovered. Add journey tests at
the seam the dash crate already uses (public AppState API + ratatui TestBackend):
navigate every tab on live telemetry, apply a wire Snapshot event and see it
reflected, drive the theme picker, and hold a chat exchange (submit → reply, plus
an error turn). Assertions key on state transitions (the reliable signal), with
rendering as a no-panic check — complementing the render-only characterization in
dash_characterization.rs. 5 tests, clippy clean under -D warnings; existing dash
tests still green.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
(cherry picked from commit f315ba0)
The consolidated report's per-platform Status column derived from raw junit
pass/fail counts, so a platform whose only failures were known-bug xfails
(e.g. mock's two EAI-7219 scenarios) was shown FAIL while the reconciled
scenario-by-platform grid correctly showed it clean. A report that
contradicts itself is not a trustworthy capability map.

Status and the summary Pass/Fail/Skip/Xfail counts now derive from the same
id-keyed reconciliation the grid uses: a known bug that fails is xfail, not a
failure; only unexpected-fail, XPASS, or ran-when-N/A red a platform.
Artifacts predating the expectation system (no platform.json) fall back to
the legacy junit status unchanged.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
(cherry picked from commit afbabc8)
The consolidated report's Tier column always read "expect-pass" after the
collapse to one job per platform (no artifact carries a known-bugs tier
anymore), so it conveyed nothing — removed it from both the markdown summary
and the HTML matrix. Rewrote the platform caption to explain what "gates the
PR" and "non-blocking" mean and to describe the actual columns, and clarified
that Mock fakes the inference backend (not the CLI). Replaced every
"not applicable" dash with n/a across the grid, summary, and command-coverage
tables.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
(cherry picked from commit d9d3adb)
Several scenarios captured engine-agnostic behaviour (end-to-end chat, tool
definitions, the readiness contract) but were pinned @requires-engine:vllm, so
they showed n/a on every platform except MI300X even though the behaviour
exists on lemonade hosts too. The shared serve precondition
(`a model is being served on GPU`) now serves the model matching the host's
effective engine — safetensors via vLLM on Instinct, GGUF via lemonade on
Strix — so those scenarios drop the vLLM pin and run on each GPU platform with
its native engine.

Renamed serve-inference-response -> serve-vllm-inference (it is the deliberate
vLLM half of a per-engine pair with serve-lemonade-inference, so it keeps the
pin — the slug now names the engine) and serve-ready-implies-inference ->
serve-readiness-contract (the behaviour is the trustworthiness of the ready
signal, not a specific engine). Added EAI-7052 lemonade+linux xfail conditions
for the broadened inference scenarios so they xfail on Strix Ubuntu (where
lemonade inference hangs) rather than reading as unexpected failures.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
(cherry picked from commit f63ca2c)
The command-coverage table recorded a stripped signature and only the
--engine flag value, so the Command column hid the flag's value, a
default-engine serve showed no engine at all, and a redundant Model column
just repeated what the command already contained.

The harness now records the full command as executed (argv) and, for a serve
with no --engine, the engine the CLI resolved itself (parsed from the serve
plan's `engine:` line, restricted to serve so another command's output is
never misattributed). The report shows the full command, an Engine column
that marks a CLI-chosen engine as "<engine> (default)", and drops the Model
column. Older artifacts without the new fields fall back to the signature.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
(cherry picked from commit 6bd0933)
The CLI-surface coverage matcher required a recorded command's base to equal a
KNOWN_COMMAND_SURFACE entry exactly, but a serve command embeds its model
(`rocm serve Qwen/...`), which never equals the bare `rocm serve` entry — so
serve was reported uncovered despite being exercised (and appeared in BOTH the
table and the Uncovered list). Match by the longest surface entry that is a
word-boundary prefix of the base, so `rocm serve <model>` maps to `rocm serve`
and `rocm install sdk` still wins over any shorter prefix.

Also reworded the command-coverage legend to explain that a blank cell means
the command was not run on that platform — usually because its scenario is not
applicable there, or the command is platform-specific by construction.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
(cherry picked from commit b67897e)
The chat scenarios sent inference by calling the model endpoint directly over
HTTP, so the `rocm chat` command itself was never exercised and showed as
uncovered in the command-surface table. Add a mock-tier scenario that runs the
real one-shot CLI (`rocm chat --provider local --model <served> --prompt`)
against the planted mock service and asserts the CLI prints the assistant's
reply — covering arg parsing, local-provider service discovery, and output
rendering. No GPU needed, so it runs on the blocking tier and every platform.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
(cherry picked from commit e58f365)
…ting (EAI-7384)

Two black-box scenarios surfaced by tester dogfounding, each tracked as xfail
until fixed:

- help-lists-subcommands-alphabetically (examine.feature, mock-tier): asserts
  `rocm help` lists subcommands alphabetically. Currently declaration order →
  xfail EAI-7383.
- runtime-path-not-nested (runtime_setup.feature, GPU-gated): asserts the active
  managed runtime folder path has no recursive `runtimes/wheel/.../runtimes/wheel/`
  segment. xfail EAI-7384 (awaiting first GPU run to confirm).

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
(cherry picked from commit 81304b9)
Serve a big model end-to-end at least once (dogfooding W9): new scenario
serve-large-model-inference serves Qwen/Qwen3.6-27B (BF16, ~54 GiB, the
catalog's Instinct recommendation) on vLLM and asserts real inference. Pinned
@requires-engine:vllm so in our fleet it runs only on MI300X (fits 1 GPU; Strix
Halo is lemonade / too little VRAM). Confirmed working on app-dev MI300X.

Adds a `@serve-timeout:<secs>` scenario tag so an expected-pass scenario with a
genuinely slow serve (a cold ~54 GiB load far exceeds the 600s default) can
lengthen its readiness window. Precedence in the before hook: tag →
expectations.toml xfail serve_timeout_secs → default.

Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
(cherry picked from commit 50a7560)
Signed-off-by: fredespi <fredrik.espinoza@gmail.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.

3 participants