# What a run gets

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

A run on a box is your suite's own `@unotest/web`, started in a container
of its own. This page is what that container looks like from inside: the
answer to "where may my suite write?" in one place.

## The filesystem

| Path | Access | What it is |
|---|---|---|
| `/box/sources` | **read-only** | The bundle you pushed: `unotest/`, its `node_modules`. The working directory of the run. |
| `/box/artifacts/unotest/.runs.<env>` | **read-write** | This environment's run history — journals, failure bundles, debug trees. Survives the run; the viewer shows it. |
| `/tmp` | **read-write**, tmpfs, `noexec` | Scratch space. Gone when the run ends. |
| everything else | read-only | The container's own filesystem. |

Those two are the only places a run may write. Everything else answers
with `EROFS` ("Read-only file system") — or `ENOENT` for a directory that
would have to be created on the read-only part first.

`/box/artifacts` itself, and `/box/artifacts/unotest`, are **not**
writable: only the one runs directory under them is mounted read-write.

## Where to write

- **Something the run needs only while it runs** — a seeded database, a
  downloaded file, a cache: `/tmp`, through `os.tmpdir()` in Node or
  `${TMPDIR:-/tmp}` in a shell script.
- **Something that should outlive the run and be seen in the viewer:**
  `$UNOTEST_ARTIFACTS_ROOT/unotest/.runs.$UNOTEST_ENV`. Write files of
  your own into a directory of your own there, not next to the runner's.

`UNOTEST_ARTIFACTS_ROOT` names the **root the runner's paths hang off**,
not a directory you can write into: `mkdir $UNOTEST_ARTIFACTS_ROOT/x`
fails, with or without `recursive`. Nothing under `unotest/` in the
sources is writable either — not `unotest/.tmp`, not `unotest/.runs`.

## Variables a run is given

| Variable | Value |
|---|---|
| `HOME` | `/tmp` |
| `TMPDIR` | `/tmp` |
| `UNOTEST_ARTIFACTS_ROOT` | `/box/artifacts` |
| `UNOTEST_FAILURES_ROOT` | the failure bundles directory inside the runs mount |
| `UNOTEST_DEBUG_DIR` | the run's debug directory inside the runs mount |
| `UNOTEST_ENV` | the environment's name |
| `UNOTEST_RUN_ID` | this run's id |

The first five are the box's: a value of the same name in an
environment does not reach the run. The last two name the run; leave
them alone. The environment's own variables and secrets arrive beside
them, as set on the box.

A box older than `TMPDIR` in this list leaves it unset; `${TMPDIR:-/tmp}`
works on both.

## `shell()` steps

A `shell()` command starts in `sandbox.shellCwd`, which defaults to the
working directory — on a box, the **read-only** `/box/sources`. A command
that writes relative to where it starts fails there. Either write to
`/tmp`, or point the shell somewhere writable in `unotest.config`:

```js
sandbox: {
  shellCwd: process.env.UNOTEST_ARTIFACTS_ROOT
    ? `${process.env.UNOTEST_ARTIFACTS_ROOT}/unotest/.runs.${process.env.UNOTEST_ENV}`
    : process.cwd(),
},
```

The command inherits the run's variables, `HOME`, `TMPDIR` and
`UNOTEST_ARTIFACTS_ROOT` included, and runs in the same container as the
scenario — a file it writes to `/tmp` is there for the next step.

## The runner version

Pin `@unotest/web` **0.35.0 or newer** in `unotest/package.json`. Older
runners do not write failure bundles into a box's layout, so a failed run
leaves nothing to look at. A box may refuse such a bundle at upload with
`min-version`, naming both versions.

## When something writes where it may not

The sign is `EROFS` / `Read-only file system` (or `ENOENT` on a `mkdir`
under `/box/artifacts`) in a step or in the suite's `prepare`, on a path
under `/box/sources` or `/box/artifacts`. Move the write to one of the
two places above. Locally the same suite writes anywhere, which is why
the error only appears on the box.

See also [Security model](/box/security/#where-code-runs) for why the
container is shaped this way.
