Adds the first Playwright spec that drives a real running zddc-server
in Chromium. Future UI debugging (the conflict-UI in phase 5, browser-
side iteration on the master's HTML pages, etc.) reuses the same
harness — beforeAll spins up a master on a random port, the spec
talks to it, afterAll tears it down.
Files:
- tests/lib/server.js: CommonJS module exporting startMaster(opts).
Builds the binary on first run via the canonical podman/zddc-go:1.24
invocation from AGENTS.md, caching at zddc/dist/zddc-server-test
with a sibling .hash file (SHA256 of cmd/+internal/+go.{mod,sum})
that invalidates on source change. Subsequent runs skip the build.
Set ZDDC_TEST_BIN=<path> to use a pre-built binary (CI / debugging).
Seeds a minimal master root in os.tmpdir() with a permissive .zddc
granting the test user (default alice@example.com) full access plus
read for *@example.com. Picks a free port via net.listen(:0), spawns
the binary on 127.0.0.1:<port>, polls until listening (max 10s).
Returns { baseURL, root, proc, logs(), stop() }.
CommonJS (require/module.exports) rather than ESM because Playwright's
loader transforms top-level `import` in *.spec.js files but not in
the .js helpers we ship alongside; mixing produces "exports is not
defined in ES module scope" at the helper's first line. Spec files
use `import { ... } from './lib/server.js'` and the import resolves
through the CJS interop layer cleanly.
- tests/tokens.spec.js: 8 server-backed scenarios covering the entire
/.tokens contract:
1. Anonymous → 401 on /.tokens (X-Auth-Request-Email empty).
2. Authenticated GET /.tokens renders the page with the user's email
visible in the .who line and the create form + tokens table both
present and populated.
3. GET /.api/tokens returns an empty list initially.
4. Create-via-page round-trip: fill the form, click submit, plaintext
appears once in #created .token-secret (hidden from later reads),
row appears in the table, API list confirms the description, the
row's Revoke button removes it from both the table and the API.
5. Plaintext token authenticates a subsequent Bearer request even
when X-Auth-Request-Email is empty — confirms the middleware
bridge from Bearer to ACL email.
6. Invalid Bearer → 401 (no silent fallback to anonymous).
7. Cross-user revoke returns 404 (not 403) — the ownership-non-leak
guarantee.
8. XSS guard: description with <img src=x onerror="window.__xss=1">
should render as text (assert window.__xss !== 1) — the inline
JS's escapeHTML is the only thing standing between an attacker
who could create tokens and stored XSS on the management page.
test.use({ extraHTTPHeaders }) injects X-Auth-Request-Email on every
request from the Playwright browser context, mimicking what an
upstream auth proxy adds in production. Per-test overrides clear it
to test anonymous paths.
- playwright.config.js: adds the `tokens` project. Bumps the global
timeout from 30s → 60s so the first run's binary-build (~30s on a
cold gocache) doesn't time out the suite. The tokens project
testMatches only tokens.spec.js, so other projects (the file://-
driven tool tests) are unaffected.
Verified: all 8 tests pass (12.5s warm; ~45s cold including the build).
The harness is ready to graft additional server-backed specs onto —
phase 5's conflict-UI in particular will follow the same pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>