This documentation is available as Markdown. For the complete index, see llms.txt. Skip to content

Environments & values

For the complete documentation index, see llms.txt

A 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:

SettingMeaning
nameStable id (test, staging, prod). It names the run history, the queue and the viewer, so renaming one is renaming its history.
targetThe URL the tests run against, injected into every run as APP_BASE_URL — which unotest.config auto-wires baseUrl from.
defaultBranchWhich branch’s newest bundle the environment follows for schedules and after a restart (main by default).
allowSwitchWhether anyone may point the environment at another bundle from the viewer. Off for environments where “what is deployed” is the point.
concurrencyRuns at a time in this environment (1 by default).
variablesNon-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

LayerWhatWho sees the value
targetThe URL, as APP_BASE_URLeveryone
variablesNon-secret values every run getsadministrators; the viewer’s Variables panel
secretsCredentialsnobody — 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:

  1. box policy (UNOTEST_BROWSER_CHANNEL=bundled — the box uses the bundled browser);
  2. APP_BASE_URL = the target (an override if set, else the configured one);
  3. variables from the configuration;
  4. variables set on the box;
  5. 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:

RowWhat it shows
APP_BASE_URL (target)The URL, tagged config or override.
a variableIts 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 box variable, a secret, a target override — and asks first. A config variable 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_URL equal 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

Terminal window
cd unotest && npx @unotest/web env push dev

The 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’s variables
  • .secrets* → its secrets
  • APP_BASE_URL, from either → its target
  • UNOTEST_* → 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

Terminal window
cd unotest
npx @unotest/web env set dev REGION eu-west
npx @unotest/web env set dev APP_BASE_URL https://pr-42.preview.example
printf '%s' "$GEMINI_API_KEY" | npx @unotest/web env set dev GEMINI_API_KEY --secret
npx @unotest/web env rm dev REGION
npx @unotest/web env rm dev APP_BASE_URL # back to the configured target

The 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:

Terminal window
cd unotest
npx @unotest/web env set dev UNOTEST_JUDGE_MODE local
npx @unotest/web env set dev UNOTEST_JUDGE_PREAMBLE < preamble.txt

APP_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:

ScopeMay
bundlespush bundles and order runs — what a CI job needs
bundles+valuesalso 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 box badge.
  • 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 from bundle push even 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.