mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-25 11:44:16 +00:00
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) <noreply@anthropic.com>
110 lines
5.1 KiB
JavaScript
110 lines
5.1 KiB
JavaScript
// batch00-admin-jobs.mjs — validates ab4a4eb (val/00-mixed): the three admin
|
|
// loops moved off the request context into detached background jobs.
|
|
//
|
|
// pkg/hold/admin/jobs_test.go already covers the job framework well. What has
|
|
// no coverage is the wiring: whether the kickoff handler renders the progress
|
|
// fragment into the right hx-target, and whether the work actually outlives the
|
|
// request. Both are invisible to Go tests and both are what ab4a4eb changed.
|
|
//
|
|
// The load-bearing check is "job seen still running with the admin tab closed".
|
|
// A one-member remap finishes in under a second, so closing the tab after it
|
|
// proves nothing — that is why this drives crew import with N entries instead,
|
|
// which does a PDS write plus a network PLC lookup per entry.
|
|
//
|
|
// Seeded members use did:plc:val00…, are created on a local-only hold
|
|
// (did:web:localhost%3A8080, in no relay), and are deleted in a finally block.
|
|
//
|
|
// node test/e2e/login.mjs && node test/e2e/batch00-admin-jobs.mjs
|
|
import { open, adminPage, csrfOf, openCrew, purgeSeeded, reporter, BASE } from './lib.mjs';
|
|
|
|
const N = Number(process.env.ATCR_E2E_SEED ?? 40);
|
|
const MARK = 'val00';
|
|
const seedDID = (i) => `did:plc:${(MARK + String(i).padStart(3, '0')).padEnd(24, 'x')}`;
|
|
|
|
const { record, summarize } = reporter();
|
|
const ctx = await open();
|
|
let page = await adminPage(ctx);
|
|
const csrf = await csrfOf(page);
|
|
|
|
try {
|
|
const [pre, preFound] = await purgeSeeded(ctx, page, MARK, csrf);
|
|
if (preFound) console.log(`(pre-clean: removed ${pre}/${preFound} leftover seeded members)`);
|
|
|
|
const file = JSON.stringify({
|
|
version: 1,
|
|
exportedAt: new Date().toISOString(),
|
|
holdDID: 'did:web:localhost%3A8080',
|
|
crew: Array.from({ length: N }, (_, i) => ({
|
|
did: seedDID(i), role: 'member', permissions: ['blob:read'], tier: 'deckhand',
|
|
})),
|
|
});
|
|
|
|
const t0 = Date.now();
|
|
const res = await ctx.request.post(`${BASE}/admin/crew/import`, {
|
|
multipart: {
|
|
csrf_token: csrf,
|
|
crew_file: { name: 'crew.json', mimeType: 'application/json', buffer: Buffer.from(file) },
|
|
},
|
|
});
|
|
const kickoff = (await res.text()).replace(/\s+/g, ' ');
|
|
|
|
record('import kickoff returns a 200 fragment immediately', res.status() === 200,
|
|
`HTTP ${res.status()} in ${Date.now() - t0}ms for ${N} entries`);
|
|
record('kickoff renders job_progress (a poller), not a finished result',
|
|
/hx-get[^>]*jobs\/crew-import\/status/i.test(kickoff), kickoff.slice(0, 100));
|
|
|
|
// Keep one blank page: closing EVERY page disposes ctx.request with
|
|
// "Request context disposed". The kickoff request has already returned, so
|
|
// this is still a fair test of the job outliving it.
|
|
const blank = await ctx.newPage();
|
|
await blank.goto('about:blank');
|
|
for (const p of ctx.pages()) if (p !== blank) await p.close();
|
|
record('admin tab closed while the job runs', ctx.pages().length === 1,
|
|
`${ctx.pages().length} page open (about:blank)`);
|
|
|
|
let running = false, done = false, body = '';
|
|
const ticks = new Set();
|
|
for (let i = 0; i < 180; i++) {
|
|
const s = await ctx.request.get(`${BASE}/admin/api/jobs/crew-import/status`);
|
|
if (s.status() !== 200) { body = `HTTP ${s.status()}`; break; }
|
|
body = (await s.text()).replace(/\s+/g, ' ');
|
|
const m = body.match(/(\d+)\s*(?:\/|of)\s*(\d+)/);
|
|
if (m) { ticks.add(m[1]); if (Number(m[1]) < Number(m[2])) running = true; }
|
|
if (!/hx-get[^>]*jobs\/crew-import\/status/i.test(body)) { done = true; break; }
|
|
await new Promise((r) => setTimeout(r, 1000));
|
|
}
|
|
|
|
record('job seen still running with the admin tab closed', running,
|
|
running ? `progress ticks: ${[...ticks].slice(0, 8).join(',')}` : 'never caught it mid-run');
|
|
record('job ran to completion with no admin page open', done,
|
|
`${((Date.now() - t0) / 1000).toFixed(1)}s — ${body.slice(0, 120)}`);
|
|
|
|
page = await ctx.newPage();
|
|
await openCrew(page);
|
|
const added = await page.evaluate(
|
|
(m) => (document.body.textContent.match(new RegExp(m, 'g')) || []).length, MARK);
|
|
record('imported members appear in the crew tab', added > 0, `${added} references`);
|
|
|
|
// Early no-op path renders job_result, not a poller.
|
|
const noop = await ctx.request.post(`${BASE}/admin/crew/remap-tier`, {
|
|
form: { csrf_token: csrf, from: 'tier-that-never-existed', to: 'deckhand' },
|
|
});
|
|
const noopBody = (await noop.text()).replace(/\s+/g, ' ');
|
|
record('unknown source tier returns the no-op result, not a job',
|
|
/No crew were on tier/i.test(noopBody), noopBody.slice(0, 110));
|
|
} finally {
|
|
const p = ctx.pages()[0] ?? (await ctx.newPage());
|
|
const [deleted, found] = await purgeSeeded(ctx, p, MARK, csrf);
|
|
record('cleanup: seeded members removed', found === 0 || deleted === found, `${deleted}/${found} deleted`);
|
|
// Re-open before counting: purgeSeeded's DOM predates the deletes it issued,
|
|
// so asserting on the stale page reports success either way.
|
|
await openCrew(p);
|
|
const left = await p.evaluate(
|
|
(m) => (document.body.textContent.match(new RegExp(m, 'g')) || []).length, MARK);
|
|
record('cleanup: no seeded members left in crew tab', left === 0, `${left} references`);
|
|
}
|
|
|
|
const failures = summarize('batch 00 — admin background jobs');
|
|
await ctx.close();
|
|
process.exit(failures ? 1 : 0);
|