test(e2e): give the suite a throwaway database of its own (#186)
Linting / lint (pull_request) Successful in 2m33s
SonarQube Analysis / sonarqube (pull_request) Successful in 28m49s

The e2e suite ran against the development database and nothing truncated it. Every run seeded more fixtures and left them, so the unfiltered storefront grew monotonically — 1,662 items by the time #186 was filed — until rendering it outran the assertions' timeouts. It failed locally, passed in CI where the database is fresh, and got steadily worse, which is the combination nobody can act on.

start-local.ps1 -E2eDb runs the stack against a separate container on a separate port with tmpfs storage, so it starts empty every time. Migrations already run on every start, so an empty volume is a working one. The development database is untouched, so anything set up there by hand survives.

Deliberately a third database rather than sharing either existing one. The integration suite truncates between tests, so an e2e run sharing with it would have its fixtures deleted underneath it (#116) — different container, different port, different credentials, so the mistake is impossible rather than discouraged.

Verified by recreating the database from the compose file, confirming it came up with zero items, and running the full suite against it: 157 of 157. An earlier attempt at this reported 23 passed and 53 not run, which was worthless — a stale backend from a previous run still held port 3000 and was talking to a database I had already deleted. The port is checked before the run now, and the same mistake produced a wrong rate-limiter measurement earlier in this work.

Pagination is the other half of #186 and is filed separately: a shop that renders its whole catalogue in one page is worth fixing on its own merits, not as a side effect of a test fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 10:57:02 -05:00
co-authored by Claude Opus 5
parent 527c0a6417
commit 6a90e957f2
6 changed files with 104 additions and 5 deletions
+4
View File
@@ -29,3 +29,7 @@ frontend/playwright-results.json
# mistaken for real migration history. Keep the mirror, drop the rest.
backend/src/db-drizzle/*.sql
backend/src/db-drizzle/meta/
# Where an end-to-end run against the throwaway database writes its uploads.
# Disposable with the database it belongs to (#186).
backend/.e2e-uploads/
+10
View File
@@ -43,6 +43,16 @@ un-tests.ps1 -Suite e2e
un-tests.ps1 -Suite all
```
Run the end-to-end suite against a throwaway database rather than your development one:
```powershell
.\scripts\start-local.ps1 -E2eDb # separate container, separate port, starts empty
.\scriptsun-tests.ps1 -Suite e2e
```
Without `-E2eDb` the suite shares the development database, which nothing truncates — every run leaves its fixtures behind, and the storefront eventually renders enough of them to outrun the assertions' timeouts. See #186.
Both scripts switch to the pinned Node 26.7.0 (`NODE_VERSION` in `scripts/NodeVersion.ps1`) and verify that is what actually ends up running, then put the machine back to 18.16.1 when they finish — including when they fail partway, so an interrupted run does not leave the version switched. **`nvm use` rewrites a machine-global symlink, so this changes the Node version for every terminal on the machine while a script is running, not only the one you ran it in.** Both scripts say so as they do it.
The Node 20 floor is not arbitrary: `node-pg-migrate` pulls in an `lru-cache` that calls `diagnostics_channel.tracingChannel()`, which does not exist before Node 19.9. On Node 18 migrations die inside minified library code with `(0 , U.tracingChannel) is not a function`, which says nothing about versions.
+39
View File
@@ -0,0 +1,39 @@
# A throwaway Postgres for the end-to-end suite.
#
# The e2e suite used to run against the development database that
# start-local.ps1 brings up, and nothing ever truncated it: every run seeded
# more fixtures and left them. The unfiltered storefront grew monotonically —
# 1,662 items by the time #186 was filed — until rendering it outran the
# assertions' timeout. It failed locally, passed in CI where the database is
# fresh, and got slowly worse, which is the combination nobody can act on.
#
# Deliberately a mirror of docker-compose.test.yml rather than a shared file.
# The two suites must not share a database: the integration suite truncates
# between tests, so running it while an e2e run is in flight would delete that
# run's fixtures underneath it (#116). Different container, different port,
# different credentials — so the mistake is impossible rather than discouraged.
#
# tmpfs, like the integration database: the data is worthless the moment the
# run ends, and a container with nothing to persist starts faster and cannot
# accumulate anything between runs.
#
# docker compose -f backend/docker-compose.e2e.yml up -d
# docker compose -f backend/docker-compose.e2e.yml down
#
# The suite and the application both have to point at it. See
# scripts/start-local.ps1 -E2eDb, which does that for you.
services:
redefined-designs-e2e-db:
image: postgres:16
container_name: redefined-designs-e2e-db
environment:
- POSTGRES_USER=redefined_e2e
- POSTGRES_PASSWORD=redefined_e2e
- POSTGRES_DB=redefined_e2e
ports:
# Not 55432 (integration) and not 55500 (development). Override with
# E2E_DB_PORT if Hyper-V has reserved this one — it silently claims
# ranges on Windows, which is why the integration suite has TEST_PGPORT.
- "${E2E_DB_PORT:-55501}:5432"
tmpfs:
- /var/lib/postgresql/data
+2
View File
@@ -20,6 +20,8 @@
"test:integration:cov": "jest -c jest.integration.config.js --runInBand --coverage --forceExit",
"bench:hashing": "tsx scripts/bench-hash-latency.ts",
"backfill:images": "node dist/backfillImageReencode.js",
"db:e2e:up": "docker compose -f docker-compose.e2e.yml up -d",
"db:e2e:down": "docker compose -f docker-compose.e2e.yml down",
"db:test:up": "docker compose -f docker-compose.test.yml up -d",
"db:test:down": "docker compose -f docker-compose.test.yml down -v",
"migrate:up": "node migrate.js up",
+9 -1
View File
@@ -18,7 +18,15 @@
integration Backend Jest integration tests against a disposable Postgres.
Brings the container up itself.
e2e Frontend Playwright tests. Needs the app stack up; start it
with .\scripts\start-local.ps1 first.
with .\scripts\start-local.ps1 -E2eDb first.
-E2eDb matters. Without it the suite runs against the
development database, which nothing truncates: every run seeds
more fixtures and leaves them, the unfiltered storefront grows
monotonically, and eventually rendering it outruns the
assertions' timeout. It then fails locally, passes in CI where
the database is fresh, and gets worse (#186). With it, the
stack runs against a throwaway database that starts empty.
all All three, in that order — cheapest and most isolated first,
so a failure that a later suite would also show up in is
reported by the suite that localises it best.
+36
View File
@@ -35,6 +35,19 @@ param(
[int]$ApiPort = 3000,
[int]$WebPort = 5173,
[switch]$Fresh,
# Runs the stack against the throwaway end-to-end database instead of the
# development one: a separate container on a separate port, with tmpfs
# storage, so it starts empty every time.
#
# The e2e suite used to run against the development database, and nothing
# ever truncated it. Every run seeded more fixtures and left them, so the
# unfiltered storefront grew monotonically — 1,662 items by the time #186
# was filed — until rendering it outran the assertions' timeout. It failed
# locally, passed in CI where the database is fresh, and got steadily worse.
#
# This does not touch the development database, so anything you have set up
# there by hand survives.
[switch]$E2eDb,
[switch]$Stop,
# What -Stop puts the machine back to. nvm's default here is 18.16.1, which
# is too old to run this project's tooling but is what everything else on
@@ -52,10 +65,25 @@ $ErrorActionPreference = 'Stop'
$RepoRoot = Split-Path -Parent $PSScriptRoot
$StateDir = Join-Path $RepoRoot '.local'
$PidFile = Join-Path $StateDir 'pids.json'
# Which database this run uses. Everything downstream reads these four, so the
# choice is made once here rather than branched at each use.
if ($E2eDb) {
$Container = 'redefined-designs-e2e-db'
$DbUser = 'redefined_e2e'
$DbPassword = 'redefined_e2e'
$DbName = 'redefined_e2e'
# Not 55500 (development) and not 55432 (the integration suite). The three
# must not collide: the integration suite truncates between tests, so
# sharing a database with an e2e run would delete that run's fixtures
# underneath it (#116).
if (-not $PSBoundParameters.ContainsKey('DbPort')) { $DbPort = 55501 }
}
else {
$Container = 'redefined-designs-local-db'
$DbUser = 'redefined_local'
$DbPassword = 'redefined_local'
$DbName = 'redefined_local'
}
function Write-Step { param([string]$Message) Write-Host "==> $Message" -ForegroundColor Cyan }
function Write-Note { param([string]$Message) Write-Host " $Message" -ForegroundColor DarkGray }
@@ -165,11 +193,19 @@ function Start-Database {
}
else {
Write-Step "Creating the database container on port $DbPort"
# tmpfs for the e2e database only: its contents are worthless the
# moment the run ends, and storing nothing is what makes it start empty
# every time rather than accumulating fixtures the way the development
# database did (#186). Migrations run on every start below, so an empty
# volume is a working one. The development database keeps its storage.
$storage = if ($E2eDb) { @('--tmpfs', '/var/lib/postgresql/data') } else { @() }
docker run -d --name $Container `
-e "POSTGRES_USER=$DbUser" `
-e "POSTGRES_PASSWORD=$DbPassword" `
-e "POSTGRES_DB=$DbName" `
-p "${DbPort}:5432" `
@storage `
postgres:16 *>$null
if ($LASTEXITCODE -ne 0) {