<!-- Generated by apps/dreamboard-cli/scripts/sync-skill-docs.ts. -->
<!-- Source: docs/reference/testing.mdx -->

# Testing

Reference for Dreamboard's reducer-native test framework.

Dreamboard's scaffolded test workspace is reducer-native. It uses typed base
definitions and typed scenarios that run against your compiled reducer and
generated contracts.

## Responsibility

Use the scaffolded test workspace for:

- reusable seeded base states
- reducer and view assertions
- happy-path coverage
- rejection and turn-gating coverage
- deterministic regression testing in CI or local development

## Workspace layout

The current scaffold uses these paths:

- `test/README.md`
- `test/bases/*.base.ts`
- `test/scenarios/*.scenario.ts`
- `test/testing-types.ts`
- `test/generated/*`

Generated files under `test/generated/*` are owned by Dreamboard and should not
be edited manually.

## Command flow

Generate reducer-native test artifacts:

```bash
dreamboard test generate
```

Run all scenarios:

```bash
dreamboard test run
```

Run one scenario:

```bash
dreamboard test run --scenario test/scenarios/win-the-game.scenario.ts
```

Re-run `dreamboard test generate` after changes that affect the runtime shape of
the game, including:

- `manifest.json`
- reducer code under `app/`
- setup-profile behavior
- generated contract output

## Base definitions

A base is a named, seeded checkpoint that scenarios can reuse.

Use `defineBase(...)` from `test/testing-types.ts`.

| Field | Required | Notes |
| --- | --- | --- |
| `id` | Yes | Stable base identifier referenced by scenarios |
| `seed` | Yes | Deterministic random seed |
| `players` | Yes | Player count used for that base |
| `setup` | Yes | Async setup callback that prepares the checkpoint |

```ts
import { defineBase } from "../testing-types";

export default defineBase({
  id: "initial-turn",
  seed: 1337,
  players: 2,
  setup: async ({ game }) => {
    await game.start();
  },
});
```

The base `seed` drives runtime-owned randomness. Effects such as
`effects.rollDie(...)`, `effects.randomInt(...)`, `effects.sample(...)`, and
`effects.shuffleSharedZone(...)` consume the seeded reducer RNG, so repeated
test runs with the same base seed are reproducible.

Use bases for:

- opening-state invariants
- post-setup checkpoints shared by many scenarios
- seeded states that would be noisy to rebuild inline

Keep bases small and composable. A base should represent one meaningful
checkpoint, not a full playthrough.

## Scenario definitions

Scenarios are typed files under `test/scenarios/*.scenario.ts`.

Use `defineScenario(...)` from `test/testing-types.ts`.

| Field | Required | Notes |
| --- | --- | --- |
| `id` | Yes | Scenario identifier |
| `description` | No | Short human-readable summary |
| `from` | Yes | Base ID from `test/bases/*.base.ts` |
| `runners` | No | Defaults to reducer runner |
| `when` | Yes | Async action flow |
| `then` | Yes | Assertions over state, view, and history |

```ts
import { defineScenario } from "../testing-types";

export default defineScenario({
  id: "player-two-wins",
  description: "The deterministic roll sequence lets player 2 reach ten first",
  from: "initial-turn",
  when: async ({ game }) => {
    await game.action("player-1", "rollDie", {});
    await game.action("player-2", "rollDie", {});
    await game.action("player-1", "rollDie", {});
    await game.action("player-2", "rollDie", {});
    await game.action("player-1", "rollDie", {});
    await game.action("player-2", "rollDie", {});
  },
  then: ({ publicState, view, expect, history }) => {
    const state = publicState();

    expect(state.lastRoll).toBe(6);
    expect(state.scores["player-1"]).toBe(9);
    expect(state.scores["player-2"]).toBe(12);
    expect(state.winnerPlayerId).toBe("player-2");
    expect(view("player-2").winnerPlayerId).toBe("player-2");
    expect(history().accepted().length).toBe(6);
  },
});
```

## Scenario context

`when(...)` and `then(...)` receive typed helpers from `test/testing-types.ts`.

### `game`

`game` is the typed scenario driver.

Use it to:

- `start()` the game inside base setup
- `action(playerId, actionType, params)` for reducer actions
- `action(playerId, command)` for generated command objects
- `respond(...)` for prompts
- `windowAction(...)` for reducer-owned window actions
- `expectPrompt(...)` and `expectWindow(...)` when testing prompt/window flow

### Shared state helpers

Use these in `when(...)` and `then(...)`:

| Helper | Notes |
| --- | --- |
| `phase()` | Current reducer phase name |
| `publicState()` | Current shared reducer state |
| `view(playerId)` | Projected player view |
| `runtime()` | Runtime metadata |
| `history()` | Accepted and rejected inputs |
| `expect(...)` | Built-in assertion API |

`then(...)` also exposes `hiddenState()` unless the active runner omits hidden
state access.

## Generated testing types

The scaffold writes `test/testing-types.ts` as the test-facing import surface.

It re-exports:

- `defineBase(...)`
- `defineScenario(...)`
- `phaseCommands`
- `windowCommands`
- reducer-derived types such as `GameState`, `GameView`, `ActionName`, and
  `PromptId`

Import from `../testing-types` instead of reaching into `test/generated/*`
directly.

## Recommended coverage

Every game should have at least:

- one opening-state scenario that asserts the initial phase, active player, and
  starting view
- one happy-path scenario that completes a typical turn or round
- one winning-condition scenario
- one rejection scenario for out-of-turn or otherwise illegal input

When the game uses prompts or windows, add at least one scenario that exercises
that continuation path end to end.

## Rejection-path coverage

Rejection scenarios are part of the main test harness, not a separate workflow.

Prefer direct reducer-native assertions for:

- wrong player acting out of turn
- wrong action for the current phase
- invalid parameters
- action attempts after game end
- repeated submission of one-time actions

Typical checks:

```ts
then: ({ phase, history, expect, publicState }) => {
  expect(history().rejected().length).toBe(1);
  expect(history().accepted().length).toBe(0);
  expect(phase()).toBe("takeTurn");
  expect(publicState().winnerPlayerId).toBe(null);
};
```

## Failure and staleness signals

If the generated test artifacts no longer match the compiled game, refresh them:

- `dreamboard test generate`

Common symptoms:

- generated files missing under `test/generated/*`
- type errors caused by stale generated contracts
- scenario runs using an outdated runtime shape after reducer or manifest edits

## Related workflows

This page documents the scaffolded reducer-native test workspace.

If you also use `dreamboard run` for manual debugging or operational inspection,
treat that as a separate workflow. It is useful for ad hoc runtime exploration,
but it is not the primary authored test surface for a new Dreamboard project.
