← back to Dead Agentabrams
verification-reports/tk11100-localhead/run-localhead.js
235 lines
#!/usr/bin/env node
/*
* TK-11100 — Local HEAD delta lane
* Proves room/index.html from git commit 5bb689d (blob 6aaa35e, 17-show catalog)
* served from a LOOPBACK-ONLY (127.0.0.1) ephemeral static server, exercised in
* Playwright Chromium + WebKit at desktop + mobile.
*
* Owns only: verification/tk11100-localhead/** (no source edits, no deploy).
* Self-contained: starts + STOPS its own server (cleanup proof in results).
*/
const http = require('http');
const fs = require('fs');
const path = require('path');
const PW = '/Users/macstudio3/.npm-global/lib/node_modules/playwright';
const { chromium, webkit, devices } = require(PW);
const ROOT = __dirname; // verification/tk11100-localhead
const SRV = path.join(ROOT, 'srv'); // serving root (contains /room/index.html)
const SHOTS = path.join(ROOT, 'shots');
const RESULTS = path.join(ROOT, 'results');
for (const d of [SHOTS, RESULTS]) fs.mkdirSync(d, { recursive: true });
const TARGET_COMMIT = '5bb689d1d98eff41c32ea4cc6e51fed608d25f6a';
const TARGET_BLOB = '6aaa35ea775e2a338cb4b04eec5b0016f5c0d213';
const MIME = { '.html':'text/html', '.png':'image/png', '.js':'text/javascript',
'.css':'text/css', '.svg':'image/svg+xml', '.json':'application/json' };
function startServer() {
return new Promise((resolve) => {
const server = http.createServer((req, res) => {
let p = decodeURIComponent(req.url.split('?')[0]);
if (p.endsWith('/')) p += 'index.html';
const fp = path.join(SRV, p);
if (!fp.startsWith(SRV)) { res.writeHead(403); return res.end('forbidden'); }
fs.readFile(fp, (err, buf) => {
if (err) { res.writeHead(404); return res.end('not found'); }
res.writeHead(200, { 'Content-Type': MIME[path.extname(fp)] || 'application/octet-stream' });
res.end(buf);
});
});
// LOOPBACK ONLY: bind explicitly to 127.0.0.1, ephemeral port 0
server.listen(0, '127.0.0.1', () => resolve(server));
});
}
// In-page inventory probe — reads the room's live JS globals + DOM controls.
const PROBE = () => {
// indirect eval reaches top-level lexical `const` globals (CATALOG/DEFAULT_SHOW)
const gEval = (expr) => { try { return (0, eval)(expr); } catch { return undefined; } };
const qa = (s) => Array.from(document.querySelectorAll(s));
const el = (id) => document.getElementById(id);
const catalogLen = gEval("typeof CATALOG!=='undefined' && Array.isArray(CATALOG) ? CATALOG.length : null");
const defaultShow = gEval("typeof DEFAULT_SHOW!=='undefined' ? DEFAULT_SHOW : null");
const firstShow = gEval("typeof CATALOG!=='undefined'&&CATALOG[0] ? (CATALOG[0].date||CATALOG[0].id) : null");
const lastShow = gEval("typeof CATALOG!=='undefined'&&CATALOG.length ? (CATALOG[CATALOG.length-1].date||CATALOG[CATALOG.length-1].id) : null");
// #mrShow <select> is populated one <option> per CATALOG entry -> runtime proof of catalog size.
// (CATALOG/DEFAULT_SHOW are IIFE-scoped in the source, so read them via the rendered DOM.)
const showSelect = el('mrShow');
const showOptionCount = showSelect && showSelect.tagName === 'SELECT' ? showSelect.options.length : 0;
const selectedShowIndex = showSelect ? showSelect.value : null; // loadShow(DEFAULT_SHOW) sets this
const firstOptText = showSelect && showSelect.options[0] ? showSelect.options[0].textContent.trim() : null;
const lastOptText = showSelect && showSelect.options.length ? showSelect.options[showSelect.options.length-1].textContent.trim() : null;
// source selector: #mrSrc renders one <button data-src> per MR_SOURCES key (gd / dc)
const srcButtons = qa('#mrSrc button[data-src]').map(b => ({
src: b.dataset.src, label: (b.textContent || '').trim().replace(/\s+/g, ' ')
}));
const srcActive = qa('#mrSrc button[data-src].on, #mrSrc button[aria-selected="true"]').map(b => b.dataset.src);
const MR = gEval('window.MR_SOURCES');
const mrSourcesLabels = MR && typeof MR === 'object' ? Object.values(MR).map(v => (v && v.label) || '').filter(Boolean) : [];
// year-tile grid: #mrMap holds one <button class="mr-yr"> per year (GD 1965-1995 => 31)
const yearTiles = qa('#mrMap .mr-yr');
const yearTileYears = yearTiles.map(b => b.dataset.y);
const mapErr = !!document.querySelector('#mrMap .mr-map-err');
// dock pills: #dock .btn — equal-height via flex align-items:stretch
const pills = qa('#dock .btn');
const pillHeights = pills.map(b => Math.round(b.getBoundingClientRect().height));
const nonZero = pillHeights.filter(h => h > 0);
const dockPillEqualHeight = nonZero.length > 1 ? (Math.max(...nonZero) - Math.min(...nonZero) <= 1) : null;
// "lit / active" pills (aria-pressed=true => the gold-wash active state)
const dockActive = qa('#dock .btn[aria-pressed="true"]').map(b => b.id);
// range controls
const rng = (id) => { const e = el(id); return e && e.tagName === 'INPUT' && e.type === 'range'
? { present: true, value: e.value, valuetext: e.getAttribute('aria-valuetext') } : { present: false }; };
const ids = ['musicroom','scene','dock','lantern','tempo','reduceBtn','trailBtn','reactBtn',
'pteroBtn','venueBtn','restartBtn','playBtn','mrPlay','mrPrev','mrNext','mrSeek','mrSearch',
'mrSrc','mrShow','mrYear','mrTracks','mrToggle','labelBtn','crowd','animals','bears'];
const present = {}; ids.forEach(i => present[i] = !!el(i));
return {
catalogLen, defaultShow, firstShow, lastShow,
showOptionCount, selectedShowIndex, firstOptText, lastOptText,
srcButtons, srcActive, mrSourcesLabels,
yearTileCount: yearTiles.length, yearTileYears, mapErr,
dockPillCount: pills.length, dockPillHeights: pillHeights, dockPillEqualHeight, dockActive,
tempo: rng('tempo'), lantern: rng('lantern'), crowd: rng('crowd'),
animals: rng('animals'), bears: rng('bears'),
reduceBtnPressed: el('reduceBtn') ? el('reduceBtn').getAttribute('aria-pressed') : null,
controlsPresent: present,
docTitle: document.title,
threePresent: gEval("typeof THREE!=='undefined' || typeof window.THREE!=='undefined'") === true,
};
};
const PROFILES = [
{ name: 'desktop', opts: { viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 } },
{ name: 'mobile', opts: { ...devices['iPhone 13'] } },
];
const ENGINES = [ { name: 'chromium', launcher: chromium }, { name: 'webkit', launcher: webkit } ];
(async () => {
const started = new Date().toISOString();
const server = await startServer();
const { port } = server.address();
const base = `http://127.0.0.1:${port}`;
const boundAddr = `${server.address().address}:${port}`;
console.log(`[server] loopback-only serving ${SRV} at ${base} (bound ${boundAddr})`);
const runs = [];
for (const eng of ENGINES) {
let browser;
try { browser = await eng.launcher.launch({ headless: true }); }
catch (e) { runs.push({ engine: eng.name, profile: '*', status: 'SKIP', reason: 'launch failed: '+e.message }); continue; }
for (const prof of PROFILES) {
const combo = `${eng.name}-${prof.name}`;
const rec = { engine: eng.name, profile: prof.name, combo, url: base + '/room/', consoleErrors: [], pageErrors: [] };
let ctx, page;
try {
ctx = await browser.newContext(prof.opts);
// Harness sandboxing (NOT source edits): supply the external deps the page
// requests so a loopback/offline box can exercise the feature deterministically.
// - three.js CDN -> local r128 fixture (scene renders; tempo affects it)
// - archive.org advancedsearch -> deterministic per-year count stub (year grid heat-colors)
const threeJs = fs.readFileSync(path.join(ROOT, 'fixtures', 'three.min.js'));
await ctx.route(/three@0\.128\.0/, r => r.fulfill({ status: 200, contentType: 'text/javascript', body: threeJs }));
await ctx.route(/archive\.org\/advancedsearch/, r => {
const u = new URL(r.request().url());
const q = u.searchParams.get('q') || '';
const ym = q.match(/year:(\d{4})/); const year = ym ? +ym[1] : 1977;
// plausible deterministic count so tiles persist + heat-color (77/89 spike)
const n = [1977, 1989].includes(year) ? 90 : 20 + (year % 40);
r.fulfill({ status: 200, contentType: 'application/json',
body: JSON.stringify({ response: { numFound: n, docs: [] } }) });
});
page = await ctx.newPage();
page.on('console', m => { if (m.type() === 'error') rec.consoleErrors.push(m.text().slice(0,300)); });
page.on('pageerror', e => rec.pageErrors.push(String(e).slice(0,300)));
const resp = await page.goto(base + '/room/', { waitUntil: 'domcontentloaded', timeout: 20000 });
rec.httpStatus = resp ? resp.status() : null;
// let the app initialize + the stubbed year-count promises settle
await page.waitForTimeout(3500);
// exercise the "slowed motion" tempo control: drive it and confirm the label reacts
try {
const before = await page.$eval('#tempoV', e => e.textContent).catch(()=>null);
await page.$eval('#tempo', e => { e.value = '40'; e.dispatchEvent(new Event('input', {bubbles:true})); });
await page.waitForTimeout(200);
const after = await page.$eval('#tempoV', e => e.textContent).catch(()=>null);
rec.tempoControl = { before, after, reacts: before !== null && after !== null && before !== after };
} catch (e) { rec.tempoControl = { reacts: false, err: String(e).slice(0,120) }; }
// Reveal the control chrome (room boots in immersive hide-chrome mode) so the dock lays out
try {
const wasHidden = await page.$eval('#stage', s => s.classList.contains('hide-chrome')).catch(()=>null);
await page.click('#chromeBtn', { timeout: 3000 }).catch(()=>{});
await page.waitForTimeout(400);
const nowHidden = await page.$eval('#stage', s => s.classList.contains('hide-chrome')).catch(()=>null);
rec.chromeToggle = { wasHidden, nowHidden, revealed: wasHidden === true && nowHidden === false };
} catch (e) { rec.chromeToggle = { revealed: false, err: String(e).slice(0,120) }; }
rec.inventory = await page.evaluate(PROBE);
const shot = path.join(SHOTS, `${combo}.png`);
await page.screenshot({ path: shot, fullPage: false });
rec.screenshot = path.relative(ROOT, shot);
// Per-combo acceptance checks
const inv = rec.inventory;
const srcBlob = JSON.stringify(inv.srcButtons) + JSON.stringify(inv.mrSourcesLabels);
rec.checks = {
catalog17: inv.showOptionCount === 17, // runtime: #mrShow options == CATALOG.length
defaultShow7: inv.selectedShowIndex === '7', // loadShow(DEFAULT_SHOW) => #mrShow.value
sourceSelector_GD_and_DeadCo:
/grateful dead/i.test(srcBlob) && /dead\s*&?\s*(and\s*)?compan/i.test(srcBlob) &&
inv.srcButtons.some(b => b.src === 'gd') && inv.srcButtons.some(b => b.src === 'dc'),
yearGridPresent: inv.yearTileCount >= 30 && !inv.mapErr,
dockEqualHeightPills: inv.dockPillCount >= 5 && inv.dockPillEqualHeight === true,
activeLanternState: inv.lantern.present === true && inv.dockActive.length >= 1,
slowedMotionControls: inv.tempo.present === true && inv.controlsPresent.reduceBtn === true
&& (rec.tempoControl ? rec.tempoControl.reacts === true : false),
musicRoomControls: !!(inv.controlsPresent.mrPlay && inv.controlsPresent.mrPrev
&& inv.controlsPresent.mrNext && inv.controlsPresent.mrSeek && inv.controlsPresent.mrSearch),
};
rec.status = Object.values(rec.checks).every(Boolean) ? 'PASS' : 'FAIL';
} catch (e) {
rec.status = 'FAIL'; rec.error = String(e).slice(0, 400);
} finally {
if (page) await page.close().catch(()=>{});
if (ctx) await ctx.close().catch(()=>{});
}
console.log(`[${combo}] ${rec.status} catalog=${rec.inventory?rec.inventory.catalogLen:'?'} yearTiles=${rec.inventory?rec.inventory.yearTileCount:'?'}`);
runs.push(rec);
}
await browser.close().catch(()=>{});
}
// Cleanup: STOP the temporary server
await new Promise(r => server.close(r));
const serverStopped = !server.listening;
console.log(`[server] stopped=${serverStopped}`);
// git blob hash of the served room file (proves it is byte-identical to 5bb689d:room/index.html)
const cp = require('child_process');
let servedBlob = null;
try { servedBlob = cp.execSync(`git hash-object "${path.join(SRV,'room','index.html')}"`,
{ cwd: ROOT }).toString().trim(); } catch {}
const summary = {
ticket: 'TK-11100', lane: 'localhead', owner: 'iterm-tk11100-localhead',
targetCommit: TARGET_COMMIT, targetBlob: TARGET_BLOB,
servedBlob, servedBlobMatchesTarget: servedBlob === TARGET_BLOB,
serverBoundAddr: boundAddr, loopbackOnly: boundAddr.startsWith('127.0.0.1'),
startedAt: started, finishedAt: new Date().toISOString(),
serverStopped,
combos: runs.map(r => ({ combo: r.combo, status: r.status, checks: r.checks || null })),
overall: runs.every(r => r.status === 'PASS') ? 'PASS' : (runs.some(r=>r.status==='PASS')?'PARTIAL':'FAIL'),
runs,
};
fs.writeFileSync(path.join(RESULTS, 'localhead-results.json'), JSON.stringify(summary, null, 2));
console.log('OVERALL:', summary.overall);
process.exit(0);
})().catch(e => { console.error('FATAL', e); process.exit(1); });