/** * E2E assertion helpers. * * These wrap vitest's `expect` with richer failure messages that include the * full stdout / stderr of the failing process — essential for debugging CI. */ import type { RunResult } from './cli.js' import { expect } from 'vitest' import './vitest-context.js' // ── ANSI ────────────────────────────────────────────────────────────────── // eslint-disable-next-line no-control-regex const ANSI_RE = /\x1B\[[0-9;]*[mGKHFA-DJsuhl]/g function redact(text: string): string { return text .replace(/\bBearer\s+[\w.-]+\b/g, 'Bearer [REDACTED]') .replace(/\bdfo[ae]_[\w-]+\b/g, 'dfo*_REDACTED') } // ── Exit code ───────────────────────────────────────────────────────────── /** * Assert the exit code matches `expected`. * On failure, prints the full stdout and stderr so the cause is visible in CI. */ export function assertExitCode(result: RunResult, expected: number): void { if (result.exitCode !== expected) { process.stderr.write( `\n[E2E assertExitCode] expected ${expected}, got ${result.exitCode}\n` + `stdout:\n${redact(result.stdout) || '(empty)'}\n` + `stderr:\n${redact(result.stderr) || '(empty)'}\n`, ) } expect(result.exitCode, `exit code should be ${expected}`).toBe(expected) } /** * Assert the exit code is NOT 0 (i.e. some error occurred). */ export function assertNonZeroExit(result: RunResult): void { expect(result.exitCode, 'exit code should be non-zero').not.toBe(0) } // ── Stdout / stderr content ─────────────────────────────────────────────── /** * Assert stdout is valid JSON and return the parsed value. */ export function assertJson(result: RunResult): T { let parsed: T try { parsed = JSON.parse(result.stdout) as T } catch { throw new Error( `stdout is not valid JSON.\nstdout:\n${redact(result.stdout)}\nstderr:\n${redact(result.stderr)}`, ) } return parsed } /** * Assert stderr contains a valid JSON error envelope of the shape: * { error: { code: string, message: string, hint?: string } } * * @param result - The run result to inspect. * @param expectedCode - When provided, also asserts that error.code equals this value. * Use the stable error codes from the CLI contract, e.g.: * 'not_logged_in', 'app_not_found', 'insufficient_scope', 'auth_expired' * * @example * assertErrorEnvelope(result, 'not_logged_in') * assertErrorEnvelope(result, 'app_not_found') */ export function assertErrorEnvelope( result: RunResult, expectedCode?: string, ): { error: { code: string, message: string, hint?: string } } { const raw = result.stderr.trim() let parsed: { error: { code: string, message: string, hint?: string } } try { parsed = JSON.parse(raw) as typeof parsed } catch { throw new Error( `stderr is not valid JSON.\nstdout:\n${redact(result.stdout)}\nstderr:\n${redact(result.stderr)}`, ) } expect(parsed, 'stderr envelope missing "error" key').toHaveProperty('error') expect(parsed.error, 'error.code must be a non-empty string').toHaveProperty('code') expect(parsed.error, 'error.message must be a non-empty string').toHaveProperty('message') expect(typeof parsed.error.code, 'error.code must be a string').toBe('string') expect(parsed.error.code.length, 'error.code must be non-empty').toBeGreaterThan(0) if (expectedCode !== undefined) { expect( parsed.error.code, `error.code should be "${expectedCode}", got "${parsed.error.code}"\nstderr:\n${redact(result.stderr)}`, ).toBe(expectedCode) } return parsed } // ── ANSI / formatting ──────────────────────────────────────────────────── /** * Assert the given text contains no ANSI escape sequences. * Pass `label` to identify which stream failed (e.g. 'stdout', 'stderr'). */ export function assertNoAnsi(text: string, label = 'output'): void { const clean = text.replace(ANSI_RE, '') expect(text, `${label} must not contain ANSI control codes`).toBe(clean) } /** * Assert stdout starts with `{` and ends with `\n` — the canonical format * for pipe-friendly JSON output. */ export function assertPipeFriendlyJson(result: RunResult): void { assertNoAnsi(result.stdout, 'stdout') expect( result.stdout.trimStart().startsWith('{') || result.stdout.trimStart().startsWith('['), 'stdout should start with { or [ for pipe-friendly JSON', ).toBe(true) expect(result.stdout.endsWith('\n'), 'stdout should end with newline').toBe(true) } // ── stdout / stderr contains ────────────────────────────────────────────── /** * Assert stdout contains the given substring, printing full output on failure. */ export function assertStdoutContains(result: RunResult, expected: string): void { if (!result.stdout.includes(expected)) { process.stderr.write( `\n[E2E assertStdoutContains] "${expected}" not found in stdout.\n` + `stdout:\n${redact(result.stdout)}\nstderr:\n${redact(result.stderr)}\n`, ) } expect(result.stdout).toContain(expected) } /** * Assert stderr contains the given substring, printing full output on failure. */ export function assertStderrContains(result: RunResult, expected: string): void { if (!result.stderr.includes(expected)) { process.stderr.write( `\n[E2E assertStderrContains] "${expected}" not found in stderr.\n` + `stdout:\n${redact(result.stdout)}\nstderr:\n${redact(result.stderr)}\n`, ) } expect(result.stderr).toContain(expected) }