diff --git a/.add/state.json b/.add/state.json index c2eb7488..c943c4be 100644 --- a/.add/state.json +++ b/.add/state.json @@ -1,7 +1,7 @@ { "project": "moon", "stage": "production", - "active_task": "resp3-type-fidelity", + "active_task": "monoio-ci-coverage", "active_milestone": "v0-9-client-compat", "tasks": { "hotpath-lock-quickwins": { @@ -200,12 +200,12 @@ }, "monoio-ci-coverage": { "title": "CI runs the test suite on a monoio build (and clippy --all-targets)", - "phase": "ground", - "gate": "none", + "phase": "done", + "gate": "PASS", "milestone": "v0-9-client-compat", "depends_on": [], "created": "2026-08-09T07:32:03+00:00", - "updated": "2026-08-09T07:32:03+00:00" + "updated": "2026-08-10T08:35:14+00:00" }, "client-identity-introspection": { "title": "HELLO/COMMAND/ROLE/RESET identity + registry-dispatch reconciliation", @@ -410,7 +410,7 @@ } }, "created": "2026-06-11T03:18:21+00:00", - "updated": "2026-08-10T06:47:03+00:00", + "updated": "2026-08-10T08:35:14+00:00", "setup": { "locked": true, "locked_at": "2026-06-11T03:28:00+00:00", diff --git a/.add/tasks/monoio-ci-coverage/TASK.md b/.add/tasks/monoio-ci-coverage/TASK.md index 65dd2583..c0e09b0c 100644 --- a/.add/tasks/monoio-ci-coverage/TASK.md +++ b/.add/tasks/monoio-ci-coverage/TASK.md @@ -2,7 +2,7 @@ slug: monoio-ci-coverage · created: 2026-08-09 · stage: production autonomy: auto -phase: ground +phase: done @@ -15,33 +15,144 @@ phase: ground @@ -53,11 +164,52 @@ Assumptions — lowest-confidence first: ```gherkin -Scenario: - Given - When - Then - And # required for every rejection +Scenario: a monoio-only defect blocks the merge # the whole point of the task + Given a PR whose change is wrong ONLY on the monoio dispatch path + And the tokio `check` job passes, because it never executes that path + When CI runs on the PR + Then `check-monoio` fails and the PR is not mergeable + And this is exactly the v0.8.6 inline-GET ACL bypass (#457), which shipped green + +Scenario: the job tests the shipped runtime, not the fallback + Given the `check-monoio` job + When it builds and runs tests + Then it uses the DEFAULT feature set (runtime-monoio) + And its command contains no `--no-default-features --features runtime-tokio` + And `MOON_NO_URING` is unset, so the io_uring driver is the one exercised + +Scenario: monoio-gated code is reachable at all + Given 30 `src/` files behind `#[cfg(feature = "runtime-monoio")]`, 3 of them test-only modules + And 26 integration test files referencing runtime-monoio + When `check-monoio` runs + Then those tests execute and are counted in its summary + And the tokio `check` job's own test count is unchanged + +Scenario: a known load-sensitive flake does not redden the job + Given `dbsize_offload_logical` fails under full-suite parallel load roughly 1 run in 3 (#459) + When `check-monoio` runs it + Then `--profile ci` retries it (retries = 2) and the job exits 0 + And the log still reports it as FLAKY, so the signal is not swallowed + +Scenario: a genuine failure is never masked by retries + Given a test that fails deterministically on monoio + When `check-monoio` retries it twice + Then all 3 attempts fail and the job FAILS + And the job is REQUIRED, so the PR cannot merge + +Scenario: the runner being offline does not fake a pass # rejection + Given the self-hosted `moon-dev` runner is offline or its service is flapping + When a PR opens + Then `check-monoio` QUEUES and the PR is not green + And it never reports success or skips — a silent skip would restore the exact blind + spot this task exists to remove (see gotcha_selfhosted_runner_offline_service_active) + +Scenario: artifacts never collide with the tokio job # rejection + Given `check` and `check-monoio` may run concurrently on the same self-hosted runner + And they build INCOMPATIBLE feature sets from one checkout + When both run + Then each uses its own CARGO_TARGET_DIR + And neither invalidates the other's cache, and neither can execute the other's binaries ``` @@ -68,14 +220,52 @@ Scenario: ## 3 · CONTRACT — freeze the shape ▸ docs/05-step-3-contract.md -``` - body: { } - 200 -> { } - 4xx -> { error: "" | "" } -Schema: +```yaml +# .github/workflows/ci.yml — NEW job, sibling of `check` +check-monoio: + name: Check (monoio — the shipped runtime) + runs-on: [self-hosted, moon-dev] # only Linux runner => only io_uring + env: + CARGO_TARGET_DIR: /home/tindang/ci-target/check-monoio # isolated from `check` + MOON_DISK_FREE_MIN_PCT: "0" + CARGO_BUILD_JOBS: "6" + # MOON_NO_URING deliberately UNSET — the tokio jobs set it; this job must not. + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@1.94.1 + - uses: taiki-e/install-action@nextest + - name: Test (default features = runtime-monoio) + run: cargo nextest run --profile ci # NOT `cargo test` — no retries there + timeout-minutes: 30 ``` -Status: DRAFT +Scope — in: `.github/workflows/ci.yml` (one new job) · `CLAUDE.md` CI section (document it) +Scope — out: `.config/nextest.toml` (the `ci` profile already covers this flake class — proven, + 5145 pass / 1 flaky / exit 0) · fixing #459 or the `blocking_peer_eof` flake · + any `src/` change · the `MOON_NO_URING` epoll leg (a later task if wanted) + +Target: a monoio-only defect fails a PR. Verified by the negative control in §4 — deliberately + break a monoio-only path, confirm `check-monoio` fails while `check` still passes. + +Invariants: + - the tokio `check` job is untouched: same features, same steps, same test count + - no `continue-on-error`, no `if: always()`, nothing that lets the job pass while not running + - the job is REQUIRED in branch protection (otherwise it is advisory and will be ignored) + +Status: **FROZEN @ v1** — approved by Tin Dang, 2026-08-10 + +Lowest-confidence flags surfaced AT the freeze (both accepted by the approver): +1. **[contract] "REQUIRED" is not something this task can deliver alone.** Adding the job to + `ci.yml` makes it RUN; only branch protection makes it BLOCK. That is a repo admin setting, not + a workflow field, so the Must "the job is REQUIRED" is half-satisfiable by code. If it is never + added to branch protection the job is advisory, and an advisory red job gets ignored — which + would leave the blind spot open while *looking* closed, the worst outcome. **Approver accepted + this and owns the branch-protection change.** Cost if forgotten: the task reports done while + delivering nothing that can stop a merge. +2. **[contract] Cold-cache build time is unmeasured.** Test time is a measured 80.3s, but the + default (monoio) feature set has never been built in CI, so the first run pays a full cold build + with no data behind it. If it proves punitive the remedy is cache warming or a prebuilt target + dir — never dropping the job, since 80s of test time is not the cost driver. +### Negative control — planned, runs at VERIFY - +The config guard proves the job is DECLARED correctly; it cannot prove the job CATCHES anything. +So the gate also requires a one-time negative control: introduce a deliberate defect on a +monoio-only path, then confirm + - the tokio suite still PASSES (demonstrating the blind spot is real, not theoretical), and + - the monoio suite FAILS (demonstrating the new job closes it), +then revert. Evidence goes in §6. Without this, "CI now covers monoio" is an unverified claim about +a YAML file. --- @@ -124,33 +341,67 @@ Constraints: do NOT change any test or the contract; allow-list packages only; a ## 6 · VERIFY — evidence + non-functional review ▸ docs/08-step-6-verify.md -- [ ] all tests pass -- [ ] coverage did not decrease -- [ ] no test or contract was altered during build -- [ ] the green was EARNED, not gamed — no overfit to fixtures, vacuous asserts, or stubbed-away logic (score with an adversarial refute-read — a subagent recommended under `autonomy: auto`; a confirmed cheat is HARD-STOP) -- [ ] concurrency / timing of the risky operation is safe -- [ ] no exposed secrets, injection openings, or unexpected dependencies -- [ ] layering & dependencies follow CONVENTIONS.md +- [x] all tests pass — `ci_covers_monoio` 5/5 under BOTH feature sets (it is runtime-agnostic, so + it must hold in each); `multi_queues_inline_get` 6/6 after the negative control was reverted. +- [x] coverage did not decrease — +5 tests, +1 CI job. Nothing removed, and + `the_tokio_job_is_still_covered` exists specifically to prove tokio coverage was not traded away. +- [x] no test or contract was altered during build — the frozen §3 was implemented verbatim + (runner, target dir, `nextest --profile ci`, no `continue-on-error`, `MOON_NO_URING` unset). +- [x] the green was EARNED — see the NEGATIVE CONTROL below. The config guard alone proves only that + a YAML file says the right words; the control proves the job CATCHES a real defect. +- [x] concurrency / timing — the job takes its own `CARGO_TARGET_DIR`, so it cannot race `check`'s + artifacts on the shared self-hosted runner. `--profile ci` supplies the retries that keep a + known load-sensitive flake class from reddening a required job. +- [x] no exposed secrets / unexpected dependencies — no new crate; the guard test reads a repo file + with `std::fs` and parses it with string ops (no YAML dependency added). +- [x] layering — CI config lives in `.github/workflows/`; the guard is a normal integration test. - [ ] a person reviewed and approved the change -### Build expectations — what "correct" looks like (fill BEFORE build; confirm each at the gate) -> Pre-declare the OBSERVABLE outcomes a correct build must produce — derived from §2 SCENARIOS -> + §3 CONTRACT — so this gate checks the build is RIGHT, not merely that tests are green. Each -> row is evidence you can SEE, not a restatement of a test name. -- [ ] — confirmed by -- [ ] — confirmed by +### NEGATIVE CONTROL — the evidence that matters -### Deep checks — do not skim (fill the path that applies; the resolver judges which) -- [ ] WIRING (code) — every new symbol is referenced; record where / how confirmed -- [ ] DEAD-CODE (code) — no new unused or orphaned symbol introduced -- [ ] SEMANTIC (prose / non-code) — read in full, not skimmed: +A CI-config change can be green and still be worthless. So the claim "CI now covers monoio" was +tested directly: a deliberate defect was injected on `try_inline_dispatch` (which is +`cfg(feature = "runtime-monoio")`, so a tokio build cannot reach it), making inline GET answer +`$6\r\nBROKEN\r\n`, and both suites were run against it. + +| leg | `acl_inline_read_enforcement` | `multi_queues_inline_get` | meaning | +|---|---|---|---| +| **tokio** — what CI ran before this task | 4 passed | **6 passed** | the blind spot is REAL: this defect ships green today | +| **monoio** — what `check-monoio` runs | 4 passed | 3 passed, **3 FAILED** | the new job CATCHES it | + +An incidental confirmation arrived first: the tokio leg *compiled* while monoio did not, because +`try_inline_dispatch` is cfg'd out entirely under tokio — the isolation is structural, not incidental. + +Reverted immediately via `git checkout --`; verified `0` residual `NEGATIVE CONTROL` / `BROKEN` +markers in `src/`, a clean `git status` for `src/`, and `multi_queues_inline_get` back to 6/6. + +### Gates + +| gate | result | +| --- | --- | +| `ci_covers_monoio` (default/monoio) | 5 / 5 | +| `ci_covers_monoio` (tokio) | 5 / 5 | +| RED run before build | 4 failed / 1 passed — all four "no `check-monoio` job" | +| negative control | tokio PASSES the defect · monoio FAILS it | +| YAML structural parse | 9 jobs, `check-monoio` well-formed | +| `cargo clippy --all-targets -D warnings` (both feature sets) | exit 0 | +| `cargo fmt --check` | exit 0 | +| pre-landing suite measurement (`moon-dev`, kernel 6.17) | **5145 passed, 1 flaky, 244 skipped, exit 0, 80.3s** | ### GATE RECORD -Outcome: -If RISK-ACCEPTED -> owner: · ticket: · expires: (never for a security gap) -Reviewed by: · date: +Outcome: **PASS** +Reviewed by: Tin Dang · date: 2026-08-10 + +Approved on the negative control, not on the config guard: the same deliberate monoio-only defect +PASSES the tokio suite (6/6) and FAILS the new job (3/6). That is the difference between "a YAML +file says the right words" and "the job catches defects". + +OPEN, and owned by the approver: `check-monoio` must be added to branch protection. Until then it +RUNS but does not BLOCK — flagged at the freeze (§3 flag 1) and accepted there. - + --- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ac6cd0b..4e03e76e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,9 +31,12 @@ concurrency: group: ci-${{ github.ref }} cancel-in-progress: true +# Workflow-level env merges into EVERY job and a job cannot unset an inherited +# key (an empty value is still a set variable). MOON_NO_URING therefore lives +# per-job, never here — see `check-monoio`, which exists to run io_uring and was +# silently running the timer fallback for as long as this was a global. env: CARGO_TERM_COLOR: always - MOON_NO_URING: "1" # 2026-07-13 CI optimization: no debuginfo in CI builds/tests — halves # artifact size (faster rust-cache save/restore) and speeds linking. # Backtraces in CI keep function names via symbol table; local builds @@ -95,6 +98,9 @@ jobs: runs-on: [self-hosted, moon-dev] env: CARGO_TARGET_DIR: /home/tindang/ci-target/check + # tokio's io_uring bridge is off in CI (it floods errors under load). + # Per-job, never workflow-level: `check-monoio` must not inherit it. + MOON_NO_URING: "1" # Spawned test servers must not trip the diskfull write-pause on the # VM's shared-volume mounts. MOON_DISK_FREE_MIN_PCT: "0" @@ -136,6 +142,55 @@ jobs: run: "cargo nextest run --profile ci --no-default-features --features runtime-tokio,jemalloc,graph --lib -E 'test(graph::)'" timeout-minutes: 5 + # ── monoio — the runtime Moon actually SHIPS ──────────────────────── + # Until this job existed, every CI job that EXECUTED tests did so under + # `--no-default-features --features runtime-tokio`. Moon's default feature + # set is runtime-monoio, and that is what ships on Linux — so 26 monoio + # integration test files and 30 monoio-gated src/ files were unreachable by + # CI. That is how the v0.8.6 inline-GET ACL bypass (#457) shipped green: it + # was wrong only on the monoio dispatch path, which no CI job could see. + # + # Self-hosted is not a preference: it is the only Linux runner, and therefore + # the only place monoio's io_uring driver executes at all. MOON_NO_URING must + # reach this job from NOWHERE — the point is io_uring. Its first cut left the + # job block clean but inherited `MOON_NO_URING: "1"` from workflow-level env, + # which a job cannot unset; every cooperative_yield() fell back to the + # sleep(ZERO) timer park and `monoio_yield_overhead_is_microscopic` failed at + # 1.45ms/yield. That var is now per-job and `ci_covers_monoio.rs` asserts both + # scopes. + # + # Measured before landing (moon-dev, kernel 6.17, 2026-08-10): + # cargo nextest run --profile ci -> 5145 passed, 1 flaky, 244 skipped, + # exit 0, 80.3s of test time. The `ci` profile's retries=2 absorbs the + # known load-sensitive flake class (#459) while still reporting it FLAKY, + # so the job is green from day one without hiding the signal. + # + # Guarded by tests/ci_covers_monoio.rs — weakening this job (wrong features, + # continue-on-error, bare `cargo test`, shared target dir) fails that suite. + check-monoio: + name: Check (monoio — the shipped runtime) + runs-on: [self-hosted, moon-dev] + env: + # MUST differ from `check`'s dir: the two jobs build incompatible feature + # sets from one checkout and can run concurrently on this runner. + CARGO_TARGET_DIR: /home/tindang/ci-target/check-monoio + MOON_DISK_FREE_MIN_PCT: "0" + CARGO_BUILD_JOBS: "6" + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@1.94.1 + - uses: taiki-e/install-action@nextest + + # Default features == runtime-monoio. Never --no-default-features here. + # `--profile ci` is load-bearing: a bare `cargo test` has no retries, and + # an intermittently-red required job gets disabled — which is worse than + # no job, because it still looks like coverage. + - name: Test (default features — monoio + io_uring) + run: cargo nextest run --profile ci + # 30m mirrors `check`: the budget is dominated by compiling test + # binaries on a cold CARGO_TARGET_DIR, not by the 80s of test time. + timeout-minutes: 30 + # ── macOS — clippy + test (tokio), clippy (monoio/kqueue) ─────────── # Main-push only (2026-07-13 queue-killer round): macOS runners are the # scarcest hosted pool and were the long pole on every PR. Development @@ -285,6 +340,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 env: + # Spawns a real (tokio-built) moon on a GitHub-hosted runner, where + # io_uring may be unavailable or restricted by seccomp. + MOON_NO_URING: "1" SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: sccache steps: @@ -322,6 +380,11 @@ jobs: runs-on: [self-hosted, moon-dev] env: CARGO_TARGET_DIR: /home/tindang/ci-target/client-compat + # Kept from when this was a workflow-level global, so this job's recorded + # waiver baseline stays comparable: the differ probes RESP wire shapes, + # which the I/O driver does not change. Drop it if the driver ever + # becomes part of what this job is meant to prove. + MOON_NO_URING: "1" MOON_DISK_FREE_MIN_PCT: "0" CARGO_BUILD_JOBS: "6" steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index b51bbf4c..185e31fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **CI now tests the runtime Moon actually ships (`check-monoio`).** Every CI job that *executed* + tests did so under `--no-default-features --features runtime-tokio,…`, while Moon's default + feature set — and what ships on Linux — is `runtime-monoio`. 26 monoio integration test files and + 30 monoio-gated `src/` files were unreachable by CI. That is how the v0.8.6 inline-GET ACL bypass + shipped green: it was wrong only on the monoio dispatch path, which no CI job could see. The new + job runs on the self-hosted Linux runner (the only place monoio's io_uring driver executes at + all), uses the default feature set, and invokes `cargo nextest run --profile ci` so the repo's + existing flake policy applies — a bare `cargo test` has no retries, and an intermittently-red + required job gets disabled, which is worse than no job because it still looks like coverage. + Measured before landing: **5145 passed, 1 flaky, 244 skipped, exit 0, 80.3s**. + + Proven by negative control rather than asserted: a deliberate defect on the monoio-only + `try_inline_dispatch` path **passes** the tokio suite 6/6 and **fails** the new job 3/6. + `tests/ci_covers_monoio.rs` guards the job against being silently weakened — wrong feature set, + `continue-on-error`, a bare `cargo test`, or a shared `CARGO_TARGET_DIR` all fail the suite. + + The job's first cut proved the premise but not the driver: `MOON_NO_URING: "1"` lived in the + workflow-level `env:`, which merges into every job and cannot be unset by one, so the job whose + whole point is io_uring ran with io_uring force-disabled. `monoio_yield_overhead_is_microscopic` + caught it at 1.45ms/yield (the `sleep(ZERO)` timer-park signature). That variable is now per-job, + and `ci_covers_monoio.rs` asserts **both** scopes. + - **Client-compat harness: raw-RESP diff against a real `redis-server` (`scripts/test-client-compat.sh`).** Moon's existing Redis comparison (`scripts/test-commands.sh`) goes through `redis-cli`, which renders replies diff --git a/tests/ci_covers_monoio.rs b/tests/ci_covers_monoio.rs new file mode 100644 index 00000000..38427494 --- /dev/null +++ b/tests/ci_covers_monoio.rs @@ -0,0 +1,191 @@ +//! CI must execute the runtime Moon actually ships. +//! +//! Moon's default feature set is `runtime-monoio`, and that is what ships on +//! Linux. Yet every CI job that EXECUTED tests did so under +//! `--no-default-features --features runtime-tokio,…` — 26 monoio integration +//! test files and 30 monoio-gated `src/` files were unreachable by CI. That is +//! how the v0.8.6 inline-GET ACL bypass (#457) shipped green: it was wrong only +//! on the monoio path, and no CI job could see it. +//! +//! These tests guard the FIX, not the bug. They read `.github/workflows/ci.yml` +//! and assert the monoio job exists and has not been quietly weakened — because +//! the failure mode of CI coverage is silent: a job that stops running, or is +//! switched to the wrong feature set, looks identical to a green build. +//! +//! Deliberately a repo-config test, not a runtime test: nothing observable at +//! runtime can tell you which runtime CI exercised. + +use std::path::PathBuf; + +fn workflow() -> String { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".github/workflows/ci.yml"); + std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display())) +} + +/// Return the body of a top-level job block, from ` :` to the next +/// top-level job key at the same two-space indent. +fn job_block(yaml: &str, name: &str) -> Option { + let needle = format!("\n {name}:\n"); + let start = yaml.find(&needle)? + 1; + let rest = &yaml[start + needle.len() - 1..]; + // Next line that begins with exactly two spaces then a non-space: the next job. + let mut end = rest.len(); + for (idx, _) in rest.match_indices('\n') { + let line = &rest[idx + 1..]; + let after_indent = line.strip_prefix(" "); + if let Some(a) = after_indent + && !a.starts_with(' ') + && !a.starts_with('#') + && a.contains(':') + { + end = idx + 1; + break; + } + } + Some(rest[..end].to_string()) +} + +/// Body of the top-level `env:` mapping — the block every job inherits. +/// Empty string when the workflow declares none. +fn workflow_env_block(yaml: &str) -> String { + let Some(start) = yaml.find("\nenv:\n") else { + return String::new(); + }; + let rest = &yaml[start + "\nenv:\n".len()..]; + // Ends at the first line that is not indented and not blank/comment. + let mut end = rest.len(); + for (idx, _) in rest.match_indices('\n') { + let line = &rest[idx + 1..]; + let head = line.split('\n').next().unwrap_or(""); + if !head.is_empty() && !head.starts_with(' ') && !head.starts_with('#') { + end = idx + 1; + break; + } + } + rest[..end].to_string() +} + +#[test] +fn ci_has_a_job_that_tests_the_default_monoio_runtime() { + let yaml = workflow(); + let job = job_block(&yaml, "check-monoio").expect( + "ci.yml has no `check-monoio` job. CI would then execute tests ONLY under \ + runtime-tokio, leaving the shipped runtime untested — the gap that let the \ + v0.8.6 inline-GET ACL bypass (#457) ship green.", + ); + + let tests = job + .lines() + .filter(|l| l.contains("cargo nextest run") || l.contains("cargo test")) + .collect::>(); + assert!( + !tests.is_empty(), + "`check-monoio` runs no test command at all; it would be theatre.\n{job}" + ); + + for line in &tests { + assert!( + !line.contains("--no-default-features"), + "`check-monoio` must test the DEFAULT (monoio) feature set. This line opts out \ + of it, which silently makes the job a duplicate of the tokio `check` job:\n {line}" + ); + assert!( + !line.contains("runtime-tokio"), + "`check-monoio` must not select runtime-tokio:\n {line}" + ); + } +} + +#[test] +fn the_monoio_job_uses_the_ci_profile_so_known_flakes_retry() { + // The suite has a load-sensitive flake class (fixed-port listeners, kill-9 + // timing under full-suite parallel load). `.config/nextest.toml`'s `ci` + // profile carries `retries = 2` for exactly that. A bare `cargo test` has + // no retries, so it would redden the job intermittently — and an + // intermittently-red required job gets disabled, which is worse than no + // job because it still looks like coverage. + let yaml = workflow(); + let job = job_block(&yaml, "check-monoio").expect("no `check-monoio` job"); + + assert!( + job.contains("cargo nextest run --profile ci"), + "`check-monoio` must run `cargo nextest run --profile ci`, never a bare \ + `cargo test` — the profile is what supplies retries for the known flake class.\n{job}" + ); +} + +#[test] +fn the_monoio_job_cannot_pass_without_running() { + // Three ways a job can look green while proving nothing. + let yaml = workflow(); + let job = job_block(&yaml, "check-monoio").expect("no `check-monoio` job"); + + assert!( + !job.contains("continue-on-error"), + "`check-monoio` must not set continue-on-error — a monoio-only failure has to \ + be able to block a merge, or the job is advisory and will be ignored.\n{job}" + ); + assert!( + !job.contains("MOON_NO_URING"), + "`check-monoio` must NOT set MOON_NO_URING. The tokio jobs set it; this job exists \ + to exercise the io_uring driver that actually ships on Linux.\n{job}" + ); + // A clean job block is not enough: workflow-level `env:` merges into every + // job, and a job cannot unset an inherited key (an empty value is still a + // set variable to `env::var_os`). This assertion is the one the first cut of + // this job was missing — the job block was clean, the comment said io_uring + // was the point, and the workflow-level `MOON_NO_URING: "1"` silently forced + // every `cooperative_yield()` onto the `sleep(ZERO)` timer fallback. + // `monoio_yield_overhead_is_microscopic` caught it at 1.45ms/yield. + assert!( + !workflow_env_block(&yaml).contains("MOON_NO_URING"), + "workflow-level `env:` must NOT define MOON_NO_URING — it merges into `check-monoio`, \ + which cannot unset it, and force-disables the io_uring driver that job exists to \ + exercise. Set it per-job on the tokio jobs instead." + ); + assert!( + job.contains("self-hosted"), + "`check-monoio` must run on the self-hosted Linux runner — it is the only runner \ + where monoio's io_uring driver executes at all.\n{job}" + ); +} + +#[test] +fn the_monoio_job_does_not_share_artifacts_with_the_tokio_job() { + // `check` and `check-monoio` build INCOMPATIBLE feature sets from one + // checkout and can run concurrently on the same self-hosted runner. Sharing + // a target dir would make them invalidate each other's cache continuously, + // and could let one job execute the other's binaries. + let yaml = workflow(); + let mono = job_block(&yaml, "check-monoio").expect("no `check-monoio` job"); + let tokio = job_block(&yaml, "check").expect("no `check` job"); + + let dir_of = |j: &str| -> Option { + j.lines() + .find(|l| l.trim_start().starts_with("CARGO_TARGET_DIR:")) + .map(|l| l.split(':').nth(1).unwrap_or("").trim().to_string()) + }; + + let m = dir_of(&mono).expect("`check-monoio` must set its own CARGO_TARGET_DIR"); + let t = dir_of(&tokio); + if let Some(t) = t { + assert_ne!( + m, t, + "`check-monoio` and `check` must not share CARGO_TARGET_DIR ({m}) — they build \ + incompatible feature sets and may run concurrently on the same runner." + ); + } +} + +#[test] +fn the_tokio_job_is_still_covered() { + // This task ADDS a runtime to CI; it must not trade one blind spot for + // another. tokio remains a supported runtime (portability, Windows). + let yaml = workflow(); + let tokio = job_block(&yaml, "check").expect("no `check` job"); + assert!( + tokio.contains("runtime-tokio"), + "the tokio `check` job must keep testing runtime-tokio — adding monoio coverage \ + must not remove tokio coverage.\n{tokio}" + ); +}