diff --git a/.gitignore b/.gitignore index f8c6afa..8525073 100755 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md index 116e7c2..a02f0ff 100755 --- a/README.md +++ b/README.md @@ -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 +.\scripts un-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. diff --git a/backend/docker-compose.e2e.yml b/backend/docker-compose.e2e.yml new file mode 100644 index 0000000..cd1b52d --- /dev/null +++ b/backend/docker-compose.e2e.yml @@ -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 diff --git a/backend/package.json b/backend/package.json index 0fa2b68..884b8c1 100755 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/scripts/run-tests.ps1 b/scripts/run-tests.ps1 index 1e49f8e..5897386 100644 --- a/scripts/run-tests.ps1 +++ b/scripts/run-tests.ps1 @@ -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. diff --git a/scripts/start-local.ps1 b/scripts/start-local.ps1 index 9d94a73..d3b7348 100644 --- a/scripts/start-local.ps1 +++ b/scripts/start-local.ps1 @@ -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' -$Container = 'redefined-designs-local-db' -$DbUser = 'redefined_local' -$DbPassword = 'redefined_local' -$DbName = 'redefined_local' +# 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) {