#!/usr/bin/env node /** * cli:uat-api — Execute the plan's API axis: every endpoint × role, asserted * against the expected status, measured (duration ms + size bytes), written to * `runs/{runId}/api-results.json`. Findings are the PRODUCT: assertion failures * land in the report (exit 0); only infrastructure errors (unreadable plan, no * credentials, login failures) exit non-zero. */ import { parseArgs } from 'node:util'; import { executeEnvelope, failExecute, printEnvelope } from '../../../lib/output.js'; import { planApiCalls } from './plan-calls.js'; import { validate } from './validate.js'; import { executeApiRun } from './execute.js'; const COMMAND = 'uat-api'; 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 calls = planApiCalls(ctx.plan, { roles: ctx.spec.roles, includeWriteProbes: ctx.spec.includeWriteProbes }); printEnvelope( executeEnvelope(COMMAND, { data: { dryRun: true, apiUrl: ctx.apiUrl, plan: ctx.planRelPath, runId: ctx.runId, totalCalls: calls.length, executable: calls.filter((c) => c.execute).length, skipped: calls.filter((c) => !c.execute).length, byMode: { exact: calls.filter((c) => c.execute && c.mode === 'exact').length, authz_only: calls.filter((c) => c.execute && c.mode === 'authz_only').length, }, }, warnings: v.warnings, }), ); process.exit(0); } const outcome = await executeApiRun(ctx); printEnvelope( executeEnvelope(COMMAND, { success: outcome.success, data: { apiUrl: ctx.apiUrl, 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 ? 'API axis green.' : `${outcome.aggregate.failed} API assertion(s) failed — inspect ${outcome.resultsFileRel}.`, 'Run `/uat ui` for the frontend axis, then `/uat report` for the HTML report.', ], }), ); process.exit(outcome.success ? 0 : 1); } void main();