Targeting Real Apps
Two recipes for pointing Jevitate at something less tidy than a static demo page: a dev server whose SPA calls its own API directly, and an app that carries real state between runs.
Dev server / SPA with a direct API origin
A Vite (or similar) dev server serves module requests from the same origin as the page — Vite's
own /@vite/*, /src/** and /node_modules/** — while the
app's own XHR/fetch calls hit an API, often under a path prefix like /api/, and the
page also pulls third-party requests (Stripe.js, web fonts, analytics). Without telling Jevitate
which is which, all of it lands in one undifferentiated timing bucket and can dominate
the "slowest requests" summary with module loads and third-party noise instead of your API.
Two flags fix this:
-
--api-prefix <path>(repeatable) — marks a path prefix as the app's own API for thetimingsummary. Any XHR/fetch under it is classifiedapiand shows up inslowestEndpoints; everything else that returns non-HTML data is also inferred asapiautomatically, and scripts/styles/fonts/images/media — including a dev server's own modules — are classifiedassetand ranked separately inslowestAssets. -
--settle-ignore <pattern>(repeatable,*wildcard) — marks a request as background so it never blocks "the page has settled." A pattern containing://matches the full URL (use this for a third-party origin likehttps://js.stripe.com/*); any other pattern matches the path and query on the target's own origin.
jevitate explore --url http://localhost:5173 \
--goal "check out with a saved card" --success urlIncludes:/order-confirmed \
--api-prefix /api/ \
--settle-ignore "https://js.stripe.com/*" --settle-ignore "https://fonts.gstatic.com/*" \
--real
You do not need --allow for the API calls themselves — --allow
authorizes origins Jevitate may navigate to, not origins a page's own fetch/XHR talks to.
A same-origin dev-server page calling a different-origin API (say, the SPA on
:5173 and the API on :8000) works without any extra flag; you'd only add
--allow if a link click or redirect actually navigates the browser to that other
origin. The exceptions: fixtures and invariant probes only call --allow origins, and a
read-only find-out goal treats writes to an origin outside --allow as third-party
unless they carry credentials, so add your API origin there when you rely on those.
Persist the same settings per-origin instead of repeating flags on every run, in ~/.jevitate/targets.json:
{ "http://localhost:5173": {
"settle": {
"ignoreRequests": ["/@vite/*", "/src/**", "https://js.stripe.com/*", "https://fonts.gstatic.com/*"],
"longPollMs": 5000
},
"timing": { "apiPrefixes": ["/api/"] }
} } The full classification rules are in docs/exploration.md ("When is a page settled?" and "Page timing").
Running against a stateful app
Most real apps are not idempotent fixtures. Three things bite when a target carries real, persistent state across runs.
1. Bound the run — and remember what doesn't bound it
--max-actions caps executed mutations/navigations, and --max-decisions
caps model round-trips. They are not the same cap: a wait (or
scroll_up/scroll_down) decision spends a decision but never an action, so
a run stuck deciding to wait on a stateful page that never finishes an async operation can exhaust
--max-decisions long before --max-actions would ever stop it — and with a
generous decision budget, it can wait far longer than you'd expect from
--max-actions alone. A built-in stall detector (three quiet waits in a row that
change nothing) provides a floor, but for a run against a target with real latency or async work,
set --max-decisions deliberately rather than relying on --max-actions to
bound wall-clock time.
jevitate explore --url https://staging.acme.test/inbox \
--goal "reply to the newest thread and confirm it sent" \
--success 'requestMade:POST /api/threads/*/messages' \
--max-actions 25 --max-decisions 60 \
--storage-state ./auth-run-1.json \
--real 2. Run conversational/stateful journeys sequentially, never concurrently
A conversational or otherwise stateful journey (the goal loop, or any run that reads back its own
writes — an inbox, a sidebar list, an inquiry thread) mutates real state under the identity your
--storage-state carries. Two runs sharing one --storage-state or tenant
race on the same account: the app's own UI is not scoped per Jevitate run, so a second run can see
— and act on — state a first run just created. Run these one at a time:
# one at a time, never &-backgrounded or xargs -P > 1, against the SAME --storage-state
jevitate explore --url https://staging.acme.test/inbox --goal "..." --storage-state auth.json --real
jevitate explore --url https://staging.acme.test/inbox --goal "..." --storage-state auth.json --real
jevitate explore --url https://staging.acme.test/inbox --goal "..." --storage-state auth.json --real
Sequential execution against a shared identity is the only safe pattern for these runs today.
--repeat, --persona and multi-actor runs already run their sessions one
after another.
3. Re-seed one-shot server state between runs
A pending approval, a one-time modal, a single-use invite link, an unread badge — anything the
server only shows or allows once — is consumed by the first run that encounters it. A second run
against the same account will not see it, and a goal written to expect it will read as
blocked or exhausted rather than a Jevitate defect. A goal run can reset
it itself: --fixtures <file> declares HTTP setup and
restore steps to an --allow origin, and --before /
--after shell hooks (with --allow-shell-hooks) run your own reseed script.
Both run before the mission and around every verify-fix and hang replay, so a
replay starts from the same state. For other strategies, run the same reset yourself between
runs. An environment in .jevitate/environments.json can carry
fixtures and hooks for Journey runs
(environments).
See also Flags Reference for the full bounds/settle flag set, and Usability Review for the same sequential-run guidance as it applies to a live usability review.