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.
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
preparecommand runs before the suite, inside the same queue slot. A collection’s ownprepare: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. concurrencyon 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
workersstill 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.