Merge pull request 'test(e2e): give the suite a throwaway database of its own (#186)' (#270) from fix/186-disposable-e2e-database into main
Linting / lint (push) Successful in 1m57s
SonarQube Analysis / sonarqube (push) Successful in 26m22s

Reviewed-on: #270
This commit was merged in pull request #270.
This commit is contained in:
2026-09-02 11:06:04 -05:00
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. # mistaken for real migration history. Keep the mirror, drop the rest.
backend/src/db-drizzle/*.sql backend/src/db-drizzle/*.sql
backend/src/db-drizzle/meta/ 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 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. 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. 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", "test:integration:cov": "jest -c jest.integration.config.js --runInBand --coverage --forceExit",
"bench:hashing": "tsx scripts/bench-hash-latency.ts", "bench:hashing": "tsx scripts/bench-hash-latency.ts",
"backfill:images": "node dist/backfillImageReencode.js", "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:up": "docker compose -f docker-compose.test.yml up -d",
"db:test:down": "docker compose -f docker-compose.test.yml down -v", "db:test:down": "docker compose -f docker-compose.test.yml down -v",
"migrate:up": "node migrate.js up", "migrate:up": "node migrate.js up",
+9 -1
View File
@@ -18,7 +18,15 @@
integration Backend Jest integration tests against a disposable Postgres. integration Backend Jest integration tests against a disposable Postgres.
Brings the container up itself. Brings the container up itself.
e2e Frontend Playwright tests. Needs the app stack up; start it 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, all All three, in that order — cheapest and most isolated first,
so a failure that a later suite would also show up in is so a failure that a later suite would also show up in is
reported by the suite that localises it best. reported by the suite that localises it best.
+40 -4
View File
@@ -35,6 +35,19 @@ param(
[int]$ApiPort = 3000, [int]$ApiPort = 3000,
[int]$WebPort = 5173, [int]$WebPort = 5173,
[switch]$Fresh, [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, [switch]$Stop,
# What -Stop puts the machine back to. nvm's default here is 18.16.1, which # 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 # 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 $RepoRoot = Split-Path -Parent $PSScriptRoot
$StateDir = Join-Path $RepoRoot '.local' $StateDir = Join-Path $RepoRoot '.local'
$PidFile = Join-Path $StateDir 'pids.json' $PidFile = Join-Path $StateDir 'pids.json'
$Container = 'redefined-designs-local-db' # Which database this run uses. Everything downstream reads these four, so the
$DbUser = 'redefined_local' # choice is made once here rather than branched at each use.
$DbPassword = 'redefined_local' if ($E2eDb) {
$DbName = 'redefined_local' $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-Step { param([string]$Message) Write-Host "==> $Message" -ForegroundColor Cyan }
function Write-Note { param([string]$Message) Write-Host " $Message" -ForegroundColor DarkGray } function Write-Note { param([string]$Message) Write-Host " $Message" -ForegroundColor DarkGray }
@@ -165,11 +193,19 @@ function Start-Database {
} }
else { else {
Write-Step "Creating the database container on port $DbPort" 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 ` docker run -d --name $Container `
-e "POSTGRES_USER=$DbUser" ` -e "POSTGRES_USER=$DbUser" `
-e "POSTGRES_PASSWORD=$DbPassword" ` -e "POSTGRES_PASSWORD=$DbPassword" `
-e "POSTGRES_DB=$DbName" ` -e "POSTGRES_DB=$DbName" `
-p "${DbPort}:5432" ` -p "${DbPort}:5432" `
@storage `
postgres:16 *>$null postgres:16 *>$null
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {