← back to Agent Cabinet
front-page-audit/proof/prove-activation.mjs
196 lines
import { createRequire } from 'module';
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 net from 'net'; import fs from 'fs'; import path from 'path'; import os from 'os';
import { spawn } from 'child_process';
const HOME = os.homedir();
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' },
{ site: 'jutewallpaper', mode: 'server', port: 9843, shape: 'items' },
{ site: 'corkwallcovering', mode: 'server', port: 9844, shape: 'items' },
{ site: 'flockedwallpaper', mode: 'static', port: 9845, shape: 'array' },
{ site: '1800swallpaper', mode: 'server', port: 9846, shape: 'items', forceMock: true, rep: true },
];
// SITE=silkwallpaper,flockedwallpaper narrows the run to a subset (folded in from the former
// prove-activation-one.mjs so there is exactly ONE copy of this gate — no drift between two files).
const __only = (process.env.SITE || '').split(',').map(s => s.trim()).filter(Boolean);
const PILOTS = __only.length ? PILOTS_ALL.filter(p => __only.includes(p.site)) : PILOTS_ALL;
const BORROW_NM = path.join(HOME, 'Projects/silkwallpaper/node_modules');
const MIME = { '.html':'text/html','.js':'text/javascript','.css':'text/css','.json':'application/json','.svg':'image/svg+xml','.png':'image/png','.jpg':'image/jpeg','.webp':'image/webp','.ico':'image/x-icon','.woff2':'font/woff2' };
const mock24 = (shape) => {
const items = Array.from({ length: 24 }, (_, i) => ({
sku: 'MOCK-' + i, handle: 'mock-' + i, title: 'Mock Product ' + i, vendor: 'DW',
image: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==',
image_url: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==',
primaryColor: 'Blue', productUrl: '#', price: '425'
}));
return shape === 'array' ? JSON.stringify(items) : JSON.stringify({ total: 24, page: 1, limit: 24, pages: 1, sort: 'newest', items });
};
const mock0 = (shape) => shape === 'array' ? '[]' : JSON.stringify({ total: 0, page: 1, limit: 24, pages: 0, sort: 'newest', items: [] });
function waitPort(port, ms = 12000) {
const t0 = Date.now();
return new Promise((res, rej) => { (function poll(){ const q=http.get({host:'127.0.0.1',port,path:'/',timeout:1500},r=>{r.destroy();res();}); q.on('error',()=>Date.now()-t0>ms?rej(new Error('timeout')):setTimeout(poll,300)); q.on('timeout',()=>{q.destroy();Date.now()-t0>ms?rej(new Error('timeout')):setTimeout(poll,300);}); })(); });
}
function startServer(p) {
const cwd = path.join(HOME, 'Projects', p.site);
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) {
const root = path.join(HOME, 'Projects', p.site);
const srv = http.createServer((req, rq) => {
let u = decodeURIComponent(req.url.split('?')[0]); if (u === '/' || u.endsWith('/')) u += 'index.html';
const fp = path.join(root, u);
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);
});
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); }
const GRID_SEL = '#grid, #productGrid';
const measure = () => {
const g = document.querySelector('#grid') || document.querySelector('#productGrid');
if (!g) return { found:false };
const skel = g.querySelectorAll('[data-ux-skel-holder], [data-ux-skel], .ux-skel').length;
const empty = g.querySelectorAll('[data-ux-empty-holder]').length;
const products = [...g.children].filter(c => c.nodeType===1 && !c.matches('[data-ux-skel-holder],[data-ux-skel],[data-ux-empty-holder]') && !c.classList.contains('ux-skel')).length;
return { found:true, skel, empty, products, selects: document.querySelectorAll('select').length, ranges: document.querySelectorAll('input[type=range]').length };
};
async function run(engineLauncher, url, shape, opts) {
const errs=[], perrs=[];
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); }
await page.waitForTimeout(opts.settleMs || 2500);
// Slow-load cases: don't measure on a fixed clock the DEFERRED bundle can outrun. goto resolves at
// domcontentloaded (before the deferred bundle runs), so the fetch start is offset from t0 by an
// unbounded amount under load. Wait (bounded) for products to actually render so a late-but-successful
// load isn't misread as a strand; on timeout we still measure (a genuine strand stays FAIL).
if (opts.awaitProductsMs) await page.waitForFunction(() => {
const g = document.querySelector('#grid') || document.querySelector('#productGrid');
if (!g) return false;
return [...g.children].some(c => c.nodeType===1 && !c.matches('[data-ux-skel-holder],[data-ux-skel],[data-ux-empty-holder]') && !c.classList.contains('ux-skel'));
}, { timeout: opts.awaitProductsMs, polling: 100 }).catch(()=>{});
rec.after = await page.evaluate(measure);
if (opts.doSort) {
await page.evaluate(() => { const s=document.querySelector('select'); if(s&&s.options.length>1){ s.selectedIndex=Math.min(1,s.options.length-1); s.dispatchEvent(new Event('change',{bubbles:true})); } const rg=document.querySelector('input[type=range]'); if(rg){ rg.dispatchEvent(new Event('input',{bubbles:true})); } });
await page.waitForTimeout(1500);
rec.afterSort = await page.evaluate(measure);
}
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;
return rec;
}
const results = [];
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; }
try {
const url = `http://127.0.0.1:${p.port}/`;
const apiLoad = p.forceMock ? { mode:'mock24' } : (p.mode==='server' ? { mode:'continue' } : { mode:'mock24' }); // mock for flocked + representative sites (isolate bundle from site data pipeline)
const rec = { site:p.site, mode:p.mode, engines:{} };
for (const [en, la] of [['chromium',chromium],['webkit',webkit]]) {
// error baseline (bundle absent), 2 normal runs
const absent = [ await run(la,url,p.shape,{absent:true, api:apiLoad}), await run(la,url,p.shape,{absent:true, api:apiLoad}) ];
const baseSet = new Set(absent.flatMap(r=>r.jsErrors.map(norm)));
// present normal + sort re-render
const normal = await run(la,url,p.shape,{ api:apiLoad, doSort:true });
// present loading (delay API to make skeleton visible), sample during
const loading = await run(la,url,p.shape,{ api:{ ...apiLoad, delay:1500 }, sampleDuringMs:600, settleMs:3500 });
// present true-zero empty (short grid timeout + zero API)
const emptyRun = await run(la,url,p.shape,{ api:{ mode:'zero' }, gridTimeout:1200, settleMs:2600 });
// slow-but-NONEMPTY load resolving PAST the 8s skel timeout — empty-state must NOT strand (Cody v1.1.1 fix)
const slowRun = await run(la,url,p.shape,{ api:{ mode:'mock24', delay:10000 }, settleMs:3000, awaitProductsMs:14000 });
const presentErrs = [normal, loading, emptyRun, slowRun].flatMap(r=>r.jsErrors.map(norm));
const newErrs = [...new Set(presentErrs)].filter(x=>!baseSet.has(x));
const BUNDLE_RX=/ux-primitives|UXPrimitives|UXGrid|\bToast\b|\bBento\b|ModalRig|Skeleton|EmptyState|CommandPalette/;
const bundleNew = newErrs.filter(x=>BUNDLE_RX.test(x));
// Page errors: isolate to the bundle the same way console errors are — subtract the bundle-absent
// baseline, then keep only bundle-attributable ones, so a site's OWN uncaught exception can't red the
// activation gate under a criterion (noBundlePageErr) whose name asserts the bundle caused it.
const basePageSet = new Set(absent.flatMap(r=>r.pageErrors.map(norm)));
const newPageErrs = [...new Set([normal,loading,emptyRun,slowRun].flatMap(r=>r.pageErrors.map(norm)))].filter(x=>!basePageSet.has(x));
const bundlePageErr = newPageErrs.filter(x=>BUNDLE_RX.test(x));
const anyPageErr = bundlePageErr.length>0;
const appearThenClear = (loading.during?.skel>0) && (loading.after?.skel===0) && (loading.after?.products>0);
const productsRender = normal.after?.products>0;
const sortNoGhost = normal.afterSort ? (normal.afterSort.skel===0 && normal.afterSort.empty===0 && normal.afterSort.products>0) : false;
const trueZeroEmpty = (emptyRun.after?.empty===1) && (emptyRun.after?.skel===0) && (emptyRun.after?.products===0);
const noEmptyWhenProducts = normal.after?.empty===0;
const slowLoadNoStrand = (slowRun.after?.empty===0) && (slowRun.after?.products>0) && (slowRun.after?.skel===0);
const controls = normal.after?.selects>=1 && normal.after?.ranges>=1;
const pass = appearThenClear && productsRender && sortNoGhost && trueZeroEmpty && noEmptyWhenProducts && slowLoadNoStrand && controls && !anyPageErr && bundleNew.length===0 && normal.ux==='object';
rec.engines[en] = { pass, criteria:{ appearThenClear, productsRender, sortNoGhost, trueZeroEmpty, noEmptyWhenProducts, slowLoadNoStrand, controls, noBundlePageErr:!anyPageErr, noNewBundleErr:bundleNew.length===0, uxInit:normal.ux==='object' },
numbers: { during:loading.during, loadingAfter:loading.after, normalAfter:normal.after, afterSort:normal.afterSort, emptyAfter:emptyRun.after, slowAfter:slowRun.after },
newBundleErrors: bundleNew, newBundlePageErrors: bundlePageErr, flakySiteNoise: newErrs.filter(x=>!BUNDLE_RX.test(x)),
absentErrRuns: absent.map(r=>r.jsErrors.length), presentPageErr: [normal,loading,emptyRun,slowRun].map(r=>r.pageErrors.length) };
}
results.push(rec);
const e = rec.engines;
console.log(`${p.site}: ch.pass=${e.chromium?.pass} wk.pass=${e.webkit?.pass} | during.skel=${e.chromium?.numbers.during?.skel} after.skel=${e.chromium?.numbers.loadingAfter?.skel} products=${e.chromium?.numbers.normalAfter?.products} afterSort.skel=${e.chromium?.numbers.afterSort?.skel} emptyAfter=${JSON.stringify(e.chromium?.numbers.emptyAfter)} bundleErr=${JSON.stringify(e.chromium?.newBundleErrors)}`);
} finally { teardown(handle); } // always tear the child down — no orphaned server / stuck port on any path
}
const outfile = path.join(EVID, `pilot-activation-${ISO}.json`);
fs.writeFileSync(outfile, JSON.stringify(results,null,2));
const allPass = results.every(r=>r.engines && Object.values(r.engines).every(x=>x.pass));
console.log('\n=== ACTIVATION SUMMARY: ' + results.filter(r=>r.engines&&Object.values(r.engines).every(x=>x.pass)).length + '/' + results.length + ' sites all-engine PASS ===');
console.log('artifact:', outfile);
process.exit(allPass?0:1);