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:
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 says | What it means |
|---|---|
was still on screen when the run ended | A loading indicator never went away. The screen has no way out of its loading state when a request fails. |
step N failed | The clean run passed and this one failed, at that step. |
never appeared | Controls the clean run showed and this run never did. Numbered rows are counted, as in order-* (×20). |
appeared only here | Controls 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
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:
| Mutation | What the app gets |
|---|---|
500 | A server error with a JSON error body. |
empty | A 200 with an empty list or object, matching what the endpoint returned. |
cut off | The real response, cut off halfway through its body. |
5 s late | The real response, five seconds late. |
404 | Not 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.
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- One run on your real network records what each request returns. It has to pass.
- 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.
- 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.
- 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.yamlbeside 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 code | Meaning |
|---|---|
0 | No invariant broken, or the flow made no request, so nothing was pending. |
1 | A finding, or the recording or baseline run failed. Fix the flow or the app first in that case. |
2 | Combined 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:
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| Result | What the warm run did |
|---|---|
cached | Fewer requests: its saved state answered the rest. |
refetched | As many requests: its saved state saved nothing for that endpoint. |
more | More requests, often something clearing what it held. |
warm only | Called 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.