mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-03 08:46:57 +00:00
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>
102 lines
4.0 KiB
JavaScript
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);
|