# Holding a test's world open

> Runs one test and, pass or fail, keeps the world it built serving on your machine — its data and its app version — and opens it in your browser signed in as the test's principal, with no password. Ctrl-C discards it.

<!-- id: testing-holding-a-test · area: testing · stability: stable · html: https://osysharp.com/reference/testing/holding-a-test/ -->

## Summary        {#summary}

A test builds a world — a fixture's rows, everything the test then wrote — and then throws it away. `osy test --hold`
keeps it. It runs exactly ONE test and, when the verdict is in, leaves that test's throwaway copy of your app serving
on the local platform, at an address of its own, and opens your browser on it **signed in as whoever the test was**.
Click through it, look at it, feel how it responds. Ctrl-C ends it and the copy is discarded.

It is the way to see an app full of data without putting that data anywhere real: seed code that lives under `tests/`
is never compiled into what `osy compile` deploys, and a held world is a test's own copy, gone when you stop.

## Signature      {#signature}

```console
osy test [path] --test <id> --hold [--no-open] [--json]
```

## Description    {#description}

### Which test does it hold?   {#one-test}

Exactly one. `--hold` refuses a selection that matches more than one test — listing the ids of what it matched — and
one that matches none, before anything is compiled. Name the test by its id (`file::fixture::name`, the form every
`osy test` report hands back):

```console
osy test --test 'tests/hold.test.osy::Floor::a_filled_desk_to_click_through' --hold
```

`--filter` and `--file` work too, as long as they narrow to one test.

### Who is the browser signed in as?   {#principal}

Whoever the test WAS, at its outermost level:

- a `[runas(P)]` test — **P** (see [[runas(Name)] test attribute and principal selectors](https://osysharp.com/reference/testing/runas-attribute/));
- otherwise the principal of the test's **last top-level `runas` block** ([runas](https://osysharp.com/reference/testing/runas/)). A `runas` nested inside
  another is a momentary switch ("now Bob tries it") and does not count;
- otherwise whoever the test signed the browser in as with `Ui.SignInAs` ([Ui — drive the app's UI from a test](https://osysharp.com/reference/testing/ui/));
- otherwise nobody: the page opens signed out, and the CLI says so.

The session is minted **locally**, over the instance's own operator key, for that principal in that copy — there is no
password and no second-step code to type. That door exists only on a local platform (`osyrin dev`) and only for a copy
`--hold` is holding right now; a deployed server refuses it by name. What it skips is the login page, never
authorization: everything you see is what that principal's own `security { }` rules let them see.

### What does it print?   {#output}

The test's verdict first, exactly as a normal run prints it — then:

```text
⏸ HELD — the test passed. Its branch is serving at http://helpdesk--held-3bf1df3f.localhost:8100/
  signed in as Ines — a session minted locally over this instance's operator key: no password, no second step
  open: http://helpdesk--held-3bf1df3f.localhost:8100/api/osy-test/held-sign-in#…
  simulated latency: none — requests are answered at local speed
  Ctrl-C stops it and discards the branch (so does the local platform idling out). Only this machine can reach it.
```

The first address is the held copy itself — open it in another browser and you are signed out. The `open:` link is
the one your browser was sent to: it signs that tab in and lands on the app's front page. `--no-open` prints both and
opens nothing.

The `open:` link is always printed **on one line, whole**, however narrow the terminal — copy the line as it is. An app
whose own sign-in refuses a tab that is not signed in (Studio's "This tab is not signed in" is one) is telling you the
link you opened carried no session: the bare address, or a link cut short.

`signed in as` names the principal as the test declared it (`principal Ines => …`) when it can, and otherwise the
account's email or display name.

The held copy has a host of its own (`<app>--held-<8 hex>.localhost`), so it is a separate origin from your live app:
signing into one never signs you into, or out of, the other.

### Does a failing test hold?   {#failing}

Yes — the world a red test left behind is usually the one most worth looking at. The verdict is printed as a failure,
the hold line says `FAILED — held anyway`, and when you stop it the command exits non-zero, exactly as the run would
have without `--hold`. A test that did not run at all (a `[Skip]`, or a capability your machine lacks) builds no
world, so there is nothing to hold and the command says so and exits non-zero.

### How does it end?   {#stopping}

Three ways, and each discards the copy — its databases are dropped:

- **Ctrl-C** (or closing the terminal, or `kill`): the CLI ends the hold, then waits until the address stops answering
  and says so — `✓ released — … no longer answers`.
- **The local platform idling out.** A held test does not keep the server alive: what you DO in the held copy counts
  as activity, the hold itself does not, so the platform's usual idle ceiling still applies. When it is reached the
  CLI prints `⏹ released — the local platform is stopping…`.
- **`osy stop`**, which ends the local platform and everything it was holding.

### How do I feel it at a distance?   {#latency}

A local platform answers in a few milliseconds, which hides how many requests a page waits on in a row.
`OSY_SIMULATED_LATENCY_MS` makes it wait that long before every request — the round trip a real network adds — so
clicking through the held copy feels like using it from far away.

The platform reads the variable **when it starts**, not per command. Setting it in the shell of an `osy test --hold`
against a platform that is already running does nothing — and the CLI says so, naming both values. Stop the platform
first, so the command starts a fresh one that carries it:

```console
osy stop
OSY_SIMULATED_LATENCY_MS=40 osy test --test 'tests/hold.test.osy::Floor::a_filled_desk_to_click_through' --hold
```

The hold line always says which delay the serving platform is actually applying (`simulated latency: 40 ms in front
of every request`, or `none`).

### Who can reach it?   {#reach}

Only this machine. A local platform listens on loopback, and a held copy additionally refuses any request that does
not come from loopback. Its address stops answering the moment the hold ends — it never falls through to your live
app.

### Scripting it   {#json}

With `--json`, standard output is the run's event stream — the test's verdict, then a `held` event carrying `url`,
`branchId`, `passed`, `principal` and `simulatedLatencyMs` — and the human lines above (including the `open:` link) go
to standard error. The stream stays open until the hold ends, so a script reads up to `held`, does its work against
`url`, and sends the process `SIGTERM` (or `SIGINT`) to end it.

## See also       {#see-also}

[Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/) — `osy test` itself, and the other ways to choose what runs.

[runas](https://osysharp.com/reference/testing/runas/) and [[runas(Name)] test attribute and principal selectors](https://osysharp.com/reference/testing/runas-attribute/) — how a test becomes somebody, which is who the held browser is.

[Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/) — the local platform a held copy is served from, and its idle ceiling.

[Stopping the local platform](https://osysharp.com/reference/local/stopping-the-local-platform/) — `osy stop`.
