Files
redefined-designs/scripts/run-tests.ps1
T
bermudalambandClaude Opus 5 06933aec75 fix(scripts): make the alias check fail closed, and correct three stale docs (#208)
The alias check was a negative match on an allowlist of English error strings, which returned True for empty output, for $null, and for "exit status 1: Access is denied." — so an alias switch producing nothing, or failing on the symlink permission error this file's own header warns about, was reported as success while the old version kept running. That is the bug #198 was filed about, narrowed rather than removed, and it also broke whenever nvm reworded an error. It is now a positive match on "Now using node v<what is actually running>".

The floor check moves ahead of the switch and reads the constant rather than the result. Where it sat, $major was always whatever NODE_VERSION says, so it validated the switch it had just made instead of the pin it exists to guard, and could never fire.

Use-NodeLatest is now Use-PinnedNode. In a change whose whole subject is that "latest" means something people do not expect, the name was an avoidable trap.

The restore default moves beside NODE_VERSION. It deliberately is not a param default: a param block runs before the dot-source, so $script:DEFAULT_NODE_VERSION is still $null there and the restore would have quietly restored nothing — leaving the machine on the pinned version, which is the exact failure the restore exists to prevent. It is resolved after the dot-source instead, and an explicit -DefaultNodeVersion still wins.

Three documents described behaviour the code no longer has: README's "both scripts run nvm use latest", run-tests.ps1's .DESCRIPTION, and project-context.md's instruction to agents. All corrected, and project-context.md now also says not to run these scripts from an agent shell, which is how this machine once ended up with no Node at all.

Part 4 of the issue is partly stale: backend/package.json already declares engines >=20.9.0. frontend now matches it. The larger question — whether local should be pinned to the Node 20 that CI and the production image actually run — is a decision rather than an oversight and is left open on the issue.

Verified by parsing all three scripts with the PowerShell AST parser, which does not execute them. They are deliberately never run from an agent shell.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 09:33:09 -05:00

195 lines
7.0 KiB
PowerShell

#Requires -Version 7
<#
.SYNOPSIS
Runs the project's test suites.
.DESCRIPTION
One script rather than three, because the Node version switch, the test
database bring-up and the TEST_PGPORT handling are shared by more than one
suite and would otherwise be copied around and drift apart.
Switches Node to the pinned version in scripts/NodeVersion.ps1 for the run and puts the
machine default back afterwards, the same way start-local.ps1 does — and for
the same reason, since ts-jest and Playwright are subject to the same Node
20 floor as the migrations.
.PARAMETER Suite
unit Backend Jest unit tests. Needs nothing running.
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.
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.
.PARAMETER TestDbPort
Host port for the integration suite's Postgres. Change it if something else
holds the default, or if Hyper-V has reserved it.
.PARAMETER KeepTestDb
Leave the integration database running afterwards. Useful when re-running
the same suite repeatedly; it starts faster.
.PARAMETER Filter
Passed through to the test runner to select tests by file or name.
.EXAMPLE
.\scripts\run-tests.ps1 -Suite unit
.\scripts\run-tests.ps1 -Suite integration -TestDbPort 55600
.\scripts\run-tests.ps1 -Suite e2e -Filter email-templates
.\scripts\run-tests.ps1 -Suite all
#>
[CmdletBinding()]
param(
[ValidateSet('unit', 'integration', 'e2e', 'all')]
[string]$Suite = 'all',
[int]$TestDbPort = 55432,
[switch]$KeepTestDb,
[string]$Filter,
# Resolved after NodeVersion.ps1 is dot-sourced below, not here. A param
# block runs before anything else in the script, so $script:DEFAULT_NODE_VERSION
# is still $null at this point and using it as the default would silently
# restore nothing — leaving the machine on the pinned version, which is the
# exact failure the restore exists to prevent. See #208.
[string]$DefaultNodeVersion = ''
)
$ErrorActionPreference = 'Stop'
$RepoRoot = Split-Path -Parent $PSScriptRoot
$Backend = Join-Path $RepoRoot 'backend'
$Frontend = Join-Path $RepoRoot 'frontend'
$WebPort = 5173
function Write-Step { param([string]$Message) Write-Host "==> $Message" -ForegroundColor Cyan }
function Write-Note { param([string]$Message) Write-Host " $Message" -ForegroundColor DarkGray }
function Write-Good { param([string]$Message) Write-Host " $Message" -ForegroundColor Green }
# $ErrorActionPreference = 'Stop' does not stop the script when a native
# executable exits non-zero, only when a cmdlet throws — and every runner here
# is npm. Without this a failing suite prints red and the script reports success.
function Invoke-Checked {
param([scriptblock]$Command, [string]$What)
& $Command
if ($LASTEXITCODE -ne 0) {
throw "$What failed (exit code $LASTEXITCODE)."
}
}
. (Join-Path $PSScriptRoot 'NodeVersion.ps1')
# The one home for this value is NodeVersion.ps1, beside NODE_VERSION. It cannot
# be a param default (see the note there), so it is filled in here instead, and
# an explicit -DefaultNodeVersion still wins.
if (-not $DefaultNodeVersion) { $DefaultNodeVersion = $script:DEFAULT_NODE_VERSION }
# Bound once so the shared switcher reports in this script's output style.
$NodeOut = @{ Step = ${function:Write-Step}; Note = ${function:Write-Note} }
function Assert-Docker {
docker info *>$null
if ($LASTEXITCODE -ne 0) {
throw 'Docker is not running. Start Docker Desktop and try again.'
}
}
function Invoke-UnitSuite {
Write-Step 'Backend unit tests'
Push-Location $Backend
try {
if ($Filter) { Invoke-Checked { npm run test:unit -- $Filter } 'Unit tests' }
else { Invoke-Checked { npm run test:unit } 'Unit tests' }
}
finally { Pop-Location }
}
function Invoke-IntegrationSuite {
Assert-Docker
# The suite's globalSetup connects on TEST_PGPORT and fails with a clear
# message if nothing answers, so the container has to be up first. Reusing a
# running one is safe: every test truncates in beforeEach.
Write-Step "Starting the test database on port $TestDbPort"
$env:TEST_PGPORT = "$TestDbPort"
Push-Location $Backend
try {
Invoke-Checked { npm run db:test:up } 'Test database'
Write-Step 'Backend integration tests'
try {
if ($Filter) { Invoke-Checked { npm run test:integration -- $Filter } 'Integration tests' }
else { Invoke-Checked { npm run test:integration } 'Integration tests' }
}
finally {
if ($KeepTestDb) {
Write-Note 'leaving the test database up (-KeepTestDb)'
}
else {
# In a finally so a failing suite still tidies up. The data is on
# tmpfs, so there is nothing to preserve for a post-mortem.
Write-Step 'Stopping the test database'
npm run db:test:down *>$null
}
}
}
finally { Pop-Location }
}
function Invoke-E2eSuite {
# Playwright's own webServer starts the dev server, but the backend it talks
# to is not its job. Checked here rather than left to twenty-five specs
# failing on a connection refused, which does not say what is missing.
$backendUp = $false
try {
$null = Invoke-WebRequest 'http://localhost:3000/api/config' -TimeoutSec 2 -UseBasicParsing
$backendUp = $true
}
catch { $backendUp = $false }
if (-not $backendUp) {
throw @"
No backend answering on http://localhost:3000/api/config.
The end-to-end suite drives real registration, cart and checkout flows against a
running stack. Start it first:
.\scripts\start-local.ps1
"@
}
Write-Good 'backend is up'
Write-Note "dev server on $WebPort is started by Playwright if it is not already running"
Write-Step 'Frontend end-to-end tests'
Push-Location $Frontend
try {
if ($Filter) { Invoke-Checked { npx playwright test $Filter --project=chromium } 'End-to-end tests' }
else { Invoke-Checked { npx playwright test --project=chromium } 'End-to-end tests' }
}
finally { Pop-Location }
}
Use-PinnedNode @NodeOut
try {
switch ($Suite) {
'unit' { Invoke-UnitSuite }
'integration' { Invoke-IntegrationSuite }
'e2e' { Invoke-E2eSuite }
'all' {
# Cheapest and most isolated first, so a break that several suites
# would show is reported by the one that localises it best.
Invoke-UnitSuite
Invoke-IntegrationSuite
Invoke-E2eSuite
}
}
Write-Host ''
Write-Host "Passed ($Suite)." -ForegroundColor Green
}
finally {
Restore-Node -Version $DefaultNodeVersion @NodeOut
}