A schedule is a declaration: this collection (or this test) runs on this
cron, in this environment, after this preparation. It is data that travels
with the tests it names — versioned with them, pushed with them — and
nothing in @unotest/web ticks: no timer is armed locally, no cron is
parsed. Whoever hosts your runs executes the schedule — a
box or the CI you already have — and
npx @unotest/web schedules says so out loud rather than implying a timer
that does not exist.
Where schedules live
Two halves, one merged list:
-
schedulesinunotest.config— hand-written entries. Each names acollection, a 5-fieldcron, and optionally anenv(the.env.<name>overlay, with its own.runs.<name>history) and aprepareshell command that runs before the collection inside the same queue slot — seeding a database, resetting a fixture. See the config reference. -
unotest/schedules.yaml— the half the viewer edits when you schedule a test or a collection from the tree:schedules:- collection: smokecron: "0 * * * *"- scenario: checkout/guestcron: "0 3 * * *"env: stagingAn entry names either a
collection:or a single test asscenario: <ref>, pluscron:;env:andprepare:are optional. A missing file means no viewer-managed schedules; a file that exists but cannot be read is an error, never silently “no schedules” — that would un-schedule every suite it names without telling anyone.
The two halves are merged into one list — the shape schedules --json
prints and --scheduled=<index> indexes into. On a duplicate target the
file wins. Every merged entry carries its source (config or file); the
viewer edits only the file half and shows config entries read-only.
The cron expression is opaque to @unotest/web: it is validated as a
non-empty string and interpreted by whoever executes it, in that executor’s
timezone (a box container is UTC unless its TZ says otherwise).
Reading them back
npx @unotest/web schedules # human listnpx @unotest/web schedules --json # the merged list, as an executor reads itnpx @unotest/web schedules --check # a gate for a pre-push hook or CIThe human list prints one line per entry — cron, target, and in brackets
test for a scenario entry, env <name>, prepare \…`, schedules.yaml for file entries — and a preview of the next tick on this machine's clock (next …). An expression outside the previewable cron subset is marked not previewable — an executor may drop it; —checkturns that mark into exit1(otherwiseschedules: N declared, all previewable.and exit0). An unreadable schedules.yamlexits1; mixing —jsonwith—check, or an unknown flag, exits 2`. With nothing declared the command prints how to
declare one.
Running a schedule now
An executor runs an entry the same way you can by hand:
npx @unotest/web collection smoke --schedulednpx @unotest/web e2e checkout/guest --scheduled--scheduled runs the target the way its entry does: in the entry’s env
(unless you set one yourself) and with its prepare command first, inside
the same run slot. A collection or test listed more than once needs
--scheduled=<index> — the zero-based position in schedules; without it
the command refuses to guess which env and preparation you meant. A failed
prepare exits 94: the environment was not prepared and nothing was
tested, which is a different alert from a red suite (exit 1).
Schedules travel with the bundle
npx @unotest/web bundle push reads the effective schedules and stamps
their fingerprint into the bundle manifest, so the box that deploys the
bundle can compare what its own reading of the config yields against what
the pusher’s machine saw. A registry the pusher’s machine cannot read is
one the box cannot read either: the push is refused (✗ error … → fix the schedules registry — the box reads it to know what runs on its own) while a
human is still watching, instead of un-scheduling the suite by tonight.