[object Object]

← 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

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 →