Write automated tests for the uptime monitor in this repo. Treat the acceptance criteria below as the spec. Use Vitest for the evaluator, maintenance, uptime math, jobs, prober checks, and API routes, and Playwright for the dashboard and public status page. Run the bundled test server from `apps/testserver/` inside the test suite so HTTP, TCP, and keyword checks hit something real. Each test file opens its own on-disk SQLite database in a temp directory (`mkdtemp` then `app.db`), runs the migrations, and deletes the directory when done. Mock only the outside services the app is allowed to use (the nodemailer transport, Twilio, Slack, Discord, webhook receivers) at the module boundary and assert on call counts and payloads. Run the `home` prober in-process and a second in-process prober with `--region laptop` for the integration flows. GitHub Actions is that same script with `--once`, so no workflow runs in tests.

## Acceptance criteria to cover

1. The admin signs in, creates an HTTP monitor, and after one interval the detail page shows results from both `home` and `laptop`.
2. The test server fails for one region only (key on a per-region header the prober sends in test mode). After four checks there is no incident and status is `up`.
3. The test server fails for everyone. After the second consecutive failure from each region there is exactly one incident with `cause = "HTTP 500"` and `single_region = 0`, and each attached channel has exactly one `down` delivery. A third failure creates no new incident or delivery.
4. Recovery from both regions sets `resolved_at`, writes `duration_seconds` correctly, and creates exactly one `up` delivery per channel.
5. Keyword `present` missing, keyword `absent` found, and `max_response_ms = 1` each fail with the documented cause strings.
6. TCP: open port ok, closed port `"Connection refused"`, blackholed address times out at `timeout_ms` (use `10.255.255.1` with a 1000 ms timeout).
7. DNS with a wrong `expected_values` entry fails and `detail.answers` contains the real answers (stub `dns.promises.Resolver`). SSL: a self-signed cert with 10 days left and `warn_days = 14` fails with `"Certificate expires in 10 days"`; an expired cert fails with the `authorizationError`. Generate the certs in a fixture with `openssl` or `node-forge`.
8. Heartbeat: no ping for `interval + grace` opens an incident on the next `evaluate` run; a ping resolves it; `?status=fail` opens one immediately; an unknown token returns 404.
9. Maintenance: failures during an active window open an `is_maintenance` incident with zero deliveries and 30-day uptime stays 100.000. `isInMaintenance` is true at 06:30 UTC in both January and July for a weekly 02:00 to 03:00 `America/New_York` window, and correct for a 23:30 to 00:30 `Asia/Kolkata` window on both sides of midnight.
10. Uptime math: one 72-minute incident in 30 days on an old monitor gives 99.833; an open incident counts up to `now`; a monitor created 10 days ago uses a 10-day denominator; maintenance seconds leave the denominator.
11. Single region: with no second region ever reported, and separately with `gha` silent for 901 seconds, two consecutive `home` failures open an incident with `single_region = 1`, the email subject contains `[single region]`, and the webhook payload carries `single_region: true`. One failure alone does not.
12. Public status page renders banner, per-monitor bars, and percentages; subscribe writes a confirmation to the outbox; confirming enables incident emails; unsubscribe removes the row and stops emails.
13. Webhook deliveries carry a valid `X-Uptime-Signature`; a receiver that returns 500 gets three attempts with backoff, then the delivery is marked failed with `last_error`.
14. Downsample: seed 8 days of `check_results`, run the `downsample` job with an injected `now`, and assert `response_buckets` rows exist for yesterday at 3600 and 21600 seconds with correct avg and p95, raw rows older than `raw_retention_days` are gone, and incidents and `daily_uptime` are untouched.
15. A fresh clone runs with `pnpm install && pnpm dev` and no env vars, and the first browser visit creates the admin. Playwright with an empty env and data directory: visit `/`, complete the setup form, assert one user, one workspace, one owner.
16. `docker compose up -d` on a clean Ubuntu VPS with one A record serves the app over HTTPS with no other setup. In CI: `docker compose config` validates, the image builds, and the container answers `GET /api/health` with 200. TLS stays on the manual checklist.
17. Killing the process mid-job and restarting it doesn't double-run or lose the job. Job lock test: two `lib/jobs.ts` runners share one database and only one acquires `rollup`; a runner that dies holding the lock (never releases) lets the other take over once `locked_until` passes; running `rollup` twice for one day leaves one `daily_uptime` row per monitor.
18. `pnpm backup` then deleting `./data/app.db` and restoring from the backup brings every record back. Seed, back up, delete, copy the backup in, and compare row counts and a checksum per table.
19. Every outbound email in dev shows up in `./data/outbox`. With `SMTP_URL` unset, trigger a down alert, a subscriber confirmation, and an invite, and assert three `.eml` files with the right subject and recipient. With `SMTP_URL` set and the transport mocked, the outbox stays empty and the transport gets three calls.
20. Two probers claiming the same region at the same time never receive the same monitor in the same interval (assert on the `UPDATE ... RETURNING` claim with `Promise.all` over two `tick()` calls).

## Layout

- `tests/unit/evaluate.test.ts`: table-driven cases for 2, 3, 4, 8, 11. Each case is `{ name, monitor, regionStates, now, maintenance, expectEffects }`.
- `tests/unit/maintenance.test.ts` and `tests/unit/uptime.test.ts`: criteria 9 and 10 with an injected `now`.
- `tests/unit/checks/*.test.ts`: criteria 5, 6, 7 against the test server and fixtures.
- `tests/unit/jobs.test.ts` and `tests/unit/mail.test.ts`: criteria 17 and 19.
- `tests/integration/`: API routes with the temp SQLite database and in-process probers for 1, 2, 3, 4, 8, 11, 13, 14, 18, 20.
- `tests/e2e/`: Playwright for 1, 12, 15. A separate `tests/docker/` script for 16.

## Rules

- Name every test after its criterion: `test("AC3: two regions with two consecutive failures open one incident")`.
- Freeze time with `vi.useFakeTimers()` or an injected clock. Never depend on the real date, and never sleep for a real interval; drive the prober loop by calling its `tick()` function directly and jobs by calling their `run()` directly.
- Never call real SMTP, Twilio, Slack, Discord, GitHub, or public DNS. Every network dependency other than the bundled test server is a mock or a fixture.
- Add `pnpm test` and a GitHub Actions workflow that runs it on push. No services needed: SQLite is a file, so the workflow is checkout, `pnpm install`, `pnpm test`.
- Run the suite. Fix the app where the app is wrong and the test where the test is wrong. Report per-criterion pass or fail and what changed.
