Files
at-container-registry/test/e2e
Evan JarrettandClaude Opus 5 5f74299bd7 test/e2e: exercise the stale-preview refusal and the real GC sweep
Three scripts, split by what they cost to run.

batch07-stale-preview.mjs stages a preview and waits out the 30-minute
maxPreviewAgeForDelete constant. batch07-stale-click.mjs is the resumable
half: it re-renders whatever preview the hold already holds and clicks delete
on it. GET /admin/api/gc/status re-renders lastPreview WITHOUT touching
lastPreviewAt, so showing an old preview does not reset its age — which is
what makes a failed run cost seconds instead of another 31 minutes.

Result against the dev hold: "preview is 34m0s old (limit 30m0s) — run Scan
again before deleting" rendered through the progress-to-error fragment chain,
with all 387 records still there afterwards. That chain is the point; the
refusal logic itself already has a Go test, but a refusal that renders as
nothing is indistinguishable from "there was nothing to delete".

batch07-sweep.mjs then runs the destructive path for real: 387 records
deleted of 387 staged, orphaned count to zero, referenced blobs unchanged at
15. Safe only against the dev hold on Storj; production is Bunny + UpCloud
and is not reachable from here.

page.on('dialog') did not reliably intercept hx-confirm on this page, and an
unaccepted native dialog blocks every later evaluate() and innerText(), so
the script hangs rather than fails — the worst failure mode for an unattended
check. Both scripts now strip the hx-confirm attribute before clicking. The
confirm is not what is under test.

Two findings worth carrying, neither introduced by this range:

  * deleteOrphanedBlobs is still unexercised. The bucket holds 19 objects,
    of which 8 are past the 7-day blob grace, and none are unreferenced — so
    there is nothing for it to collect. More pushes cannot help: fresh blobs
    are inside the grace window by definition.

  * Storage accounting is derived from layer records, so this sweep moved the
    dashboard from 1.3 GB to 1.1 KB while the bucket held 147 MB throughout.
    It was overstating by ~9x before (records for blobs held by another hold)
    and understates now (referenced blobs with no layer records). Quotas and
    billing read the same number. Belongs to batch 12.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 16:34:25 -05:00
..

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

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 <button hx-post>, not a <form>. Scraping for forms finds nothing, deletes nothing, and cheerfully reports a clean tab while every seeded member is still live. Assert against page text after a reload, not against the scrape that just ran.

Crew rows hydrate per-row via hx-trigger="load". The tab needs a real settle window (~6s here) before anything is scrapeable.

A seeded fixture makes the second run lie. Crew import skips DIDs that already exist, so a re-run finishes instantly and the detachment check silently passes without ever exercising a running job. Purge before re-running.

Preconditions are easy to miss. The tier reconciliation card only renders for crew on a tier absent from quota config (handleCrewList), so it is invisible on a healthy hold. Crew add/update do not validate the tier against config, which is how these tests manufacture the condition without restarting the hold.

Per-batch stack switching

Use val-switch.sh. The appview DB migrates forward only, so older batch code hits a schema from the future: batch 00 selects tags.id (dropped by 0032) and cannot write manifests.manifest_key (added NOT NULL by 0033/0034, which kills every backfill insert). The script destroys and re-migrates the appview volume so the DB matches the branch.

It deliberately does not touch atcrio_atcr-hold, which holds the hold's did:web signing key and the CAR store — captain, crew, layer, stats and scan records. Losing it means a new hold identity and every pushed layer gone.

docker-compose.yml is pinned to main throughout. a7c7db6 (batch 01) is what makes the appview share the hold's netns so did:web:localhost%3A8080 resolves, and every compose-based batch needs it — including batch 00, which lands before it. The file is dev-only, so pinning it is a fixture decision rather than a change to what is under validation. Never commit it from a batch branch; the script unstages it for you, because git checkout main -- <path> stages what it restores.