Files
at-container-registry/test/e2e/batch07-stale-click.mjs
T
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

102 lines
4.0 KiB
JavaScript

// batch07-stale-click.mjs — click delete on an already-stale preview.
//
// Companion to batch07-stale-preview.mjs, which stages a preview and waits out
// maxPreviewAgeForDelete. This half assumes the wait is already done: it
// re-renders whatever preview the hold is holding and clicks delete on it.
//
// GET /admin/api/gc/status re-renders lastPreview WITHOUT touching
// lastPreviewAt, so displaying an old preview does not reset its age. That is
// what makes this resumable after a failed run, instead of costing another 31
// minutes.
//
// Two things learned the hard way, both encoded below:
//
// * page.on('dialog') did not reliably intercept the hx-confirm on this page.
// The unaccepted native dialog then blocks every subsequent evaluate() and
// innerText(), so the script hangs rather than failing. The confirm is not
// what is under test, so the attribute is stripped before clicking and no
// dialog is ever raised.
//
// * The delete button is not the first "Delete" on the page when a Reconcile
// button is also rendered, so match the full label.
//
// node test/e2e/batch07-stale-click.mjs
import { open, adminPage, reporter } from './lib.mjs';
const ctx = await open();
const page = await adminPage(ctx);
const r = reporter();
const results = page.locator('#gc-results');
await page.click('a[href="/admin#storage"]');
await page.waitForTimeout(1500);
// How old does the hold think its preview is? Rendered on the tab itself.
const age = await page.locator('text=/Last scan:/').first().innerText().catch(() => '(none)');
console.log(`Hold reports: ${age}`);
// --- Re-render the staged preview without restaging it ---------------------
await page.evaluate(() =>
window.htmx.ajax('GET', '/admin/api/gc/status', { target: '#gc-results', swap: 'innerHTML' }),
);
let staged = '';
const renderDeadline = Date.now() + 30_000;
while (Date.now() < renderDeadline) {
staged = await results.innerText().catch(() => '');
if (staged.includes('Referenced Blobs')) break;
await page.waitForTimeout(1000);
}
r.record('staged preview re-rendered without restaging', staged.includes('Referenced Blobs'));
const m = staged.match(/Delete (\d+) Orphaned Records/);
const before = m ? Number(m[1]) : null;
r.record('delete control present', before !== null, `${before} orphaned records`);
if (before === null) {
r.summarize('batch 07 — stale-preview refusal');
await ctx.close();
process.exit(1);
}
// --- Click, with the confirm removed --------------------------------------
const btn = page.locator(`button:has-text("Delete ${before} Orphaned Records")`).first();
await btn.evaluate((el) => el.removeAttribute('hx-confirm'));
await btn.click();
let out = '';
const deadline = Date.now() + 120_000;
while (Date.now() < deadline) {
out = await results.innerText().catch(() => '');
if (/preview is .* old \(limit/.test(out)) break;
if (/Records Deleted/.test(out)) break;
await page.waitForTimeout(2000);
}
console.log('\n#gc-results after the click:\n' + out.slice(0, 500));
r.record('stale preview is refused, visibly',
/preview is .* old \(limit .*\)/.test(out),
out ? out.split('\n').filter(Boolean)[0] : 'nothing rendered');
r.record('refusal names the remedy',
out.includes('run Scan again'),
'the operator has to be told what to do next');
// --- Nothing was deleted ---------------------------------------------------
await page.locator('button:has-text("Scan for Orphans")').first().click();
let after = '';
const rescanDeadline = Date.now() + 180_000;
while (Date.now() < rescanDeadline) {
after = await results.innerText().catch(() => '');
if (after.includes('Referenced Blobs')) break;
await page.waitForTimeout(2000);
}
const m2 = after.match(/Delete (\d+) Orphaned Records/);
r.record('no records were deleted by the refused click',
m2 !== null && Number(m2[1]) === before,
`${before} before, ${m2 ? m2[1] : 'unknown'} after`);
const failed = r.summarize('batch 07 — stale-preview refusal');
await ctx.close();
process.exit(failed ? 1 : 0);