From 414f51d2d185ebdaa0b55ca2c13d680faf611f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Saparelli?= Date: Fri, 29 May 2026 18:13:03 +1200 Subject: [PATCH 1/6] test: opt-in RAM-backed Postgres harness for fast, grind-free test runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each test creates/drops its own database and runs every migration, with nextest running many in parallel. Against a disk-backed cluster the CREATE/DROP DATABASE fsync storm saturates disk I/O and makes the whole machine unresponsive — hence the habit of prefixing runs with nice. scripts/ramdisk-pg.sh spins up a throwaway Postgres on tmpfs (/dev/shm) with fsync/synchronous_commit/full_page_writes off, points DATABASE_URL at it, runs the given command, then tears down. It reuses the installed initdb/pg_ctl so the server version matches the system Postgres with no container or image to manage. On a full package this took a ~54s run down to ~2s with no I/O grind. Wired up as opt-in just recipes (test-fast passes args through to nextest; fast wraps arbitrary commands); default just test is unchanged for machines without spare RAM. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 21 ++++++++ justfile | 15 ++++++ scripts/ramdisk-pg.sh | 119 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100755 scripts/ramdisk-pg.sh diff --git a/AGENTS.md b/AGENTS.md index ebb02dd1..ca9d3748 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,6 +98,27 @@ End-to-end tests use Playwright. Run with `npm run test:e2e` from `/private-web/ - Run specific tests: `just test-name ` - Verify no compilation warnings in tests and main code +### Fast, RAM-backed tests (opt-in) +Each test creates and drops its own database (and runs every migration), and +nextest runs many in parallel. Against a disk-backed Postgres the resulting +`CREATE DATABASE`/`DROP DATABASE` fsync storm saturates disk I/O and can make +the whole machine unresponsive — that's why disk-backed runs are usually +prefixed with `nice`. + +If you have RAM to spare, `just test-fast [nextest args]` runs the suite +against a throwaway Postgres that `scripts/ramdisk-pg.sh` spins up on tmpfs +(`/dev/shm`) with `fsync`/`synchronous_commit`/`full_page_writes` off, then +tears down. Nothing touches a physical disk, so there's no I/O grind and runs +are dramatically faster. It reuses your installed `initdb`/`pg_ctl`, so the +server version matches your system Postgres with no container or image to +manage. Args pass through to nextest, so `just test-fast`, `just test-fast -p +database`, and `just test-fast ` mirror `test`/`test-package`/`test-name`. +For other commands (e.g. e2e), wrap them: `just fast just test-e2e`. + +This is opt-in because it assumes free RAM; default `just test` still uses your +system Postgres. Overrides: `CANOPY_TEST_PG_DIR`, `CANOPY_TEST_PG_PORT`, +`CANOPY_TEST_PG_ROLE`. + ## Version Control - If the working copy is a jujutsu repo (a `.jj` directory exists at the repo root), prefer `jj` commands over `git` for VCS operations (status, diff, log, commit/describe, etc.). The repo may be colocated with git, but `jj` is the source of truth for local work. - If there is no `.jj` directory, use `git` as normal. diff --git a/justfile b/justfile index bdeaded7..ca859d64 100644 --- a/justfile +++ b/justfile @@ -63,6 +63,21 @@ test-name name: test-verbose: DATABASE_URL={{ DATABASE_URL }} cargo nextest run --no-capture +# Opt-in fast tests: run nextest against a throwaway, RAM-backed Postgres +# (tmpfs + fsync off) so the per-test CREATE/DROP DATABASE churn never hits +# disk. Needs spare RAM. Args pass straight to nextest, so this subsumes +# test / test-package / test-name on the fast path: +# just test-fast # everything +# just test-fast -p database # one package +# just test-fast some_test_name # by name +test-fast *args: + scripts/ramdisk-pg.sh cargo nextest run {{ args }} + +# Run any command against the throwaway RAM-backed Postgres (escape hatch for +# things test-fast doesn't cover, e.g. `just fast just test-e2e`). +fast +cmd: + scripts/ramdisk-pg.sh {{ cmd }} + # Run the private-web Playwright end-to-end suite. Builds the # private-server + migrate binaries first (the e2e fixture spawns its # own server/Vite per worker — no `just watch-*` needed). diff --git a/scripts/ramdisk-pg.sh b/scripts/ramdisk-pg.sh new file mode 100755 index 00000000..a30ec6d2 --- /dev/null +++ b/scripts/ramdisk-pg.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# Run a command against a throwaway, RAM-backed PostgreSQL instance. +# +# Why: the test suite creates and drops a fresh database (plus runs every +# migration) for *each* of its hundreds of tests, dozens in parallel. Against a +# disk-backed cluster the CREATE DATABASE / DROP DATABASE fsync storm saturates +# the machine's I/O and makes the whole system unresponsive. This spins up a +# disposable cluster on tmpfs with durability turned off, so none of that churn +# ever reaches a physical disk. +# +# This is opt-in (you need RAM to spare) and self-contained: it uses the +# `initdb`/`pg_ctl` already on your machine, so the server version matches your +# system Postgres exactly and there's no container runtime or image to manage. +# +# Usage: +# scripts/ramdisk-pg.sh cargo nextest run -p database +# scripts/ramdisk-pg.sh just test +# +# Env overrides: +# CANOPY_TEST_PG_DIR data directory (default: a fresh dir under /dev/shm) +# CANOPY_TEST_PG_PORT starting TCP port to probe (default: 5433) +# CANOPY_TEST_PG_ROLE superuser role / db owner (default: canopy) +set -euo pipefail + +if [ "$#" -eq 0 ]; then + echo "usage: $0 [args...]" >&2 + exit 64 +fi + +ROLE="${CANOPY_TEST_PG_ROLE:-canopy}" + +# Locate the Postgres server binaries. They're on PATH on Arch/macOS-brew, but +# Debian/Ubuntu hide them under a versioned directory, so fall back to those. +if ! command -v initdb >/dev/null 2>&1; then + for d in /usr/lib/postgresql/*/bin /usr/local/opt/postgresql*/bin /opt/homebrew/opt/postgresql*/bin; do + if [ -x "$d/initdb" ]; then + PATH="$d:$PATH" + break + fi + done +fi +for bin in initdb pg_ctl createdb; do + command -v "$bin" >/dev/null 2>&1 || { + echo "error: '$bin' not found on PATH. Install the Postgres server tools." >&2 + exit 69 + } +done + +# Pick a tmpfs-backed data directory. /dev/shm is tmpfs on Linux; if it's +# missing (e.g. macOS) fall back to $TMPDIR but warn that it won't be RAM-backed. +if [ -n "${CANOPY_TEST_PG_DIR:-}" ]; then + DATADIR="$CANOPY_TEST_PG_DIR" + mkdir -p "$DATADIR" + OWN_DATADIR=0 +else + if [ -d /dev/shm ] && [ -w /dev/shm ]; then + BASE=/dev/shm + else + BASE="${TMPDIR:-/tmp}" + echo "warn: /dev/shm unavailable; using $BASE (NOT RAM-backed, so no speedup)" >&2 + fi + DATADIR="$(mktemp -d "$BASE/canopy-test-pg.XXXXXX")" + OWN_DATADIR=1 +fi + +# Find a free TCP port, starting from the requested one. +port_in_use() { (exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null; } +PORT="${CANOPY_TEST_PG_PORT:-5433}" +for _ in $(seq 0 20); do + port_in_use "$PORT" || break + PORT=$((PORT + 1)) +done +if port_in_use "$PORT"; then + echo "error: no free port found near ${CANOPY_TEST_PG_PORT:-5433}" >&2 + exit 69 +fi + +STARTED=0 +cleanup() { + status=$? + if [ "$STARTED" = 1 ]; then + pg_ctl -D "$DATADIR" -m immediate stop >/dev/null 2>&1 || true + fi + if [ "${OWN_DATADIR:-0}" = 1 ]; then + rm -rf "$DATADIR" + fi + exit "$status" +} +trap cleanup EXIT INT TERM + +echo "ramdisk-pg: initialising disposable cluster in $DATADIR (port $PORT)" >&2 +# --no-sync: don't fsync the freshly-created cluster; it's disposable. +initdb -D "$DATADIR" -U "$ROLE" --auth=trust --no-sync -E UTF8 >/dev/null + +# Durability-off settings are safe here precisely because the data is thrown +# away. max_connections is bumped well past Postgres's default of 100 to absorb +# the parallel test pools. unix_socket_directories points at the data dir so a +# stray socket never lands in a shared /tmp or /run. +pg_ctl -D "$DATADIR" -l "$DATADIR/postmaster.log" -w start -o "\ + -p $PORT \ + -h 127.0.0.1 \ + -k $DATADIR \ + -c fsync=off \ + -c synchronous_commit=off \ + -c full_page_writes=off \ + -c autovacuum=off \ + -c max_connections=300" >/dev/null +STARTED=1 + +createdb -h 127.0.0.1 -p "$PORT" -U "$ROLE" "$ROLE" + +export DATABASE_URL="postgresql://${ROLE}@127.0.0.1:${PORT}/${ROLE}" +export RO_DATABASE_URL="$DATABASE_URL" +# So `just test-e2e` (whose fixture creates its own per-worker databases) can +# ride on the same RAM-backed cluster when invoked through this wrapper. +export CANOPY_E2E_ADMIN_DATABASE_URL="postgresql://${ROLE}@127.0.0.1:${PORT}/postgres" + +echo "ramdisk-pg: DATABASE_URL=$DATABASE_URL" >&2 +"$@" From 6678574cc481f66471168955258dacf592896a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Saparelli?= Date: Fri, 29 May 2026 18:17:36 +1200 Subject: [PATCH 2/6] ramdisk-pg: broaden Postgres binary search + accurate macOS messaging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Homebrew (opt/postgresql@NN) and Postgres.app bundle paths to the fallback so the script finds initdb/pg_ctl on macOS. macOS has no /dev/shm and no default tmpfs, so it lands on disk there — but fsync is off, which is what removes the I/O grind, so it stays fast. Reword the fallback notice accordingly and point at hdiutil for a real ramdisk via CANOPY_TEST_PG_DIR. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/ramdisk-pg.sh | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/scripts/ramdisk-pg.sh b/scripts/ramdisk-pg.sh index a30ec6d2..9f5c73b3 100755 --- a/scripts/ramdisk-pg.sh +++ b/scripts/ramdisk-pg.sh @@ -29,10 +29,15 @@ fi ROLE="${CANOPY_TEST_PG_ROLE:-canopy}" -# Locate the Postgres server binaries. They're on PATH on Arch/macOS-brew, but -# Debian/Ubuntu hide them under a versioned directory, so fall back to those. +# Locate the Postgres server binaries. They're on PATH on Arch, but other +# installs hide them: Debian/Ubuntu under a versioned dir, Homebrew under +# opt/postgresql@NN, and Postgres.app inside its bundle. Fall back to those. if ! command -v initdb >/dev/null 2>&1; then - for d in /usr/lib/postgresql/*/bin /usr/local/opt/postgresql*/bin /opt/homebrew/opt/postgresql*/bin; do + for d in \ + /usr/lib/postgresql/*/bin \ + /opt/homebrew/opt/postgresql*/bin \ + /usr/local/opt/postgresql*/bin \ + /Applications/Postgres.app/Contents/Versions/*/bin; do if [ -x "$d/initdb" ]; then PATH="$d:$PATH" break @@ -56,8 +61,12 @@ else if [ -d /dev/shm ] && [ -w /dev/shm ]; then BASE=/dev/shm else + # No tmpfs by default on macOS. We still turn fsync off below, which is + # what actually removes the I/O grind, so this stays fast — it just lands + # on disk. For a true ramdisk, create one and pass CANOPY_TEST_PG_DIR, + # e.g. macOS: diskutil erasevolume APFS canopy-pg $(hdiutil attach -nomount ram://1048576) BASE="${TMPDIR:-/tmp}" - echo "warn: /dev/shm unavailable; using $BASE (NOT RAM-backed, so no speedup)" >&2 + echo "note: no /dev/shm; using $BASE (disk-backed, but fsync is off so still fast)" >&2 fi DATADIR="$(mktemp -d "$BASE/canopy-test-pg.XXXXXX")" OWN_DATADIR=1 From f1fb39ffaac3d7ad35706eccf406c3754cbde615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Saparelli?= Date: Fri, 29 May 2026 18:21:31 +1200 Subject: [PATCH 3/6] test: default test recipes to the RAM-backed Postgres MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flip just test / test-package / test-name / test-verbose / test-e2e to run through scripts/ramdisk-pg.sh, so the throwaway tmpfs + fsync-off cluster is the default and there's no disk-fsync grind out of the box. The per-test database footprint is tiny (~40MB base + ~8MB per concurrently-live test DB, peak well under ~250MB), so RAM isn't a real constraint. just test now takes nextest args (just test -p database, just test ). Add test-system as the escape hatch to run against $DATABASE_URL instead — for inspecting the DB afterwards or where initdb/pg_ctl aren't available. CI is unchanged: it calls cargo nextest directly against its own Postgres service, not the just recipes. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 37 ++++++++++++++++++--------------- justfile | 48 +++++++++++++++++++++++-------------------- scripts/ramdisk-pg.sh | 3 ++- 3 files changed, 49 insertions(+), 39 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ca9d3748..b4d8c1b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,27 +98,32 @@ End-to-end tests use Playwright. Run with `npm run test:e2e` from `/private-web/ - Run specific tests: `just test-name ` - Verify no compilation warnings in tests and main code -### Fast, RAM-backed tests (opt-in) +### Tests run on a throwaway RAM-backed Postgres by default Each test creates and drops its own database (and runs every migration), and nextest runs many in parallel. Against a disk-backed Postgres the resulting `CREATE DATABASE`/`DROP DATABASE` fsync storm saturates disk I/O and can make -the whole machine unresponsive — that's why disk-backed runs are usually -prefixed with `nice`. - -If you have RAM to spare, `just test-fast [nextest args]` runs the suite -against a throwaway Postgres that `scripts/ramdisk-pg.sh` spins up on tmpfs -(`/dev/shm`) with `fsync`/`synchronous_commit`/`full_page_writes` off, then -tears down. Nothing touches a physical disk, so there's no I/O grind and runs -are dramatically faster. It reuses your installed `initdb`/`pg_ctl`, so the -server version matches your system Postgres with no container or image to -manage. Args pass through to nextest, so `just test-fast`, `just test-fast -p -database`, and `just test-fast ` mirror `test`/`test-package`/`test-name`. -For other commands (e.g. e2e), wrap them: `just fast just test-e2e`. - -This is opt-in because it assumes free RAM; default `just test` still uses your -system Postgres. Overrides: `CANOPY_TEST_PG_DIR`, `CANOPY_TEST_PG_PORT`, +the whole machine unresponsive. + +So `just test` (and `test-package`/`test-name`/`test-verbose`/`test-e2e`) run +through `scripts/ramdisk-pg.sh`, which spins up a disposable Postgres on tmpfs +(`/dev/shm`) with `fsync`/`synchronous_commit`/`full_page_writes` off, points +`DATABASE_URL` at it, then tears it down. Nothing touches a physical disk, so +there's no grind and runs are dramatically faster. It reuses your installed +`initdb`/`pg_ctl`, so the server version matches your system Postgres with no +container or image to manage. `just test` takes nextest args, so `just test`, +`just test -p database`, and `just test ` all work. Wrap other commands +with `just fast `. + +Requirements/caveats: needs the Postgres *server* tools (`initdb`/`pg_ctl`), not +just the `psql` client. On macOS there's no `/dev/shm`, so it falls back to disk +— still fast (fsync is off), just not RAM-backed unless you point +`CANOPY_TEST_PG_DIR` at a real ramdisk. Other overrides: `CANOPY_TEST_PG_PORT`, `CANOPY_TEST_PG_ROLE`. +To run against your **system** Postgres instead (to inspect the DB afterwards, +or where `initdb` isn't available), use `just test-system [nextest args]` — +prefix with `nice` to soften the I/O grind. + ## Version Control - If the working copy is a jujutsu repo (a `.jj` directory exists at the repo root), prefer `jj` commands over `git` for VCS operations (status, diff, log, commit/describe, etc.). The repo may be colocated with git, but `jj` is the source of truth for local work. - If there is no `.jj` directory, use `git` as normal. diff --git a/justfile b/justfile index ca859d64..c8ad5ca0 100644 --- a/justfile +++ b/justfile @@ -47,43 +47,47 @@ watch-private-api: watch-private-web: cd private-web && npm run dev -# Run all tests -test: - DATABASE_URL={{ DATABASE_URL }} cargo nextest run +# Run all tests. Uses a throwaway RAM-backed Postgres (tmpfs + fsync off) via +# scripts/ramdisk-pg.sh so the per-test CREATE/DROP DATABASE churn never hits +# disk — fast, no I/O grind. Args pass straight to nextest, so `just test`, +# `just test -p database`, and `just test some_name` all work. Use test-system +# to run against $DATABASE_URL instead. +test *args: + scripts/ramdisk-pg.sh cargo nextest run {{ args }} -# Run tests for a specific package +# Run tests for a specific package (RAM-backed; see `test`) test-package package: - DATABASE_URL={{ DATABASE_URL }} cargo nextest run -p {{ package }} + scripts/ramdisk-pg.sh cargo nextest run -p {{ package }} -# Run a specific test +# Run a specific test (RAM-backed; see `test`) test-name name: - DATABASE_URL={{ DATABASE_URL }} cargo nextest run {{ name }} + scripts/ramdisk-pg.sh cargo nextest run {{ name }} -# Run tests with no capture (show output) +# Run tests with no capture (show output) (RAM-backed; see `test`) test-verbose: - DATABASE_URL={{ DATABASE_URL }} cargo nextest run --no-capture - -# Opt-in fast tests: run nextest against a throwaway, RAM-backed Postgres -# (tmpfs + fsync off) so the per-test CREATE/DROP DATABASE churn never hits -# disk. Needs spare RAM. Args pass straight to nextest, so this subsumes -# test / test-package / test-name on the fast path: -# just test-fast # everything -# just test-fast -p database # one package -# just test-fast some_test_name # by name -test-fast *args: - scripts/ramdisk-pg.sh cargo nextest run {{ args }} + scripts/ramdisk-pg.sh cargo nextest run --no-capture + +# Run tests against your system Postgres ($DATABASE_URL) rather than the +# throwaway RAM-backed one — e.g. to inspect the DB afterwards, or where +# initdb/pg_ctl aren't available. Args pass through to nextest. Prefix with +# `nice` to soften the I/O grind. `just test-system`, `just test-system -p +# database`, `just test-system some_name`. +test-system *args: + DATABASE_URL={{ DATABASE_URL }} cargo nextest run {{ args }} # Run any command against the throwaway RAM-backed Postgres (escape hatch for -# things test-fast doesn't cover, e.g. `just fast just test-e2e`). +# things the test recipes don't cover). fast +cmd: scripts/ramdisk-pg.sh {{ cmd }} # Run the private-web Playwright end-to-end suite. Builds the # private-server + migrate binaries first (the e2e fixture spawns its -# own server/Vite per worker — no `just watch-*` needed). +# own server/Vite per worker — no `just watch-*` needed). Runs against the +# throwaway RAM-backed Postgres; the fixture creates its per-worker databases +# on the cluster the wrapper points CANOPY_E2E_ADMIN_DATABASE_URL at. test-e2e: cargo build --bin private-server --bin migrate - cd private-web && npm run test:e2e + cd private-web && {{ justfile_directory() }}/scripts/ramdisk-pg.sh npm run test:e2e # Same as `test-e2e` but launches Playwright's interactive UI runner. # Useful for stepping through failures and inspecting traces. diff --git a/scripts/ramdisk-pg.sh b/scripts/ramdisk-pg.sh index 9f5c73b3..cac7fed4 100755 --- a/scripts/ramdisk-pg.sh +++ b/scripts/ramdisk-pg.sh @@ -8,9 +8,10 @@ # disposable cluster on tmpfs with durability turned off, so none of that churn # ever reaches a physical disk. # -# This is opt-in (you need RAM to spare) and self-contained: it uses the +# This is what `just test` uses by default. It's self-contained: it uses the # `initdb`/`pg_ctl` already on your machine, so the server version matches your # system Postgres exactly and there's no container runtime or image to manage. +# To run against your system Postgres instead, use `just test-system`. # # Usage: # scripts/ramdisk-pg.sh cargo nextest run -p database From d322f105cc31191c27b144b32f03ad012a1fd55c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Saparelli?= Date: Fri, 29 May 2026 18:25:52 +1200 Subject: [PATCH 4/6] ci: run tests through the just recipes (shared ramdisk Postgres) CI was inlining 'cargo nextest run' plus its own systemctl start postgresql + role/db creation, which diverged from 'just test' once the recipe started wrapping nextest in scripts/ramdisk-pg.sh. Point CI at the recipes instead so it exercises the exact path developers run locally. - Test job: install just, drop the Postgres-service setup, run 'just test'. The wrapper builds its own throwaway tmpfs cluster from the runner's preinstalled initdb/pg_ctl, so no system Postgres is needed. - Playwright job: drop the Postgres-service setup and wrap the test run with scripts/ramdisk-pg.sh, which exports CANOPY_E2E_ADMIN_DATABASE_URL at the throwaway cluster for the fixture's per-worker databases. Its explicit build/cache steps stay (CI caching concerns, not test-runner divergence). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a27b9df..b1053a64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,15 +28,13 @@ jobs: rustup default stable - uses: taiki-e/install-action@v2.77.4 with: - tool: cargo-nextest - - run: | - sudo systemctl start postgresql.service - sudo -u postgres psql --command="CREATE ROLE canopy SUPERUSER LOGIN PASSWORD 'canopy'" - sudo -u postgres createdb --owner=canopy canopy + tool: cargo-nextest,just - uses: Swatinem/rust-cache@v2 - - run: cargo nextest run - env: - DATABASE_URL: postgresql://canopy:canopy@localhost:5432/canopy + # `just test` runs nextest against a throwaway tmpfs Postgres that + # scripts/ramdisk-pg.sh spins up with the runner's own initdb/pg_ctl + # (preinstalled on ubuntu-latest), so no system Postgres service or + # role/db setup is needed here — same path developers run locally. + - run: just test clippy: name: Clippy @@ -77,12 +75,6 @@ jobs: node-version: lts/* cache: npm cache-dependency-path: private-web/package-lock.json - - name: Start Postgres - # Mirrors the canopy/canopy role + admin DB used by the rust test job. - run: | - sudo systemctl start postgresql.service - sudo -u postgres psql --command="CREATE ROLE canopy SUPERUSER LOGIN PASSWORD 'canopy'" - sudo -u postgres createdb --owner=canopy canopy - name: Build private-server + migrate # SKIP_FRONTEND_BUILD avoids private-server/build.rs running an npm # install + vite build to embed dist/ — the e2e fixture uses Vite @@ -104,10 +96,11 @@ jobs: run: npx playwright install --with-deps chromium working-directory: private-web - name: Playwright tests - run: npm run test:e2e + # The wrapper spins up a throwaway tmpfs Postgres and exports + # CANOPY_E2E_ADMIN_DATABASE_URL pointing at it; the fixture creates its + # per-worker databases on that cluster. Same DB harness as `just test`. + run: ../scripts/ramdisk-pg.sh npm run test:e2e working-directory: private-web - env: - CANOPY_E2E_ADMIN_DATABASE_URL: postgresql://canopy:canopy@localhost:5432/postgres - name: Upload Playwright report if: failure() uses: actions/upload-artifact@v7 From e1aa927580021a0189ff832de4daca5e3362d956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Saparelli?= Date: Fri, 29 May 2026 18:44:38 +1200 Subject: [PATCH 5/6] test: run nextest with --no-fail-fast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that tests are fast (RAM-backed), there's little value in stopping at the first failure — running the whole suite surfaces every failure in one go. Bake --no-fail-fast into the test recipes (test, test-package, test-name, test-verbose, test-system); CI inherits it via just test. Co-Authored-By: Claude Opus 4.8 (1M context) --- justfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/justfile b/justfile index c8ad5ca0..5a4949da 100644 --- a/justfile +++ b/justfile @@ -53,19 +53,19 @@ watch-private-web: # `just test -p database`, and `just test some_name` all work. Use test-system # to run against $DATABASE_URL instead. test *args: - scripts/ramdisk-pg.sh cargo nextest run {{ args }} + scripts/ramdisk-pg.sh cargo nextest run --no-fail-fast {{ args }} # Run tests for a specific package (RAM-backed; see `test`) test-package package: - scripts/ramdisk-pg.sh cargo nextest run -p {{ package }} + scripts/ramdisk-pg.sh cargo nextest run --no-fail-fast -p {{ package }} # Run a specific test (RAM-backed; see `test`) test-name name: - scripts/ramdisk-pg.sh cargo nextest run {{ name }} + scripts/ramdisk-pg.sh cargo nextest run --no-fail-fast {{ name }} # Run tests with no capture (show output) (RAM-backed; see `test`) test-verbose: - scripts/ramdisk-pg.sh cargo nextest run --no-capture + scripts/ramdisk-pg.sh cargo nextest run --no-fail-fast --no-capture # Run tests against your system Postgres ($DATABASE_URL) rather than the # throwaway RAM-backed one — e.g. to inspect the DB afterwards, or where @@ -73,7 +73,7 @@ test-verbose: # `nice` to soften the I/O grind. `just test-system`, `just test-system -p # database`, `just test-system some_name`. test-system *args: - DATABASE_URL={{ DATABASE_URL }} cargo nextest run {{ args }} + DATABASE_URL={{ DATABASE_URL }} cargo nextest run --no-fail-fast {{ args }} # Run any command against the throwaway RAM-backed Postgres (escape hatch for # things the test recipes don't cover). From 547248baa0fc649d8e4d361e79dbfe90de2f6d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Saparelli?= Date: Fri, 29 May 2026 18:44:48 +1200 Subject: [PATCH 6/6] ci: build the frontend before the rust test job The private-server tests serve the embedded React SPA through rust-embed, so a client-side route like GET /status only returns 200 when private-web/dist/ exists. Routing CI through just test set SKIP_FRONTEND_BUILD=1, so build.rs no longer built it and a fresh checkout had an empty dist/, 404ing that test. (Dev machines have a dist/ from working on the frontend, which masked it locally.) Add a Node setup + npm ci + npm run build step to the test job so dist/ is populated before just test runs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1053a64..67b1fca2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,19 @@ jobs: with: tool: cargo-nextest,just - uses: Swatinem/rust-cache@v2 + - uses: actions/setup-node@v6 + with: + node-version: lts/* + cache: npm + cache-dependency-path: private-web/package-lock.json + # The private-server tests serve the embedded React SPA via rust-embed, + # so they need private-web/dist/ to exist. `just test` sets + # SKIP_FRONTEND_BUILD=1 (build.rs won't build it) and a fresh checkout has + # no dist/, so build it explicitly here. Dev machines already have a dist/ + # from working on the frontend. + - name: Build frontend + run: npm ci && npm run build + working-directory: private-web # `just test` runs nextest against a throwaway tmpfs Postgres that # scripts/ramdisk-pg.sh spins up with the runner's own initdb/pg_ctl # (preinstalled on ubuntu-latest), so no system Postgres service or