#!/usr/bin/env node /** * cli:uat-ui — Execute the plan's UI axis in a real Chromium: login through the * login page per role, REAL clicks through the sidebar and the list rows, access * verdict asserted per page, write journeys (create → edit → delete) on * UAT-marked rows, display timings + page weight + console/network errors * measured, screenshots per policy. Results land in `runs/{runId}/ui-results.json`. * * Findings are the PRODUCT (exit 0 with allPassed:false); infrastructure errors * (missing plan/credentials, Playwright not installed, login failures) exit 1. */ import { parseArgs } from 'node:util'; import { executeEnvelope, failExecute, printEnvelope } from '../../../lib/output.js'; import { buildJourneys } from './walker.js'; import { validate } from './validate.js'; import { executeUiRun } from './execute.js'; import { PlaywrightDriver, PlaywrightMissingError } from './playwright-driver.js'; const COMMAND = 'uat-ui'; async function main(): Promise { let values: { spec?: string; dry_run?: boolean }; try { values = parseArgs({ options: { spec: { type: 'string' }, dry_run: { type: 'boolean', default: false }, }, strict: true, }).values; } catch (e) { printEnvelope(failExecute(COMMAND, [`Invalid arguments: ${(e as Error).message}`])); process.exit(1); return; } if (!values.spec) { printEnvelope(failExecute(COMMAND, ['--spec is required'])); process.exit(1); } let raw: unknown; try { raw = JSON.parse(values.spec); } catch { printEnvelope(failExecute(COMMAND, ['Invalid JSON in --spec'])); process.exit(1); } const v = await validate(raw); if (!v.valid || !v.context) { printEnvelope(failExecute(COMMAND, v.errors)); process.exit(1); return; } const ctx = v.context; if (values.dry_run || ctx.spec.dryRun) { const journeys = buildJourneys(ctx.plan, { roles: ctx.roles, writes: ctx.spec.writes, runTag: 'dryrun', maxSteps: ctx.plan.execution.caps.actions_per_role, }); printEnvelope( executeEnvelope(COMMAND, { data: { dryRun: true, frontendUrl: ctx.frontendUrl, plan: ctx.planRelPath, runId: ctx.runId, journeys: journeys.map((j) => ({ role: j.role, steps: j.steps.length, pages: j.steps.filter((s) => s.kind === 'page').length, writeFlows: j.steps.filter((s) => s.kind !== 'page').length, truncated: j.truncated, })), }, warnings: v.warnings, }), ); process.exit(0); } const driver = new PlaywrightDriver({ frontendUrl: ctx.frontendUrl, headless: ctx.spec.headless, slowMo: ctx.spec.slowMo, readinessTimeoutMs: ctx.readinessTimeoutMs, retryOnNotReady: ctx.retryOnNotReady, }); let outcome; try { outcome = await executeUiRun(ctx, { driver }); } catch (e) { if (e instanceof PlaywrightMissingError) { printEnvelope(failExecute(COMMAND, [e.message])); process.exit(1); return; } throw e; } printEnvelope( executeEnvelope(COMMAND, { success: outcome.success, data: { frontendUrl: ctx.frontendUrl, plan: ctx.planRelPath, runId: ctx.runId, resultsFile: outcome.resultsFileRel, allPassed: outcome.allPassed, ...outcome.aggregate, }, report: { results: outcome.results } as unknown as Record, errors: outcome.errors, warnings: [...v.warnings, ...outcome.warnings], nextSteps: [ outcome.allPassed ? 'UI axis green.' : `${outcome.aggregate.failed} UI assertion(s) failed — inspect ${outcome.resultsFileRel}.`, 'Generate the HTML report with `/uat report`.', ], }), ); process.exit(outcome.success ? 0 : 1); } void main();