Spec — Automated dry run¶
Run Part 1 of an event's Control Center runbook (the rehearsal) with one command, instead of someone working through it by hand.
Status: not started — idea only. This page records what is known and what has to be proven before a full spec is written. It is not an implementation guide: §2 separates what was checked against the code from what is only believed, and §4 lists the spikes that settle the rest.
The runbook is _templates/dry-run-checklist-template.md in
sage-match-control.github.io, copied per event into
events/<event-key>/dry-run-checklist.md.
1. The problem¶
The rehearsal is manual, takes an operator's full attention, and is the step most likely to be skipped when an event is close. It exists to prove one chain end to end:
human edit in the facility sheet
-> installable onEdit trigger (sheets-sync.gs)
-> POST /sync/:day, from the edit itself
-> the live Worker (pushed to open pages), archived to event-data/<event-key>/data/<day>.json
-> Control Center + public pages render it correctly
Part 2 of the runbook (the day itself) is out of scope: venue wifi, HDMI, a phone check and judgement calls during play are not automatable.
2. What we already know¶
Checked against the code¶
- Only a UI edit tests the trigger.
onEditInstallablefires on user edits only; script and API edits don't fire it (sheets-sync.gssays so itself). So anything that writes through the Sheets API or Apps Script has to call/syncdirectly and skips the one link the rehearsal exists to prove. - Control Center's Resync this day now does not exercise Apps Script.
It calls
POST /sync/:dayon Cloud Run straight from the browser, and Cloud Run reads the sheet through the Sheets API. It proves the sheet ID is registered and readable, not that the trigger works. The runbook's §2.1 says so. - Sync timing.
sheets-sync.gssyncs from the edit itself, under the workbook's document lock: the lock holder waitsSYNC_SETTLE_MS(1.5 s), syncs, and goes round again for newer edits, never starting two syncs less thanSYNC_MIN_GAP_MS(5 s) apart. Only in an edit storm does it fall back to a time-based trigger (DEBOUNCE_MS, 3 s), which is best-effort and can fire up to about a minute late. A checker still has to wait for the snapshot to change, not sleep a fixed time. With live push on, the change reaches the Worker before GitHub, so watching the page's own socket (or the publicwss://…/live/<event>/<day>) sees it sooner than pollingevent-data. - A paused workbook silently does nothing. With SAGE → Pause live
sync on,
onEditInstallablereturns before scheduling anything. A missing sync after an edit therefore means "trigger broken, or paused, or unconfigured" — a checker must say so rather than report a bare timeout. - Rendering can be tested without the sheet. The console already loads
_fixtures/on localhost via?fixture=<name>. The rehearsal's three sheet edits (one finished match, one live court, one unmapped category code) only exist to produce a snapshot in a known state; a script can build that snapshot directly from the real published one. Of the real events, only PickleDrive has fixtures today (the other set,attendance-demo-2026, is a fixture-only event for the attendance pages). - Team codes and player names are often formulas. Overwriting one in the UI deletes the formula. The published snapshot only carries the computed value, so restoring from it would leave a plain value behind.
- Live/Hide is a real write.
POST /sync/:day/livecommits toevent-data/config/events.json. A test of it is a real config change and must always end onauto. - Per-type branches. The team-event runbook has extra steps (the
MatchUpslineup and seed cells, e.g.D604), so checks vary by the event'stype. - Puppeteer is already a
sage-tools-apidependency.
Believed, not yet verified¶
- Puppeteer keystrokes (
page.keyboard) arrive as trusted input, so Sheets treats them as a user edit and the installable trigger fires. - Sheets draws its grid on a canvas, so cells can't be targeted by selector.
They can be reached by opening
…/edit#gid=<gid>&range=<A1>, or through the Name Box (currently#t-name-box). - Google refuses sign-in from an automated browser. A dedicated Chrome profile directory, signed into by hand once and then reused by Puppeteer, avoids that — and keeps the password out of the script.
- Ctrl+Z in the same browser session restores an overwritten formula
exactly, and fires
onEditlike any other edit. - Sheets autocomplete can change typed text: Enter accepts a suggested completion when the typed value is a prefix of one already in the column.
3. Proposed shape¶
One Node script, sage-tools-api/scripts/verify-event-dry-run.mjs
<event-key> <day>, beside the existing verify-* scripts, in three layers
that each run on their own:
| Layer | Does | Touches the live workbook? |
|---|---|---|
| Preflight | GET /ping (version, config sha), GET /sync/config (event and day registered), POST /sync/:day, every facility in the snapshot recently synced, go-live state |
No |
| Rendering | Builds a fixture from the real snapshot with matches A/B/C applied, opens the console and public pages on localhost, and runs the site test suite's runbook checks on them (_tests/checks/runbook.mjs) |
No |
| Rehearsal | Drives the real facility sheet in Puppeteer: records originals, makes the three edits, waits for the snapshot to change, runs the rendering checks against live data, then restores | Yes |
Rendering assertions run against the real console code, never a Node reimplementation of its rules: the played/BYE and team-event rules already exist twice, and a checker must not become a third copy.
With the rehearsal layer in place, the runbook's Part 1 shrinks to whatever the script can't cover.
The rendering layer is shared with the site test suite
(its §2). That suite owns the browser harness (Playwright, with the page's
data URLs answered in the browser rather than through ?fixture=, which only
the console and PickleDrive's pages support), the runbook checks, and committed
A/B/C fixtures per event type. This script contributes only the fixture
generated from the real snapshot, and calls the same checks. That suite has no
blocking spikes and is built first. Whether the rehearsal layer also moves
from Puppeteer to Playwright is decided with the spikes in §4.
4. Before the full implementation¶
Spikes, in order. Each has a pass condition; a failure changes the design.
- Sign-in and editing spike. Puppeteer with a dedicated profile opens a
copy of a facility workbook, types into one cell, and reads it back.
Passes if the edit persists and the Apps Script execution log shows an
onEditInstallablerun. If Google blocks the profile, the rehearsal layer becomes "Claude in Chrome with an operator watching" instead of a script. - Cell targeting. Confirm
#gid=…&range=…selects the cell on load, and whether it still works when the tab is already open. Fall back to the Name Box if not. - Restore. Overwrite a formula cell, then restore it with Ctrl+Z; confirm the formula (not its value) is back and that the undo also syncs.
- Autocomplete. Confirm whether it fires on team-code columns and the reliable way to suppress it (Delete before Enter, or reading back).
- Court Control input. Check whether its cells carry data validation that would reject or flag the test value.
- Choosing matches A/B/C automatically from the snapshot, per event type — including what the "unmapped code" edit is for a team event.
Then, before writing the full spec:
- Decide where operator credentials for the console come from (local
.env, never committed) and whether the rehearsal layer tests Live/Hide at all. - Decide how a crashed run is recovered: the script writes originals to a
local restore file first, and needs a
--restoremode that works without the browser session Ctrl+Z depends on.
5. Open questions¶
- One facility per run, or every facility workbook for the day?
- Should the rehearsal layer refuse to run once the day is live (
autopast its go-live time, ortrue)? - Does the rendering layer need its own fixtures committed per event, or are they always generated fresh and discarded? (The site test suite commits fixtures per event type; per-event ones would be this script's.)