19 Commits
Author SHA1 Message Date
synAdminandClaude Opus 5 022ab8edcf fix(local): install dependencies when the lockfile changes, not only when node_modules is absent
Starting the stack locally failed to build with four copies of

    error TS2307: Cannot find module '@simplewebauthn/server'

naming a package that is right there in package.json. That reads as a broken checkout rather than a missing install, which is why it costs more than it should.

The cause is one line in Install-IfMissing. It asked whether node_modules existed and returned early if it did, which is true for anyone who has ever run the script. So a branch that ADDS a dependency never installs it: the pull brings a new package.json and a new lockfile, the script says dependencies already installed, and the build then fails on an import the source is entirely right to make.

The passkeys work is what surfaced it, adding @simplewebauthn/server to the backend and @simplewebauthn/browser to the frontend, but nothing about it is specific to those. Any dependency added on any branch would have done the same, and the failure would have looked equally unrelated to its cause each time.

It now compares timestamps instead. npm writes node_modules/.package-lock.json describing exactly what it put there, so holding that against package-lock.json answers the question actually being asked: is what is installed what is currently asked for. A pull that changes dependencies makes the lockfile newer and this notices; a pull that does not leaves the check skipping the install exactly as before, which is the whole reason the check exists.

Both branches were exercised against the real working tree: stale before installing, up to date after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 10:06:58 -05:00
synAdminandClaude Opus 5 f9c40146d4 fix(ci): the cleanup script forced TLS onto a plaintext endpoint (#324)
The workflow failed on its first real run with:

    write EPROTO ... ssl3_get_record:wrong version number

which reads like a TLS misconfiguration and sends you looking at certificates and protocol versions. It is neither. The server answered in cleartext and OpenSSL tried to parse that as a TLS record.

The script required node's https module and always used it, defaulting to port 443. That was fine while the host was typed by hand, and it stopped being fine the moment the workflow started supplying it from github.server_url. Inside the runner that is the address act_runner reaches Gitea on, not the public one, and here it is plain HTTP on a container port.

So the scheme in GITEA_HOST is honoured rather than assumed, and the default port follows from it. A URL naming neither http nor https is refused up front, because this script speaks nothing else and reporting that as a bad input beats failing later inside a request.

The endpoint is now printed before the first request rather than after one succeeds. That is the part that made this cost more than it should have: a transport failure said nothing about where it had been pointed, so the message named a symptom in OpenSSL and nothing about the run at all.

Verified against a plaintext HTTP stub end to end: the listing, the age selection and the dry-run report all work over http, and a bad scheme exits 1 with a message naming the value it was given.

Refs #324

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 07:19:47 -05:00
synAdminandClaude Opus 5 d7bfd47797 chore(ci): a manually-run workflow to delete old Actions runs (#324)
Linting / lint (pull_request) Successful in 4m46s
SonarQube Analysis / sonarqube (pull_request) Failing after 22m15s
Gitea 1.27.3 expires a run's logs and artifacts but never the run record itself, so the Actions list grows without limit and fills with entries whose logs are already gone. Clearing it meant 455 API calls from a scratch file on one machine, which is the wrong home for something that has to happen again every few weeks.

Dry run unless apply is typed as true. The operation cannot be undone and there is no confirmation once it starts, so the harmless answer has to be the default rather than the one you get by leaving a box alone.

Manual only, deliberately. The list is an annoyance rather than a problem, and a cron that quietly deletes history deserves to be a decision taken on its own rather than one that arrives bundled with the tool.

A run that is not completed is never a candidate, which is also what stops the cleanup deleting the run it is executing in.

Ages a run by started_at, falling back to completed_at. That fallback is the whole reason this is worth committing rather than repeating from memory: a run cancelled before it ever started reports an epoch started_at while carrying a real completed_at, so reading only the first makes every cancelled run look undateable. The manual pass did exactly that and left nineteen runs from three weeks earlier in a list that was supposed to hold seven days. Neither timestamp usable still means keep — an epoch read as 1969 would delete the runs that have not happened yet.

Uses a dedicated ACTIONS_CLEANUP_TOKEN secret rather than the automatic per-job token, since deleting a run may be beyond what that token permits. If it turns out to be enough, the secret and the env line both go. Host and repository come from the run's own context, so the file carries no hostname and survives the move #313 may yet make.

No npm install: the script uses node's own https module, so there is nothing to fetch and nothing to break when a dependency moves.

Verified by running the script against the live instance: a dry run reported 19 stale cancelled runs the earlier pass had missed, and applying it removed them with no failures.

Closes #324

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 09:30:36 -05:00
bermudalambandClaude Opus 5 936dcb60a8 docs(scripts): record the Node pin decision at the constant (#208)
Linting / lint (pull_request) Successful in 2m57s
SonarQube Analysis / sonarqube (pull_request) Successful in 24m21s
The last open item on #208, and the only one that needed a person rather than a patch. Local runs stay pinned to 26.7.0 while CI and the production image run 20, and CI on 20 is the backstop.

The comment said this was "an open decision rather than an oversight", which was true when it was written and is not any more. Left as-is it would read to the next person as something still to settle, and they would either re-litigate it or quietly change the pin.

What the decision costs is written down rather than glossed: passing locally does not mean it ships, because a post-20 syntax or node: API is caught after a push rather than before one. That is the whole of the trade, and it is acceptable precisely because it is known — the failure mode this file's own docstring warns about is the one nobody knew they were exposed to.

Items 1, 2, 3 and 5 were already done in 06933ae and the commits around it: the alias check fails closed on a positive match, three stale documents were corrected, the throw quotes `nvm install $Version`, Use-NodeLatest is gone, DEFAULT_NODE_VERSION sits beside NODE_VERSION rather than being duplicated in two scripts, and the floor check runs before the switch where it can actually fire.

Closes #208

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 19:48:23 -05:00
bermudalambandClaude Opus 5 4e2357356b fix(scripts): make the database the e2e suite reads a recorded fact (#273)
Linting / lint (pull_request) Successful in 7m20s
SonarQube Analysis / sonarqube (pull_request) Successful in 32m55s
The end-to-end helper opens its own connection to read a password-reset token, and nothing guaranteed it pointed at the database the application was actually using. With -E2eDb the app runs on redefined_e2e at 55501 while the helper kept its redefined_local default at 55500, so the app wrote to one database and the suite read another — and the specs failed for a reason that had nothing to do with them.

start-local.ps1 now records the coordinates it chose in .local/database.json, and run-tests.ps1 reads them into TEST_PGHOST, TEST_PGPORT, TEST_PGUSER, TEST_PGPASSWORD and TEST_PGDATABASE before Playwright starts. The answer now comes from one place, written by the thing that made the decision at the moment it made it.

It is written after Start-Database rather than before, so the file never names a database that failed to come up, and removed by -Stop, so a stopped stack does not leave a record pointing at a container that is gone.

Setting all five closes the second fault in the same change. Invoke-IntegrationSuite sets TEST_PGPORT and PowerShell keeps it for the rest of the session, so a -Suite all run leaked the integration port into the e2e run that followed — with none of the matching credentials, leaving the helper offering redefined_local's password to the integration database. Overwriting every one of them is what makes that leak harmless.

A missing record throws rather than falling back. A default is what produced both faults in the first place: always plausible, silently wrong, and it fails in ways that look like application bugs rather than configuration.

The guard test is the point of the change as much as the fix is. This is the third instance today of two files having to agree with nothing comparing them — #107 and #118 were envValidation against a compose file, #287 was the workflow against start-local.ps1, and this is three files rather than two. The test pins the whole chain: that the writer records the five settings, that the runner reads each of them from the record rather than a default, and that the helper reads no connection variable the runner does not set. Removing a single line from the runner fails four of its assertions, which was checked rather than assumed.

What it cannot do is run PowerShell, so these are text assertions against the scripts. That is weaker than executing them and still catches the drift that actually happened.

Closes #273

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 16:57:53 -05:00
bermudalambandClaude Opus 5 1902db6d04 feat(intake): offer background removal on the submission page, ticked (#281)
Adds a checkbox to the public submission page that lets a sender opt out of background removal, ticked by default because most items look better cut out and the reverse default would mean almost nobody got it. It only renders when the server reports the sidecar is configured, matching the intake link's new backgroundRemoval flag from Task 5 — an unconfigured environment gets no checkbox rather than one that would do nothing.

submitItem now takes removeBackground as a required fourth parameter, sent as the multipart string 'true' or 'false' to match the backend's exact-string opt-out contract. Making the parameter required rather than optional was deliberate, so the compiler would catch any call site left unupdated; the frontend build (which also type-checks tests/ via tsconfig.test.json) confirmed the only call site, in Submit.tsx, was updated.

scripts/start-local.ps1 now sets REMBG_URL for the local backend so the checkbox is visible during local and e2e runs; the value need not resolve, since no e2e submission reaches the sidecar without a configured drafting step.

Adds two e2e cases to intake-submit.spec.ts: the checkbox appears ticked by default, and a sender can uncheck it and still submit successfully. Both are written per the task-7 brief but not run in this session, since running Playwright requires the full local stack (database, backend, frontend dev server) which was not started.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 13:07:17 -05:00
bermudalambandClaude Opus 5 d75c45cf91 chore(sonar): name the findings instead of counting them (#181)
The measures step reported "Quality gate ERROR" and "Code smells 5", which is enough to notice debt and useless for clearing it. It now also prints the failing gate conditions, the open issues with their rule, file, line and effort, and the security hotspots awaiting review.

#181 is why this matters. It assumed the five smells were the five eslint-plugin-sonarjs warnings, on the strength of the counts matching, and hedged that the server's rule set is not the plugin's. The hedge was right: those five warnings were fixed under #261, both workspaces lint at zero, and the analysis of the #261 merge still reported five smells and 24 minutes. They are a different five, and nothing short of the list settles which.

The gate condition list matters for the same reason. "ERROR" sends a reader to a dashboard, which is the thing this script exists to avoid needing.

Issues are capped at 25 rather than paged: past a couple of dozen the answer is not "read the list", and an unbounded fetch on every CI run is a cost with no reader.

Verified against a stub SonarQube serving canned responses, so the parsing and formatting are exercised rather than reasoned about — failing conditions filtered from passing and NO_VALUE ones, components stripped of their project-key prefix, absent metrics rendered as a dash, and every path still exiting 0. The no-server and unreachable-server paths were exercised too; the step still cannot fail the job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 17:21:44 -05:00
bermudalambandClaude Opus 5 175d21d11c fix(scripts): refuse to reuse a backend that is serving the previous database (#257)
Linting / lint (pull_request) Successful in 2m13s
SonarQube Analysis / sonarqube (pull_request) Successful in 26m47s
start-local.ps1 said "something is already listening on 3000; leaving it alone" and carried on. That is safe only while the database has not changed underneath that process. When it has, the old backend is serving a database it no longer owns and its in-memory state describes rows that no longer exist.

The rate limiter is the sharpest example. It keys on customer id and keeps buckets in memory for an hour, so a recreated database restarting ids at 1 hands a brand-new customer a previous run's spent allowance. That is #257: the resend-verification allowance test failing roughly one full run in three, never in isolation, with three refusal toasts where one was expected.

Reproduced deterministically rather than reasoned about. Fresh database and fresh backend: three of three pass. Recreate the database only, leaving the same backend running: the same test fails with exactly the reported "resolved to 3 elements". Control — recreate the ids again but restart the backend as well: passes. So the variable is the process outliving the database, not the id restart on its own.

That also explains why #257 could not find the mechanism. It had ruled out contention, a mis-keyed limiter, a shared fixture and identity reuse in the test helpers, all correctly. The recycling happens outside the suite entirely, in a process the suite never sees.

Now it refuses, names the reason, and says to run -Stop. A run that stops loudly is recoverable; one that quietly tests the wrong thing is not — and a stale listener on 3000 has already produced two wrong measurements in this project, a rate-limiter reading and an e2e run reported as 23 passed when the backend was talking to a deleted database.

DatabaseIsNew is set when -Fresh removes the container, when the container is created, and always under -E2eDb, whose tmpfs storage means it comes up empty whether created or restarted.

Verified by AST-parsing the script and confirming every reference to the flag is script-scoped — a function-local read would see $null and the guard would never fire. The script is deliberately never executed from an agent shell.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 11:13:02 -05:00
bermudalambandClaude Opus 5 6a90e957f2 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>
2026-09-02 10:57:02 -05:00
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
bermudalambandClaude Opus 5 fe28c97e0f chore(sonar): remove the rejected Tinqer spike, clear the lint debt, and report measures in CI (#261)
Linting / lint (pull_request) Successful in 2m7s
SonarQube Analysis / sonarqube (pull_request) Successful in 25m21s
The standing cleanup, three features behind. Four changes.

Report the measures in CI. This is the one that matters, because the rest was only findable by reading the tree. SonarQube here is 9.9 Community: no Bearer auth, so the official MCP cannot connect, and the host is a CI secret, so hotspots, duplication, debt and coverage existed only on a dashboard — which made "reduce the debt" an instruction nobody could act on without a browser open beside them. scripts/summarize-sonar.js queries the measures API with the secrets the workflow already holds and prints the result into the job log. The scanner masks the URL and token; measures are not secret.

It polls the compute task before reading. The workflow does not set sonar.qualitygate.wait, so the scan step returns once the report is uploaded and the server computes measures afterwards — reading immediately would return the previous analysis, indistinguishable from this one and quietly wrong. When it cannot confirm, it says so in the output rather than presenting stale numbers as current. It is deliberately not guarded with continue-on-error: it exits 0 on every path, and guarding it would oblige it to appear in the final gate, whose job is to fail the build.

Remove the Tinqer spike. #216 evaluated Drizzle against Tinqer and rejected Tinqer, and its closing comment said the throwaway src/db-tinqer/ probe must not reach main. The whole spike commit was merged, so it did. The probe is 71 lines imported by nothing, and @tinqerjs/tinqer, @tinqerjs/pg-promise-adapter and pg-promise were dependencies for a library nobody chose. The condition_note column that warning also named did not reach main.

Clear the lint debt, both projects now at zero warnings from six and two. One of these was a real defect rather than tidiness: the third catch block in shippingAddresses.ts rolled back and returned 500 while discarding the error, so a failed default-address change left nothing behind to say why — the two catch blocks above it in the same file already logged, and this one had simply been missed. The Express namespace augmentation is a false positive and is disabled with the reason written beside it, because an interface that must merge into one Express declares inside a namespace has no ES module spelling.

Dedupe the extension map. backfillImageReencode.ts kept its own .jpg/.png/.webp table whose comment named uploadTypes.ts as the source of truth, directly above duplicating it. That file rewrites stored images, so the two disagreeing would silently skip files it should re-encode.

src/db-drizzle/ deliberately stays. #217 is open to promote exactly those files properly, with tablesFilter and the sql.param() array rule; deleting them here would be doing #217 badly in the wrong issue. Only their unused-symbol warnings are fixed, and if drizzle-kit pull regenerates schema.ts the table warning returns — worth #217 knowing.

Hotspots and coverage are untouched because both numbers are still invisible. They are the next pass, once the step above has printed them once.

Closes #261

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 13:30:29 -05:00
bermudalambandClaude Opus 5 95325c75d4 fix(scripts): switch Node to a pinned version rather than asking nvm for latest (#198)
`start-local.ps1` failed on a machine that had everything it needed, and then blamed the one thing that was definitely not the problem: "the newest version nvm has installed is too old. Install a newer one" — printed on a machine holding 26.7.0 and 24.13.1, both well past the floor.

`nvm use latest` does not mean "the newest version I have installed". nvm-windows resolves `latest` against the remote release list, and `newest` is the alias for the newest installed. The docstring stated the opposite and the code was written against it. Here that resolved to 26.8.1, which is not installed, so nvm reported `activation error: Version not installed`, left v18.16.1 running, and exited 0.

Two things had to change, and fixing either alone leaves it broken.

The version asked for is now pinned in `NODE_VERSION` rather than chosen by alias, so two machines run the same Node instead of whatever each happens to have installed, and there is one line to bump for both entry points. The alias names are recorded in the docstring anyway, because `latest` and `newest` are easy to swap back by accident and the difference is the whole of this bug.

`Use-Node` no longer treats an alias as automatically successful. That special case is why the error was wrong rather than merely unhelpful: it short-circuited on `$Version -eq 'latest'` regardless of what was running, swallowing nvm's `activation error` — which the function had already captured in `$output` for exactly this purpose — and returned success holding v18. The floor check downstream then reported the only explanation left to it. An alias switch is now verified against nvm's own report, so a failure says what nvm said.

Keeping that half matters even with a pinned version, because no caller passes an alias today. The bug was someone reaching for one, and the next person reaching for one gets a truthful failure rather than a confident wrong answer.

The floor check survives as a backstop against pinning `NODE_VERSION` below 20, and its message now says that rather than describing installed versions — a version that is not installed is `Use-Node`'s error to report, and it reports nvm's reason.

Verified from a real v18.16.1 baseline: the pinned switch takes 18.16.1 to 26.7.0; a concrete version that is not installed throws with nvm's reason; `latest` throws instead of silently succeeding. `start-local.ps1` then runs the whole way through — Node switch, migrations, backend build, ready. Parse check clean.

Shared by `start-local.ps1` and `run-tests.ps1`, so this broke both and fixes both.

Closes #198

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 17:05:48 -05:00
bermudalamb 5d427bc1a1 fix(ci): stop the test summarisers failing the job on an unreadable results file (#178)
Linting / lint (pull_request) Successful in 2m0s
SonarQube Analysis / sonarqube (pull_request) Failing after 21m21s
Run 525 was the first with #174's graceful failure, and it worked: the integration suite failed, the end-to-end suite ran again, and `SonarQube Scan` succeeded for the first time since #154 started. But `Summarize integration tests` failed, and that step should not be able to.

Both scripts already carried the principle in a comment — report plainly and exit 0, because the job fails on the real step and a stack trace here would only bury it — and both only implemented it for the file being absent. A file that exists and cannot be read crashed them.

Two ways to reach that, both reproduced. `--forceExit`, which the integration script passes to paper over a post-run hang, can end the process around the write and leave partial JSON. And a suite that fails to *run* rather than to assert arrives without the array the failure renderer walks, which is exactly the shape this suite has been producing under #154.

Reading is now guarded as thoroughly as `summarize-playwright.js` already guarded its traversal, and that traversal's `|| []` discipline is extended to the jest renderer. `summarize-playwright.js` had the same hole by the narrower path of an unguarded `JSON.parse`.

The reason reaches the log rather than being swallowed. "Could not read the results file" with the parse error is diagnostic; a silent empty summary is not.

This matters beyond tidiness because of where the failure lands. A crash here reports the job as failing at a step named for summarising rather than for testing, which is the misdirection #142 fixed once already — and a summariser whose job is to make a failing run readable should not crash on the output of the worst failures, which is the moment it is most needed.

Verified against a truncated file, a suite entry with no `testResults`, an absent file, and a real 278-test run: the first three now exit 0 naming the reason, the absent case is unchanged, and the happy path still reports its counts.

Closes #178
2026-08-25 09:20:34 -05:00
bermudalamb 491c2652f3 ci: let the summarisers summarise and the gate do the failing (#142)
Linting / lint (pull_request) Successful in 2m10s
SonarQube Analysis / sonarqube (pull_request) Failing after 5m53s
A failing end-to-end run reported itself like this:

    Run node scripts/summarize-playwright.js frontend/playwright-results.json
        Failure - Main Summarize end-to-end tests
    exitcode '1': failure

which reads as a broken summary script. It was not — it was the summariser correctly reporting that tests had failed, with nothing said about it.

Two things combined to produce that.

The summariser doubled as the gate. It ended with `process.exit(stats.unexpected > 0 ? 1 : 0)`, so it failed the job itself. The workflow already has a step written for exactly that — `Fail if either suite failed`, whose comment explains it exists so `continue-on-error` on the suites cannot turn a failing suite into a passing job. That step was being skipped while its condition was true, because a step whose `if:` omits `always()` still implicitly requires its predecessors to have succeeded, and the summariser had already failed the job one step earlier. The gate written to be the place the job fails was dead code.

And the explanation went where the log is not. `report()` writes to GITEA_STEP_SUMMARY when it is set, which under Actions is always, so the counts and the list of failing tests landed in the Summary tab while the log showed a bare exit with no output at all.

So both scripts now exit 0 whatever they find, both print one line of counts to stdout as well as the markdown to the summary, and the gate carries `always() &&` so it actually runs. A failing suite now fails at a step named for what failed, and the log says how many.

Verified by executing both scripts against crafted results rather than by reading them: failing counts, passing counts, and a missing results file, each with and without GITEA_STEP_SUMMARY set. All four exit 0, the headline appears on stdout in every case, and the step summary still receives the full table and the failure list.

Not covered: nothing in this repository runs the scripts under `scripts/`. Jest's testMatch is scoped to backend/tests/unit, so a permanent regression test would need a runner these files do not have. Worth its own issue rather than widening the backend suite's roots to reach the repository root.

Closes #142
2026-08-24 09:46:54 -05:00
bermudalamb 9ac2fa3ba1 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
2026-08-23 09:36:40 -05:00
bermudalamb 40b483fc30 feat(scripts): a PowerShell script to start the local environment (#125)
Linting / lint (pull_request) Successful in 1m45s
SonarQube Analysis / sonarqube (pull_request) Failing after 13m24s
Bringing the app up locally was seven or eight commands in a particular order: a Postgres container on a port that is not blocked, the six environment variables the backend refuses to boot without, migrations, a TypeScript build, the backend, then Vite. None of it hard, all of it tedious, and documented nowhere outside CI workflows written for a Linux runner.

scripts/start-local.ps1 does the sequence. It reuses an existing container rather than recreating one, skips npm install when node_modules is already there, leaves alone anything already listening on a port it wanted, and waits on pg_isready and a 200 from /api/config rather than sleeping a fixed number of seconds. -Fresh recreates the database, -Stop tears everything down by the process ids it recorded rather than by port, since killing by port would also kill whatever else happened to be listening.

Two things this got wrong first time round, both found by running it rather than by reading it.

$ErrorActionPreference = 'Stop' does not stop a PowerShell script when a native executable exits non-zero, only when a cmdlet throws. Every command here is node, npm or docker, so the first run printed a stack trace from a failed migration, carried straight on, and reported a healthy stack sitting on a database with no tables in it. That is the worst kind of wrong: a green summary over a broken environment. Native calls now go through Invoke-Checked, which tests $LASTEXITCODE and throws.

The migration failed because node on the PATH was v18.16.1. node-pg-migrate pulls in an lru-cache that calls diagnostics_channel.tracingChannel, which does not exist before Node 20, and the failure surfaces as "(0 , U.tracingChannel) is not a function" from a minified file - which says nothing whatsoever about Node versions. The script now checks the major version first and says what to do about it, so the confusing crash becomes one clear line before anything else runs.

The port default is 55500 rather than anything near 55432, which is reserved by Hyper-V on this machine. Docker's message when it cannot bind a reserved port does not mention reservations, so the failure path names the likely cause and prints the netsh command that lists the reserved ranges.

Verification, all observed rather than assumed: the version guard was made to fire on Node 18 and produced the intended message. A -Fresh run on Node 24 applied all six migrations, and psql then showed thirteen tables where the broken run had none. /api/config and the storefront both answer 200. -Stop stopped both tracked processes and the container. A second run with the dev server already up detected it and left it alone rather than failing.

.local/ holds the logs, pids and uploads, and is gitignored.

Closes #125
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 11:07:06 -05:00
bermudalamb ae8f0eb12c fix(ci): make SonarQube actually analyse the frontend (#67)
SonarQube Analysis / sonarqube (pull_request) Successful in 3m52s
Tests / lint (pull_request) Successful in 1m55s
Tests / backend-unit (pull_request) Successful in 46s
Tests / frontend-e2e (pull_request) Failing after 9m41s
SonarQube was analysing none of frontend/src. All 34 files were indexed and reported ncloc 0, so the quality gate had been measuring roughly a third of the codebase while appearing to cover it. The scanner log gives the cause: frontend/tsconfig.json sets "moduleResolution": "bundler", which is correct for Vite, and the TypeScript bundled with SonarQube 9.9 predates 5.0 and rejects it. Building the frontend program throws, every frontend file is dropped, and the scan still exits EXECUTION SUCCESS — which is why a green job hid this indefinitely.

Adds frontend/tsconfig.sonar.json, an analysis-only mirror that differs only in using "node", and points the scan at it via sonar.typescript.tsconfigPaths. The app's own tsconfig is deliberately untouched: "bundler" is right for the build, and changing it to satisfy an old analyser would let the tool dictate the build. The mirror cannot use `extends` — the old compiler validates the base file while reading it, so the error just moves to pointing at tsconfig.json.

Verified locally against a scratch project: 59/59 files analysed, no skips. Analysed lines go from 2,069 to 5,481, code smells from 2 to 14, security hotspots from 3 to 4, and technical debt from 21 to 85 minutes. The frontend had been hiding twelve code smells and a hotspot, which is part of why the React problems behind #60 and #62 had to be found by hand.

A standalone copy drifts, and drift here does not fail anything — it silently returns to skipping the frontend while reporting success. scripts/check-sonar-tsconfig.js compares the two and fails when they diverge in anything but moduleResolution, and runs before the scan so the scan is never what discovers it. Confirmed it catches drift by introducing some.

Moves scan settings into sonar-project.properties at the repo root so a local scan and the CI scan analyse the same thing, leaving only the host and token in secrets. Adds scripts/scan-local.sh, which runs the scanner in Docker because it needs Java 11+ and the dev machine has Java 8, and which defaults to a scratch project key: the server is Community edition with no branch analysis, so any scan overwrites the single main analysis of whichever key it is given.

Closes #67
2026-08-19 14:50:20 -05:00
bermudalambandClaude Opus 5 ec63b9cdb0 fix: unset NODE_ENV in e2e job so devDependencies install
npm treats NODE_ENV=production as --omit=dev, so `npm install` in the
frontend-e2e job skipped typescript and the build died on `tsc: not found`.
The same env var would have stripped vite and @playwright/test from the
frontend install, and flipped the session cookie to Secure on a run served
over plain http.

The reported summarize crash was a symptom: the job aborted before Playwright
ran, but Summarize is `if: always()` and threw ENOENT on the missing JSON,
burying the real failure. Both summarize scripts now report the missing file
and exit 0 -- the job still fails via its own step.

Also split build from start, replaced `sleep 3` with a readiness poll against
/api/config, and dump the backend log when e2e fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 08:56:25 -05:00
bermudalamb 4b524e0da3 fix: move scripts folder to correct location
SonarQube Analysis / sonarqube (pull_request) Successful in 4m31s
Tests / backend-unit (pull_request) Successful in 59s
Tests / backend-integration (pull_request) Failing after 4s
Tests / frontend-e2e (pull_request) Failing after 1s
2026-08-14 08:15:52 -05:00