← back to Agent Cabinet

front-page-audit/proof/prove-negctl.mjs

166 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 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');
const SCREENS = path.join(HOME, 'Projects/agent-cabinet/front-page-audit/screens');
fs.mkdirSync(EVID, { recursive: true });

// site -> { mode: 'server'|'static', port, webroot, activation }
const PILOTS = [
  { site: 'silkwallpaper',    mode: 'server', port: 9841, webroot: 'public', activation: 'silent' },
  { site: 'linenwallpaper',   mode: 'server', port: 9842, webroot: 'public', activation: 'silent' },
  { site: 'jutewallpaper',    mode: 'server', port: 9843, webroot: 'public', activation: 'silent' },
  { site: 'corkwallcovering', mode: 'server', port: 9844, webroot: 'public', activation: 'silent' },
  { site: 'flockedwallpaper', mode: 'static', port: 9845, webroot: '.',      activation: 'silent' },
];
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','.jpeg':'image/jpeg','.webp':'image/webp','.ico':'image/x-icon','.woff2':'font/woff2','.woff':'font/woff' };

function waitPort(port, ms = 12000) {
  const t0 = Date.now();
  return new Promise((res, rej) => {
    (function poll() {
      const req = http.get({ host: '127.0.0.1', port, path: '/', timeout: 1500 }, r => { r.destroy(); res(true); });
      req.on('error', () => (Date.now() - t0 > ms) ? rej(new Error('port timeout ' + port)) : setTimeout(poll, 300));
      req.on('timeout', () => { req.destroy(); (Date.now() - t0 > ms) ? rej(new Error('port timeout ' + port)) : 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', 'pipe', 'pipe'] });
  const log = fs.createWriteStream(`/tmp/claude-501/uxbuild/srv-${p.site}.log`);
  child.stdout.pipe(log); child.stderr.pipe(log);
  return child;
}
function startStatic(p) {
  const root = path.join(HOME, 'Projects', p.site, p.webroot);
  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);
  });
  return new Promise(r => 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); }

async function runOnce(engineName, launcher, url, { absent }) {
  const browser = await launcher.launch();
  const errs = [], perrs = [];
  const ctx = await browser.newContext();
  const page = await ctx.newPage();
  if (absent) {
    // BUNDLE ABSENT: fulfill the bundle files with empty 200 so the page is
    // byte-identical HTML but no bundle code runs (true negative control).
    await page.route(/ux-primitives\.bundle\.(js|css)(\?|$)/, r => r.fulfill({ status: 200, contentType: r.request().url().endsWith('.css') ? 'text/css' : 'text/javascript', body: '' }));
  }
  // Mock /api/products to provide product data (both absent and present runs test against real data conditions)
  const mock24 = JSON.stringify([...Array(24).keys()].map(i => ({ sku: 'MOCK-' + i, handle: 'mock-' + i, title: 'Mock Product ' + i, vendor: 'DW', image: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', productUrl: '#', price: '425' })));
  await page.route(/\/api\/products(\?|$)/, r => r.fulfill({ status: 200, contentType: 'application/json', body: mock24 }));
  page.on('console', m => { if (m.type() === 'error') errs.push(m.text()); });
  page.on('pageerror', e => perrs.push(String(e)));
  const r = { engine: engineName, absent };
  try {
    await page.setViewportSize({ width: 1280, height: 800 });
    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });
    await page.waitForTimeout(2500); // let async /api/products resolve + render
    r.grid = await page.evaluate(() => {
      const g = document.querySelector('#grid, #productGrid');
      if (!g) return { found: false };
      return {
        found: true,
        products: g.querySelectorAll('a,.product-card,.card,[data-sku],article').length,
        skeletons: g.querySelectorAll('.ux-skel, [data-ux-skel]').length,
        selects: document.querySelectorAll('select').length,
        ranges: document.querySelectorAll('input[type=range]').length,
      };
    });
    if (!absent) {
      r.bundle = await page.evaluate(() => ({ UX: typeof window.UXPrimitives, Skeleton: typeof window.Skeleton }));
      // exercise sort + density; assert no throw + grid stays populated
      r.controls = await page.evaluate(async () => {
        const g = document.querySelector('#grid, #productGrid');
        const before = g ? g.querySelectorAll('a,.product-card,.card,[data-sku],article').length : 0;
        try {
          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.value = rg.max || rg.value; rg.dispatchEvent(new Event('input', { bubbles: true })); }
        } catch (e) { return { threw: e.message, before }; }
        await new Promise(z => setTimeout(z, 900));
        const after = g ? g.querySelectorAll('a,.product-card,.card,[data-sku],article').length : 0;
        return { threw: null, before, after };
      });
    }
    r.jsErrors = errs; r.pageErrors = perrs;
  } catch (e) { r.error = String(e); r.jsErrors = errs; r.pageErrors = perrs; }
  await browser.close();
  return r;
}

const results = [];
for (const p of PILOTS) {
  let handle;
  try {
    if (p.mode === 'server') { handle = startServer(p); await waitPort(p.port); }
    else { handle = await startStatic(p); }
  } catch (e) { results.push({ site: p.site, bootError: String(e) }); if (handle && handle.kill) handle.kill(); else if (handle && handle.close) handle.close(); continue; }
  const url = `http://127.0.0.1:${p.port}/`;
  const rec = { site: p.site, mode: p.mode, activation: p.activation, engines: {} };
  const BUNDLE_RX = /ux-primitives|UXPrimitives|\bToast\b|\bBento\b|ModalRig|Skeleton|EmptyState|CommandPalette|\bCopy\b/;
  for (const [en, la] of [['chromium', chromium], ['webkit', webkit]]) {
    // MULTI-RUN baselines to absorb flaky third-party site errors (chat widget, 502s):
    // baseline = union of normalized errors across 3 ABSENT runs; present = union of 2.
    const absentRuns = [await runOnce(en, la, url, { absent: true }), await runOnce(en, la, url, { absent: true }), await runOnce(en, la, url, { absent: true })];
    const presentRuns = [await runOnce(en, la, url, { absent: false }), await runOnce(en, la, url, { absent: false })];
    const baseSet = new Set(absentRuns.flatMap(r => r.jsErrors.map(norm)));
    const presentSet = [...new Set(presentRuns.flatMap(r => r.jsErrors.map(norm)))];
    const newErrs = presentSet.filter(x => !baseSet.has(x));      // present-only, after flaky baseline
    const bundleNew = newErrs.filter(x => BUNDLE_RX.test(x));     // the ONLY class that fails the bundle
    const flakyNoise = newErrs.filter(x => !BUNDLE_RX.test(x));   // reported, does NOT fail (site/env noise)
    const present = presentRuns[0], absent = absentRuns[0];
    const anyPageErr = presentRuns.some(r => r.pageErrors.length);
    // silent bundle: must NOT touch the grid (0 skeletons); real-server grids still populate.
    const gridOK = present.grid && present.grid.skeletons === 0 &&
      (p.mode === 'server' ? present.grid.products > 0 : present.grid.found);
    // no-wipe: bundle-present product count == bundle-absent (bundle changed nothing).
    const noWipe = p.mode !== 'server' ? true
      : (absent.grid && present.grid && present.grid.products === absent.grid.products);
    const controlsOK = present.controls ? (!present.controls.threw) : true;
    rec.engines[en] = {
      pass: !anyPageErr && bundleNew.length === 0 && gridOK && noWipe && controlsOK && present.bundle?.UX === 'object',
      criteria: { noBundlePageErr: !anyPageErr, noNewBundleErr: bundleNew.length === 0, gridOK, noWipe, controlsOK, uxInit: present.bundle?.UX === 'object' },
      newBundleErrors: bundleNew,
      flakySiteNoise: flakyNoise,
      present: { grid: present.grid, controls: present.controls, bundle: present.bundle, pageErrorRuns: presentRuns.map(r=>r.pageErrors.length), jsErrorRuns: presentRuns.map(r=>r.jsErrors.length) },
      absent: { grid: absent.grid, jsErrorRuns: absentRuns.map(r=>r.jsErrors.length) },
    };
  }
  results.push(rec);
  if (handle && handle.kill) handle.kill(); else if (handle && handle.close) handle.close();
  const e = rec.engines;
  console.log(`${p.site} [${p.activation}] chromium.pass=${e.chromium?.pass} webkit.pass=${e.webkit?.pass} | present.grid=${JSON.stringify(e.chromium?.present.grid)} newBundleErr(ch)=${JSON.stringify(e.chromium?.newBundleErrors)} flakyNoise(ch)=${e.chromium?.flakySiteNoise.length}`);
}
const outfile = path.join(EVID, `pilot-negctl-${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=== NEG-CTL 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);