# Schedules & queue

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

## Schedules

What runs on its own is declared **in the suite**, next to the tests:
`schedules` in `unotest/unotest.config.*` and entries in
`unotest/schedules.yaml`. `npx @unotest/web schedules` prints the merged
set (the file wins on a duplicate target), and a box picks both up
unchanged.

```js
// unotest/unotest.config.mjs
export default {
  schedules: [
    { collection: "smoke", cron: "0 * * * *" },
    { collection: "nightly", cron: "0 3 * * *", prepare: "node seed.mjs" },
  ],
};
```

Locally nothing ticks — `schedules` is data. On a box the daemon is the
clock.

### What the box runs

- The schedules come from the **newest bundle of the environment's
  `defaultBranch`**, not from whatever somebody selected in the viewer. A
  tester who switched the environment to their branch has not redirected
  tonight's suite.
- The set is re-read when a new bundle arrives, not on every tick. A
  bundle that cannot be installed leaves the previous schedules running and
  is reported, rather than stopping the clock.
- **Missed ticks are not caught up.** A box that was down for six hours
  does not wake up and fire six suites; the next run is the next one due.
- **Preview environments (`pr-<n>`) never arm schedules.** They learn the
  set — so "run what this environment normally runs" still works for CI —
  but their clock stays off.
- An entry's `prepare` command runs before the suite, inside the same queue
  slot. A collection's own `prepare:` runs however the collection was
  started. A failed prepare ends the run with exit **94** — "nothing was
  tested" is a different event from "the suite is red".

### Telling people

A run's outcome can be announced — in Slack, in Telegram, or to a
webhook: a scheduled series on a change of state (red, green again, "did
not run"), a run somebody ordered on its own. See
[Notifications](/box/notifications/).

### Fingerprint and drift

`bundle push` records a fingerprint of the effective schedule set in the
bundle. The box recomputes the set in its own environment and, when a
config that reads `process.env` yields something different there, logs a
warning and the viewer shows a drift line. A schedules registry that cannot
be read is refused at push time, while a human is still watching.

`npx @unotest/web schedules --check` exits non-zero for an entry outside
the previewable cron subset — a pre-push hook or CI gate against a cron the
box would drop.

### In the viewer

On a box the schedule set is **read-only**, with a badge: *declared in the
repo — this box runs whatever the deployed bundle declares; edit there and
push*. The Schedule popup is a read-only card, the Home tab's schedule mode
shows the same badge, and a write is refused (403). Edit the file in your
repository, commit, push a bundle.

## The queue

Runs of one environment take turns instead of colliding. Each environment
has its own queue beside its own run history; a run writes a ticket, waits
until it is at the head, takes a slot, runs, and gives the slot back.

- **Every producer is in it**: a scheduled tick, a CI run order, the Run
  button in the viewer, and an agent's `run_test`.
- **`concurrency`** on the environment (1 by default) is the number of
  slots. Raise it only when the target can genuinely take parallel suites.
- A collection is **one** run; its `workers` still run in parallel inside
  it.
- The box may add a host-wide budget shared by all environments, because
  browsers are the scarce resource on the machine; a collection weighs its
  worker count against it.
- A run that exceeds the box's run timeout (30 minutes by default) is
  ended.

### Coalescing

A newer CI order for the same pull request withdraws the **waiting** runs
of that change; a run that already holds a slot is never killed. Three
pushes in five minutes cost one suite, and the replacing run's log says
how many it superseded.

### Maintenance tickets

Switching an environment's bundle and installing a new one go through the
same queue as a **maintenance ticket** that takes every slot of the
environment: a running suite finishes, the switch happens, the next run
starts on the new code. Nothing is swapped out from under a run.

### In the viewer

The **Active** panel lists what is waiting — including tickets from CI,
named by their change (`PR #412@1a2b3c4`) — with a button to withdraw any
of them. `POST /api/run` never answers "busy": a run ordered while another
is active is accepted and waits.
