# Dev seeds: a fixture you can load into the running app

> Mark a [TestFixture] with [DevSeed("label")] and the local dev page (`osy launch --dev`) offers it as a button that loads that fixture's world into the running app — the same world its tests fork from. The page lists the accounts the app holds, with their roles, and signs you in as any of them. `osy seed <name>` is the same load from a terminal.

<!-- id: testing-dev-seeds · area: testing · stability: preview · html: https://osysharp.com/reference/testing/dev-seeds/ -->

## Summary        {#summary}
A **dev seed** is a `[TestFixture]` you also mark `[DevSeed("label")]`. You write the world once: its tests fork from
it, and the local dev page loads it into the app you are clicking through.

```osy title="a seed and its test, in one place" test app=testing-dev-seeds
[Role] enum TowerRole { Authenticator, Ringer, Captain }

[Principal]
entity Ringer {
  [Required, Unique, MaxLength(200)] string Email;
  [MaxLength(200)] string? PasswordHash;
  security {
    allow read when IsAuthenticated || IsAuthenticator;
    allow create when IsAuthenticator;
    deny read PasswordHash when !IsAuthenticator;
  }
}

[RoleGrant]
entity RoleGrant {
  [Required] Ringer Member;
  [Required] TowerRole Role;
  security { allow read when IsAuthenticated; }
}

policy IsAuthenticator => RoleGrant.Any(g => g.Member == user && g.Role == TowerRole.Authenticator);
policy IsCaptain       => RoleGrant.Any(g => g.Member == user && g.Role == TowerRole.Captain);

entity Bell {
  [Required, MaxLength(40)] string Name;
  security { allow read when IsAuthenticated; allow create, update when IsCaptain; }
}
```

```osy title="the seed — a fixture with a label" run app=testing-dev-seeds
[TestFixture, DevSeed("A tower of bells")]
void ATowerOfBells() {
  var captain = new Ringer { Email = "captain@bells.test", PasswordHash = Security.HashPassword("a-long-captain-password") };
  new RoleGrant { Member = captain, Role = TowerRole.Captain };
  new Ringer { Email = "ringer@bells.test", PasswordHash = Security.HashPassword("a-long-ringer-password") };
  new Bell { Name = "Treble" };
  new Bell { Name = "Tenor" };
}

principal Captain => Ringer.Single(r => r.Email == "captain@bells.test");

// A test body runs as its `[runas]` principal — with none it is anonymous, and a Bell is readable only when signed in.
[Test(ATowerOfBells)]
[runas(Captain)]
void The_tower_has_two_bells() {
  Assert.Equal(2, Bell.Count());
}
```

## Signature      {#signature}
```osy syntax
[TestFixture, DevSeed("<label>")]          // offered on the dev page under <label>
void <Name>() { … }

[TestFixture(<Parent>), DevSeed]            // builds on <Parent>; with no label, the name in words
void <Layer>() { … }
```

```text
osy launch --dev           open the app's dev page
osy seed                   list the seeds the tests declare
osy seed <name|label>      load one into the running app
osy seed --json            the same answers as one JSON line, for a tool: the seeds, the one loaded last, the accounts and their roles
```

## Description    {#description}

### What does loading a seed do?   {#load}
It **replaces the app's data with the seed's world**. The fixture is built exactly as `osy test` builds it — from an
empty app, past every `security { }` block, with `runas`, stubs and its parent fixtures — and the rows it leaves (and
the files it wrote) become the running app's data. What was there before is removed first, so loading a seed twice
gives you the same world twice, and **Reset** on the page empties the app and loads the last seed again.

**The platform's own Admin is the one exception.** Its data is the platform's registry — the installation, the
operator, the system apps — so a seed loaded in `admin/` goes in **beside** those rows instead of replacing them, and
the platform keeps running. Loading again replaces only the previous seed, and **Reset** removes only the seed's rows
(and any row that points at one). `osy seed` says how many of the platform's own rows it kept.

A row whose field a `workflow` tracks arrives **at the state the seed left it in**, with a run of the app's own — an
order the seed left waiting for payment is waiting for payment, and nothing the seed's workflows already did runs again.
A field a `Counter` numbers arrives **with the number the seed gave it**, and the counter carries on from the seed's
last number — so the next order the app makes after loading a seed with 28 orders is the 29th, however many times you
load it.

### What if a seed does not load?   {#failed-load}
The message says where it left the app, because the two failures leave it in different places:

- the seed's world could not be **built** (its fixture threw, an assertion failed) — the app was not touched:
  *"The app's data was not changed."*
- the world was built and could not be **moved in** — what got in is removed and the app's own data is written back:
  *"What the app held before was put back."* Only if putting it back fails too is the app left empty, and the message
  says so: *"The app is empty now."*

`osy seed <name> --json` prints that as one JSON line and exits 1: `{"error": "…", "appUnchanged": true}`.

### What is on the dev page?   {#page}
`osy launch --dev` opens it at `http://<app>.localhost:<port>/_osy/dev`:

- **Seeds** — one button per `[DevSeed]`, with the `osy seed` line that does the same;
- **Accounts** — every account the app holds, with its roles, and **Sign in as** beside each: you land on the app
  signed in as that person, with no password asked;
- **Secrets** — every secret the app declares, set or not set. A value you type is saved to the project's `.secrets`
  (kept out of git) and applied to the running app, exactly as `osy secret set` does; it is never shown again;
- **Open the app** — the app itself.

`osy launch` (without `--dev`) opens the app and prints one line saying the dev page is there.

### Where does the dev page exist?   {#local-only}
**Only on your local platform** (`osyrin dev`). A seed writes past security and "sign in as" asks for no password, so
on any deployed platform the page and everything behind it do not exist at all — the address answers 404. On your
machine it opens only through `osy launch --dev`, which holds your instance's own key.

### Why a fixture, and not a separate seed file?   {#why-a-fixture}
Because the world you click through should be the world your tests check. A seed written apart from the tests drifts
from them the first time either changes; a fixture marked `[DevSeed]` is one method doing both jobs, and a test that
forks from it fails the moment the seed stops building what the app needs.

## See also       {#see-also}
- [Fixtures: built once, cloned for every test, and built on each other](https://osysharp.com/reference/testing/fixtures/) — how a fixture is built, layered and copied
- [Adding an account](https://osysharp.com/reference/local/adding-an-account/) — `osy user add`, for one account without a seed
- [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) — `[RoleGrant]`, where the roles the page lists come from
- [Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/) — `osy test`
