From 5aefa850483f6a20c729dd6fdfb61f7a9d5040ac Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Thu, 13 Aug 2026 20:18:03 -0500 Subject: [PATCH] test/e2e: cover the admin job wiring ab4a4eb changed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jobs_test.go covers the job framework thoroughly, but nothing covers the wiring: whether the kickoff handler renders the progress fragment into the right hx-target, and whether the loop actually outlives the request it was started from. Both are what ab4a4eb changed, and both are invisible to Go tests — a typo in an hx-target or a fragment that renders blank passes every assertion we have. The load-bearing check drives crew import rather than the tier remap. A one-member remap completes in under a second, so closing the tab "mid-run" proves nothing; import does a PDS write plus a network PLC lookup per entry, which leaves a real window to close the browser and watch the job keep going. It is caught mid-flight at a progress tick with no admin page open. Seeded members are created on the local-only dev hold and removed in a finally block. README records the environment traps found while building this: 127.0.0.1 vs localhost, in-memory sessions dying on every hold rebuild, UA/IP pinning that makes curl log you out, and the forward-only appview migrations that require a per-batch DB reset. Co-Authored-By: Claude Opus 5 (1M context) --- package-lock.json | 69 ++++++++++++++++++- package.json | 1 + test/e2e/README.md | 85 ++++++++++++++++++++++++ test/e2e/batch00-admin-jobs.mjs | 109 +++++++++++++++++++++++++++++++ test/e2e/cleanup-seeded-crew.mjs | 27 ++++++++ test/e2e/lib.mjs | 95 +++++++++++++++++++++++++++ test/e2e/login.mjs | 42 ++++++++++++ test/e2e/val-switch.sh | 73 +++++++++++++++++++++ 8 files changed, 499 insertions(+), 2 deletions(-) create mode 100644 test/e2e/README.md create mode 100644 test/e2e/batch00-admin-jobs.mjs create mode 100644 test/e2e/cleanup-seeded-crew.mjs create mode 100644 test/e2e/lib.mjs create mode 100644 test/e2e/login.mjs create mode 100755 test/e2e/val-switch.sh diff --git a/package-lock.json b/package-lock.json index 5aeef10..5cbfea2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,15 +9,16 @@ "version": "1.0.0", "dependencies": { "htmx-ext-json-enc": "^2.0.3", - "htmx.org": "^2.0.8", - "lucide": "^1.8.0" + "htmx.org": "^2.0.8" }, "devDependencies": { + "@playwright/test": "^1.62.1", "@tailwindcss/cli": "^4.2.2", "@tailwindcss/typography": "^0.5.19", "daisyui": "^5.5.19", "esbuild": "^0.28.0", "glob": "^13.0.6", + "lucide": "^1.8.0", "tailwindcss": "^4.2.2" } }, @@ -822,6 +823,22 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@playwright/test": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@tailwindcss/cli": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.2.tgz", @@ -1223,6 +1240,21 @@ "@esbuild/win32-x64": "0.28.0" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -1570,6 +1602,7 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/lucide/-/lucide-1.8.0.tgz", "integrity": "sha512-JjV/QnadgFLj1Pyu9IKl0lknrolFEzo04B64QcYLLeRzZl/iEHpdbSrRRKbyXcv45SZNv+WGjIUCT33e7xHO6Q==", + "dev": true, "license": "ISC" }, "node_modules/magic-string": { @@ -1662,6 +1695,38 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/postcss-selector-parser": { "version": "6.0.10", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", diff --git a/package.json b/package.json index 626d381..d47d307 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "watch": "npm run css:watch & npm run js:watch" }, "devDependencies": { + "@playwright/test": "^1.62.1", "@tailwindcss/cli": "^4.2.2", "@tailwindcss/typography": "^0.5.19", "daisyui": "^5.5.19", diff --git a/test/e2e/README.md b/test/e2e/README.md new file mode 100644 index 0000000..3fde46b --- /dev/null +++ b/test/e2e/README.md @@ -0,0 +1,85 @@ +# Browser-driven batch validation + +Checks for the `val/*` validation stack — the branch-per-batch series used to +sign off the range between the deployed commit and `main`. + +These complement, and do not replace, `go test` and the in-process integration +harness (`internal/testharness`, `test/integration`, `make integration-test`). +Pick by what needs proving: + +| Prove | Use | +|---|---| +| A function's logic, a query's shape, a guard's behaviour | Go unit test | +| A push/pull/delete round trip across appview + hold + S3 | `make integration-test` | +| A fragment renders into the right target, a job outlives its request, a 500 dressed as an empty state | these scripts | + +## Running + +```bash +npm i -D @playwright/test && npx playwright install chromium + +node test/e2e/login.mjs # interactive, once per hold rebuild +node test/e2e/batch00-admin-jobs.mjs # then the batch checks +``` + +Env overrides: `ATCR_HOLD_URL`, `ATCR_APPVIEW_URL`, `ATCR_E2E_PROFILE`, +`ATCR_E2E_SEED`. + +## Things that will cost you an afternoon + +**Use `127.0.0.1`, never `localhost`.** The appview canonicalises to +`http://127.0.0.1:5000` and answers `localhost` with a 307. Any snippet written +against `localhost:5000` measures the redirect, not the endpoint. + +**Admin sessions are in-memory and die on every hold rebuild.** They live in +`ui.sessions` (`pkg/hold/admin/admin.go`), not the `admin_sessions` table, which +is vestigial for this path. Air rebuilds the hold whenever tracked source +changes — including a batch checkout — so budget one interactive login per +switch. There is no test-mode bypass; `server.test_mode` only affects OAuth +redirect URLs. + +**Never drive the admin panel with curl.** Sessions are pinned to User-Agent and +client IP prefix, and a mismatch does not merely reject the request — it calls +`deleteSession` and logs you out. Drive everything through `ctx.request`, which +inherits the browser's cookie jar and UA. + +**Closing every Playwright page disposes `ctx.request`.** It fails with "Request +context disposed". Keep one `about:blank` page open when the test needs the +browser out of the way. + +**Crew delete is a `