Repository navigation
Make E2E test server port and cleanup safe for parallel runs - #1420
Conversation
Two E2E suites running at once (e.g. separate worktrees driven by parallel agent jobs) could not coexist: - SERVER_PORT was hardcoded to 8888 in three places. - global-setup.ts's stale-port cleanup SIGKILLed whatever was listening on the port via lsof, destroying a second run's backend. - PID/tempdir pointers lived in fixed files in os.tmpdir(), so a second run overwrote the first run's pointers. - Only db_path/log_path were isolated per run; compiled/draft script paths and the two remaining log paths defaulted into server/conf/. Fix, all in client-v3/: - New e2e/env.ts is the single source for SERVER_PORT/BASE_URL/ RUN_DIR/STATE_FILE, imported by config, setup, teardown, helpers, and db-snapshot. Port resolution: E2E_PORT env var if set, else a port deterministically derived from process.cwd() (20000-39999), so each checkout gets its own stable port with no cross-process coordination needed. - global-setup.ts no longer kills whatever holds the port. It only reaps a process it can positively identify as its own leftover, by matching `ps` output against `main.py ... --port=<port>` directly (not solely via the PID recorded in state.json, which a crashed run's teardown can delete without having killed the live process - found via a real race against db-snapshot.ts's retry-restart hook). If the port is genuinely held by something else, setup fails fast with a message pointing at E2E_PORT instead of touching it. - State (PID + tempDir) moves from fixed tmpdir files to a per-port state.json under os.tmpdir()/digiscript-e2e-<port>/, so two runs can never clobber each other's pointers. - The generated settings JSON now isolates compiled_script_path, draft_script_path, client_log_path and db_log_path into the run's tempDir too, alongside the existing db_path/log_path. - Server stdout/stderr now goes to a log file surfaced on setup failure, instead of being discarded. Verified: two full suites run concurrently from separate directories, 221/221 passed each, confirmed via lsof that distinct ports were live simultaneously, and an unrelated server already on the old fixed port 8888 was left undisturbed throughout. Also verified: a genuine foreign listener on the derived port is refused (not killed); a stale server left behind by a killed runner is reaped, both with and without a matching state.json. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Client V3 Test Results209 tests 209 ✅ 0s ⏱️ Results for commit e5a6224. ♻️ This comment has been updated with latest results. |
Client Test Results95 tests 95 ✅ 0s ⏱️ Results for commit e5a6224. ♻️ This comment has been updated with latest results. |
Python Test Results 1 files 1 suites 47s ⏱️ Results for commit e5a6224. ♻️ This comment has been updated with latest results. |
Playwright E2E Results (chromium)221 tests 221 ✅ 2m 17s ⏱️ Results for commit e5a6224. ♻️ This comment has been updated with latest results. |
Playwright E2E Results (firefox)221 tests 221 ✅ 2m 12s ⏱️ Results for commit e5a6224. ♻️ This comment has been updated with latest results. |
Fixes all 9 issues SonarCloud flagged as new code on PR #1420 (client-v3/e2e/env.ts, global-setup.ts), which had failed the PR's Quality Gate on Security Rating: - typescript:S4790 (CRITICAL/security): env.ts derived the E2E port using SHA-1, flagged as a weak hash algorithm. Not actually a sensitive use (just spreading a cwd path into a port number), but switching to SHA-256 removes the finding for free with no behaviour change (still deterministic, still fine for port selection). - typescript:S4036 (security): global-setup.ts's stale-server reap invoked `ps` by bare name, resolved via $PATH. Now resolved from a fixed list of unwriteable absolute paths (/bin/ps, /usr/bin/ps), same behaviour on macOS/Linux, no PATH-hijack surface. - typescript:S7772 (x5): prefer `node:` protocol-prefixed imports for builtins (crypto/os/path/net/fs/child_process). Applied consistently across all touched e2e/ files, not just the flagged lines. - typescript:S7773 (x4): prefer `Number.parseInt`/`Number.isNaN` over the global `parseInt`/`isNaN`. Re-verified after these changes: full E2E suite green (221/221), and specifically re-ran the stale-orphan-server reap check (a server with no matching state.json) against the *new* SHA-256-derived port to confirm the /bin/ps-based matching still reaps it correctly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Replaces npm with pnpm 12.5.1 as the package manager for both `client/` and `client-v3/` (each keeps its own independent pnpm-lock.yaml — no shared workspace). This directly addresses the recurring npm lockfile problems recorded against this repo: npm only records the optional platform/CPU package variants it actually installed locally (npm/cli #7961/#8320), so a lockfile regenerated on macOS silently drops the Linux CI variants, and separately, targeted `npm install` runs have been observed to non-deterministically drop unrelated optional dependency entries. pnpm resolves from registry metadata rather than local `node_modules`, so the lockfile is platform-independent by construction — verified by spot-checking the generated pnpm-lock.yaml for @rolldown/binding-* entries across all target platforms, not just darwin-arm64 (the machine that generated it). Electron is gone (#1416), which removes the one blocker to a clean pnpm migration: Electron Forge needs pnpm's `node-linker=hoisted`, giving up its strict node_modules; with Electron gone, both remaining projects use pnpm's default strict linker. - `packageManager: "pnpm@12.5.1"` pinned in both package.json files; `engines.npm` removed (meaningless under pnpm). - `pnpm-workspace.yaml` added per project (pnpm 10+ moved `overrides` and `onlyBuiltDependencies` out of package.json's `pnpm` field into this file). Each allowlists the install/postinstall scripts actually needed: `@parcel/watcher` (native file-watcher binding used by chokidar for HMR/watch mode), `vue-demi` (client-v3 only — selects the Vue 2 vs 3 interop shim, functionally required), `esbuild` (client only — downloads its native binary, required to run at all), `bootstrap-vue`/`core-js` (both harmless donation-message no-ops, allowed for completeness). - `client/.npmrc`: dropped `legacy-peer-deps=true` (npm-specific, no pnpm equivalent needed — pnpm warns rather than hard-fails on the pre-existing `@vue/test-utils@2.5.1` peer mismatch against vue@2.7.14; `pnpm peers check` shows the warning, but it's cosmetic: the full test suite already exercises this pairing and passes). - CI (`nodelint.yml`, `client-test.yml`, `playwright-test.yml`, `build-server.yml`) and the two Dockerfile frontend build stages switched from `actions/setup-node` + `npm ci` to `pnpm/action-setup@v4` + `actions/setup-node` (cache: pnpm) + `pnpm install --frozen-lockfile`. `client-test.yml` also picked up a pre-existing actions/checkout@v3 → v4 bump for consistency with the rest of the CI (unrelated drift, fixed while touching the file). `dependabot.yml` needs no change: its `npm` ecosystem key already covers pnpm-lock.yaml. - `scripts/update-version.sh`, `README.md`, `client-v3/README.md` and `documentation/development.md` updated to reference pnpm instead of npm. Also fixed two pre-existing doc inaccuracies noticed while editing: the root README's "Client" build instructions pointed at `client` while describing client-v3's output path, and `development.md`'s "Project Structure" section had dropped `client-v3` entirely (introduced by the Electron-removal pass in #1416, which turned "three main components: server/client/electron" into "two: server/client" without noticing client-v3 was the actual third). Testing (both projects): typecheck, ci-lint, full unit test suite, and production build all pass under pnpm. The full 221-test Playwright suite against client-v3 also passes cleanly under pnpm (confirmed with a temporary, since-reverted port/tmpfile override to rule out interference from an unrelated concurrent E2E run on this machine — PR #1420 will fix that class of collision generally, but it hasn't merged yet). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces npm with pnpm 12.5.1 as the package manager for both `client/` and `client-v3/` (each keeps its own independent pnpm-lock.yaml — no shared workspace). This directly addresses the recurring npm lockfile problems recorded against this repo: npm only records the optional platform/CPU package variants it actually installed locally (npm/cli #7961/#8320), so a lockfile regenerated on macOS silently drops the Linux CI variants, and separately, targeted `npm install` runs have been observed to non-deterministically drop unrelated optional dependency entries. pnpm resolves from registry metadata rather than local `node_modules`, so the lockfile is platform-independent by construction — verified by spot-checking the generated pnpm-lock.yaml for @rolldown/binding-* entries across all target platforms, not just darwin-arm64 (the machine that generated it). Electron is gone (#1416), which removes the one blocker to a clean pnpm migration: Electron Forge needs pnpm's `node-linker=hoisted`, giving up its strict node_modules; with Electron gone, both remaining projects use pnpm's default strict linker. - `packageManager: "pnpm@12.5.1"` pinned in both package.json files; `engines.npm` removed (meaningless under pnpm). - `pnpm-workspace.yaml` added per project (pnpm 10+ moved `overrides` and `onlyBuiltDependencies` out of package.json's `pnpm` field into this file). Each allowlists the install/postinstall scripts actually needed: `@parcel/watcher` (native file-watcher binding used by chokidar for HMR/watch mode), `vue-demi` (client-v3 only — selects the Vue 2 vs 3 interop shim, functionally required), `esbuild` (client only — downloads its native binary, required to run at all), `bootstrap-vue`/`core-js` (both harmless donation-message no-ops, allowed for completeness). - `client/.npmrc`: dropped `legacy-peer-deps=true` (npm-specific, no pnpm equivalent needed — pnpm warns rather than hard-fails on the pre-existing `@vue/test-utils@2.5.1` peer mismatch against vue@2.7.14; `pnpm peers check` shows the warning, but it's cosmetic: the full test suite already exercises this pairing and passes). - CI (`nodelint.yml`, `client-test.yml`, `playwright-test.yml`, `build-server.yml`) and the two Dockerfile frontend build stages switched from `actions/setup-node` + `npm ci` to `pnpm/action-setup@v4` + `actions/setup-node` (cache: pnpm) + `pnpm install --frozen-lockfile`. `client-test.yml` also picked up a pre-existing actions/checkout@v3 → v4 bump for consistency with the rest of the CI (unrelated drift, fixed while touching the file). `dependabot.yml` needs no change: its `npm` ecosystem key already covers pnpm-lock.yaml. - `scripts/update-version.sh`, `README.md`, `client-v3/README.md` and `documentation/development.md` updated to reference pnpm instead of npm. Also fixed two pre-existing doc inaccuracies noticed while editing: the root README's "Client" build instructions pointed at `client` while describing client-v3's output path, and `development.md`'s "Project Structure" section had dropped `client-v3` entirely (introduced by the Electron-removal pass in #1416, which turned "three main components: server/client/electron" into "two: server/client" without noticing client-v3 was the actual third). Testing (both projects): typecheck, ci-lint, full unit test suite, and production build all pass under pnpm. The full 221-test Playwright suite against client-v3 also passes cleanly under pnpm (confirmed with a temporary, since-reverted port/tmpfile override to rule out interference from an unrelated concurrent E2E run on this machine — PR #1420 will fix that class of collision generally, but it hasn't merged yet). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Migrate client and client-v3 from npm to pnpm Replaces npm with pnpm 12.5.1 as the package manager for both `client/` and `client-v3/` (each keeps its own independent pnpm-lock.yaml — no shared workspace). This directly addresses the recurring npm lockfile problems recorded against this repo: npm only records the optional platform/CPU package variants it actually installed locally (npm/cli #7961/#8320), so a lockfile regenerated on macOS silently drops the Linux CI variants, and separately, targeted `npm install` runs have been observed to non-deterministically drop unrelated optional dependency entries. pnpm resolves from registry metadata rather than local `node_modules`, so the lockfile is platform-independent by construction — verified by spot-checking the generated pnpm-lock.yaml for @rolldown/binding-* entries across all target platforms, not just darwin-arm64 (the machine that generated it). Electron is gone (#1416), which removes the one blocker to a clean pnpm migration: Electron Forge needs pnpm's `node-linker=hoisted`, giving up its strict node_modules; with Electron gone, both remaining projects use pnpm's default strict linker. - `packageManager: "pnpm@12.5.1"` pinned in both package.json files; `engines.npm` removed (meaningless under pnpm). - `pnpm-workspace.yaml` added per project (pnpm 10+ moved `overrides` and `onlyBuiltDependencies` out of package.json's `pnpm` field into this file). Each allowlists the install/postinstall scripts actually needed: `@parcel/watcher` (native file-watcher binding used by chokidar for HMR/watch mode), `vue-demi` (client-v3 only — selects the Vue 2 vs 3 interop shim, functionally required), `esbuild` (client only — downloads its native binary, required to run at all), `bootstrap-vue`/`core-js` (both harmless donation-message no-ops, allowed for completeness). - `client/.npmrc`: dropped `legacy-peer-deps=true` (npm-specific, no pnpm equivalent needed — pnpm warns rather than hard-fails on the pre-existing `@vue/test-utils@2.5.1` peer mismatch against vue@2.7.14; `pnpm peers check` shows the warning, but it's cosmetic: the full test suite already exercises this pairing and passes). - CI (`nodelint.yml`, `client-test.yml`, `playwright-test.yml`, `build-server.yml`) and the two Dockerfile frontend build stages switched from `actions/setup-node` + `npm ci` to `pnpm/action-setup@v4` + `actions/setup-node` (cache: pnpm) + `pnpm install --frozen-lockfile`. `client-test.yml` also picked up a pre-existing actions/checkout@v3 → v4 bump for consistency with the rest of the CI (unrelated drift, fixed while touching the file). `dependabot.yml` needs no change: its `npm` ecosystem key already covers pnpm-lock.yaml. - `scripts/update-version.sh`, `README.md`, `client-v3/README.md` and `documentation/development.md` updated to reference pnpm instead of npm. Also fixed two pre-existing doc inaccuracies noticed while editing: the root README's "Client" build instructions pointed at `client` while describing client-v3's output path, and `development.md`'s "Project Structure" section had dropped `client-v3` entirely (introduced by the Electron-removal pass in #1416, which turned "three main components: server/client/electron" into "two: server/client" without noticing client-v3 was the actual third). Testing (both projects): typecheck, ci-lint, full unit test suite, and production build all pass under pnpm. The full 221-test Playwright suite against client-v3 also passes cleanly under pnpm (confirmed with a temporary, since-reverted port/tmpfile override to rule out interference from an unrelated concurrent E2E run on this machine — PR #1420 will fix that class of collision generally, but it hasn't merged yet). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fix CI: pin pnpm/action-setup by SHA, pass explicit package_json_file Every job broke with 'No pnpm version is specified' — pnpm/action-setup resolves package_json_file relative to GITHUB_WORKSPACE (repo root) by default, not the job's defaults.run.working-directory (that setting only applies to run: steps, not uses: steps), and there's no root package.json. Pass package_json_file explicitly per job. Also fixes the SonarCloud Quality Gate failure (C Security Rating on New Code, githubactions:S7637 x8): pin pnpm/action-setup to the exact commit the v4 tag currently resolves to, rather than a mutable major-version tag. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Address pnpm migration review round 1 Each finding was reproduced before fixing. - scripts/update-version.sh: pnpm 12's `pnpm version` is not a passthrough to `npm version`; it has a clean-working-tree check, so the second bump failed (ERR_PNPM_UNCLEAN_WORKING_TREE) and left a half-applied version bump. Add --no-git-checks. Verified end to end on a dirty tree. - Engine enforcement: pnpm 12 silently ignores `engine-strict` in .npmrc (an install with an impossible dependency engine succeeded). Set `engineStrict: true` in both pnpm-workspace.yaml files, delete both .npmrc files and the Dockerfile COPY for client/.npmrc. Verified it now rejects Node 24.12.0 for nopt@10.0.1, as npm's engine-strict used to. - pnpm-workspace.yaml: drop `onlyBuiltDependencies` (a no-op in pnpm 12; only `allowBuilds` counts) and set core-js/bootstrap-vue to false (their postinstalls only print donation messages). - Dockerfile: plain `corepack enable`; the version comes from `packageManager` only. Built both frontend stages locally and confirmed `pnpm --version` is 12.5.1 in each. - Add devEngines.packageManager (pnpm, onFail: error) to both package.json files so `npm install` is refused (EBADDEVENGINES) and cannot create a package-lock.json; ignore package-lock.json too. This also makes any `npm run` inside a script fail, so convert the lint/ci-lint scripts and eslint-filter.sh to pnpm. - Docs: add `corepack enable` guidance (Node 24 ships corepack switched off) to README.md, client/README.md, client-v3/README.md and documentation/development.md; convert remaining npm/npx references in client/README.md, client-v3/e2e/README.md and code comments. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Raise engines.node floor to the real minimum (24.15.0) Review round 2 suggestion. With engineStrict now working, the declared `>=24.0.0` was lower than what the install needs: abbrev@5.0.0 and nopt@10.0.1 (in both lockfiles) require ^24.15.0 on the 24 line, the highest floor of any dependency. Note: pnpm 12 does not enforce the root project's own `engines`, even with engineStrict (checked with an impossible `>=99`), so this is accurate documentation rather than a friendlier failure; someone on Node 24.12 still gets the transitive-dependency error. Also update "Node 24.x" in the READMEs and development docs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>



Problem
Two E2E suites running at once (e.g. separate git worktrees driven by parallel CI/agent jobs) could not coexist:
SERVER_PORTwas hardcoded to8888in three places (playwright.config.ts,e2e/global-setup.ts,e2e/helpers.ts).global-setup.ts's stale-port cleanup SIGKILLed whatever was listening on the port (vialsof) — a second run would destroy the first run's backend mid-suite.os.tmpdir(), so a second run overwrote the first run's pointers, causing its teardown to kill the wrong server and delete the wrong database.db_path/log_pathwere isolated into the per-run temp dir;compiled_script_path,draft_script_path,client_log_pathanddb_log_pathdefaulted intoserver/conf/of the checkout.Fix
All changes are in
client-v3/:e2e/env.tsis the single source forSERVER_PORT/BASE_URL/RUN_DIR/STATE_FILE, imported byplaywright.config.ts,global-setup.ts,global-teardown.ts,helpers.tsanddb-snapshot.ts.E2E_PORTenv var if set, else a port deterministically derived fromprocess.cwd()(range 20000–39999). Each checkout gets its own stable port with no cross-process coordination needed, andE2E_PORTremains available to pin a specific port (e.g. to match CI locally).global-setup.tsno longer kills whatever holds the port. It only reaps a process it can positively identify as its own leftover, by matchingpsoutput againstmain.py ... --port=<port>directly — not solely via the PID recorded instate.json(a real race was found empirically between this anddb-snapshot.ts's retry-restart hook, see below). If the port is genuinely held by something else, setup fails fast with a message pointing atE2E_PORTinstead of touching it.state.jsonunderos.tmpdir()/digiscript-e2e-<port>/, so two runs can never clobber each other's pointers.compiled_script_path,draft_script_path,client_log_pathanddb_log_pathinto the run's temp dir.stdio: 'ignore').Verification
lsofthat two distinct derived ports were live at the same time, and a third, unrelated server already running on the old fixed port 8888 (from an unrelated parallel job) was left completely undisturbed throughout.127.0.0.1specifically, while Tornado'sapp.listen(port)binds all interfaces, which could give a false "port free" result on some stacks.)kill -9of the Playwright runner mid-suite, leavingstate.json+ a live server → reaped cleanly on next run.state.jsonat all (the retry/teardown race mentioned above) → also reaped cleanly on next run, via the process-identity match.npm run ci-lint/npm run typecheckclean.server/conf/by the E2E harness.Design + verification detail:
plans/parallel-e2e-isolation.md(gitignored, not included in this PR).🤖 Generated with Claude Code