Skip to content

mechababs test-cluster: validate a cluster from the campaign - #104

Merged
asmacdo merged 5 commits into
con:mainfrom
lobennett:feat/test-cluster
Jul 29, 2026
Merged

mechababs test-cluster: validate a cluster from the campaign#104
asmacdo merged 5 commits into
con:mainfrom
lobennett:feat/test-cluster

Conversation

@lobennett

Copy link
Copy Markdown
Collaborator

mechababs test-cluster: validate a cluster from the campaign

What this is

mechababs test-cluster --cluster <config>, run from the campaign venv. Validating a cluster config was a repo-dev operation — checkout, pip install -e '.[test]', MECHABABS_E2E_WORKDIR, BABS_SPEC, pytest by hand. The campaign already carries every one of those, so validation moves to the operate side, next to iterate and status. The container shim stays the only prerequisite, and it is now checked before pytest starts instead of surfacing as a skip.

How it works

  • It does not run in the campaign you point at. The scenario configures a campaign, adds a dataset, and retires a derivative, so it provisions its own throwaway campaign from the target campaign's pins and works there. The campaign supplies the environment, not the workspace — and because the pins come from it, a green run means those exact tools work on this cluster.
  • One provisioning input, so dev-exercises-prod holds. Adding --campaign beside the existing checkout route would have left the user-facing path exercised only by users. I had it that way briefly and it drifted — a detached-HEAD fix landed on one route and not the other. So --campaign is required and the checkout route is gone: a dev run reaches the same code the way a user does, bootstrapping a dev campaign pinned at the checkout (87s of a 40-minute run). The two rungs still differ only in which cluster config and whether a container wraps them.
  • Fails loudly. pytest exits 0 when everything skips, so the fixtures raise instead. Reporting success having validated nothing is the failure mode worth engineering against.
  • Leaves the campaign clean. -p no:cacheprovider plus a redirected PYTHONPYCACHEPREFIX: a .pytest_cache/ inside code/mechababs reads as a dirty pin to guard.require_clean_pins, and one early run left a campaign refusing every later command.
  • Takes the skeleton, not the ledger. configure writes the ledger, so gating on it made bootstrap → validate → configure impossible. Both commands now share one "campaign enough to run" check.

The suite moved into the package

Your plan had test-cluster read the vendored code/mechababs/tests/e2e. This ships it as package data instead, resolved through importlib.resources — option 1 from the issue thread. Under #101 the tools arrive via uvx --from git+… against a uv.lock, so there is no clone to reach into and tests/ is not in a wheel; packaging is the part that survives.

Two consequences. The SimBIDS phantom pipelines move out of examples/pipelines/ into the suite that consumes them — this reverses that bit of #100; they are fixtures, and a packaged suite cannot reach a fixture outside the package. And testpaths becomes load-bearing: without it a bare pytest also walks mechababs/, where the packaged conftest registers --cluster-config twice and collection fails. exclude-package-data keeps the dev wrappers out of the wheel, since package-data globs are additive rather than restrictive once setuptools_scm's file finder is involved.

Say the word if you would rather it stay in tests/ until #101 lands. The command only needs some way to find the scenario.

Changes

  • mechababs/validate.py — the command body: cluster resolution, re-clonable ref resolution, the shim precheck, the pytest invocation.
  • mechababs/cli.py — the test-cluster subcommand; the skeleton and campaign-venv guards extracted so configure and test-cluster share one copy.
  • mechababs/testing/ — the e2e scenario, shipped as package data and located through importlib.
  • requirements-campaign.txt — pytest in the campaign venv.
  • docs — the tutorial leads with the campaign route (its ASPIRATIONAL block is gone), reference.md documents the subcommand, installation.md drops the driver venv.

Five commits, each green on its own.

Verified

  • Real cluster (Sherlock login node, real slurm): 3 passed (38m54s) — bootstrap → configure → add-dataset → iterate: scaffold → submit → wait → merge, both pipelines, derivative zips landing in the output RIA.
  • In-container (rootless podman): 2 passed, 1 failed. The failure is allow full BIDS-acceptable chars as derivative names PennLINC/babs#394 (grep -E reads the + in a zip name as a metacharacter). The cluster run's third test is green only because it pinned a patched babs, so 2/3 is the honest number on babs main.
  • Unit: 58 passed.

The podman rung earned its keep: once the conftest imported mechababs.validate it could no longer load under the container's ambient pytest — invisible on a cluster, where the campaign venv is what runs. It now uses the dev campaign's own pytest.

run_in_podman.sh is the one change no full run covers. Its ${A[@]+"${A[@]}"} fix — an empty array aborts under set -u on bash before 4.4, so the script could not start on a RHEL 7 login node — is verified only in isolation and with bash -n.

What #101 will unlearn

The durable pieces are the command surface, the packaged suite, and having one provisioning input. What moves is lookup, each in a single place: _pins_from_campaign reads a vendored clone's branch and will read uv.lock; the skeleton check tests .datalad + code/<tool>, which a study-first campaign explicitly will not have. The campaign-venv guard is your open 💬 on environment enforcement — this reuses it rather than deciding it.

Closes #98

lobennett and others added 5 commits July 25, 2026 21:43
`mechababs test-cluster` (con#98) has to find the e2e scenario wherever mechababs is
installed, not only in a source checkout. A suite in `tests/` reaches a campaign
today only because bootstrap.sh clones the whole repo into `code/mechababs`; once
the code is referenced and locked rather than cloned in — the direction sketched in
the study-first redesign (con#101), where `.mechababs/` pins the tools in a `uv.lock`
and bootstrap runs them via `uvx` — there is no checkout to reach into and `tests/`
is not in a wheel.

So move it to `mechababs/testing/e2e/` and ship it as package DATA. Data rather than
an importable subpackage because pytest collects from a path, which keeps an
`__init__.py` out of a directory of test modules. `mechababs.testing.suite_path()`
resolves it through `importlib.resources` and fails loudly on an incomplete install,
rather than letting pytest report "no tests collected" much later.

Packaging needs care here: setuptools_scm's file finder plus the default
include-package-data would ship every git-tracked file under the package, so the
`package-data` globs are additive and cannot keep anything out. The dev wrapper
scripts are removed with an explicit `exclude-package-data` — they only make sense
from a checkout — and a test pins both halves of that.

`testpaths` is now required: without it a bare `pytest` also walks `mechababs/`,
where the packaged conftest registers `--cluster-config` a second time and
collection fails outright.

This commit only relocates the suite. `tests/` becomes the unit suite alone: it
tests the code and never leaves the repo, so it has no reason to travel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`mechababs test-cluster` runs the packaged scenario with the campaign venv's own
pytest, so the campaign has to carry it. That is the one new prerequisite con#98 names
beyond the container shim, and it is what removes the `pip install -e '.[test]'` step
from validating a cluster.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Validating a cluster config was a repo-dev operation: check mechababs out, `pip
install -e '.[test]'`, export MECHABABS_E2E_WORKDIR and BABS_SPEC, run pytest by
hand. A campaign already carries everything that setup reconstructs — the pinned
babs, an isolated venv, the packaged scenario, and a natural workdir — so validation
belongs on the operate side, next to `iterate` and `status`:

    cd my-campaign && source .venv/bin/activate
    mechababs test-cluster --cluster ~/config/your-site.yaml

**It does not run in the campaign you point at.** The scenario calls `configure`
(which refuses an existing ledger), `add-dataset`, and `retire-derivative`, so
running it in a campaign holding real work would be destructive. It provisions its
own throwaway campaign from that campaign's pins and works there — which is also
what makes a green run mean "these tools work on this cluster", since the tools under
test are the ones the campaign records.

Three things it does deliberately:

- **Fails loudly.** pytest exits 0 when everything skips, and a missing container
  shim is the likeliest reason a first run has nothing to do, so the shim is checked
  before pytest starts. Exiting 0 having validated nothing is the worst outcome a
  validation command has.
- **Leaves no trace in the campaign.** `-p no:cacheprovider` and a redirected
  PYTHONPYCACHEPREFIX, because a `.pytest_cache/` or a `__pycache__/` written into
  `code/mechababs` makes `guard.require_clean_pins` refuse every later command.
- **Runs on a campaign that is not configured yet.** It takes the campaign SKELETON
  — a datalad dataset with both code pins — not the ledger, which only `configure`
  writes. Validating a cluster before committing real data to it is the point, so
  gating on the ledger would make the documented order impossible. `configure` and
  `test-cluster` now share that check, since both run before a ledger exists.

`--cluster` takes a path, or a name in the campaign's own `clusters/` once
`configure` has copied one in. Deliberately NOT resolved against a vendored clone's
`examples/`: that path disappears with the vendoring. Arguments after a literal `--`
reach pytest; `argparse.REMAINDER` cannot see flag-like tokens without that fence.

The module is `validate.py`, not `test_cluster.py`, so a bare `pytest` does not
collect it as a test module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The `campaign` fixture forked on `--campaign`: with it (what `test-cluster` passes)
it provisioned the scenario's campaign from an existing campaign's pins; without it,
a dev-only route resolved the checkout above the package, refused a dirty tree, and
read BABS_SPEC. So the provisioning code users run was exercised only by users —
never by a dev run — which is the failure docs/overview.md's "never a dev-only
branch, field, or code path" exists to prevent, and it had already bitten us: a
detached-HEAD bug got fixed on one route and left broken on the other.

con#98 asks for exactly this ("reworking the e2e's 'I run from a repo checkout'
assumption to 'I run from a campaign'"), so collapse to the one path. `--campaign` and
`--cluster-config` are now required, as UsageErrors rather than skips — pytest exits 0
when everything skips, and a validation command that passes by doing nothing is worse
than one that fails. `repo_under_test`, the ad-hoc `git status` check, the BABS_SPEC
env-var read, and `cluster_config`'s `examples/clusters/` fallback are gone, and with
them the second bootstrap wrapper and the second cluster-config resolver, since
`validate.clone_ref` and `validate.resolve_cluster` already serve both.

Dev becomes config rather than a code path: the wrapper scripts bootstrap a DEV
CAMPAIGN whose mechababs pin is the checkout, then run `mechababs test-cluster`
against it — the same two steps a user runs, with a different value for the pin.
`guard.require_clean_pins` then guards provenance on the campaign under test, and each
wrapper keeps a dirty-tree refusal where the checkout is actually its input. This is
not less code overall (the conftest shrinks, the wrappers grow more) — it is one path,
with each guard where it belongs.

The two SimBIDS phantom pipelines move to the suite that uses them. They are test
fixtures, not starters a site would copy from `examples/`, and the scenario can no
longer build a path into `code/mechababs/examples/` anyway. Note this reverses that
part of con#100.

The podman rung gains a fix for free: it used to run the container's ambient pytest,
which since the conftest started importing `mechababs.validate` could not even load
it. Now pytest is the dev campaign venv's own, where the pinned mechababs is
installed by construction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tutorial's cluster-validation section described the env-var + checkout path and
flagged `test-cluster` as aspirational; it now leads with the campaign route and the
aspirational block is gone. Two things it used to require are no longer true and are
corrected: a site profile does not have to be committed into `examples/clusters/` to
be validated (`--cluster` reads the path you hand it), and the dev invocation is now
two steps — bootstrap a dev campaign from the checkout, then run `test-cluster`
against it — so the passthrough moved with it and pytest args sit after a literal `--`.

installation.md drops the driver venv entirely: the campaign venv bootstrap builds
carries pytest and the packaged scenario, so no second env exists to describe. It also
stops framing MECHABABS_E2E_WORKDIR as required setup, which is only true when driving
the scenario from a checkout — a campaign gets its workdir from its own location, or
`--workdir`.

reference.md documents the subcommand and states the throwaway-campaign semantics next
to it, since that is where someone reading the CLI will look. CONTRIBUTORS.md covers
the dev-campaign invocation and why the suite lives in the package rather than
`tests/`. README's pointer no longer advertises a driver venv.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@asmacdo

asmacdo commented Jul 29, 2026

Copy link
Copy Markdown
Member

Without #101 the UX isn't as clean as I'd like it to end up, but this is a move in the right direction. I ran on-cluster and it worked well. Thanks @lobennett!!

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.

mechababs test-cluster: validate a cluster from the campaign, not env-vars + a repo checkout

2 participants