← back to Agent Cabinet
proof(prove-activation): kill every spawned server on any throw/interrupt so the next run can't test a stale one
5b74e924763d216dab48041b7f4380a54c280585 · 2026-09-23 13:50:50 -0700 · Steve
Register each spawned server/static handle in a module-level list and add a cleanup() wired to process exit and SIGINT (exit 130); move the browser launch/context/page setup inside run()'s try with a finally that always closes the browser, so a per-engine launch failure is caught instead of aborting the process and orphaning a bound port. Refuse to start when a pilot port is already in use rather than silently attaching to an orphan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzbjT22E2Q7Q64Wy8xMeH5
Files touched
M front-page-audit/proof/prove-activation.mjs
Diff
commit 5b74e924763d216dab48041b7f4380a54c280585
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Sep 23 13:50:50 2026 -0700
proof(prove-activation): kill every spawned server on any throw/interrupt so the next run can't test a stale one
Register each spawned server/static handle in a module-level list and add a cleanup() wired to process exit and SIGINT (exit 130); move the browser launch/context/page setup inside run()'s try with a finally that always closes the browser, so a per-engine launch failure is caught instead of aborting the process and orphaning a bound port. Refuse to start when a pilot port is already in use rather than silently attaching to an orphan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzbjT22E2Q7Q64Wy8xMeH5
---
front-page-audit/proof/prove-activation.mjs | 58 ++++++++++++++++++-----------
1 file changed, 37 insertions(+), 21 deletions(-)
diff --git a/front-page-audit/proof/prove-activation.mjs b/front-page-audit/proof/prove-activation.mjs
index 3f77c7c..b62d8ac 100644
--- a/front-page-audit/proof/prove-activation.mjs
+++ b/front-page-audit/proof/prove-activation.mjs
@@ -3,7 +3,7 @@ const require = createRequire(import.meta.url);
process.env.NODE_PATH = '/Users/macstudio3/.npm-global/lib/node_modules';
require('module').Module._initPaths();
const { chromium, webkit } = require('playwright');
-import http from 'http'; import fs from 'fs'; import path from 'path'; import os from 'os';
+import http from 'http'; import net from 'net'; import fs from 'fs'; import path from 'path'; import os from 'os';
import { spawn } from 'child_process';
const HOME = os.homedir();
@@ -11,6 +11,16 @@ const ISO = new Date().toISOString().replace(/[:.]/g, '-');
const EVID = path.join(HOME, 'Projects/agent-cabinet/front-page-audit/proof');
fs.mkdirSync(EVID, { recursive: true });
+// Every spawned server / static handle registers here the moment it is created, so a throw ANYWHERE
+// (a webkit launch failure, a mid-loop error, an interrupt) still tears them all down instead of leaking
+// a bound port that the NEXT run's waitPort() would silently attach to (testing stale code -> false verdict).
+const handles = [];
+function cleanup() { for (const h of handles.splice(0)) { try { if (h?.kill) h.kill(); else if (h?.close) h.close(); } catch {} } }
+process.on('exit', cleanup);
+process.on('SIGINT', () => { cleanup(); process.exit(130); });
+// A live port before we start means an orphan from a prior run — fail loudly rather than attach to it.
+function portInUse(port) { return new Promise(res => { const s = net.connect({ host:'127.0.0.1', port, timeout:600 }); s.on('connect', () => { s.destroy(); res(true); }); s.on('error', () => res(false)); s.on('timeout', () => { s.destroy(); res(false); }); }); }
+
const PILOTS_ALL = [
{ site: 'silkwallpaper', mode: 'server', port: 9841, shape: 'items' },
{ site: 'linenwallpaper', mode: 'server', port: 9842, shape: 'items' },
@@ -46,6 +56,7 @@ function startServer(p) {
const env = { ...process.env, PORT: String(p.port), NODE_ENV: 'production' };
if (!fs.existsSync(path.join(cwd, 'node_modules'))) env.NODE_PATH = BORROW_NM;
const child = spawn('node', ['server.js'], { cwd, env, stdio: 'ignore' });
+ handles.push(child);
return child;
}
function startStatic(p) {
@@ -56,7 +67,8 @@ function startStatic(p) {
if (!fp.startsWith(root) || !fs.existsSync(fp) || fs.statSync(fp).isDirectory()) { rq.writeHead(404); return rq.end('404'); }
rq.writeHead(200, { 'content-type': MIME[path.extname(fp)] || 'application/octet-stream' }); fs.createReadStream(fp).pipe(rq);
});
- return new Promise(r => srv.listen(p.port, '127.0.0.1', () => r(srv)));
+ handles.push(srv);
+ return new Promise((r, rej) => { srv.once('error', rej); srv.listen(p.port, '127.0.0.1', () => r(srv)); });
}
function norm(s){ return String(s).replace(/:\d{4,5}\b/g,':PORT').replace(/\d{13,}/g,'TS').replace(/\s+/g,' ').trim().slice(0,200); }
@@ -71,26 +83,29 @@ const measure = () => {
};
async function run(engineLauncher, url, shape, opts) {
- const browser = await engineLauncher.launch();
const errs=[], perrs=[];
- const ctx = await browser.newContext();
- const page = await ctx.newPage();
- // Abort third-party ad/tracking noise (404/502 under localhost) that otherwise delays the deferred
- // bundle so far that products load first and uxInit misreads false. Isolates the bundle fairly.
- await page.route(/ads\.agentabrams|googletagmanager|google-analytics|doubleclick|facebook\.|fbcdn|fburl|connect\.facebook|hotjar|segment\.|mixpanel|\/embed\.js/, r => r.abort().catch(()=>{}));
- if (opts.gridTimeout) await page.addInitScript(t => { window.__uxGridTimeout = t; }, opts.gridTimeout);
- if (opts.absent) await page.route(/ux-primitives\.bundle\.(js|css)(\?|$)/, r => r.fulfill({ status:200, contentType: r.request().url().endsWith('.css')?'text/css':'text/javascript', body:'' }));
- if (opts.api) await page.route(/\/api\/products(\?|$)/, async r => {
- if (opts.api.delay) await new Promise(z=>setTimeout(z, opts.api.delay));
- if (opts.api.mode === 'continue') return r.continue();
- if (opts.api.mode === 'zero') return r.fulfill({ status:200, contentType:'application/json', body: mock0(shape) });
- if (opts.api.mode === 'mock24') return r.fulfill({ status:200, contentType:'application/json', body: mock24(shape) });
- return r.continue();
- });
- page.on('console', m => { if (m.type()==='error') errs.push(m.text()); });
- page.on('pageerror', e => perrs.push(String(e)));
const rec = {};
+ let browser;
try {
+ // Browser/context/page setup lives INSIDE the try so a per-engine launch failure is caught, the finally
+ // still closes the browser, and one bad engine can't abort the whole run (which would leak the servers).
+ browser = await engineLauncher.launch();
+ const ctx = await browser.newContext();
+ const page = await ctx.newPage();
+ // Abort third-party ad/tracking noise (404/502 under localhost) that otherwise delays the deferred
+ // bundle so far that products load first and uxInit misreads false. Isolates the bundle fairly.
+ await page.route(/ads\.agentabrams|googletagmanager|google-analytics|doubleclick|facebook\.|fbcdn|fburl|connect\.facebook|hotjar|segment\.|mixpanel|\/embed\.js/, r => r.abort().catch(()=>{}));
+ if (opts.gridTimeout) await page.addInitScript(t => { window.__uxGridTimeout = t; }, opts.gridTimeout);
+ if (opts.absent) await page.route(/ux-primitives\.bundle\.(js|css)(\?|$)/, r => r.fulfill({ status:200, contentType: r.request().url().endsWith('.css')?'text/css':'text/javascript', body:'' }));
+ if (opts.api) await page.route(/\/api\/products(\?|$)/, async r => {
+ if (opts.api.delay) await new Promise(z=>setTimeout(z, opts.api.delay));
+ if (opts.api.mode === 'continue') return r.continue();
+ if (opts.api.mode === 'zero') return r.fulfill({ status:200, contentType:'application/json', body: mock0(shape) });
+ if (opts.api.mode === 'mock24') return r.fulfill({ status:200, contentType:'application/json', body: mock24(shape) });
+ return r.continue();
+ });
+ page.on('console', m => { if (m.type()==='error') errs.push(m.text()); });
+ page.on('pageerror', e => perrs.push(String(e)));
await page.setViewportSize({ width:1280, height:800 });
await page.goto(url, { waitUntil:'domcontentloaded', timeout:20000 });
if (opts.sampleDuringMs) { await page.waitForTimeout(opts.sampleDuringMs); rec.during = await page.evaluate(measure); }
@@ -112,14 +127,15 @@ async function run(engineLauncher, url, shape, opts) {
}
rec.ux = await page.evaluate(() => typeof window.UXPrimitives);
} catch(e){ rec.error = String(e); }
+ finally { try { if (browser) await browser.close(); } catch {} }
rec.jsErrors = errs; rec.pageErrors = perrs;
- await browser.close();
return rec;
}
const results = [];
-const teardown = (h) => { try { if (h?.kill) h.kill(); else if (h?.close) h.close(); } catch {} };
+const teardown = (h) => { const i = handles.indexOf(h); if (i >= 0) handles.splice(i, 1); try { if (h?.kill) h.kill(); else if (h?.close) h.close(); } catch {} };
for (const p of PILOTS) {
+ if (await portInUse(p.port)) { results.push({ site:p.site, bootError:`port ${p.port} already in use before start (orphan? refusing to attach)` }); continue; }
let handle;
try { if (p.mode==='server'){ handle=startServer(p); await waitPort(p.port); } else handle=await startStatic(p); }
catch(e){ teardown(handle); results.push({site:p.site, bootError:String(e)}); continue; }
← 56486bd proof(live-ux-canary): gate marker against the bundle hash c
·
back to Agent Cabinet
·
auto-data-snapshot: 2026-09-23T15:21:10 (1 data files) — fro 715ce58 →