/** * Reading the FLAT expect a flow file used to store. * * The older format held one slot per kind — `{ signal, net, element, text, state, console, route }` * — which could not nest, compose or negate. A step's expect is a `Predicate` now. This is the * reader that bridges them, and it lives in core because the schema that parses a flow is here and * the lift happens during the parse. * * A READ NEVER REWRITES. A v1 file stays v1 on disk until something saves it again; only the value * in memory is lifted. That is what makes the migration safe to ship to people whose flows are * committed to a repository they share. */ import { z } from 'zod'; import type { Predicate } from '../verdict/predicate.js'; /** * A post-condition a step asserts (compiled from a structured annotation; optional). * * Strict, at every nesting level: an unrecognized key (a typo, or a predicate kind this schema * doesn't model, e.g. `allOf`, or a typo'd `net`/`console`/`element`/`state` sub-field) must fail * to parse instead of being silently dropped. A loose z.object() here would quietly discard the * extra key and leave the step with a weaker (or empty) assertion than its author wrote — the * flow then replays green against nothing, having proved no real regression. */ export declare const FlowExpectSchema: z.ZodObject<{ /** * The route the step must have reached — the consequence of a NAVIGATION. * * Absent until now, and its absence was not a corner case: navigation is one of the commonest * journeys there is, and every route-asserted drive saved a flow that could never go red. Found * by driving a real app, where `act_and_wait { until: { kind: "route" } }` returned * `verified: "yes"` and the flow saved from that same drive graded `assertion-free`. * * Additive and optional, so a flow file written before it still parses and FLOW_FILE_VERSION * stays 1 — the same treatment `signalData` and `signalCount` had. */ route: z.ZodOptional; contains: z.ZodOptional; }, "strip", z.ZodTypeAny, { contains?: string | undefined; pathname?: string | undefined; }, { contains?: string | undefined; pathname?: string | undefined; }>>; signal: z.ZodOptional; /** * Optional payload shape an `assert-signal` annotation requires the signal * to match (the predicate DSL's signal.dataMatches). Additive/optional — a flow file with a * bare `signal` still parses, and the on-disk version stays FLOW_FILE_VERSION 1. */ signalData: z.ZodOptional>; /** * Exact number of times the signal must have fired — the signal-side twin of `net.count`, and the * only way a saved flow can keep a cardinality the agent actually asserted. Without it a * `count: 1` drive would be recorded as bare presence, which is a strictly WEAKER claim than the * one made: the replayed flow then goes green on the double-fire it was recorded to catch. * Additive/optional — a flow file with a bare `signal` still parses, and FLOW_FILE_VERSION stays 1. */ signalCount: z.ZodOptional; net: z.ZodOptional; urlContains: z.ZodOptional; status: z.ZodOptional; /** * Exact number of matching requests since the action — turns presence into a cardinality * assertion. Catches the double-submit / useEffect-double-fire / retry-storm regression class: * the request fired (presence passes) but fired the WRONG number of times. Omit = presence (≥1). */ count: z.ZodOptional; }, "strict", z.ZodTypeAny, { status?: number | undefined; method?: string | undefined; urlContains?: string | undefined; count?: number | undefined; }, { status?: number | undefined; method?: string | undefined; urlContains?: string | undefined; count?: number | undefined; }>>; /** * Console golden end-condition: assert the action logged (or, with absent:true, did NOT log) a * console message at `level` (default 'error'). `absent:true` is the common case — "the action * completed with a clean console" — catching the regression where an action throws a caught error * / logs an uncaught rejection while the UI still renders fine (a presence check passes it). */ console: z.ZodOptional; absent: z.ZodOptional; }, "strict", z.ZodTypeAny, { level?: string | undefined; absent?: boolean | undefined; }, { level?: string | undefined; absent?: boolean | undefined; }>>; element: z.ZodOptional; role: z.ZodOptional; name: z.ZodOptional; }, "strict", z.ZodTypeAny, { testid?: string | undefined; role?: string | undefined; name?: string | undefined; }, { testid?: string | undefined; role?: string | undefined; name?: string | undefined; }>>; /** * Rendered text as an end-condition, using the shapes the assert surface already accepts. * * The gap this closes: an app with no testids, no `reticle.signal`, no registrable store and no * network call on the interaction under test -- a discount price computed and rendered, a * formatted total, a derived label -- could not produce an asserted flow AT ALL. Everything the * vocabulary offered needed a channel that app does not have, so the only honest outcome was * `assertion-free`, which is a permanent green (#811). * * Derived-DOM rendering is a large share of what actually breaks in a UI, and it was the share * that could not be pinned. * * Classified PRESENCE, not consequence, and that is not a technicality: text is read from the * DOM, so a locator healed to the wrong element can still satisfy it. It earns `presence-only`, * which is a real grade above `assertion-free` and honestly below `signal`/`net`/`state`. */ text: z.ZodOptional; /** Assert the text is GONE — the regression check for a cleared error or a dismissed toast. */ absent: z.ZodOptional; /** Require the match to be visible, not merely present in the DOM. */ visible: z.ZodOptional; }, "strict", z.ZodTypeAny, { contains: string; visible?: boolean | undefined; scope?: string | undefined; absent?: boolean | undefined; }, { contains: string; visible?: boolean | undefined; scope?: string | undefined; absent?: boolean | undefined; }>>; /** * Assert a registered store's value — the source of truth no DOM/network read can reach. Compiles * to the predicate engine's `state` predicate. Additive/optional — a flow without it still parses * and the on-disk version stays FLOW_FILE_VERSION 1. `equals` accepts a literal, omitted = presence, * or a `{ $gte | $contains | $length }` operator pattern. */ state: z.ZodOptional; path: z.ZodString; equals: z.ZodOptional; /** * Treat this as an INVARIANT that must still hold AFTER the action settles, rather than a * condition to wait for. Set it for a blast-radius check ("this unrelated path must NOT have * moved") — without it a wait-until-true read passes before an over-reaching side-effect lands. */ hold: z.ZodOptional; }, "strict", z.ZodTypeAny, { path: string; hold?: boolean | undefined; store?: string | undefined; equals?: unknown; }, { path: string; hold?: boolean | undefined; store?: string | undefined; equals?: unknown; }>>; }, "strict", z.ZodTypeAny, { signal?: string | undefined; net?: { status?: number | undefined; method?: string | undefined; urlContains?: string | undefined; count?: number | undefined; } | undefined; state?: { path: string; hold?: boolean | undefined; store?: string | undefined; equals?: unknown; } | undefined; element?: { testid?: string | undefined; role?: string | undefined; name?: string | undefined; } | undefined; text?: { contains: string; visible?: boolean | undefined; scope?: string | undefined; absent?: boolean | undefined; } | undefined; route?: { contains?: string | undefined; pathname?: string | undefined; } | undefined; console?: { level?: string | undefined; absent?: boolean | undefined; } | undefined; signalData?: Record | undefined; signalCount?: number | undefined; }, { signal?: string | undefined; net?: { status?: number | undefined; method?: string | undefined; urlContains?: string | undefined; count?: number | undefined; } | undefined; state?: { path: string; hold?: boolean | undefined; store?: string | undefined; equals?: unknown; } | undefined; element?: { testid?: string | undefined; role?: string | undefined; name?: string | undefined; } | undefined; text?: { contains: string; visible?: boolean | undefined; scope?: string | undefined; absent?: boolean | undefined; } | undefined; route?: { contains?: string | undefined; pathname?: string | undefined; } | undefined; console?: { level?: string | undefined; absent?: boolean | undefined; } | undefined; signalData?: Record | undefined; signalCount?: number | undefined; }>; export type FlowExpect = z.infer; /** * Lift a flat `FlowExpect` into the predicate a current file stores directly. * * THE READER FOR THE OLDER FORMAT. A flow written before `FlowStep.expect` became a `Predicate` holds one slot * per kind, and this is what turns that into the tree everything downstream now evaluates. It runs * on READ and changes nothing on disk: a v1 file stays a v1 file until something re-saves it. * * Faithful on purpose, including one wart it does NOT fix: a `route` clause returns immediately and * drops every other clause beside it. That is what v1 files have always meant, and a migration that * quietly started asserting a clause those files used to ignore would turn greens red for a reason * nobody could see in their diff. It is a real lossy path and it is being fixed separately, where it * can carry its own test and its own explanation. * * `undefined` means nothing assertable, which is vacuously met rather than failed. */ export declare function flowExpectToPredicate(success: FlowExpect): Predicate | undefined;