test: add BDD E2E tests with cucumber-rs#69
Conversation
9c0732e to
a4cc8eb
Compare
68b4c94 to
cd83918
Compare
5caff76 to
ffab5e2
Compare
rominf
left a comment
There was a problem hiding this comment.
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 thee2enore2e-gpujob 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/modelsand echoes it back); therocmbinary is never invoked, so it can't fail for a product reason. The mock-basedservices listscenarios 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→ anxtasksubcommand. We already standardize dev/CI tasks inxtask(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 betweenrun.shand the CI job. Acargo 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 miscountsundefined/ambiguoussteps as passed and prints a placeholder-xx-xxdate. 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) beatsformat!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'sDropremove_dir_alls it out from under the others (risking a fall-back to the real~/.rocm). Add a per-World unique suffix. reqwest::blockinginside 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 warningsto the e2e job. .featurefiles aren't in theheavypath filter, so a feature-only change skips the e2e job. Add**/*.feature.
🔵 Nit
@expected-failure-EAI-XXXXputs 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.
e3a2c13 to
6741054
Compare
|
Thanks for the thorough review — all points addressed. Rebased on 🔴 Blocking
🟠 Fit the project
🟡 Should-fix
🔵 Nit
Also, while rebasing
The full workspace test suite and the mock-tier e2e selection pass on Linux. |
6741054 to
1ea8909
Compare
a6216e8 to
55b3aec
Compare
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>
96b8bbb to
0d5645e
Compare
…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()?; |
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>
Summary
Adds a behavioral end-to-end test layer using cucumber-rs — standard Gherkin
.featurefiles 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:
assert_cmd/instacouples to output format and breaks on cosmetic changes.Scenario Outline,Background,Rule, and@tags— not Robot Framework's cosmetic prefix stripping. The.featurefile is the executable test, not a rewrite of a separate spec.What's included
.featurefiles — the spec is the testchat_steps.rs,examine_steps.rs,serving_steps.rs,runtime_steps.rs)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.@expected-failure-EAI-7218@expected-failure-EAI-7219@expected-failure-EAI-7220@expected-failure-EAI-7221@expected-failure-EAI-7223Each Jira ticket has a comment linking back to its test scenario(s).
Running tests
Test plan