* feat(tmachine): add the Session API and a test-runner package
Add a library API that boots an environment, runs commands in it with timeouts,
copies files in and out, collects diagnostics and tears it down, so a caller can
drive a machine without writing an Ansible playbook per suite. SSH and exec
timeouts and QEMU cleanup on SIGTERM keep a hung guest or a cancelled CI job from
leaving QEMU running.
Add the `openshell-test-runner` flake package, which runs tests/runner with the
toolchains and VM tools it needs.
The Session API and test-runner package are adapted from NVIDIA/OpenShell#4629.
Co-authored-by: Evan Lezar <elezar@nvidia.com>
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
* build(cargo): add a check that aligned workspaces lock shared crates like the root
`cargo --locked` only checks a lockfile against its own manifest. A workspace
outside the root that builds root crates through path dependencies, such as the
testkit suites building `openshell-sdk`, has its own Cargo.lock, so the two
lockfiles can resolve a shared crate to different versions without any check
failing. That is how aws-lc-sys 0.46 was picked up in #4629, which does not link
against the AWS-LC that Nix provides.
check_cargo_lockfile_alignment.py fails when an aligned workspace locks a
registry crate that the root lockfile also contains at a version the root
lockfile does not have. With --fix it re-locks the workspace starting from the
root lockfile. tests/testkit is the first aligned workspace.
The script and its tests come from #4629 (a37ebe917).
Co-authored-by: Evan Lezar <elezar@nvidia.com>
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
* test(conformance): add a sandbox smoke suite built on testkit
Add the `sandbox` crate to the conformance workspace with one testkit test that
creates a sandbox, runs `echo` in it with exec, deletes it and checks it is gone.
It is deliberately small: it exists so the runner can run a real test in a
tmachine environment on CI before the existing suites move to testkit.
The workspace's other crate, the CLI conformance suite, keeps running as it does
today. The conformance workspace joins the lockfile alignment check, so its
lockfile is realigned with the root lockfile.
The test is adapted from the `sandbox-lifecycle` suite in #4629.
Co-authored-by: Evan Lezar <elezar@nvidia.com>
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
* feat(runner): add a test runner that runs a test family on tmachine
`openshell-test-runner` builds a family's packages (a workspace under
tests/suites/) on the host as a nextest archive for the guest's musl target,
boots one tmachine environment by name with the named installer through the
Session API, runs nextest in it and brings back the JUnit report and log. No
per-suite Ansible playbook is involved:
nix run .#openshell-test-runner -- --environment ubuntu-docker-rootful \
--installer deb --family conformance --package sandbox
The runner points the tests at the system under test with environment
variables: OPENSHELL_GATEWAY (the gateway the installer registered, `--gateway`,
default `openshell`) and OPENSHELL_BIN (the installed CLI). It waits for the
gateway with `testkit-ready` before the tests start. Nextest runs with
`--test-threads 1` and a slow-timeout that terminates a hung test.
A run passes if nextest succeeds and its JUnit report shows at least one test
and no failures. `--filter` narrows the tests with a nextest filterset, and
`--keep` leaves the VM up for debugging. Results from an earlier run are
removed first, and the guest's diagnostics are collected after a failure.
Which tests run in which environments is left to the caller for now; there are
no plans, environment registry or per-test requirements, and a broken test is
`#[ignore]`d.
Most of this comes from #4629, adapted to testkit finding the gateway the way the
CLI does instead of reading a target file.
Co-authored-by: Evan Lezar <elezar@nvidia.com>
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
* ci(tests): lint and test the runner, tmachine and conformance suite
Run fmt, clippy and nextest for tests/tmachine and tests/runner, and fmt and
clippy for the new `sandbox` suite crate, wherever tests/testkit already runs
them: the mise tasks and Branch Checks. The suite's tests need a gateway, so they
run through `mise run e2e:testkit` against a local Docker gateway and, on CI,
through the test runner rather than the unit-test job.
The lockfile task and the Cargo lockfiles job also run the alignment check, and
its own tests run with `mise run test`.
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
* ci(tests): run testkit suites on tmachine from a reusable E2E Suites workflow
Add a reusable workflow with an explicit matrix: each row names a tmachine
environment and installer, a family and package, and the runner label. Each
row runs `nix run .#openshell-test-runner` once. The jobs use the same
integration inputs as the existing tmachine lanes, cache the tmachine disks
keyed on the Ansible tree, upload the JUnit report and logs whether or not the
run passes, and an aggregator job reports the overall result.
The only row runs the conformance sandbox suite on amd64 Ubuntu with rootful
Docker and the Debian package, which CI can provision today. Branch E2E calls
the workflow when the test:e2e label is set. The job is not in the Core E2E
result's needs yet, so it starts out advisory. The existing conformance,
feature and driver lanes are unchanged.
Modeled on the E2E Suites workflow in #4629.
Co-authored-by: Evan Lezar <elezar@nvidia.com>
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
* docs(tests): document the runner and the conformance suite
Describe how the runner runs a family in a tmachine environment, locally and
on CI, in TESTING.md, add a README for the sandbox suite, point the testkit
README at the runner, and add rows for tests/suites, tests/tmachine and
tests/runner to the project-structure tables in AGENTS.md and CONTRIBUTING.md.
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
* refactor(tests): simplify the test runner and tmachine
- Replace tmachine's global QEMU PID registry with until_signal, which
drops the work on SIGTERM/SIGINT/SIGHUP so kill_on_drop stops QEMU.
This also lets Ctrl-C end a --keep run.
- Decide a run's status from nextest's exit code (with --no-tests fail)
instead of parsing JUnit; print the end of the nextest log on failure.
- Move gateway diagnostics from tmachine's Session into the runner, drop
Session::run_playbooks, and make Session own its VM directly.
- Read TMACHINE_SSH_PORT once and reject invalid values; drop the racy
port pre-check.
- Build the runner with Nix so tmachine's firmware paths are compiled in,
and set ANSIBLE_CONFIG only in the wrapper.
- Copy the bundle directory in directly, fold lib.rs into main.rs, and
make the gateway name required.
- Drop the semver hint from the lockfile alignment check and trim
duplicated docs.
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
* fix(tmachine): keep the SSH port check and name a run with no tests
Restore the check that the SSH forward port is free before QEMU starts. QEMU
reports a taken port only by exiting, so a layer build would otherwise run its
playbooks against the VM that already holds the port and cache the result.
Report nextest's no-tests exit code as "no test ran" instead of a bare exit
status.
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
* fix(runner): keep logs from a timed-out run and gate on the E2E suites
nextest now writes its output to a file in the guest, and the runner copies
nextest.log and junit.xml out after every run, so a run that times out or
loses SSH still leaves a log. A failed JUnit copy no longer turns a test
failure into an error.
The runner exits with 1 when a test failed and 2 when the run could not
complete, and annotates the GitHub Actions run with which it was.
The E2E suites lane is now part of the Core E2E result. Its disk cache key
also covers tmachine's cache versions in setup.rs and install.rs.
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
* ci(e2e): push test runner builds to Cachix
E2E Suites set up Nix without a Cachix token, so every job rebuilt
openshell-test-runner from source. Accept an optional CACHIX_AUTH_TOKEN
secret, pass it to setup-nix, and pass it from Branch E2E, as Branch Checks
and the binary builds already do.
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
---------
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
Co-authored-by: Evan Lezar <elezar@nvidia.com>
31 KiB
Testing
Running Tests
mise run test # Rust + Python unit tests
mise run e2e # End-to-end tests (starts a Docker-backed gateway)
mise run ci # Everything: lint, compile checks, and tests
Test Layout
crates/*/src/ # Inline #[cfg(test)] modules
crates/*/tests/ # Rust integration tests
python/openshell/ # Python unit tests (*_test.py suffix)
e2e/python/ # Python E2E tests (test_*.py prefix)
e2e/rust/ # Rust CLI E2E tests
tests/testkit/ # Support library for SDK-driven E2E suites
tests/suites/ # E2E suites, grouped by test family
tests/tmachine/ # QEMU test machines, Ansible playbooks, and the Session API
tests/runner/ # openshell-test-runner: runs a test family in a tmachine environment
Rust Tests
Unit tests live inline with #[cfg(test)] mod tests blocks. Integration tests
go in crates/*/tests/ and are named *_integration.rs.
Use #[tokio::test] for anything async:
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn store_round_trip() {
let store = Store::connect("sqlite::memory:").await.unwrap();
store.put("sandbox", "abc", "my-sandbox", b"payload").await.unwrap();
let record = store.get("sandbox", "abc").await.unwrap().unwrap();
assert_eq!(record.payload, b"payload");
}
}
Run Rust tests only:
mise run test:rust # cargo test --workspace
Rust validation checks tracked Cargo lockfiles; run mise run rust:lockfiles:check to check them directly. If one is stale, refresh it with Cargo using its adjacent manifest, review the diff, and commit the update.
PostgreSQL-backed tests
Tests that need a real PostgreSQL server, such as advisory-lock concurrency
across two stores, are #[ignore]d and named postgres_*. Run them with:
mise run test:rust:postgres
The task starts a disposable PostgreSQL container with Docker or Podman
(set CONTAINER_ENGINE to choose), runs the tests one at a time, and removes
the container. Each test works in its own temporary schema, except the replay
and provider refresh lock tests, which use the default schema. To use your own
disposable database, set OPENSHELL_TEST_POSTGRES_URL. The task overrides
OPENSHELL_REPLAY_TEST_DATABASE_URL so legacy tests use that same database.
Never point it at a database that a running gateway uses: the tests take
fleet-wide advisory locks.
Branch Checks runs them on x86_64 Linux.
Load-sensitive capacity checks are named bench_postgres_* instead, so that
task skips them. Run them on an idle machine with
mise run test:rust:postgres:bench.
Native Windows validation
Start with the Windows build prerequisites in a checkout
based on windows. Run the lane matching the native host:
# Native x64 host
mise run --skip-tools windows:lint:x64
mise run --skip-tools windows:build:x64
mise run --skip-tools windows:test:x64
mise run --skip-tools windows:test:unsupported:x64
# Native ARM64 host
mise run --skip-tools windows:lint:arm64
mise run --skip-tools windows:build:arm64
mise run --skip-tools windows:test:arm64
mise run --skip-tools windows:test:unsupported:arm64
The test tasks use the CI nextest profile and server test support. They reject
a target that does not match the host architecture. windows:ci is the x64-host
aggregate, including ARM64 cross-check/build unless explicitly skipped; it
does not establish native ARM64 test coverage.
For runtime validation, provision a capable MXC host and build its native release binaries first. On x64, run:
mise run --skip-tools windows:test:mxc-real:x64
mise run --skip-tools windows:e2e:mxc
Use windows:test:mxc-real:arm64 on ARM64. The real-runtime unit tests can
report SKIP when the executor or backend is absent; E2E returns nonzero
when required runtime coverage is missing. Report passed, failed, and
skipped/ignored executions separately, including why any probe gated coverage.
Passing E2E does not qualify the known Windows enforcement gaps.
See the Windows test task definitions
for boundary, loopback, network, and audit suites.
Use mise run --skip-tools pre-commit with the existing Rust/MSVC toolchain.
Windows now checks tracked Cargo lockfiles through PowerShell rather than
skipping them.
mise run --skip-tools sdk:ts:ci selects the x64 Biome executable on Windows
(including ARM64 hosts running it under emulation), resolves the protobuf
plugin through its Windows .cmd launcher, and installs the matching locked
ARM64 Rolldown binding when Node itself is ARM64. The helper preserves lockfile
resolution; it must not upgrade unrelated test dependencies.
mise run --skip-tools go:ci retains race detection except on Windows ARM64,
where Go does not support it. Windows token-file tests explicitly skip POSIX
mode-bit assertions; those skips do not establish Windows ACL protection.
Use a checkout with LF text files when running Unix-shell fixture checks.
Python Unit Tests
Python unit tests use the *_test.py suffix convention (not test_* prefix)
and live alongside the source in python/openshell/. They use mock-based
patterns with fake gRPC stubs:
def test_exec_python_serializes_callable_payload() -> None:
stub = _FakeStub()
client = _client_with_fake_stub(stub)
def add(a: int, b: int) -> int:
return a + b
result = client.exec_python("sandbox-1", add, args=(2, 3))
assert result.exit_code == 0
Run Python unit tests only:
mise run test:python # uv run pytest python/
E2E Tests
E2E tests run against a live gateway. By default, mise run e2e starts an
ephemeral standalone gateway with the Docker compute driver, runs the suite,
and cleans it up afterward. To run the suite against an existing plaintext
gateway, set OPENSHELL_GATEWAY_ENDPOINT:
OPENSHELL_GATEWAY_ENDPOINT=http://127.0.0.1:18080 mise run e2e
Raw endpoint mode is HTTP-only. Use a named gateway config when a gateway requires mTLS.
mise run test:gateway-config validates generated gateway TOML without a live
runtime. It covers the current schema and the Podman in-tree versus external
driver configuration boundary.
testkit (tests/testkit/)
testkit is the support library for SDK-driven end-to-end suites. Tests are
#[testkit::test] functions with a required doc comment. They find the
gateway the same way the CLI does, through OPENSHELL_GATEWAY_ENDPOINT,
OPENSHELL_GATEWAY, or the active gateway, and cx.cli runs the CLI named
by OPENSHELL_BIN. See
tests/testkit/README.md for how to write and run
tests.
Suites live under tests/suites/<family>/. Each family is a Cargo workspace
with its own lockfile. Run the conformance suites against a local Docker
gateway with:
mise run e2e:testkit
Workspaces that build root crates through path dependencies must lock shared
crates at the versions in the root Cargo.lock; otherwise CI can build against
dependencies the Nix toolchain doesn't support. mise run rust:lockfiles:check
verifies this, and uv run tasks/scripts/check_cargo_lockfile_alignment.py --fix
re-locks a workspace from the root lockfile. Add new testkit workspaces to
ALIGNED_WORKSPACES in that script.
Running suites in a tmachine environment (tests/runner/)
openshell-test-runner runs a test family inside a tmachine VM, against the
OpenShell that the VM's installer set up.
nix run .#openshell-test-runner -- \
--environment ubuntu-docker-rootful --installer deb \
--family conformance --package sandbox
It needs KVM, and tmachine only boots guests of the host's architecture. The
runner points the tests at the installed CLI and the gateway the installer
registered (--gateway, default openshell). A run passes if nextest
succeeds; nextest fails a run with no tests. The runner exits with 1 when a
test failed and 2 when the run could not complete, for example because the
environment didn't come up. It writes junit.xml, nextest.log and, after a
failure, the guest's diagnostics to artifacts/test-results/ (--results),
even when the run timed out.
--filter <filterset> narrows the tests with a nextest filterset, and
--keep leaves the VM running until you press Enter. Two runs on one host
need different TMACHINE_SSH_PORT values (default 2222). The installer
inputs, such as the Debian package and the images, come from artifacts/,
built by nix run .#build-artifacts or downloaded by CI.
A flaky or broken test is marked #[ignore = "<issue link>"], which takes it
out of every environment until it's fixed.
CI. .github/workflows/e2e-suites.yml lists the environments and suites as
an explicit matrix and runs the runner once per row. Branch E2E calls it when
the test:e2e label is set, and a failure fails the Core E2E result. To run
a suite in another environment on CI, add a row.
Python E2E (e2e/python/)
mise run e2e:python builds openshell/e2e-python:dev from
e2e/python/Dockerfile.workload and selects it only for the test gateway.
This Noble-based fixture supplies the sandbox user, Python tooling, Git,
and a writable /sandbox/.venv. Its Python version comes from .python-version
so cloudpickle code objects match the test runner. The production workload
default remains the unmodified NVIDIA Ubuntu image.
The Rust Docker harness also selects this fixture for tests that need tools
or the named user. Explicit --from or --template arguments and the default-image
tests retain their own image selection. Docker sandbox and support-container
fixtures use the fixed image name; no workload-image override is needed.
Podman, VM, and Kubernetes retain their existing pinned, pullable fixture and
image setup. Conformance-only runs do not need the Docker fixture.
Build the Docker fixture separately with mise run e2e:workload:build.
Tests use the sandbox fixture from conftest.py to create real sandboxes:
def test_exec_returns_stdout(sandbox):
with sandbox(delete_on_exit=True) as sb:
result = sb.exec(["echo", "hello"])
assert result.exit_code == 0
assert "hello" in result.stdout
Sandbox.exec_python
exec_python serializes a Python callable with cloudpickle, sends it to the
sandbox, and returns the result. Because cloudpickle serializes module-level
functions by reference (which fails inside the sandbox), use one of these
patterns:
Closures from factory functions:
def _make_adder():
def add(a, b):
return a + b
return add
def test_addition(sandbox):
with sandbox(delete_on_exit=True) as sb:
result = sb.exec_python(_make_adder(), args=(2, 3))
assert result.stdout.strip() == "5"
Bound methods on local classes:
def test_multiply(sandbox):
class Calculator:
def multiply(self, a, b):
return a * b
with sandbox(delete_on_exit=True) as sb:
result = sb.exec_python(Calculator().multiply, args=(6, 7))
assert result.stdout.strip() == "42"
Shared Fixtures (e2e/python/conftest.py)
| Fixture | Scope | Purpose |
|---|---|---|
sandbox_client |
session | gRPC client connected to the active gateway |
sandbox |
function | Factory returning a Sandbox context manager |
Rust CLI E2E (e2e/rust/)
Rust-based e2e tests that exercise the openshell CLI binary as a subprocess.
They live in the openshell-e2e crate and use a shared harness for sandbox
lifecycle management, output parsing, and cleanup.
Exposed service URLs use virtual hostnames for gateway routing. Host-side tests
must connect the TCP socket directly to a reachable gateway listener address,
normally loopback, and send the service URL authority in the HTTP Host
header. Do not resolve *.openshell.localhost; resolver support for arbitrary
.localhost subdomains varies across local and CI environments.
Treat the advertised service URL scheme as authoritative. For HTTPS, use the
virtual service hostname for TLS SNI and the configured gateway trust roots.
When the listener requires mTLS, present the active gateway client identity;
the local e2e wrappers register these materials under
$XDG_CONFIG_HOME/openshell/gateways/$OPENSHELL_GATEWAY/mtls/. Do not downgrade
an HTTPS service URL to plaintext when dialing loopback. Parse the URL and load
TLS material before entering a readiness loop so permanent configuration
errors fail immediately. Retry only transient connection failures and
documented readiness responses, and include the last observation in timeout
diagnostics.
Verify exposed-service tests in both the default local mode and the CI-equivalent HTTPS mode:
mise run e2e:rust
OPENSHELL_ENABLE_LOOPBACK_SERVICE_HTTP=false mise run e2e:rust
When more than one test needs this behavior, put the transport in the shared
Rust e2e harness and require callers to use it instead of duplicating DNS,
HTTP Host, TLS SNI, and mTLS handling.
Suites:
- Common suite (
--features e2e) - driver-neutral CLI behavior, sandbox lifecycle, sync, port forwarding, policy, and provider tests. - CLI conformance (
tests/suites/conformance) - portable Cargo tests for lifecycle, mechanistic drafts, file transfer, and the sandbox-local API, including agent-authored permission requests. Driver E2E runs the complete Cargo test package. The installed-artifact conformance suite runs the same tests from a nextest archive and offers a focusedpolicy-advisortestsuite for manual integration runs. - Driver suites (
--features e2e-docker,e2e-podman,e2e-kubernetes, ore2e-vm) - CLI conformance plus the common and driver-specific coverage for the selected deployment. - Docker suite (
--features e2e-docker) - includes Docker-only coverage such as Dockerfile image builds, Docker preflight checks, and managed Docker gateway start. - Docker GPU suite (
--features e2e-docker-gpu) - Docker suite plus GPU sandbox smoke coverage. - Configuration push suite (
--features e2e-config-push) - CLI conformance and the policy tests against a Docker gateway withconfig_delivery_mode = "push". Run it withmise run e2e:rust:push. - VM suite (
--features e2e-vm) - runs e2e tests on a VM. - Kubernetes credential-driver suite (
--features e2e-kubernetes-credential-drivers) - targeted Kubernetes Secrets and Vault provider credential storage coverage.
The Docker, Podman, Kubernetes, and VM runners execute CLI
conformance before their Rust tests. Set the driver's feature variable
(OPENSHELL_E2E_DOCKER_FEATURES, OPENSHELL_E2E_PODMAN_FEATURES,
OPENSHELL_E2E_KUBERNETES_FEATURES, or OPENSHELL_E2E_VM_FEATURES) to
e2e-conformance-only to skip the Rust tests; the runner logs a notice when
it skips them. Unset or empty values select the driver's default Rust
features. The selector is handled by the runner; it is not a Cargo feature.
OPENSHELL_E2E_KUBERNETES_FEATURES=e2e-conformance-only mise run e2e:kubernetes
GPU device-selection tests compare OpenShell sandboxes against a plain Docker or
Podman container that requests --device nvidia.com/gpu=all. The probe image
defaults to the image used by the gateway stage in
deploy/docker/Dockerfile.images; set OPENSHELL_E2E_GPU_PROBE_IMAGE to
override it. Per-device checks run only for NVIDIA CDI device IDs reported by
the runtime's discovered devices list, so WSL2 hosts that expose only
nvidia.com/gpu=all skip the index-based cases. Exact CDI device selection is
passed through --driver-config-json with the active Docker or Podman driver
key.
Run the Docker-backed Rust CLI e2e suite:
mise run e2e:docker
Run the portable CLI conformance suite against the gateway selected in your OpenShell CLI configuration:
cargo build --package openshell-cli
OPENSHELL_BIN="$PWD/target/debug/openshell" \
cargo test \
--locked \
--manifest-path tests/suites/conformance/Cargo.toml \
--package openshell-test-conformance-cli \
--no-fail-fast \
-- \
--test-threads=1 \
--nocapture
The gateway must already be installed, reachable, and selected before the tests
start. The test suite does not provision a gateway or select a compute driver.
Set OPENSHELL_BIN to another executable to test a different prebuilt CLI.
The phase-1 scenario verifies the complete CLI-to-gateway-to-driver path without
depending on how the gateway was installed or which driver is configured. It
requires machine-readable gRPC status, creates a uniquely named detached
sandbox with the configured default image, verifies the sandbox is Ready by finding its
unique name in paginated JSON list output, executes echo with a run-specific
marker, deletes the sandbox, and verifies that its name no longer appears.
Driver suites enable the same profile
instead of maintaining a separate smoke implementation. Sandbox lifecycle,
label matrices, VM overlay, and TLS-key permission assertions remain regular
E2E coverage.
Each invocation prints a ten-character run ID before creating resources.
Conformance sandboxes use names such as ct-<run-id>-01. The runner tracks the
exact name and uses it for cleanup; phase 1 does not add ownership labels.
The runner deletes owned resources after both success and failure. If the test process is interrupted before cleanup, locate leftovers without touching unrelated gateway state:
openshell sandbox list --output json
openshell sandbox delete <sandbox-name>
Gateway-backed Rust E2E tasks run the CLI conformance Cargo tests against the configured gateway, then run any lane-specific Rust tests that still apply. Run the Podman-backed Rust CLI e2e suite:
mise run e2e:podman
The nix run commands below need the local Nix setup in
CONTRIBUTING.md.
nix run .#build-artifacts also builds
artifacts/packages/openshell.deb for tmachine's deb installer, targeting the
host's Linux architecture (arm64 or amd64). Set OPENSHELL_DEB_VERSION to
override the default package version, 0.0.0-local.
It also builds artifacts/packages/rpm/openshell.rpm and
artifacts/packages/rpm/openshell-gateway.rpm for tmachine's rpm installer.
The RPM step runs packit build locally with openshell.spec in the Fedora
container that CI uses, so it needs a Docker daemon that runs Linux containers
for the host's architecture (aarch64 or x86_64), such as Docker Desktop on
macOS. The package version defaults to the development version that branch CI
computes; set OPENSHELL_RPM_VERSION and OPENSHELL_RPM_RELEASE to override
it. After nix run .#build-artifacts-binaries, nix run .#build-artifacts-rpm
rebuilds only the RPMs. Run conformance against the installed RPMs:
nix run .#build-artifacts
nix run .#tmachine -- test fedora-podman-rootful rpm conformance
Run the portable subset in a disposable rootless Podman guest:
nix run .#build-artifacts
nix run .#tmachine -- test fedora-podman-rootless binaries e2e-podman
nix run .#tmachine -- test fedora-podman-rootless binaries driver-podman
Print the exact tmachine archive selection as a shell PODMAN_CI_TESTS array:
nix run .#generate-podman-e2e-ci-tests
The e2e-podman testsuite runs a nextest archive built with the corresponding
Rust feature and preloads its Python workload image into the rootless Podman
store. The separate driver-podman testsuite compares OpenShell and direct
Podman user-namespace mappings for the default, auto, keep-id, and private
profiles. The E2E archive excludes binaries that still depend on wrapper-owned
gateway controls, SPIFFE fixtures, the caller driver-config setting, or other
guest setup. The driver-podman suite replaces the removed podman_userns E2E
binary. tests/tmachine/artifacts.nix keeps the follow-up exclusions explicit and uses
the same filter for the generated inventory, so excluded binaries cannot appear
as false passes or silently re-enter the archive.
The oci-image feature testsuite (tests/suites/features/oci-image) checks
OCI image identity and working-directory behavior shared by the Docker and
Podman drivers against installed deb packages on Ubuntu and rpm packages on
Fedora. CI runs it on Docker rootful, Podman rootful, and Podman rootless guests:
nix run .#tmachine -- test ubuntu-docker-rootful deb oci-image
The shared suite also runs in the existing ARM64 Docker E2E workflow for
branch checks and both release workflows. That interim lane compiles the same
test crate natively and uses the architecture-matched prebuilt candidate CLI
and gateway plus the candidate multi-architecture sandbox and supervisor
images through e2e/with-docker-gateway.sh. The tmachine lanes still use
x86_64 artifacts; they do not replace ARM64 image coverage.
Run it against a local Docker or Podman gateway:
mise run e2e:oci-image
mise run e2e:oci-image:podman
Both tasks set OPENSHELL_TEST_CONTAINER_ENGINE, the command that builds test
images into the gateway's image store.
Run the VM-backed Rust CLI e2e suite:
mise run e2e:vm
Run the targeted Kubernetes credential-driver e2e suite. This deploys an OpenBao fixture for the Vault-compatible driver path and validates Kubernetes Secrets and Vault storage backends one at a time:
mise run e2e:kubernetes:credential-drivers
Kubernetes E2E (e2e/rust/e2e-kubernetes.sh)
The e2e-kubernetes nextest profile temporarily excludes only
canonical_main_connect_recovers_its_ssh_transport while
#4445 investigates delayed SSH
input-owner release through port-forwarding. The test remains available to other
driver profiles; the remaining Kubernetes tests stay enabled.
Kubernetes e2e tests deploy an OpenShell gateway into a real Kubernetes cluster
via Helm and run the Rust e2e suite against it. On vanilla Kubernetes the harness
reaches the gateway through kubectl port-forward; on OpenShift it instead uses a
passthrough Route secured with mandatory mTLS (see the OpenShift note below).
Run with an ephemeral k3d cluster (macOS; created and torn down automatically):
mise run e2e:kubernetes
Target an existing cluster (kind, k3d, or OpenShift):
OPENSHELL_E2E_KUBE_CONTEXT=my-context mise run e2e:kubernetes
Scope to a single test for local debugging:
OPENSHELL_E2E_KUBE_TEST=smoke mise run e2e:kubernetes
OpenShift: when the target cluster exposes the route.openshift.io API
group, the harness automatically applies SCC-compatible Helm overrides and
drives the gateway through a passthrough Route with mandatory mTLS instead of
port-forward. No extra flags are needed, but oc must be installed and
authenticated against the target cluster; the harness exits early if oc is
missing. DB scenarios grant anyuid to the PostgreSQL fixture and require
permission to modify SCC bindings (oc adm policy add-scc-to-user). That grant
and extracted client mTLS material are removed during cleanup, including on
failure or interrupt.
Sandbox pods use the namespace-assigned UID/GID and run under restricted-v2
without a privileged SCC grant to openshell-sandbox. Earlier versions required
that grant for in-container isolation setup. RFC 0012
explains the isolation backend separation behind the capability-free runtime.
On a remote cluster, drop the e2e-host-gateway feature. Those tests rely
on the sandbox-side host.openshell.internal alias reaching the machine running
the tests, which is unreachable from pods on a remote cluster, so they fail.
Left enabled, the host_gateway_alias suite fails because
host.openshell.internal does not resolve inside the pod, so the gateway
SSRF-denies the request (DNS resolution failed / ssrf_denied) — a networking
property of remote pods, not a gateway or transport fault. Override
OPENSHELL_E2E_KUBERNETES_FEATURES to exclude it:
OPENSHELL_E2E_KUBE_CONTEXT=$(oc config current-context) \
OPENSHELL_E2E_KUBERNETES_FEATURES="e2e,e2e-kubernetes" \
mise run e2e:kubernetes
On an existing cluster the harness builds the CLI from your branch but pulls the
published gateway/supervisor image (default tag latest). The CLI and the
image can therefore be different versions. If tests fail because of this version
difference — for example, sandbox tests fail with Pod exists with phase: Failed
or connect-based tests stall because the deployed image predates a feature your
branch CLI needs — set IMAGE_TAG to an image that matches your branch.
The latest tag lags to the last semver release, so it is often older than
main. Two better choices:
IMAGE_TAG=dev— a floating tag that tracks the latestmainbuild. Good for an ad-hoc run when your branch is close tomainHEAD. Because it floats, two runs on different days can pull different images, so it is not reproducible.- Pin the exact commit your branch is based on — deterministic and immune to
a floating tag moving. Published tags are the full 40-char git SHA (semver tags
without a
vprefix also exist but only for released versions):
OPENSHELL_E2E_KUBE_CONTEXT=$(oc config current-context) \
OPENSHELL_E2E_KUBERNETES_FEATURES="e2e,e2e-kubernetes" \
IMAGE_TAG=$(git rev-parse "$(git merge-base HEAD upstream/main)") \
mise run e2e:kubernetes
To pin a specific released version, use its semver tag without a v prefix
(0.0.115, not v0.0.115):
OPENSHELL_E2E_KUBE_CONTEXT=$(oc config current-context) \
OPENSHELL_E2E_KUBERNETES_FEATURES="e2e,e2e-kubernetes" \
IMAGE_TAG=0.0.115 \
mise run e2e:kubernetes
A semver tag matches a released commit, which may be behind main; if your
branch CLI needs a newer feature, pin the SHA of your branch's base instead.
Confirm a tag exists before relying on it (set TAG to the tag you plan to use):
TAG=0.0.115
skopeo inspect "docker://ghcr.io/nvidia/openshell/gateway:${TAG}"
IMAGE_TAG sets the default tag for the gateway/supervisor image pair; the CLI
under test is always built from your branch. To validate against images from
your exact commit instead, build and push them and point
OPENSHELL_REGISTRY/IMAGE_TAG at them.
Test wrappers accept independent image overrides:
GATEWAY_IMAGE=registry.example.com/custom/gateway:test \
SUPERVISOR_IMAGE=registry.example.com/custom/supervisor:test \
SANDBOX_IMAGE=registry.example.com/custom/sandbox:test \
mise run e2e:kubernetes
GATEWAY_IMAGE applies to the Kubernetes gateway container. SUPERVISOR_IMAGE
applies to the trusted supervisor image selected by the Kubernetes, Docker, and
Podman wrappers. SANDBOX_IMAGE applies to the trusted workload-side runtime
image that stages the openshell-sandbox binary. A repository-only value
inherits IMAGE_TAG; a value with an explicit tag or @sha256: digest is used
as-is. When these variables are unset, the existing OPENSHELL_REGISTRY plus
IMAGE_TAG behavior is retained.
The Docker and Podman wrappers continue to give
OPENSHELL_DOCKER_SUPERVISOR_IMAGE and OPENSHELL_SUPERVISOR_IMAGE precedence
over SUPERVISOR_IMAGE.
Digest-pinned Kubernetes overrides require disabling local image builds, because Docker cannot tag a locally built image with a digest reference:
OPENSHELL_E2E_KUBE_BUILD_IMAGES=0 \
GATEWAY_IMAGE=registry.example.com/custom/gateway@sha256:<digest> \
SUPERVISOR_IMAGE=registry.example.com/custom/supervisor@sha256:<digest> \
mise run e2e:kubernetes
Available task variants:
| Task | Purpose |
|---|---|
e2e:kubernetes |
Default Rust e2e against Helm-deployed gateway |
e2e:kubernetes:db |
All database backend scenarios (SQLite + external PostgreSQL) |
e2e:kubernetes:sidecar |
Supervisor sidecar topology overlay |
e2e:kubernetes:credential-drivers |
Kubernetes Secrets and Vault credential storage |
e2e:kubernetes:workspace-managed |
Managed workspace mode (auto-created namespaces) |
e2e:kubernetes:workspace-operator |
Operator workspace mode (pre-provisioned namespaces) |
e2e:kubernetes:v1alpha1 |
Agent Sandbox v1alpha1 compatibility |
e2e:kubernetes:external-driver |
External Kubernetes driver sidecar |
e2e:kubernetes:shared-supervisor |
One-member shared supervisor pool for the gateway's sandboxes (gateway TLS through the port-forward) |
e2e:kubernetes:shared-supervisor-ha |
Two-replica shared supervisor pool: kills the member that owns a sandbox and checks the sandbox is adopted, not restarted |
e2e:kubernetes:shared-supervisor-scaling |
One-member shared supervisor pool: scales it to two members and back with helm upgrade, checking sandboxes stay running and the removed member hands its sandboxes off |
Kubernetes e2e environment variables:
| Variable | Purpose |
|---|---|
OPENSHELL_E2E_KUBE_CONTEXT |
kubectl context for an existing cluster (skips k3d creation) |
OPENSHELL_E2E_KUBE_TEST |
Scope to a single test (e.g. smoke) |
OPENSHELL_E2E_KUBE_EXTRA_VALUES |
Colon-separated additional Helm values files |
OPENSHELL_E2E_KUBE_CHART, OPENSHELL_E2E_KUBECONFIG |
Set by the harness for tests that upgrade the release, such as the supervisor pool scaling test: the chart it installed, and the kubeconfig passed as --kubeconfig to kubectl and helm. Set them yourself when running such a test directly against an existing release |
OPENSHELL_E2E_KUBE_TLS |
1 reaches the gateway over mTLS through the port-forward; pair it with an overlay that keeps gateway TLS on, such as ci/values-tls-port-forward.yaml |
OPENSHELL_E2E_KUBERNETES_FEATURES |
Cargo feature flags (default: e2e,e2e-host-gateway,e2e-kubernetes) |
IMAGE_TAG |
Gateway/supervisor image tag (default: latest for existing clusters) |
OPENSHELL_REGISTRY |
Image registry prefix (default: ghcr.io/nvidia/openshell) |
GATEWAY_IMAGE |
Kubernetes gateway image repository or complete tagged/digest-pinned image reference; digests require OPENSHELL_E2E_KUBE_BUILD_IMAGES=0 |
SUPERVISOR_IMAGE |
Gateway/supervisor image repository or complete tagged/digest-pinned image reference; Kubernetes digests require OPENSHELL_E2E_KUBE_BUILD_IMAGES=0 |
SANDBOX_IMAGE |
Trusted sandbox runtime image repository or complete tagged/digest-pinned image reference |
Run a single test directly with cargo:
cargo test --manifest-path e2e/rust/Cargo.toml --features e2e --test sync
Run a single Docker-only test directly with cargo:
cargo test --manifest-path e2e/rust/Cargo.toml --features e2e-docker --test docker_preflight
The harness (e2e/rust/src/harness/) provides:
| Module | Purpose |
|---|---|
binary |
Builds and resolves the openshell binary from the workspace |
container |
Container-engine selection and support containers for proxy tests |
gateway |
Managed gateway restart controls for gateway-owned e2e runs |
sandbox |
SandboxGuard RAII type — creates sandboxes and deletes them on drop |
output |
ANSI stripping and field extraction from CLI output |
port |
wait_for_port() and find_free_port() for TCP testing |
Environment Variables
| Variable | Purpose |
|---|---|
OPENSHELL_GATEWAY |
Override active gateway name for E2E tests |
OPENSHELL_GATEWAY_ENDPOINT |
Run E2E tests against an existing plaintext HTTP gateway endpoint |
OPENSHELL_E2E_DRIVER |
Driver name exported by the e2e gateway wrapper (docker, podman, or vm) |
OPENSHELL_E2E_CREDENTIAL_DRIVERS |
Enables the Kubernetes credential-driver fixture path in e2e/with-kube-gateway.sh |
OPENSHELL_E2E_KUBE_CONTEXT |
kubectl context for Kubernetes e2e (skips ephemeral k3d) |
OPENSHELL_E2E_KUBE_TEST |
Scope Kubernetes e2e to a single test by name |