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 the timing summary. Any XHR/fetch under it is classified api and shows up in slowestEndpoints; everything else that returns non-HTML data is also inferred as api automatically, and scripts/styles/fonts/images/media — including a dev server's own modules — are classified asset and ranked separately in slowestAssets.
  • --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 like https://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.