Tests reach a box in one direction only: you push them. A box holds no deploy key and no token to anybody’s repository and never pulls, so nothing is on it that was not pushed to it.
npx @unotest/web bundle push --box https://tests.example.com --token unob_…UNOTEST_BOX_URL and UNOTEST_BOX_TOKEN supply the two values instead of
flags — a CI job usually takes them from its secret store.
What a bundle is
bundle push packs the unotest/ directory — the suite package: its
scenarios, helpers, collections, unotest.config, package.json and
lockfile — into one tar.gz and uploads it. The product around it never
travels, and neither do its dependencies: the box runs npm ci over the
suite’s own manifest.
- In a git project, git decides what belongs. The file list is
git ls-files --cached --others --exclude-standard, so whatever your.gitignorekeeps out stays out. Content is read from the working tree, so uncommitted changes travel — the manifest recordsdirty: trueand the viewer labels the bundlewip. - Secrets never travel.
unotest/.env,.env.<name>,.secrets,.secrets.<name>— and any<name>.env/<name>.secrets— are excluded even when committed. A file that looks like one and still reaches the packer stops the push. Environment values live on the box; see Environments & values. - Also excluded:
node_modules,.git, run and queue directories,.unotest/,.tmp,.debug, the viewer lock file. - Install hooks are stripped from the packed
package.json(postinstall,prepare, …): a box installs the suite, it does not run your project’s hooks. The push reports what it removed. - Your dependencies’ install scripts run on the box, so a native
module builds or fetches its binary as usual. They run in a container
of their own — the bundle tree is its only writable mount, and none of
the box’s state is in reach — which is what makes running somebody’s
postinstallon a shared machine affordable. - The id is the content.
bundleIdis a hash of the packed files, so re-pushing an unchanged suite is an instant no-op (already had this exact bundle), and two people packing the same tree get the same id. - Names of externals travel, values do not. The bundle carries the
names your
.env/.secretsfiles (or their.exampletwins) declare, so the box can answer the push with what an environment cannot supply.
Refused before upload
What would fail on the box is refused on your machine, with the file, the line and the fix:
- a scenario that reads a file outside
unotest/(MODULE_NOT_FOUNDon the box, hours later); - no lockfile, or no
@unotest/webin the suite’s dependencies; - a symlink;
- a secrets file inside
unotest/; - a suite over 256 MB;
- a schedules registry that cannot be read — the box reads it to know what runs on its own.
--dry-run packs and checks without uploading; --out <file> keeps the
archive (tar -tzf shows what is inside); --json prints
{ bundleId, status }.
Refused by the box
The box verifies the archive before writing a single file and answers with a typed refusal; the CLI prints it with what to do next:
| Refusal | Meaning |
|---|---|
unauthorized | The token is unknown, revoked, or for another box. |
min-version | The bundle pins an @unotest/web older than the box runs. Bump it in unotest/package.json, commit the lockfile, push again. |
scope-mismatch | This repository belongs to another project on the box than the token’s. |
too-large | Over the box’s upload limit (128 MB by default). |
malformed / id-mismatch | The upload did not arrive intact — retry; if it repeats, a proxy is rewriting the body. |
The token alone decides which project a bundle lands in. Nothing in the request can name one.
After a successful push the box may add notes: environments that hold no value for a name the suite declares, with the command that sets it.
Running after a push
npx @unotest/web bundle push --run --env test --collection smoke--run orders a run of the bundle just pushed, on the same token:
--env <name>is required — which environments a box has is the operator’s decision.--collection <name>names what to run; repeat for several. Without it the box runs what that environment’s schedules name.--pr <number>ties the push to a pull request. The queue shows it (PR #412@1a2b3c4), and a newer push of the same pull request withdraws its older runs that are still waiting; a run that already started is never killed.
The box answers 202 as soon as the runs are queued, with their run
ids — a suite takes minutes, and a request held open that long is a
timeout, not a result. The ids are real from that moment; --json carries
them (runs: [{ runId, collection }], plus superseded when older runs
were withdrawn), so a CI log can print links into the viewer at once.
Run requests are rate-limited (60 per minute per box, and per token).
Refusals: unknown-environment, scope-mismatch (the environment belongs
to another project), unknown-bundle (the box no longer holds it — push
again), no-collections (nothing named and no schedule for that
environment), malformed (the box is older than this CLI — push without
--run).
A minimal CI job
jobs: e2e: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 20 } - run: npx @unotest/web bundle push --run --env test --collection smoke --json env: UNOTEST_BOX_URL: ${{ secrets.UNOTEST_BOX_URL }} UNOTEST_BOX_TOKEN: ${{ secrets.UNOTEST_BOX_TOKEN }}Exit codes: 0 pushed (and queued), 1 the box refused or was
unreachable, 2 the suite was refused on your machine — nothing left it.
With --json, a failed run order after a successful upload still reports
the bundle, so the job does not push it again for nothing.
From a GitHub webhook
With the box’s GitHub App installed (see Administration), the box also reacts to your repository’s events. Every mapping lives on the box, never in the repository:
- a
deploymentevent’s environment name is mapped to one of the box’s environments; an unmapped name is logged and dropped, never guessed; - pull requests run in a named environment, or each in its own preview environment (below), running the collections the project’s CI configuration names or the environment’s schedules;
- the bundle is matched by commit sha against what has been pushed. A commit no bundle was pushed for waits up to 15 minutes for your CI job to upload, then the check fails saying so;
- fork pull requests are not run. Their head is somebody else’s branch, and running it would hand it the environment’s secrets. A maintainer re-requesting the check in GitHub is the opt-in.
The box reports one check per collection (unotest / <collection>):
queued when the event arrives, in progress when the run takes its slot,
completed with a summary and a link into the viewer.
Pull-request environments
A project with preview deployments can give each pull request an environment of its own. unotest configures a template with you:
- the target URL shape, with
{pr}where the number goes (https://pr-{pr}.preview.acme.dev) — the box substitutes, it never deploys; - variables every preview run gets;
- how many may exist at once (5 by default) and how long one survives after its last run (72 hours by default).
The environment is named pr-<n>, created when the pull request is first
seen and removed when it closes or goes unused past the TTL. Preview
environments learn the suite’s schedules but never arm them: what CI asks
for runs, the clock does not. Secrets are set per environment by an
administrator; a preview environment that needs one is not one that should
be created automatically.
Bundles in the viewer
On a box the viewer shows what it is running:
- Bundle badge in the status bar —
main@1a2b3c4, orwipfor a bundle from an uncommitted tree, with the author, the pinned@unotest/weband who switched the environment last. - Switching. Clicking the badge lists the bundles pushed to this
project and switches the environment to one. The switch is a maintenance
ticket: a running suite finishes, then the code changes, then the next
run starts on the new bundle. An environment with
allowSwitchoff refuses (follows its own branch — it cannot be switched by hand). - Following the branch. An environment follows the newest bundle of
its
defaultBranchuntil somebody selects one by hand; that selection survives restarts and later pushes until the next person switches. Schedules always run the newest bundle ofdefaultBranch, regardless of what anyone selected — a tester’s switch never redirects tonight’s suite. - Runs remember their bundle. The runs list marks runs that ran on another bundle, and a this bundle only filter narrows the history to the bundle in front of you.
- Project / environment switcher in the tree header, where a local viewer shows the checkout directory. Each environment is its own viewer process, so switching is a full page load.
Retention
Each push leaves an archive and, once installed, a node_modules. The box
keeps the newest 5 bundles per branch regardless of age and removes older
ones after 30 days (both configurable) — never a bundle an environment
currently points at or a waiting run has asked for.