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

Variables & secrets

For the complete documentation index, see llms.txt

Environments, tokens and credentials live in variables — not hardcoded in scenarios.

Two files

  • unotest/.env — ordinary variables.
  • unotest/.secrets — secrets, git-ignored.

Reference them by bare UPPER_SNAKE identifiers in scenarios:

goto(APP_BASE_URL + "/login");
fill(getByLabel("Email"), TEST_USER_EMAIL);
fill(getByLabel("Password"), TEST_PASSWORD);

Secrets are masked

Values registered as secrets are redacted in logs and failure artifacts (shown as ‹secret:NAME›), so they never leak into a trace or a screenshot bundle. That covers everything a run says out loud as well as everything it writes: the run journal, what the runner prints to its terminal, the same lines as the viewer’s System pane streams them, and the copy kept beside the run in stdout.log / stderr.log. Masking is by value, wherever the value appears — with one explicit exception: the run’s own identifiers in runtime.json (the run id, the scenario path, the test function, the file and function names of the call stack) are never rewritten, so LOGIN=admin does not turn guard-admin.js into guard-‹secret:LOGIN›.js.

What masking cannot cover

Masking knows the values you gave it. A secret the application shows for the first time — a token it has just minted, a one-time code, a recovery key — is not one of them, and an assertion that reads the block containing it puts it in the journal, the screenshot and the failure bundle, because an assertion’s job is to report what it saw:

// Leaks the token into every artifact of a failed run.
assertText(page.locator('.token-panel'), 'copy it now');

Assert on the label, the heading or the presence of the element instead, and never on the text of the block that displays the value.

The judge’s connection (UNOTEST_JUDGE_MODE, _URL, _TOKEN, …) belongs in the same files — the token in .secrets; see LLM judge.

Across environments

Because the test references variables, the same scenario runs against dev, staging or prod. Named environments are overlay files layered over the base pair — an overlay holds only the keys that differ (later file wins; ambient shell env beats every file):

# unotest/.env — base: shared values + local defaults
APP_BASE_URL=http://localhost:3000
TEST_USER_EMAIL=demo@example.com
# unotest/.env.staging — overlay: only what differs on staging
APP_BASE_URL=https://staging.app.dev

Secrets overlay the same way: unotest/.secrets.staging over unotest/.secrets. An environment exists as soon as either overlay does — .env.<name> or .secrets.<name>; the names example, sample and template are never offered as environments. Each environment keeps its own run history under unotest/.runs.<name>/; a target adds its own axis in front (unotest/.runs-mobile.staging/).

unotest/.env.example is the template init writes once. It is your file: a later init never rewrites it, but reports the keys the current template declares that your file does not mention (skipped — the current template also declares: …) — add them by hand, or re-run with --force to take the template.

Select the environment per run:

Terminal window
npx @unotest/web e2e auth/login --env staging
# or: UNOTEST_ENV=staging npx @unotest/web e2e auth/login

No --env / UNOTEST_ENV means base files only.

Run history follows the same axis: a run started with --env staging writes its artifacts under unotest/.runs.staging/, while base runs stay in unotest/.runs/. Each environment keeps its own history, so results from different environments never mix (keep the wildcard unotest/.runs*/ line in .gitignore — that’s what init writes).

The viewer’s Variables panel lets you edit values, reveal/hide secrets, and toggle boolean flags without leaving the window. It also has an env switcher at the top of the panel — the same .env.<name>-overlay switcher the status bar shows as env overlay: pick base or any overlay and the panel shows the merged layers exactly as a run in that environment resolves them — overlay-owned values carry an env badge, edits go to the file that defines the key, and new variables are always created in the base file. Switching is server-side: the Overview and Runs views re-scope to that environment’s history, and runs started from the viewer spawn with its UNOTEST_ENV. Template files like .env.example are never offered as environments. On a box, the project and environment the viewer serves are a different thing: they are named and switched in the tree header, not here.

On a box the panel also lists what the box injects into every run of the environment — its target, the operator’s variables and the names of its secrets — each with a box badge. Those rows are read-only here: a write that names one is refused (HTTP 409, <NAME> is set by the box for this environment — change it on the box), because no file defines it and the box’s value is applied after the files.