← 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);