Skip to content

Commit 4b2ada4

Browse files
committed
Merge remote-tracking branch 'origin/main' into t3code/fix-preview-auth-and-faster-ci
# Conflicts: # docs/operations.md # docs/testing.md # package.json # src/test/docs/check-docs.test.ts # src/test/scripts/backend-status.test.ts
2 parents 85deb5c + 34d1573 commit 4b2ada4

24 files changed

Lines changed: 835 additions & 47 deletions

‎.gitignore‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,14 @@ coverage/
3030
*.tsbuildinfo
3131
.vite/
3232
.playwright-cli/
33+
.playwright-mcp/
3334

3435
# Verification skill state, logs, and evidence
3536
.verify/
3637

38+
# CI failure artifacts downloaded by `npm run ci:failures`
39+
.ci-failures/
40+
3741
# Playwright artifacts
3842
test-results/
3943
playwright-report/
@@ -62,16 +66,12 @@ deno.lock
6266
tasks.txt
6367
humanizer.md
6468

65-
# Local notes/docs, except the project README
66-
*.md
69+
# Local notes. Other Markdown files are not ignored: a blanket *.md rule
70+
# silently dropped new root-level docs.
71+
/TASKS.md
72+
*.local.md
6773
*.txt
6874
*.info
69-
!README.md
70-
!AGENTS.md
71-
!**/README.md
72-
!docs/**/*.md
73-
!plans/**/*.md
74-
!.claude/skills/**/*.md
7575
.vercel
7676
.env*
7777
.worktrees/

‎.nvmrc‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
22

‎.superset/db-gate.sh‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,18 @@
77
# once collide in Docker, not just on a socket. Use this instead of calling
88
# `supabase db start` directly whenever more than one workspace might be
99
# active.
10+
#
11+
# --db-only stops after `npm run test:db` (pgTAP and the database scripts),
12+
# still holding the lock and stopping the stack on exit.
1013
set -euo pipefail
1114

15+
DB_ONLY=false
16+
case "${1:-}" in
17+
--db-only) DB_ONLY=true ;;
18+
"") ;;
19+
*) echo "usage: $0 [--db-only]" >&2; exit 2 ;;
20+
esac
21+
1222
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1323
source "$DIR/lib/lock.sh"
1424

@@ -25,7 +35,12 @@ if [ "$LIVE_PROVIDERS" = 1 ] && { [ -z "${LIVE_OPENROUTER_API_KEY:-}" ] || [ -z
2535
echo "LIVE_PROVIDERS=1 requires LIVE_OPENROUTER_API_KEY and LIVE_EXA_API_KEY" >&2
2636
exit 1
2737
fi
38+
if [ "$DB_ONLY" = true ] && [ "$LIVE_PROVIDERS" = 1 ]; then
39+
echo "--db-only runs the database tests, which LIVE_PROVIDERS=1 skips" >&2
40+
exit 2
41+
fi
2842
stack_started=false
43+
provider_started=false
2944
runtime_dir=""
3045
owned_pids=()
3146

@@ -44,7 +59,10 @@ cleanup() {
4459
trap - EXIT
4560

4661
stop_owned_processes
47-
docker rm -f "$FAKE_PROVIDER" >/dev/null 2>&1 || true
62+
# The container name is shared by every worktree, so only remove our own.
63+
if [ "$provider_started" = true ]; then
64+
docker rm -f "$FAKE_PROVIDER" >/dev/null 2>&1 || true
65+
fi
4866

4967
if [ -n "$runtime_dir" ] && [ -f "$runtime_dir/function.env" ]; then
5068
rm -- "$runtime_dir/function.env"
@@ -83,6 +101,7 @@ for attempt in 1 2 3; do
83101
sleep $((attempt * 30))
84102
done
85103
if [ "$LIVE_PROVIDERS" != 1 ]; then npm run test:db; fi
104+
if [ "$DB_ONLY" = true ]; then exit 0; fi
86105

87106
status_json="$(supabase status -o json 2>/dev/null)"
88107
status_value() {
@@ -114,6 +133,7 @@ fi
114133

115134
# Edge Functions reach the fake provider by container name on the stack network.
116135
docker rm -f "$FAKE_PROVIDER" >/dev/null 2>&1 || true
136+
provider_started=true
117137
docker run -d --rm --name "$FAKE_PROVIDER" --network "supabase_network_$project_id" \
118138
"${provider_mode[@]}" -p 127.0.0.1:18090:8787 -v "$(pwd)/e2e/full-stack/fake-openrouter.mjs:/fake.mjs:ro" \
119139
node:22-alpine node /fake.mjs >/dev/null

‎.superset/e2e-port.sh‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env bash
2+
# Prints this worktree's port for the credential-free Playwright server.
3+
# Every worktree used to run `npm run verify` on 18080, so two worktrees
4+
# verifying at once failed on "port already in use". Ports come from
5+
# 18100-18179 through the same registry dev.sh uses, keyed by worktree.
6+
set -euo pipefail
7+
8+
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9+
source "$DIR/lib/lock.sh"
10+
11+
PORTS_LOCK="$HOME/.superset/debatelab-ports/lock"
12+
WORKSPACE_KEY="e2e:$(git -C "$DIR/.." rev-parse --show-toplevel)"
13+
14+
lock_acquire "$PORTS_LOCK" 30 >&2
15+
trap 'lock_release "$PORTS_LOCK"' EXIT
16+
node "$DIR/lib/port-alloc.cjs" allocate "$WORKSPACE_KEY" 18100 18179

‎.superset/lib/port-alloc.cjs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ function readData() {
2525
}
2626
}
2727

28+
// Keys are worktree paths, optionally prefixed ("e2e:/path"). Drop entries
29+
// whose worktree is gone so deleted worktrees don't hold ports forever.
30+
function pruneRemoved(data) {
31+
for (const key of Object.keys(data)) {
32+
const dir = key.replace(/^[a-z0-9]+:(?=\/)/, '');
33+
if (path.isAbsolute(dir) && !fs.existsSync(dir)) delete data[key];
34+
}
35+
return data;
36+
}
37+
2838
function writeData(data) {
2939
fs.mkdirSync(path.dirname(file), { recursive: true });
3040
fs.writeFileSync(file, JSON.stringify(data, null, 2));
@@ -38,7 +48,7 @@ if (!cmd || !workspace) {
3848
}
3949

4050
if (cmd === 'allocate') {
41-
const data = readData();
51+
const data = pruneRemoved(readData());
4252
if (data[workspace]) {
4353
console.log(data[workspace]);
4454
process.exit(0);

‎.superset/teardown.sh‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ PORTS_LOCK="$HOME/.superset/debatelab-ports/lock"
1212

1313
lock_acquire "$PORTS_LOCK" 30
1414
node "$DIR/lib/port-alloc.cjs" release "$WORKSPACE_KEY" || true
15+
node "$DIR/lib/port-alloc.cjs" release "e2e:$(git rev-parse --show-toplevel)" || true
1516
lock_release "$PORTS_LOCK"
1617

1718
if command -v supabase >/dev/null 2>&1 && supabase status >/dev/null 2>&1; then

‎AGENTS.md‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
- The repository is DebateLab. Never infer its directory from an old repository name.
66
- Run `git rev-parse --show-toplevel` and stay inside that returned worktree. On Adam's VPS, `/home/codex/Projects/DebateLab` is the primary checkout, but Superset and other agents use linked worktrees elsewhere.
77
- Before editing, inspect the branch and working tree. Preserve unrelated changes.
8-
- Use Node 22, npm, and the committed `package-lock.json`. Install with `npm ci`.
8+
- Use Node 22, npm, and the committed `package-lock.json`. `.nvmrc` pins 22, but the VPS default is Node 24: check `node -v` and run `nvm use` (with `NVM_DIR=$HOME/.nvm` set) when it isn't 22. Install with `npm ci` when `node_modules` is missing.
99

1010
## Parallel Superset workspaces
1111

@@ -30,7 +30,7 @@ The frontend is hosted on Vercel at `https://debate.pi-stack.org`. Merging to `m
3030
## Normal ports
3131

3232
- Vite development server: `8080` by default outside Superset; Superset allocates `8080-8179` per worktree.
33-
- Automated browser server: isolated `127.0.0.1:18080`, never an existing dev server.
33+
- Automated browser server: a per-worktree port from `18100-18179`, printed by `bash .superset/e2e-port.sh`, never an existing dev server. Protected staging keeps `127.0.0.1:18080`, the origin deployed CORS allows.
3434
- Tailnet previews for Adam: `npm run dev:preview` listens locally on `9200-9299` and is published on tailnet HTTPS `9100-9149`; it pins the app to staging. See [docs/development.md](docs/development.md#preview-on-the-tailnet).
3535
- Local Supabase: CLI defaults beginning at `54321`. The stack is not parallel-safe across worktrees.
3636

@@ -46,17 +46,20 @@ npm test
4646
npm run test:collaboration
4747
npm run test:edge
4848
npm run build
49-
CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e
49+
P=$(bash .superset/e2e-port.sh) && CI=1 PORT=$P PLAYWRIGHT_BASE_URL=http://127.0.0.1:$P npm run test:e2e
5050
```
5151

5252
`npm run verify` runs the credential-free ladder with the desktop Chromium `@smoke` browser set (about a minute). `npm run verify:full` adds the full desktop and mobile suite.
5353

5454
If GitHub Actions jobs fail within 2 to 5 seconds with no logs, they did not run: the account is out of Actions minutes (billing), not failing on the code. In that case run the same checks locally at the final head (`npm run verify` and the database gate) and record the exact results in the PR. A local pass is not a CI pass; say which one you have.
5555

56+
When a CI job fails, start with `npm run ci:failures -- <pr>`. It prints the failing jobs and specs, downloads the failure artifact into `.ci-failures/`, and shows each failed test's error and the dialogs and headings on the page from its `error-context.md`. Read that before guessing at timeouts or infrastructure.
57+
5658
Database migrations and pgTAP policies use a separate Docker-backed gate. In Superset, use the locking wrapper:
5759

5860
```bash
5961
./.superset/db-gate.sh
62+
./.superset/db-gate.sh --db-only # pgTAP and database scripts only
6063
```
6164

6265
Outside Superset, run the underlying commands directly:
@@ -73,7 +76,7 @@ WebKit is a separate release smoke test:
7376

7477
```bash
7578
npx playwright install --with-deps webkit
76-
CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e:webkit
79+
P=$(bash .superset/e2e-port.sh) && CI=1 PORT=$P PLAYWRIGHT_BASE_URL=http://127.0.0.1:$P npm run test:e2e:webkit
7780
```
7881

7982
## Verifying features in the real app
@@ -134,7 +137,7 @@ Open only what the task needs:
134137
- [docs/testing.md](docs/testing.md): what each gate proves, browser matrices, staging, artifacts, and cleanup.
135138
- [docs/operations.md](docs/operations.md): CI, Vercel hosting, staging, deployment, rollback, and live verification.
136139
- [supabase/README.md](supabase/README.md): Edge Functions, shared backend modules, migrations, auth boundaries, and required secret names.
137-
- [plans/README.md](plans/README.md): work in progress, the backlog, and shipped plans. Every plan lives in `plans/`; write new plans there. Revalidate every plan against current source before executing it.
140+
- [plans/README.md](plans/README.md): work in progress, the backlog, and shipped plans. Every plan lives in `plans/` and opens with a `**Status:**` line; write new plans there. Commit a plan before splitting it across agent threads, and put its path in each thread's prompt. Revalidate every plan against current source before executing it.
138141
- [docs/README.md](docs/README.md): complete documentation index and freshness labels.
139142

140143
When commands, ports, workflows, or runtime boundaries change, update the owning focused document in the same change. Do not copy large source trees, schemas, dependency lists, or model catalogs into documentation.

‎docs/development.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ Never overwrite unrelated changes. Do not switch to the primary checkout because
2525
- Playwright browser binaries for browser gates
2626
- Xcode and CocoaPods or Swift Package Manager support for native iOS work
2727

28-
Install the locked JavaScript tree:
28+
Install the locked JavaScript tree with the Node version from `.nvmrc`:
2929

3030
```bash
31+
nvm use
3132
npm ci
3233
```
3334

@@ -47,7 +48,7 @@ For another explicit local port:
4748
npm run dev -- --host 127.0.0.1 --port 8081
4849
```
4950

50-
Automated browser tests own `127.0.0.1:18080` by default and always start a fresh server.
51+
Automated browser tests always start a fresh server. `npm run verify` puts it on this worktree's port from `18100-18179` (`bash .superset/e2e-port.sh`); a bare Playwright run falls back to `127.0.0.1:18080`.
5152

5253
## Preview on the tailnet
5354

@@ -142,7 +143,7 @@ Never point local tests at staging or production. Never apply a migration to an
142143
[`playwright.config.ts`](../playwright.config.ts) starts Vite on an isolated port and refuses to reuse an existing server.
143144

144145
```bash
145-
CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e
146+
P=$(bash .superset/e2e-port.sh) && CI=1 PORT=$P PLAYWRIGHT_BASE_URL=http://127.0.0.1:$P npm run test:e2e
146147
```
147148

148149
Failure artifacts go to ignored `test-results/` and `playwright-report/`. Protected staging disables traces, screenshots, and video.

‎docs/operations.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ If Actions jobs fail within 2 to 5 seconds with no runner and no logs, the accou
3636

3737
## Frontend hosting on Vercel
3838

39-
Vercel builds and serves the frontend from the Git repository. Merging to `main` starts a production deployment to `https://debate.pi-stack.org`; pull requests get two preview deployments (see [Per-PR previews](#per-pr-previews)). The production canary runs after each successful Vercel `Production` deployment.
39+
Vercel builds and serves the frontend from the Git repository. Merging to `main` starts a production deployment to `https://debate.pi-stack.org`; pull requests get two preview deployments (see [Per-PR previews](#per-pr-previews)). For a staging-pinned preview on the tailnet, use `npm run dev:preview` (see [development.md](development.md#preview-on-the-tailnet)). The production canary runs after each successful Vercel `Production` deployment.
4040

4141
[`vercel.json`](../vercel.json) owns:
4242

@@ -66,14 +66,14 @@ The staging browser suite disables trace, screenshot, and video capture. It crea
6666

6767
Do not enable the staging repository variable before every prerequisite is present. A skipped staging job is not a pass.
6868

69-
`STAGING_E2E_ENABLED` has been `true` since 2026-09-18, and the job passed through 2026-09-23. Since then it has been skipped because the `Quality` run it waits for fails before any runner starts (see [When GitHub Actions cannot run](#when-github-actions-cannot-run)). Restoring Actions minutes restores the staging job; nothing in the workflow needs changing.
69+
`STAGING_E2E_ENABLED` has been `true` since 2026-09-18. The job runs after every successful `Quality` run from a push to `main`. Quality runs from pull requests also trigger it, and those show as skipped by design. Check the latest real result with `gh run list --workflow staging-e2e.yml` and `npm run ci:failures -- <run-id>`, not this page. The run uploads no artifact, so the job log is the only evidence.
7070

7171
## Staging sync
7272

7373
Staging is project `debate-trainer-staging` (`brifeyypjauyusmkptkx`). Production is `debate-trainer` (`uziwdtkzofptihahfooo`). Every backend change goes to staging first, passes the staging browser suite, and only then goes to production. Steps 1 and 2 are read-only. Steps 3 and 4 change staging and need Adam's authorization each time.
7474

7575
1. Confirm the target. `supabase projects list` must show the staging ref under the staging name. Stop if a command, link, or environment variable points at the production ref.
76-
2. Find what is pending. Run `npm run backend:status -- --project-ref brifeyypjauyusmkptkx --check` from a checkout of the commit you mean to ship. It compares staging with the committed tree and exits 1 when a migration is not applied. Where that script is not available, run its two read-only queries directly:
76+
2. Find what is pending. Run `npm run backend:status -- --project-ref brifeyypjauyusmkptkx --check` from a checkout of the commit you mean to ship. It compares staging with the committed tree and exits 1 when a migration is not applied. It also lists expected function secrets the project lacks (names only) and prints the Auth site URL and redirect allowlist. Where that script is not available, run its two read-only queries directly:
7777

7878
```bash
7979
supabase migration list --project-ref brifeyypjauyusmkptkx
@@ -110,7 +110,7 @@ The staging project's name must not start with `debate-navigator-`, or productio
110110

111111
## Backend drift check
112112

113-
`npm run backend:status` shows what the current commit has that a Supabase project lacks. It inspects the project the Supabase CLI resolves, which is `SUPABASE_PROJECT_ID` when that is set and the linked project otherwise; `npm run backend:status -- --project-ref <ref>` inspects another one. It runs `supabase migration list` and `supabase functions list` and changes no schema, data, functions, or secrets. To connect to the database, the CLI creates its usual short-lived login role.
113+
`npm run backend:status` shows what the current commit has that a Supabase project lacks. It inspects the project the Supabase CLI resolves, which is `SUPABASE_PROJECT_ID` when that is set and the linked project otherwise; `npm run backend:status -- --project-ref <ref>` inspects another one. It runs `supabase migration list`, `supabase functions list` and `supabase secrets list`, reads the Auth site URL and redirect allowlist from the Management API with the CLI's access token (`SUPABASE_ACCESS_TOKEN`, or the `access-token` file in `SUPABASE_HOME`, default `~/.supabase`), and changes no schema, data, functions, or secrets. It prints secret names only, never values or digests. To connect to the database, the CLI creates its usual short-lived login role.
114114

115115
The comparison uses the committed tree at `HEAD`, so commit or stash local edits first. It reports:
116116

‎docs/testing.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,12 @@ Run this after `npm run build`. It serves `dist/` through Vite preview and prove
107107
## Credential-free browser gate
108108

109109
```bash
110-
CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e
110+
P=$(bash .superset/e2e-port.sh) && CI=1 PORT=$P PLAYWRIGHT_BASE_URL=http://127.0.0.1:$P npm run test:e2e
111111
```
112112

113113
Pull requests run only tests tagged `@smoke` on desktop Chromium (`npm run test:e2e:smoke`, about 30 tests). Tag a test `@smoke` only when it covers a whole user path that would otherwise go unchecked until the nightly run.
114114

115-
Playwright starts a new Vite server on `127.0.0.1:18080` and runs:
115+
`.superset/e2e-port.sh` gives each worktree its own port from `18100-18179`, recorded in `~/.superset/debatelab-ports/allocations.json`, so two worktrees can run the gate at once. Without `PORT`, Playwright falls back to `18080`. Playwright starts a new Vite server on that port and runs:
116116

117117
- desktop Chromium at 1280 by 800
118118
- mobile Chromium using the Pixel 5 device profile
@@ -165,7 +165,7 @@ In Superset, run:
165165
./.superset/db-gate.sh
166166
```
167167

168-
The wrapper acquires `~/.superset/debatelab-db.lock`, starts full local Supabase, runs `npm run test:db`, then holds the same lock while a controlled Mailpit relay, collaboration process, fake model provider, and Vite client run the real-Auth desktop/mobile workspace and full-stack feature journeys. The `Database` workflow runs this same gate on pull requests that touch its paths, and retains its logs and browser traces on failure. It stops every owned process and the stack on exit, including after failure. Outside Superset, run only the database portion with:
168+
The wrapper acquires `~/.superset/debatelab-db.lock`, starts full local Supabase, runs `npm run test:db`, then holds the same lock while a controlled Mailpit relay, collaboration process, fake model provider, and Vite client run the real-Auth desktop/mobile workspace and full-stack feature journeys. The `Database` workflow runs this same gate on pull requests that touch its paths, and retains its logs and browser traces on failure. It stops every owned process and the stack on exit, including after failure. To run only the database portion under the same lock, use `./.superset/db-gate.sh --db-only`. Outside Superset, run it with:
169169

170170
```bash
171171
supabase db start
@@ -265,7 +265,7 @@ npx playwright install --with-deps webkit
265265
Run:
266266

267267
```bash
268-
CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e:webkit
268+
P=$(bash .superset/e2e-port.sh) && CI=1 PORT=$P PLAYWRIGHT_BASE_URL=http://127.0.0.1:$P npm run test:e2e:webkit
269269
```
270270

271271
This is the Safari-engine smoke gate. State explicitly when it did not run.
@@ -319,6 +319,8 @@ Tests are not the proof that a change works; using it is. Two local recipes that
319319

320320
[`.github/workflows/live-e2e.yml`](../.github/workflows/live-e2e.yml) runs the live provider journeys, and [`.github/workflows/prod-canary.yml`](../.github/workflows/prod-canary.yml) runs the production canary. `main` has no branch protection or rulesets (checked 2026-10-01; GitHub Free does not enforce them on private repositories), so no check is required. Path-filtered workflows that skip therefore don't block merges. Read the PR's checks before merging.
321321

322+
To see why a run failed, use `npm run ci:failures` (current branch's PR), `npm run ci:failures -- <pr>` or `npm run ci:failures -- <run-id>`. It works while the run is still going, as soon as a job fails. It lists failed jobs and steps, prints the Playwright or Vitest summary from each job log, downloads the `*-failure-<run-id>` artifacts into ignored `.ci-failures/`, and prints each failed test's error plus the dialogs and headings from its `error-context.md` page snapshot. A dialog the spec never answered shows up there. Protected staging uploads no artifact, so only its job log is available.
323+
322324
Before calling CI green:
323325

324326
1. identify the current head commit;

0 commit comments

Comments
 (0)