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

Schedules & queue

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

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.

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.