#!/usr/bin/env node /** * QA tool UI — a small local web page over the Phase-1 `qa` commands. * * Pick a scenario card, see the grounded generation prompt (copy it into Claude * Code), run the card's tests, and view the evidence inline. It wraps the exact * same functions the CLI uses (scripts/qa-tool.mjs), so there is one engine. * * node scripts/qa-ui.mjs http://localhost:9500 * node scripts/qa-ui.mjs --port 8200 --no-open * * Local dev tool: binds localhost only. The /run endpoint executes the test * runner, so do not expose this port beyond your machine. */ import { createServer } from 'node:http'; import { createReadStream, existsSync, statSync } from 'node:fs'; import { spawn } from 'node:child_process'; import { extname, join, normalize } from 'node:path'; import { pathToFileURL } from 'node:url'; import { ROOT, buildPrompt, layerFor, listCards, readCard, writeIndex } from './qa-tool.mjs'; const args = process.argv.slice(2); const PORT = args.includes('--port') ? Number(args[args.indexOf('--port') + 1]) : 9500; const shouldOpen = !args.includes('--no-open'); const EVIDENCE_DIR = join(ROOT, 'e2e', 'evidence-report'); const TYPES = { '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': 'text/javascript', '.json': 'application/json', '.png': 'image/png', '.md': 'text/markdown; charset=utf-8', }; function sendJson(res, code, body) { const s = JSON.stringify(body); res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(s) }); res.end(s); } // Run run-tests.ps1 for a card, buffering output; resolve when it exits. function runCard(id) { const card = readCard(id); if (!card) return Promise.resolve({ ok: false, output: `No card "${id}".` }); const layer = layerFor(card); const grep = card.title.split(/\s+/).slice(0, 4).join(' '); return new Promise((resolve) => { const ps = spawn( 'powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', 'run-tests.ps1', '-Layer', layer, '-Grep', grep], { cwd: ROOT }, ); let out = ''; ps.stdout.on('data', (d) => (out += d)); ps.stderr.on('data', (d) => (out += d)); ps.on('close', (code) => resolve({ ok: code === 0, exitCode: code, layer, grep, output: out })); ps.on('error', (e) => resolve({ ok: false, output: String(e) })); }); } const server = createServer(async (req, res) => { const url = new URL(req.url, `http://localhost:${PORT}`); const path = url.pathname; try { if (path === '/') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); return res.end(PAGE); } if (path === '/qa/cards') return sendJson(res, 200, listCards()); if (path === '/qa/index') { const m = writeIndex(); return sendJson(res, 200, { ok: true, counts: { routes: m.routes.length, api: m.api.length, pageObjects: m.pageObjects.length, accounts: m.auth.accounts.length } }); } const pm = path.match(/^\/qa\/prompt\/([\w-]+)$/); if (pm) { try { return sendJson(res, 200, { prompt: buildPrompt(pm[1]) }); } catch (e) { return sendJson(res, 404, { error: String(e.message || e) }); } } const rm = path.match(/^\/qa\/run\/([\w-]+)$/); if (rm && req.method === 'POST') return sendJson(res, 200, await runCard(rm[1])); // Serve the evidence report for the inline iframe. if (path.startsWith('/evidence/')) { const target = join(EVIDENCE_DIR, normalize(path.slice('/evidence/'.length)) || 'index.html'); if (!target.startsWith(EVIDENCE_DIR) || !existsSync(target) || statSync(target).isDirectory()) { res.writeHead(404); return res.end('not found'); } res.writeHead(200, { 'Content-Type': TYPES[extname(target).toLowerCase()] || 'application/octet-stream' }); return createReadStream(target).pipe(res); } res.writeHead(404); res.end('not found'); } catch (e) { sendJson(res, 500, { error: String(e.message || e) }); } }); server.on('error', (e) => { if (e.code === 'EADDRINUSE') { console.error(`Port ${PORT} in use. Try: node scripts/qa-ui.mjs --port ${PORT + 1}`); process.exit(1); } throw e; }); server.listen(PORT, () => { const url = `http://localhost:${PORT}/`; console.log(`\n QA tool UI: ${url}\n Ctrl+C to stop.\n`); if (shouldOpen) { const [c, a] = process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] : process.platform === 'darwin' ? ['open', [url]] : ['xdg-open', [url]]; spawn(c, a, { detached: true, stdio: 'ignore' }).unref(); } }); // --- the page (self-contained, no external resources) ----------------------- const PAGE = ` TravelHub QA tool

TravelHub QA tool

scenario → grounded prompt → run → evidence
Select a scenario card on the left.
`;