# Troubleshooting

> For the complete documentation index, see [llms.txt](/llms.txt)

Most refusals on a box are deliberate and say why. This page maps the
message to the cause and to what to do.

## Signing in

| Symptom | Cause · what to do |
|---|---|
| *This box has no free seats. Ask your administrator to free one or raise the licensed seat count.* | Every licensed seat is held by someone who signed in within the last 30 days. People already holding a seat still get in. An administrator disables someone on **Administration → Users** to free a seat, or you ask unotest to raise the count. The refusal is in the trail as `seat.denied`. |
| *This account has been disabled on this box.* | An administrator disabled the account here. **Administration → Users → enable.** |
| *Those credentials were not accepted.* | Break-glass username or password wrong, or the identity provider rejected the login (`login.refused` in the trail carries the provider's reason). |
| *That login attempt expired or was already used. Start again.* | The browser came back to a callback whose attempt has been used or timed out (a replayed link, a long pause at the provider). Start from `https://<box>/`. |
| `redirect_uri_mismatch` at the provider | The URI registered at your provider is not exactly `https://<box>/_guard/login/callback`. |
| Sign-in works, everyone is `readonly` | No group mapping is in effect. For Google Workspace, part 2 (directory service account) is unfinished, or the mapping is empty. |
| *none of this account's groups are mapped to a role* | The person is in no mapped group — or in a parent group; nesting is not followed. Add the group they are directly in. |
| Everyone is refused right after setup | Usually the impersonated administrator cannot read the directory, or a delegation scope is missing. The box refuses logins it cannot confirm. |
| Access outlives someone leaving | Groups are read from the sign-in token rather than the directory, so nothing re-checks membership. Only a directory source can offboard. |
| `429 too many login attempts` | 10 attempts per minute per client address by default; wait for `Retry-After`. Behind your own proxy, every visitor may share one bucket — tell unotest the proxy's address. |

## Identity provider unavailable

- **Open sessions keep working.** A re-check that cannot reach the
  directory leaves the session alone and retries later; it never revokes on
  silence.
- **New logins are refused** while the directory cannot answer — admitting
  a person nobody could vouch for is the one case where waiting is wrong.
- **Administrators use the break-glass account** at
  `https://<box>/_guard/login/static`. It exists for exactly this day.
- The box boots even if the provider is unreachable at start-up; discovery
  is retried, never cached as failed.

## Pushing a bundle

| Message | Meaning · what to do |
|---|---|
| `this box runs @unotest/web X or newer; the bundle pins Y` (`min-version`) | The suite pins an older `@unotest/web` than the box supports. Bump it in `unotest/package.json`, commit the lockfile, push again. |
| `this token is not valid for this box` (`unauthorized`) — the CLI adds `the token is unknown, revoked, or for another box` | Get a new one from unotest (tokens are issued per project by the box's operator). |
| `… belongs to project "a" on this box, but the token is for "b"` (`scope-mismatch`) | The repository is mapped to another project than the token's. Use that project's token. |
| `bundles on this box may not exceed N MB` (`too-large`) | Something got into `unotest/` that should not travel (recordings, databases, downloads). Ignore it in git. |
| `the upload did not arrive intact` (`malformed` / `id-mismatch`) | Retry. If it repeats, a proxy between you and the box is rewriting the body. |
| `this token pushes bundles only` (`forbidden`, on `env push` / `env set`) | The token has the `bundles` scope. Values need a token minted with the values scope. |
| `not pushed — N problem(s) above would fail on the box` (exit 2) | Refused on your machine; each line names the file and the fix. Nothing left it. |
| `… answered 200 with something that is not a box's answer` | The URL is not a box's API — a login page, a proxy error. Check `UNOTEST_BOX_URL`; the box may also be older than this CLI. |
| `--run needs --env` | Name the box environment to run in. |
| `no environment by that name on this box` (`unknown-environment`) | Environments are the operator's decision; ask which exist. |
| `the box no longer holds that bundle` (`unknown-bundle`) | It was collected under the retention policy. Push again. |
| `name what to run with --collection <name>, or add a schedule` (`no-collections`) | Nothing was named and the environment's schedules name nothing. |
| `note: <env> has no value for NAME` | The suite declares a name that environment neither sets nor holds. Runs there stop at the first step that reads it — set it with `env push <env>` or `env set <env> NAME`. |

## An environment does not get its values

- **The viewer's Variables panel shows nothing under `box`.** The
  environment has no values yet: push them with
  `npx @unotest/web env push <env>` (a token with the values scope), or set
  them on **Administration → Environments → the environment**.
- **A value set on the box is not what a run sees.** A secret wins over a
  variable of the same name, and a box-set value wins over the
  configuration. `UNOTEST_*` names are skipped by `env push` on purpose;
  set one with `env set` if you mean it.
- **A push removed something.** `env push` replaces the layers: a name your
  files no longer carry is removed and listed. Put it back in the file, or
  use `env set`.
- **`malformed` from `env push`.** The box has no values endpoint yet — it
  is older than the CLI. Ask unotest to update it.
- **The viewer reloaded and a debug session ended.** Changing an
  environment's values restarts its viewer, by design.

## Environments page is empty

*No environment is being served yet.* The box has no bundle for that
project. Push one; the viewer starts once there is something to show.

## Schedules

- **A schedule did not run after a switch in the viewer.** Schedules run
  the newest bundle of the environment's `defaultBranch`, not the bundle
  someone selected. Push to the branch.
- **A missed night was not caught up.** By design: the next run is the next
  one due.
- **A drift warning in the viewer.** The schedule set the box computed
  differs from the one pushed — usually a config that reads `process.env`.
  Make the config compute the same set everywhere, or accept the box's.
- **Cannot edit a schedule in the viewer.** On a box schedules are
  read-only. Edit `unotest/schedules.yaml` or `unotest.config`, commit,
  push.
- **Nothing runs in a `pr-<n>` environment on its own.** Preview
  environments never arm schedules; CI orders their runs.

## Licence

| Symptom | Meaning |
|---|---|
| Banner *License expired — N day(s) of grace left* on the Environments page | The licence has expired; the box keeps working for the grace period (14 days by default). Contact unotest to renew. |
| Page *License required — The license has expired.* | Grace is over. The box answers every request outside sign-in with this page until a renewed licence is installed. |
| *No license file found.* / *The license file is invalid.* | The licence file is missing or its signature does not verify. Contact unotest. |
| `GET /_guard/health` → `{"ok":true,"license":"grace"}` | The same states, for your monitoring: `valid`, `grace`, `expired`, `missing`, `invalid`. |

## What a box cannot reach

A run on the box executes inside the box's container, on the box's
network. A scenario that passes on a laptop and fails on the box in its
first step usually leans on something that exists only on the laptop:

- **`localhost` and port-forwards.** `kubectl port-forward`, an SSH tunnel,
  a dev server on `127.0.0.1` — none of them exist on the box. Point
  preconditions and `APP_BASE_URL` at the environment's public URL, and
  keep the health probe on that URL too.
- **Tools on the host.** `shell()` runs inside the container: no `kubectl`,
  no `gcloud`, no project scripts outside the pushed suite. A scenario may
  call only files under `unotest/` — the bundle is the whole file system it
  gets (the push refuses a scenario that imports from outside).
- **Secrets in `shell()`.** `.secrets` values are not in a child process's
  environment by default, on a box or locally; the scenario names the ones
  it exports through the `sandbox.exportSecrets` allowlist.
- **A judge service.** `UNOTEST_JUDGE_MODE=remote` with a service on your
  machine does not work from the box; use `local` mode with
  `@unotest/judge` in the suite — see [Judge on a box](/guides/judge/#on-a-box).
- **Your repository.** The box never reads it: it runs the pushed bundle
  and nothing else — see [Pushing suites](/box/pushing-suites/).

## GitHub checks

- **The check failed with "nothing was pushed for this commit".** The box
  waited 15 minutes for a bundle with that commit's sha and none arrived.
  Make sure your CI job runs `bundle push` for every commit the App
  reports on.
- **A pull request from a fork is not run.** By design — it would receive
  the environment's secrets. A maintainer re-requests the check in GitHub
  to opt in.
- **A deployment did nothing.** Its environment name is not mapped to a
  box environment; the box logs the missing name and drops the event.
