Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ coverage/
*.tsbuildinfo
.vite/
.playwright-cli/
.playwright-mcp/

# Verification skill state, logs, and evidence
.verify/

# CI failure artifacts downloaded by `npm run ci:failures`
.ci-failures/

# Playwright artifacts
test-results/
playwright-report/
Expand Down Expand Up @@ -62,16 +66,12 @@ deno.lock
tasks.txt
humanizer.md

# Local notes/docs, except the project README
*.md
# Local notes. Other Markdown files are not ignored: a blanket *.md rule
# silently dropped new root-level docs.
/TASKS.md
*.local.md
*.txt
*.info
!README.md
!AGENTS.md
!**/README.md
!docs/**/*.md
!plans/**/*.md
!.claude/skills/**/*.md
.vercel
.env*
.worktrees/
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
15 changes: 15 additions & 0 deletions .superset/db-gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,18 @@
# once collide in Docker, not just on a socket. Use this instead of calling
# `supabase db start` directly whenever more than one workspace might be
# active.
#
# --db-only stops after `npm run test:db` (pgTAP and the database scripts),
# still holding the lock and stopping the stack on exit.
set -euo pipefail

DB_ONLY=false
case "${1:-}" in
--db-only) DB_ONLY=true ;;
"") ;;
*) echo "usage: $0 [--db-only]" >&2; exit 2 ;;
esac

DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$DIR/lib/lock.sh"

Expand All @@ -25,6 +35,10 @@ if [ "$LIVE_PROVIDERS" = 1 ] && { [ -z "${LIVE_OPENROUTER_API_KEY:-}" ] || [ -z
echo "LIVE_PROVIDERS=1 requires LIVE_OPENROUTER_API_KEY and LIVE_EXA_API_KEY" >&2
exit 1
fi
if [ "$DB_ONLY" = true ] && [ "$LIVE_PROVIDERS" = 1 ]; then
echo "--db-only runs the database tests, which LIVE_PROVIDERS=1 skips" >&2
exit 2
fi
stack_started=false
provider_started=false
runtime_dir=""
Expand Down Expand Up @@ -87,6 +101,7 @@ for attempt in 1 2 3; do
sleep $((attempt * 30))
done
if [ "$LIVE_PROVIDERS" != 1 ]; then npm run test:db; fi
if [ "$DB_ONLY" = true ]; then exit 0; fi

status_json="$(supabase status -o json 2>/dev/null)"
status_value() {
Expand Down
16 changes: 16 additions & 0 deletions .superset/e2e-port.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Prints this worktree's port for the credential-free Playwright server.
# Every worktree used to run `npm run verify` on 18080, so two worktrees
# verifying at once failed on "port already in use". Ports come from
# 18100-18179 through the same registry dev.sh uses, keyed by worktree.
set -euo pipefail

DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$DIR/lib/lock.sh"

PORTS_LOCK="$HOME/.superset/debatelab-ports/lock"
WORKSPACE_KEY="e2e:$(git -C "$DIR/.." rev-parse --show-toplevel)"

lock_acquire "$PORTS_LOCK" 30 >&2
trap 'lock_release "$PORTS_LOCK"' EXIT
node "$DIR/lib/port-alloc.cjs" allocate "$WORKSPACE_KEY" 18100 18179
12 changes: 11 additions & 1 deletion .superset/lib/port-alloc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ function readData() {
}
}

// Keys are worktree paths, optionally prefixed ("e2e:/path"). Drop entries
// whose worktree is gone so deleted worktrees don't hold ports forever.
function pruneRemoved(data) {
for (const key of Object.keys(data)) {
const dir = key.replace(/^[a-z0-9]+:(?=\/)/, '');
if (path.isAbsolute(dir) && !fs.existsSync(dir)) delete data[key];
}
return data;
}

function writeData(data) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, JSON.stringify(data, null, 2));
Expand All @@ -38,7 +48,7 @@ if (!cmd || !workspace) {
}

if (cmd === 'allocate') {
const data = readData();
const data = pruneRemoved(readData());
if (data[workspace]) {
console.log(data[workspace]);
process.exit(0);
Expand Down
1 change: 1 addition & 0 deletions .superset/teardown.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ PORTS_LOCK="$HOME/.superset/debatelab-ports/lock"

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

if command -v supabase >/dev/null 2>&1 && supabase status >/dev/null 2>&1; then
Expand Down
11 changes: 7 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
- The repository is DebateLab. Never infer its directory from an old repository name.
- 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.
- Before editing, inspect the branch and working tree. Preserve unrelated changes.
- Use Node 22, npm, and the committed `package-lock.json`. Install with `npm ci`.
- 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.

## Parallel Superset workspaces

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

- Vite development server: `8080` by default outside Superset; Superset allocates `8080-8179` per worktree.
- Automated browser server: isolated `127.0.0.1:18080`, never an existing dev server.
- 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.
- 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).
- Local Supabase: CLI defaults beginning at `54321`. The stack is not parallel-safe across worktrees.

Expand All @@ -46,17 +46,20 @@ npm test -- --coverage
npm run test:collaboration
npm run test:edge
npm run build
CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e
P=$(bash .superset/e2e-port.sh) && CI=1 PORT=$P PLAYWRIGHT_BASE_URL=http://127.0.0.1:$P npm run test:e2e
```

`npm run verify` runs the complete credential-free ladder, including desktop and mobile Chromium against a fresh Vite server.

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.

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.

Database migrations and pgTAP policies use a separate Docker-backed gate. In Superset, use the locking wrapper:

```bash
./.superset/db-gate.sh
./.superset/db-gate.sh --db-only # pgTAP and database scripts only
```

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

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

## Verifying features in the real app
Expand Down
7 changes: 4 additions & 3 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ Never overwrite unrelated changes. Do not switch to the primary checkout because
- Playwright browser binaries for browser gates
- Xcode and CocoaPods or Swift Package Manager support for native iOS work

Install the locked JavaScript tree:
Install the locked JavaScript tree with the Node version from `.nvmrc`:

```bash
nvm use
npm ci
```

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

Automated browser tests own `127.0.0.1:18080` by default and always start a fresh server.
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`.

## Preview on the tailnet

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

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

Failure artifacts go to ignored `test-results/` and `playwright-report/`. Protected staging disables traces, screenshots, and video.
Expand Down
10 changes: 6 additions & 4 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ Run this after `npm run build`. It serves `dist/` through Vite preview and prove
## Credential-free browser gate

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

Playwright starts a new Vite server on `127.0.0.1:18080` and runs:
`.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:

- desktop Chromium at 1280 by 800
- mobile Chromium using the Pixel 5 device profile
Expand Down Expand Up @@ -164,7 +164,7 @@ In Superset, run:
./.superset/db-gate.sh
```

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. Pull requests run this same gate in the `database-tests` job and retain 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:
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. Pull requests run this same gate in the `database-tests` job and retain 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:

```bash
supabase db start
Expand Down Expand Up @@ -264,7 +264,7 @@ npx playwright install --with-deps webkit
Run:

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

This is the Safari-engine smoke gate. State explicitly when it did not run.
Expand Down Expand Up @@ -311,6 +311,8 @@ A skipped staging workflow is not a pass. Missing credentials or provider access

[`.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. Branch protection on `main` requires `quality`, `database-tests`, `browser-tests`, and `live-e2e`. It also requires a pull request, and administrators are not exempt.

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.

Before calling CI green:

1. identify the current head commit;
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"dev:preview": "VITE_STAGING_PREVIEW=true vite --host 127.0.0.1",
"docs:check": "node scripts/check-docs.mjs",
"backend:status": "node scripts/backend-status.mjs",
"ci:failures": "node scripts/ci-failures.mjs",
"build": "vite build && node scripts/check-build-boundaries.mjs",
"build:dev": "vite build --mode development",
"ios:build": "vite build --mode capacitor && cap sync ios",
Expand All @@ -29,7 +30,7 @@
"test:e2e:webkit": "playwright test --project=webkit",
"test:sw": "node scripts/test-service-worker-offline.mjs",
"test:e2e:staging": "playwright test --project=staging",
"verify": "npm run docs:check && npm run lint && npm run typecheck && npm test -- --coverage && npm run test:collaboration && npm run test:edge && npm run build && npm run test:sw && CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e"
"verify": "npm run docs:check && npm run lint && npm run typecheck && npm test -- --coverage && npm run test:collaboration && npm run test:edge && npm run build && npm run test:sw && P=$(bash .superset/e2e-port.sh) && CI=1 PORT=$P PLAYWRIGHT_BASE_URL=http://127.0.0.1:$P npm run test:e2e"
},
"dependencies": {
"@capacitor/app": "^8.1.0",
Expand Down
Loading
Loading