Write automated tests for the scheduling app in this repo. Treat the acceptance criteria below as the spec. Use Vitest for the slot algorithm, jobs, and API routes, and Playwright for guest booking flows. Every test file gets its own on-disk temp SQLite file (`./data/test/<file>-<pid>.db`), migrated in `beforeAll` and deleted in `afterAll`. The only things you mock are the Google Calendar client and the SMTP transport, both at the module boundary; record one real-shaped freebusy and events.insert response as fixtures. Run everything else for real: the local calendar provider, the outbox, the job lock.

## Acceptance criteria to cover

1. First visit with no hosts creates the admin; sign-in sets a signed cookie; connecting Google (mocked) stores an encrypted refresh token and lists calendars; without Google the Calendars screen shows the local provider.
2. Weekly hours Mon–Fri 09:00–17:00 `America/New_York`, 30-minute event, no busy time: 16 slots on a weekday, 0 on Saturday.
3. Busy 10:00–11:00 with 15-minute buffers removes 09:30, 10:00, 10:30, 11:00.
4. `min_notice_hours = 4` hides slots inside the next four hours relative to an injected `now`.
5. Date override `[]` hides all slots that day; override `[["10:00","14:00"]]` shows only those.
6. Guest in `Asia/Kolkata` sees correct half-hour-offset times; a host week spanning the US DST change in March and November produces correct UTC starts on both sides.
7. Two concurrent bookings for the same slot: exactly one confirmed booking and one 409.
8. On the local provider a booking inserts a `busy_blocks` row with `source = booking`; on Google it calls `events.insert` with both attendees and `conferenceDataVersion = 1` and stores the returned `hangoutLink`. Either way it queues two confirmation notifications plus two reminders, and the confirmation `.eml` carries a parseable `.ics`.
9. Cancelling deletes the calendar event (busy block or `events.delete`) and nulls `send_at` on unsent reminders.
10. Rescheduling calls `patchEvent` once, never `createEvent`, and creates a `rescheduled` notification.
11. Running the notifications job twice in a row sends each due notification once (count `.eml` files in the outbox).
12. Guest booking page: pick a day, pick a slot, fill the form, see the confirmation with manage link (Playwright).
13. Booting with an empty `./data` and no env vars creates `app.db`, runs migrations, and redirects the first request to `/setup`.
14. `docker compose config` validates and lists `app`, `caddy`, and `backup`; the Caddyfile references the `app` service. (Static check; the real HTTPS check is manual.)
15. Job lock: two job runners started at once for `notifications` take the lock exactly once; a runner killed mid-tick leaves a `job_locks` row that expires, and the next tick re-claims it and retries rows with a stale `claimed_at` without double-sending.
16. `pnpm backup` writes `./data/backups/<date>.db`; deleting `app.db` and restoring it brings back every host, event type, booking, and notification row.
17. With `SMTP_URL` unset, sending mail writes one `.eml` per message to `./data/outbox` with the right `To` and `Subject`.
18. A guest who submits a slot that was taken between page load and submit sees the "just taken" message and a refreshed slot list.
19. `max_per_day = 2` hides all slots on a day that already has two confirmed bookings.

## Layout

- `tests/unit/slots.test.ts`: table-driven cases for 2, 3, 4, 5, 6, 19. Each case is `{ name, availability, eventType, busy, bookings, now, guestTz, expectStarts }`.
- `tests/integration/`: API routes, jobs, and scripts against the temp SQLite file for 1, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17. Use `Promise.all` with two requests for criterion 7 and two runner instances for criterion 15.
- `tests/e2e/`: Playwright for 12 and 18, on the local calendar provider with the outbox, so no mocks are needed.

## Rules

- Name every test after its criterion: `test("AC7: concurrent bookings produce one confirmed and one 409")`.
- Freeze time with `vi.useFakeTimers()` or an injected clock; never depend on the real date.
- Point every test at its own `DATABASE_PATH` and its own `./data/test/<file>/outbox` so files run in parallel without touching each other.
- Add `pnpm test` and a GitHub Actions workflow that runs it. No services needed: SQLite is a file and the outbox is a folder.
- Run the suite. Fix the app where the app is wrong and the test where the test is wrong. Report per-criterion pass/fail and what changed.
