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

What a run gets

For the complete documentation index, see 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

PathAccessWhat it is
/box/sourcesread-onlyThe bundle you pushed: unotest/, its node_modules. The working directory of the run.
/box/artifacts/unotest/.runs.<env>read-writeThis environment’s run history — journals, failure bundles, debug trees. Survives the run; the viewer shows it.
/tmpread-write, tmpfs, noexecScratch space. Gone when the run ends.
everything elseread-onlyThe 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

VariableValue
HOME/tmp
TMPDIR/tmp
UNOTEST_ARTIFACTS_ROOT/box/artifacts
UNOTEST_FAILURES_ROOTthe failure bundles directory inside the runs mount
UNOTEST_DEBUG_DIRthe run’s debug directory inside the runs mount
UNOTEST_ENVthe environment’s name
UNOTEST_RUN_IDthis 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:

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 for why the container is shaped this way.