feat(scripts): switch Node automatically, and add a test runner (#140)

start-local.ps1 knew exactly what was wrong when Node was too old and then made you fix it by hand. Assert-NodeVersion read node --version, found a major below 20, and threw a message telling you to run `nvm use 24.13.1` and start again in a new shell. A good error for a problem the script could simply solve — and since nvm's default here is 18.16.1, it was hit on every fresh shell.

It now runs `nvm use latest` itself and -Stop puts the machine back to 18.16.1. The revert also runs when a start fails partway: without that, a run dying in migrations leaves the machine switched with nothing started, and the -Stop that would restore it is never reached.

nvm rewrites a machine-global symlink rather than changing one shell, so this changes the Node version for every terminal on the machine while a script runs. That is the intent — the point is to work in whatever shell is already open — but it is announced every time rather than done quietly.

The switch is verified rather than trusted. nvm-windows exits 0 for switches that did not happen: a version it cannot find, a symlink it cannot rewrite without elevation, and — observed here — a rewrite issued immediately after another one, where the directory symlink is briefly still the old target. That last case turned up while testing this change: `nvm use latest` reported success and left Node on 18.16.1. So the result is read back and retried once, and nvm's own output is captured rather than discarded, because suppressing it hid the only message that explained the failure.

run-tests.ps1 runs the suites: -Suite unit|integration|e2e|all. One script with a parameter rather than three, because the version switch, the database bring-up and the TEST_PGPORT handling are shared and three copies would drift. The integration suite gets its own throwaway Postgres started and stopped around it, in a finally so a failing suite still tidies up. The e2e suite checks the backend is answering first and says what to start, rather than leaving twenty-five specs to fail on a refused connection that names nothing.

`all` runs cheapest and most isolated first, so a break several suites would show is reported by the one that localises it best.

The version switching lives in scripts/NodeVersion.ps1, dot-sourced by both, since two copies of it would drift and the half that drifts is the half nobody runs.

Closes #140
This commit is contained in:
2026-08-23 09:36:40 -05:00
parent 792daccafb
commit 9ac2fa3ba1
4 changed files with 374 additions and 29 deletions
+140
View File
@@ -0,0 +1,140 @@
<#
.SYNOPSIS
Switching the machine's Node version, shared by start-local.ps1 and
run-tests.ps1.
.DESCRIPTION
Both scripts need Node 20 or newer for the same reason: node-pg-migrate
pulls in an lru-cache that calls diagnostics_channel.tracingChannel(), which
does not exist before Node 19.9. On Node 18 the migration dies inside
minified library code with "(0 , U.tracingChannel) is not a function", a
message that says nothing about versions. ts-jest and Playwright are subject
to the same floor.
Kept in one file because two copies of a version switch would drift, and the
half that drifts is the half nobody runs.
nvm-windows rewrites a machine-global symlink (NVM_SYMLINK, typically
C:\nvm4w\nodejs) rather than changing one shell, so switching here changes
the Node version for every terminal on the machine. That is intended — the
point is to work in whatever shell is already open — but it is announced
rather than done quietly, and it is put back afterwards.
#>
function Get-NodeVersionString {
return (node --version 2>$null)
}
function Get-NodeMajor {
$raw = Get-NodeVersionString
if (-not $raw) { return 0 }
return [int](($raw -replace '^v', '') -split '\.')[0]
}
<#
Switches, then checks what is actually running.
nvm-windows exits 0 for switches that did not take: a version it cannot
find, a symlink it cannot rewrite without elevation, and — seen on this
machine — a rewrite immediately after another one, where the directory
symlink is briefly still the old target. So the result is verified rather
than trusted, and retried once, because reporting a version that is not the
one running is worse than not switching at all.
nvm's own output is captured rather than discarded. Suppressing it hid the
only message that explained a failed switch.
#>
function Use-Node {
param(
[Parameter(Mandatory)][string]$Version,
[Parameter(Mandatory)][string]$Why,
[scriptblock]$Step,
[scriptblock]$Note
)
if ($Step) { & $Step "Switching Node to $Version ($Why)" }
if ($Note) { & $Note 'nvm changes the version for every shell on this machine, not just this one' }
$output = $null
foreach ($attempt in 1..2) {
$output = (nvm use $Version 2>&1 | Out-String).Trim()
$raw = Get-NodeVersionString
if ($raw) {
# `latest` is whatever nvm decided, so there is nothing to compare a
# version string against — the caller checks the major instead.
if ($Version -eq 'latest' -or $raw.TrimStart('v') -eq $Version.TrimStart('v')) {
if ($Note) { & $Note "node $raw" }
return $raw
}
}
if ($attempt -eq 1) {
if ($Note) { & $Note 'the switch has not taken yet; retrying' }
Start-Sleep -Milliseconds 750
}
}
$running = Get-NodeVersionString
if (-not $running) {
throw "Node is not on PATH after 'nvm use $Version'. Check that nvm-windows is installed.`n`nnvm said:`n$output"
}
throw @"
Asked nvm for Node $Version, but $running is still what runs.
nvm said:
$output
nvm-windows rewrites a symlink at $env:NVM_SYMLINK, and can report success
without having rewritten it. Check the version is installed, and that this shell
can write that link:
nvm list
"@
}
<#
Switches to the newest installed version and insists it is new enough.
`nvm use latest` picks the highest version nvm has installed locally, which
can still be older than this project needs.
#>
function Use-NodeLatest {
param([scriptblock]$Step, [scriptblock]$Note)
Use-Node -Version 'latest' -Why 'this project needs Node 20 or newer' -Step $Step -Note $Note | Out-Null
$major = Get-NodeMajor
if ($major -lt 20) {
throw @"
Node is still v$major after 'nvm use latest'. This project needs Node 20 or newer.
The newest version nvm has installed is too old. Install a newer one:
nvm install 24.13.1
Installed versions: nvm list
"@
}
}
<#
Best effort by design. A failure to switch back must not mask the error that
got us here, nor turn a passing run into a failing one — but it must still
say so, because leaving the machine on the wrong version silently is how the
next confusing failure starts.
#>
function Restore-Node {
param(
[Parameter(Mandatory)][string]$Version,
[scriptblock]$Step,
[scriptblock]$Note
)
try {
Use-Node -Version $Version -Why 'restoring the machine default' -Step $Step -Note $Note | Out-Null
}
catch {
if ($Note) { & $Note "could not restore Node ${Version}: $($_.Exception.Message)" }
}
}