Environments & values
For the complete documentation index, see llms.txtA suite’s externals — the URL it points at, non-secret settings, credentials
— live with the environment on the box, never in a bundle. The same
bundle runs in test and in prod, and a new bundle cannot reset an
environment’s configuration.
An environment
Each environment is declared in the box’s configuration, set when the box is
made: box create takes your project’s unotest/.env.<name> overlays (the
base unotest/.env is only the layer they share, not an environment of its
own, unless there is no overlay at all — then it is the one environment,
default; a public address belongs in an overlay such as
unotest/.env.staging), the dashboard asks for an address and the environment names on it, and a box
unotest provisions for you gets the list you agree on. The configuration lists
the environments; installing a bundle fills them — see
Overview → Projects and environments.
A pushed suite cannot create or rename one. Each environment has:
| Setting | Meaning |
|---|---|
name | Stable id (test, staging, prod). It names the run history, the queue and the viewer, so renaming one is renaming its history. |
target | The URL the tests run against, injected into every run as APP_BASE_URL — which unotest.config auto-wires baseUrl from. |
defaultBranch | Which branch’s newest bundle the environment follows for schedules and after a restart (main by default). |
allowSwitch | Whether anyone may point the environment at another bundle from the viewer. Off for environments where “what is deployed” is the point. |
concurrency | Runs at a time in this environment (1 by default). |
variables | Non-secret values every run gets. |
defaultBranch and allowSwitch meet in one place. Where switching is
allowed, picking a bundle in the viewer holds the environment on it: later
pushes to defaultBranch are stored but do not move it, and the bundle
badge names who holds it. Where the bundle menu offers follow
<branch> again, that lets go of the selection — the environment moves to
the branch’s newest bundle and follows it from then on. See
Bundles in the viewer.
Three layers of values
| Layer | What | Who sees the value |
|---|---|---|
| target | The URL, as APP_BASE_URL | everyone |
| variables | Non-secret values every run gets | administrators; the viewer’s Variables panel |
| secrets | Credentials | nobody — only a run’s process |
The configuration gives each environment its initial target and
variables. Everything set through the page or the commands below layers
over that: a value set on the box wins over the configuration, and a
secret wins over a variable of the same name. No restart is needed — a run
reads the current values when it starts.
What a run sees
For every run the box builds the process environment in this order, later entries winning:
- box policy (
UNOTEST_BROWSER_CHANNEL=bundled— the box uses the bundled browser); APP_BASE_URL= the target (an override if set, else the configured one);variablesfrom the configuration;variablesset on the box;secrets.
The viewer’s Variables panel lists the same names as read-only rows
with a box badge; a secret’s value is never sent to the browser, only its
name. Changing values restarts that environment’s viewer: the page reloads,
and a debug session open in it ends.
The values page
Administration → Environments → the environment. One table, one row per key:
| Row | What it shows |
|---|---|
APP_BASE_URL (target) | The URL, tagged config or override. |
| a variable | Its value, tagged config (from the configuration) or box (set here or by env push / env set). |
a secret (secret tag) | Dots. The value is not on the page. |
- Edit (pencil) opens the value in place; Enter or the check mark saves, Esc or the cross cancels. Overriding the target and reverting it (back to configured) work the same way.
- Remove (bin) is offered for what the box itself holds — a
boxvariable, a secret, a target override — and asks first. Aconfigvariable comes from the configuration and is not removable here. - The add row at the bottom:
NAME, value, a secret checkbox, Add. Ticked, the value goes into the secret store.
Each change is one audit event: env.value.set or env.value.removed,
carrying the name and never the value.
Revealing a secret
The eye asks the box for a secret’s value and shows it in the row (the eye becomes hide); the copy button puts it on the clipboard without showing it.
Every look is audited. Each reveal or copy is one secret.revealed
event with who, which secret(s), and how: show | copy. The answer is
served with Cache-Control: no-store, so a revealed value does not survive
in a browser cache. If you see reveals you did not expect in Recent
audit, that is the trail doing its job.
JSON view
The JSON switch shows the same rows as one document:
{ "APP_BASE_URL": "https://staging.acme.io", "REGION": "eu", "GEMINI_API_KEY": "••••"}Reveal all fills in the secrets (one audited look, listing every name). Edit makes the document editable; Save sends the whole environment at once; Cancel discards. On save:
APP_BASE_URLequal to the configured target, or absent, clears the override; any other URL sets one.- A secret left as
"••••"— or as the value Reveal all showed — is kept as it is. A different string replaces it. A removed key removes the secret. To add a secret, use the table’s add row. - Every other key is a variable: a new key becomes a variable, a removed one is removed.
- Anything that is not an object, a non-string value, or a name that could not be a variable is refused on the page; nothing is sent.
The batch is applied or refused as a whole (env.values.replaced in the
trail). A refusal from the box is shown as the box phrased it — for
instance cannot keep what the environment does not hold when the page
and the box disagree about which secrets exist.
Without JavaScript
Every row is an ordinary form: set / remove post it. Reveal, copy and the JSON view need the script.
A project’s environments come from the box’s configuration — or, on a box from 0.48.0, from its operator’s shell, without a restart: see Adding an environment from the box’s shell.
From the command line
The same values can be set from a laptop or a deploy job, with a project token minted for values (see Tokens).
env push — send your files
cd unotest && npx @unotest/web env push devThe box address and the token are resolved the way bundle push resolves
them: --box / --token first, then UNOTEST_BOX_URL /
UNOTEST_BOX_TOKEN, then the project’s own files — unotest/.env for the
address, unotest/.secrets for the token, where box create and
box access wrote them. In a project set up by either of those commands
there is nothing to pass; in CI, where those files are not checked in, the
two variables are the way.
dev is the local environment — the one --env dev uses — and the
command reads exactly what a local run reads: unotest/.env, .env.dev,
.secrets, .secrets.dev, later files winning. Then:
.env*→ the environment’svariables.secrets*→ itssecretsAPP_BASE_URL, from either → itstargetUNOTEST_*→ not sent, listed as skipped: they configure the runner on your machine- empty values → not sent, listed as skipped
The box environment is assumed to have the same name; --box-env <name>
says otherwise. --env-file / --secrets-file read named files instead
(repeat for layers; later files win). --dry-run reads and lists without
sending; --json prints the box’s answer.
From @unotest/web 0.48.0, env push merges into the three layers:
every name your files carry is set, and a name the box holds that your
files do not stays — a value set by hand (env set, the values page)
survives a push. The command lists those names as kept. --prune asks for
the other way: it removes what your files do not carry, the target
override included, and lists what went. UNOTEST_* names on the box are
never pruned — a push does not send them, so their absence from your files
says nothing. A box too old to merge refuses the push, changes nothing and
says to update it.
An earlier CLI replaces the three layers on every push: a name your files no longer carry is removed from the box, values set by hand included, and the command says which.
A secret’s value is never printed — not on success, not with --json,
not in --dry-run; where it would stand, the mask used everywhere else
(‹secret:NAME›) stands instead.
--dry-run does print what the overlays resolved each variable to,
quoted: the question that mode exists to answer is what would be sent, and
a value that kept its quotes (NAME="Two Words" in a .env file) or
picked up a trailing space is worth seeing before a box gets it.
--dry-run --json carries the same under values.
env set, env rm — one value
cd unotestnpx @unotest/web env set dev REGION eu-westnpx @unotest/web env set dev APP_BASE_URL https://pr-42.preview.exampleprintf '%s' "$GEMINI_API_KEY" | npx @unotest/web env set dev GEMINI_API_KEY --secretnpx @unotest/web env rm dev REGIONnpx @unotest/web env rm dev APP_BASE_URL # back to the configured targetThe value is the third word, or — when there is none — whatever arrives
on stdin. --secret makes it a secret; APP_BASE_URL is always the
target.
Pipe the secrets. An argument lands in the shell’s history and in ps,
which is the one thing a credential must not do, and a value beginning with
- reads as a flag. A value read from stdin loses one trailing newline —
the one echo adds — and nothing else.
env set has no UNOTEST_* filter: unlike env push, it accepts
them too. The asymmetry is deliberate — env push reads the files a local run
reads, and those UNOTEST_* configure the runner on your machine, so
sending them would be an accident. Naming one by hand is a decision, so it
is allowed. That is how a box gets the runner settings it does need, such
as UNOTEST_JUDGE_MODE and the judge’s key:
cd unotestnpx @unotest/web env set dev UNOTEST_JUDGE_MODE localnpx @unotest/web env set dev UNOTEST_JUDGE_PREAMBLE < preamble.txtAPP_BASE_URL is not a plain variable — it is the environment’s target,
set as shown above. Nor are the names the box sets on every run to say
which run it is — leave them to the box:
UNOTEST_RUN_ID, UNOTEST_ENV, UNOTEST_BUNDLE_ID, UNOTEST_EDITOR_TYPES,
UNOTEST_BOX_SECRET_NAMES, UNOTEST_QUEUE_LEASE, UNOTEST_RUN_TRIGGER,
UNOTEST_RUN_ACTOR. A value cannot be empty: the
box has no way to declare one, so use a non-empty sentinel and treat it as
the default in your scenario.
Tokens
The CLI authenticates with a project token (unob_…). Tokens are
issued per project by unotest and come in two scopes:
| Scope | May |
|---|---|
bundles | push bundles and order runs — what a CI job needs |
bundles+values | also write the project’s environment values |
A bundles token used with env push or env set is refused with
forbidden (this token pushes bundles only). That is deliberate: the
token in your CI secret store that pushes suites on every commit should
not also be able to rewrite an environment’s credentials.
The token decides the project; a command names only the environment. Nothing in a request can point it at another project’s values.
Where values go, and where they never go
- Into a run’s process — target, variables and secrets, in the order above. The runner masks secret values in logs and artifacts as it does locally.
- Into the viewer — names only, with a
boxbadge. - Over the wire from the CLI — values go in; no answer ever carries one back. Secrets are write-only on that door.
- Into the audit trail — names only, never a value.
- Into a bundle — never.
unotest/.env*and.secrets*are excluded frombundle pusheven when committed.
The one place a secret’s value is ever shown again is the values page, to
an administrator, with a secret.revealed event for each look.