Find what breaks on a bad network

For AI agents: this page is Markdown at https://squiggle.sh/docs/flows/network.md. Every docs page is listed at https://squiggle.sh/llms.txt, and all of them are one file at https://squiggle.sh/llms-full.txt.

Run a flow once on a working network and again on a slow, failing or offline one, and see what your app did differently: the spinner that never leaves, the empty state shown for a server error, the request behind each.

These commands drive a simulator app that squiggle up has prepared. They turn on network capture themselves. Requests to Metro and to Squiggle are never degraded.

Each one chooses the network or the starting state for its own runs, so they don’t combine: --twin, --fuzz and --race each refuse the other two, --chaos and --stubs, and --cache-twins refuses --twin, --fuzz and --fresh. A clash stops before any run with exit code 2 (E_TWIN_CLASH or E_RACE_CLASH).

Compare a flow with its twin

--twin runs the flow on a working network, then once under each profile you name: slow, flaky, truncated, offline-midstep, or all. The profiles are described in Run settings. Each run is recorded, and the report compares every degraded run with the clean one:

Terminal
squiggle flow orders --twin alltwin orders · 4 against a clean run  ✔ slow: no difference    no difference under slow: the app behaved as it did on a working network  ! flaky: 1 finding    under flaky, appeared only here: orders-empty — GET https://jsonplaceholder.typicode.com/todos returned 500  ! truncated: 3 findings    under truncated, orders-loading was still on screen when the run ended, 1.2 s after it appeared — GET https://jsonplaceholder.typicode.com/todos returned 200 with a body that does not parse  run-failed: step 4 (assert) failed: visible text=Loaded 2 times: fail (no matching mounted node)  missing: never appeared: order-* (×20), orders-count  ! offline-midstep: 2 findings    under offline-midstep, orders-loading was still on screen when the run ended, 0.3 s after it appeared — GET https://jsonplaceholder.typicode.com/todos failed on the network  run-failed: step 4 (assert) failed: visible text=Loaded 2 times: fail (no matching mounted node)  .squiggle/twins/mve8yhe6/report.json
The report saysWhat it means
was still on screen when the run endedA loading indicator never went away. The screen has no way out of its loading state when a request fails.
step N failedThe clean run passed and this one failed, at that step.
never appearedControls the clean run showed and this run never did. Numbered rows are counted, as in order-* (×20).
appeared only hereControls only this run showed: often an empty state shown in place of an error.
went to …A screen the clean run never reached, such as an error screen.

Every finding names the requests that came back differently: the ones that failed, returned 5xx, returned a body that doesn’t parse, or arrived at least a second later than in the clean run.

A flow can pass under a profile and still have a finding. In the example above the flaky run passed, but the screen said “No orders yet” for a server error. Read the findings, not only the verdict.

Break one endpoint at a time

Terminal
squiggle flow orders --fuzz

--fuzz runs the flow once on a working network, then breaks each endpoint it called, one at a time, while every other request still reaches your server:

MutationWhat the app gets
500A server error with a JSON error body.
emptyA 200 with an empty list or object, matching what the endpoint returned.
cut offThe real response, cut off halfway through its body.
5 s lateThe real response, five seconds late.
404Not found, with no body.

Each mutation is a full run, so a flow that calls four endpoints is twenty runs. --fuzz-max caps them (10 by default), first endpoint first. If the clean run made no request that came back, the command says nothing to vary and exits 0.

Find races

A race is a tap the user repeats, or a Back they press, while the tap’s request is still in flight. --race searches for the two that break a screen whatever the timing: a request that changes something (anything but a GET) sent more times than in a run with no racer, and a late response that navigates after the user went Back.

Terminal
squiggle flow checkout --racerace checkout · 16 racers · window 2000 ms  ✗ duplicate-mutation: POST api.example.com/orders sent 2 times, 1 in the baseline    step 4 · repeat at 200 ms  ! minimal duplicate-mutation: 1 step(s) before the tap, repeat at 0 ms, reproduced 3 of 3    .squiggle/races/mve9k2a1/duplicate-mutation.min.yaml  .squiggle/races/mve9k2a1/report.json
  1. One run on your real network records what each request returns. It has to pass.
  2. From then on every request is answered from that recording 2 seconds late, so each tap has the same 2-second window on every run. A baseline run with no racer has to pass under that delay too.
  3. For each tap that left a request in flight, a racer (the same tap again, or Back) fires at 10%, 50% and 90% of the window, and once just after the response. Racers that failed in an earlier sweep of this flow run first.
  4. For each kind of finding, Squiggle drops every step it can from before the tap, moves the racer to 0 ms if it still fails there, and replays the result three times. It writes the shortest reproduction as <kind>.min.yaml beside the report.

Every run relaunches the app. --race-max caps the racers (16 by default). The report is .squiggle/races/<id>/report.json, and --json prints the findings. squiggle perf labels exports each race run marked synthetic, with the invariant it broke.

Exit codeMeaning
0No invariant broken, or the flow made no request, so nothing was pending.
1A finding, or the recording or baseline run failed. Fix the flow or the app first in that case.
2Combined with a flag it can’t run with.

Check what your cache saves

--cache-twins runs the flow from two save slots, one taken before anything was cached and one taken after, and lists each endpoint the two runs called with how often each called it:

Terminal
squiggle save catalog-cold           # on a fresh launchsquiggle flow catalog                 # fills the cachesquiggle save catalog-warmsquiggle flow catalog --cache-twins catalog-cold,catalog-warmcache twins catalog · catalog-cold against catalog-warm  ✔ cached: GET jsonplaceholder.typicode.com/albums: 1 cold, 0 warm — the warm state answered 1
ResultWhat the warm run did
cachedFewer requests: its saved state answered the rest.
refetchedAs many requests: its saved state saved nothing for that endpoint.
moreMore requests, often something clearing what it held.
warm onlyCalled an endpoint the cold run never did.

Whether a refetch is wrong is yours to say: a list that must always be fresh should refetch. Both runs have to pass, and a slot that won’t load stops the command. A refetch is not a failure: the command exits 0. --fresh can’t be combined with it, because a relaunch wipes the state each slot restores, and nor can --twin or --fuzz, because it chooses what each run starts from itself.

Read the report

The run prints where it saved everything: a folder under .squiggle/twins/ with one recording per run and report.json. --json prints the reports instead. Compare any two of the recordings yourself with squiggle diff.

--twin, --fuzz and --cache-twins exit 0 when the clean run passes, whatever they find. To gate CI on a finding, read report.json.