/** * run-results.test — artifact schemas (v3âˆİv4-safe defaults) + the aggregation * math the report and the envelopes rely on. */ import { describe, it, expect } from 'vitest'; import { ApiRunFileSchema, ApiRunResultSchema, DEFAULT_THRESHOLDS, UiRunFileSchema, UiRunResultSchema, aggregateApi, aggregateUi, classifyDuration, humanBytes, percentile, rollupByRole, type ApiRunResult, type UiRunResult, } from '../run-results.js'; import { PlanTestSchema } from '../plantest-schema.js'; describe('schemas fill full-literal defaults (zod v4-safe)', () => { it('UiRunResult minimal parse gets perf/network/arrays defaults', () => { const r = UiRunResultSchema.parse({ routeId: 'X', role: 'admin', url: '/x', expected: 'allowed', actual: 'allowed', ok: true, }); expect(r.kind).toBe('page'); expect(r.executed).toBe(true); expect(r.perf).toEqual({ navMs: 0, fullyReadyMs: 0 }); expect(r.network).toEqual({ requestCount: 0, transferredBytes: 0, failed: [] }); expect(r.consoleErrors).toEqual([]); expect(r.warnings).toEqual([]); }); it('ApiRunResult minimal parse gets numeric defaults', () => { const r = ApiRunResultSchema.parse({ id: 'get:x', method: 'GET', route: '/api/x', role: 'admin', mode: 'exact', expected: 200, actual: 200, ok: true, executed: true, }); expect(r.durationMs).toBe(0); expect(r.sizeBytes).toBe(0); }); }); describe('backward compatibility + new optional diagnostic fields', () => { const meta = { runId: '20260810-120000', application: 'RH', planPath: '.application-test/uat/RH/rh.plantest.yml', startedAt: '2026-08-10T12:00:00.000Z', }; it('a LEGACY artifact (no permission/controller/bodyExcerpt) still parses via both file schemas', () => { const apiFile = ApiRunFileSchema.safeParse({ kind: 'uat-api', meta, results: [ { id: 'get:administration/users', method: 'GET', route: '/api/administration/users', role: 'admin', mode: 'exact', expected: 200, actual: 200, ok: true, executed: true, durationMs: 120, sizeBytes: 2048, }, ], }); expect(apiFile.success).toBe(true); const uiFile = UiRunFileSchema.safeParse({ kind: 'uat-ui', meta, results: [ { routeId: 'USERS_LIST', role: 'admin', url: '/administration/users', expected: 'allowed', actual: 'allowed', ok: true }, ], }); expect(uiFile.success).toBe(true); }); it('new fields round-trip on both axes', () => { const a = ApiRunResultSchema.parse({ id: 'post:sales/orders', method: 'POST', route: '/api/sales/orders', role: 'viewer', mode: 'exact', expected: 403, actual: 500, ok: false, executed: true, permission: 'sales.orders.create', controller: 'OrdersController', bodyExcerpt: 'Internal error — NullReferenceException', }); expect(a.permission).toBe('sales.orders.create'); expect(a.controller).toBe('OrdersController'); expect(a.bodyExcerpt).toContain('NullReferenceException'); const u = UiRunResultSchema.parse({ routeId: 'ORDERS_LIST', role: 'viewer', url: '/sales/orders', expected: 'denied', actual: 'allowed', ok: false, permission: 'sales.orders.read', }); expect(u.permission).toBe('sales.orders.read'); }); }); describe('DEFAULT_THRESHOLDS is the single source of the duration bands', () => { it('matches the plan schema execution.perf defaults (UI pair)', () => { const plan = PlanTestSchema.parse({ meta: { application: 'RH', path: 'RH' }, roles: ['admin'], execution: { screenshots: { out_dir: 'shots' } }, }); expect(plan.execution.perf.warn_ms).toBe(DEFAULT_THRESHOLDS.uiWarnMs); expect(plan.execution.perf.slow_ms).toBe(DEFAULT_THRESHOLDS.uiSlowMs); }); }); describe('classifyDuration / percentile / humanBytes', () => { it('bands durations with inclusive thresholds', () => { expect(classifyDuration(999, 1000, 3000)).toBe('ok'); expect(classifyDuration(1000, 1000, 3000)).toBe('warn'); expect(classifyDuration(2999, 1000, 3000)).toBe('warn'); expect(classifyDuration(3000, 1000, 3000)).toBe('slow'); }); it('percentile nearest-rank', () => { expect(percentile([], 95)).toBe(0); expect(percentile([10], 95)).toBe(10); expect(percentile([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 95)).toBe(10); expect(percentile([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 50)).toBe(5); }); it('humanBytes', () => { expect(humanBytes(512)).toBe('512 B'); expect(humanBytes(2048)).toBe('2.0 KB'); expect(humanBytes(3 * 1024 * 1024)).toBe('3.0 MB'); }); }); const api = (over: Partial): ApiRunResult => ({ id: 'get:x', method: 'GET', route: '/api/x', role: 'admin', mode: 'exact', expected: 200, actual: 200, ok: true, executed: true, durationMs: 100, sizeBytes: 1000, ...over, }); const ui = (over: Partial): UiRunResult => ({ routeId: 'X', role: 'admin', kind: 'page', url: '/x', navigationStrategy: 'menu_click', navigationUsed: 'menu_click', expected: 'allowed', actual: 'allowed', ok: true, executed: true, perf: { navMs: 50, fullyReadyMs: 400 }, network: { requestCount: 5, transferredBytes: 10_000, failed: [] }, consoleErrors: [], warnings: [], ...over, }); describe('aggregateApi', () => { it('splits executed/passed/failed/skipped + duration stats + bytes', () => { const agg = aggregateApi([ api({}), api({ ok: false, durationMs: 300 }), api({ executed: false, ok: false, reason: 'route_params', durationMs: 0, sizeBytes: 0 }), ]); expect(agg).toMatchObject({ total: 3, executed: 2, passed: 1, failed: 1, skipped: 1 }); expect(agg.avgDurationMs).toBe(200); expect(agg.p95DurationMs).toBe(300); expect(agg.totalBytes).toBe(2000); }); }); describe('aggregateUi', () => { it('excludes indeterminate from pass/fail and counts perf warnings on pages only', () => { const agg = aggregateUi( [ ui({}), ui({ ok: false, actual: 'denied' }), ui({ actual: 'indeterminate', ok: false }), ui({ executed: false, ok: false, actual: 'indeterminate' }), ui({ kind: 'create_flow', perf: { navMs: 0, fullyReadyMs: 9999 } }), // flow → not in perf stats ui({ perf: { navMs: 0, fullyReadyMs: 3500 }, consoleErrors: ['boom'] }), ], 3000, ); expect(agg.total).toBe(6); expect(agg.executed).toBe(5); expect(agg.skipped).toBe(1); expect(agg.indeterminate).toBe(1); expect(agg.passed).toBe(3); expect(agg.failed).toBe(1); expect(agg.perfWarnings).toBe(1); // only the 3500ms page (indeterminate page at 0 + flow excluded) expect(agg.consoleErrorCount).toBe(1); expect(agg.totalBytes).toBe(50_000); }); }); describe('rollupByRole', () => { it('rolls both axes per role, executed+determinate only', () => { const rollup = rollupByRole( ['admin', 'viewer'], [api({}), api({ role: 'viewer', ok: false }), api({ role: 'viewer', executed: false, ok: false })], [ui({}), ui({ role: 'viewer', actual: 'indeterminate', ok: false })], ); expect(rollup).toEqual([ { role: 'admin', apiPassed: 1, apiFailed: 0, uiPassed: 1, uiFailed: 0 }, { role: 'viewer', apiPassed: 0, apiFailed: 1, uiPassed: 0, uiFailed: 0 }, ]); }); });