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, which unotest maintains with you:
| 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. |
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.
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
UNOTEST_BOX_URL=https://tests.example.com \UNOTEST_BOX_TOKEN=unob_… \npx @unotest/web env push devdev 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.
env push replaces the three layers: a name your files no longer carry
is removed from the box, and the command says which. Values set by hand
(env set, the values page) are part of what gets replaced.
A value is never printed — not on success, not with --json, not in
--dry-run. Names are.
env set, env rm — one value
printf '%s' "$GEMINI_API_KEY" | npx @unotest/web env set dev GEMINI_API_KEY --secretprintf '%s' 'eu-west' | npx @unotest/web env set dev REGIONprintf '%s' 'https://pr-42.preview.example' | npx @unotest/web env set dev APP_BASE_URLnpx @unotest/web env rm dev REGIONnpx @unotest/web env rm dev APP_BASE_URL # back to the configured targetThe value comes from stdin, never from the command line, so it does not
land in shell history or ps. --secret makes it a secret;
APP_BASE_URL is always the target.
env set has no name filter: unlike env push, it accepts UNOTEST_*
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:
printf '%s' local | npx @unotest/web env set dev UNOTEST_JUDGE_MODEnpx @unotest/web env set dev UNOTEST_JUDGE_PREAMBLE < preamble.txtThe only name the box refuses is APP_BASE_URL as a plain variable — it is
the environment’s target, set as shown above. 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.