/** * Stateful journey parity. * * Where `parity` probes one-shot independent paths, `journey` replays * a recorded sequence (POST /todos → GET /todos, login → fetch profile, * checkout → list orders) against both runtimes and surfaces step-level * divergence. Catches the bug class that's invisible to single-path * probes: a write that's silently dropped, a token that flips to 401 * later, a sort order that drifts after N writes. * * Cookies are tracked per-side automatically: a response's Set-Cookie * is parsed and replayed on subsequent requests on the same side. Each * side has an isolated jar so the comparison stays apples-to-apples. * * Step-level comparison reuses parity's `classify()` and `SideResult` * — same precedence rules (status → header → body → exception), same * opt-in checks (`checkBody`, `checkHeaders`). Exception checking is * intentionally NOT exposed here in v1: replaying a journey in a * browser per side per step is browser-launch overhead times step * count, and the failure mode that motivated this (silent write drop) * surfaces in body/status without it. */ import { type BodyDiffResult } from "./body-diff.js"; import type { MismatchKind, ParityReport, RunParityOptions, SideResult } from "./parity.js"; export interface CaptureSpec { /** * Source to extract from. Two prefixes are supported: * - `body.` — parse the response as JSON and walk the path * (e.g. `body.id`, `body.user.id`, `body.items.0.id`). * - `header.` — case-insensitive response header (e.g. * `header.x-request-id`). * * Extraction failures (non-JSON body, missing path) leave the var * unset on that side, so a subsequent `{{var}}` template renders as * the literal `{{var}}` — visible noise in the URL that the parity * comparison then flags as a status mismatch. The journey doesn't * silently swallow extraction failures. */ from: string; /** Variable name to bind. Substituted as `{{as}}` in later steps. */ as: string; } export interface JourneyStep { /** HTTP method. Case-insensitive; uppercased internally. */ method: string; /** * Pathname (joined with the base URL). May contain `{{var}}` * placeholders that reference variables captured by earlier steps. */ path: string; /** * Request body. Strings sent verbatim; objects JSON-stringified with * `application/json` content-type unless `headers` overrides. Both * forms may contain `{{var}}` placeholders. */ body?: string | Record; /** * Extra request headers (case-insensitive merged with content-type). * Header values may contain `{{var}}` (useful for Authorization * tokens captured from a login step). */ headers?: Record; /** * Variables to capture from this step's response. Each is bound on * the side it ran on, so a token captured from left's login is only * visible to subsequent left steps. Asymmetric values across sides * (e.g. server-generated IDs that differ) are exactly the point — * the comparison runs on the substituted result. */ capture?: CaptureSpec[]; /** * Actor identity for multi-tenant journeys. Each actor on each side * has its own cookie jar and variable bag. Catches the bug class * where v2 leaks one user's session state into another's * subsequent request. * * Steps without an `actor` use the implicit `"_default"` actor, so * single-actor journeys are unchanged. Switching actors mid-flow is * the point — see the playground's tenant-isolation demo for the * canonical shape (Alice creates → Bob lists → Bob must not see * Alice's data). */ actor?: string; /** Optional label for reporting — defaults to ` `. */ label?: string; } export interface JourneyStepResult { /** 0-based step index in the input list. */ index: number; label: string; request: { method: string; path: string; }; left: SideResult; right: SideResult; } export interface JourneyMismatch extends JourneyStepResult { /** See parity.ParityMismatch.kinds — all detected kinds in precedence order. */ kinds: MismatchKind[]; /** Localised JSON body diff, populated only when `kinds` contains `"body"`. */ bodyDiff?: BodyDiffResult; } /** Independent of `PARITY_REPORT_SCHEMA_VERSION` — bumped per shape. */ export declare const JOURNEY_REPORT_SCHEMA_VERSION = 1; export interface JourneyReport { /** Stable integer. See `JOURNEY_REPORT_SCHEMA_VERSION`. */ schemaVersion: number; left: string; right: string; stepsChecked: number; mismatches: JourneyMismatch[]; matches: JourneyStepResult[]; /** Thresholds + opt-ins that produced this report (see parity.ParityReport.config). */ config: { checkBody: boolean; checkHeaders: string[]; stopOnMismatch: boolean; timeoutMs: number; perfDeltaMs?: number; perfRatio?: number; }; } export interface RunJourneyOptions { left: string; right: string; steps: JourneyStep[]; /** Per-request timeout. Defaults to 10s. */ timeoutMs?: number; /** * Read + hash response bodies. Same semantics as `parity.checkBody`. * On for journeys by default because the most common journey bug * (silent write drop) surfaces in the read step's body. */ checkBody?: boolean; /** Compare named response headers per step. Same semantics as parity. */ checkHeaders?: string[]; /** * Flag a `perf` mismatch when `right.durationMs - left.durationMs` * exceeds this many milliseconds on any step. Same semantics + * caveats as `parity.RunParityOptions.perfDeltaMs` — single-sample, * set well above your jitter floor. */ perfDeltaMs?: number; /** * Flag a `perf` mismatch when `right.durationMs > left.durationMs * ratio`. * Composes with `perfDeltaMs` via OR. Skipped on a step where * `left.durationMs === 0` to avoid divide-by-zero noise. */ perfRatio?: number; /** * Stop the journey on the first mismatch instead of running every * step. Useful when later steps depend on earlier ones succeeding * (a failed login means the rest of the flow is meaningless). */ stopOnMismatch?: boolean; /** Override fetch for testing. */ fetcher?: typeof fetch; } export declare function runJourney(opts: RunJourneyOptions): Promise; export type { MismatchKind, ParityReport, RunParityOptions, SideResult }; //# sourceMappingURL=journey.d.ts.map