Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Force the states a happy-path run never reaches — a failing API, an empty list, a slow request, a timeout, an expired session, a toast that auto-dismisses — and check the UI actually handles them. Use when error handling was written but never run, when a loading or empty state needs verifying, when a bug only happens on a slow connection, or when a timer, poll, debounce or retry needs testing without sleeping.
.claude/skills/reticlehq-test-error-states/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 15% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 16% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 28% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 14% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 12% | 0% |
Every app has a catch block nobody has executed and an empty state nobody has seen. They are written from imagination, shipped untested, and discovered by a user on a bad day.
Reticle can force those conditions in the running app. Not installed? RETICLE_INSTALL_SOURCE=npx_skill npx @reticlehq/server@latest init, then the install-and-verify skill.
reticle_network_mock intercepts requests in a browser Reticle owns, and the always-on SDK cannot do it. A connected tab with neither reticle drive nor a lease still returns { ok: false, reason: "no-cdp-provider" }.
Your route is a leased Playwright tab (reticle_lease acquire) or RETICLE_CDP_URL pointed at a Chrome started with remote debugging:
bash# macOS — the user runs this once, in their own Chrome /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 RETICLE_CDP_URL=http://localhost:9222 npx @reticlehq/server@latest mcp
If you have neither a lease nor a driven browser, say so in one line and offer the clock half of this skill anyway: reticle_clock needs none of it. What you may not do is drive the happy path, watch it pass, and report that error handling works.
reticle_run({ tool: "reticle_network_mock", sessionId, args: {
mocks: [{ urlContains: "/api/deploys", status: 500 }],
}})Then drive the flow and name the recovery you expect before you act:
reticle_act_and_wait({ sessionId, ref, action: "click", until: { kind: "allOf", predicates: [
{ kind: "element", query: { testid: "error-banner" } },
{ kind: "console", level: "error", absent: true },
]}})Note the second predicate. A UI that "handles" an error by logging an uncaught exception has not handled it. Clear mocks with { clear: true } when you are done, or every later check runs against a lie.
Worth forcing, in rough order of how often they are broken: 500, a 4xx with a real error body, an empty 200 ([], the empty state), a malformed payload, and a request that never resolves (the spinner that spins forever).
reticle_run({ tool: "reticle_clock", args: { sessionId, freeze: true } })
reticle_run({ tool: "reticle_clock", args: { sessionId, advanceMs: 5000 } })
reticle_run({ tool: "reticle_clock", args: { sessionId, reset: true } })Toasts that auto-dismiss, debounced search, polling, session timeouts, retry backoff. All of these are normally verified by sleeping, which is slow and flaky in equal measure. A timing assertion is a statement about the machine, so it passes on your laptop and fails in CI.
Freeze, advance by exactly the interval, assert the consequence. Same result on a fast laptop and a loaded runner. Always reset when you finish, or a frozen clock silently breaks everything that runs after you.
The recovery, not the absence of a crash:
reticle_state shows the failure, not a half-applied optimistic update. A UI that rolled back visually while the store kept the optimistic value is the classic bug here, and only the store read finds it.Mocking changes the app's world, so a verdict taken under a mock is a statement about the mocked condition and nothing else. Say which mock was active when you report a pass, and clear every mock and reset the clock before handing back. An audit that leaves a 500 pinned on /api/deploys breaks the next person's session and looks like a real outage.
Capability reference: curl https://docs.reticle.sh/capabilities.md. Everything else: curl https://docs.reticle.sh/llms.txt.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 19,767 | 18,999 | -4% | 1 | 1 | 0% | 3,878 | 4,441 | +15% | 0 | 0 | — |
case-02 | fail→fail | 23,286 | 7,254 | -69% | 1 | 1 | 0% | 3,963 | 1,401 | -65% | 0 | 0 | — |
case-03 | fail→fail | 28,382 | 5,897 | -79% | 1 | 1 | 0% | 3,848 | 1,388 | -64% | 0 | 0 | — |
case-04 | pass→pass | 24,768 | 10,292 | -58% | 1 | 1 | 0% | 2,289 | 2,964 | +29% | 0 | 0 | — |
case-05 | pass→pass | 13,172 | 14,959 | +14% | 1 | 1 | 0% | 2,282 | 3,825 | +68% | 0 | 0 | — |
case-06 | pass→pass | 11,929 | 9,553 | -20% | 1 | 1 | 0% | 2,068 | 2,687 | +30% | 0 | 0 | — |
case-07 | fail→pass | 9,468 | 3,513 | -63% | 1 | 1 | 0% | 1,389 | 1,617 | +16% | 0 | 0 | — |
case-08 | fail→pass | 13,636 | 11,202 | -18% | 1 | 1 | 0% | 2,086 | 2,675 | +28% | 0 | 0 | — |
case-09 | fail→pass | 10,560 | 4,577 | -57% | 1 | 1 | 0% | 1,538 | 1,760 | +14% | 0 | 0 | — |
case-10 | fail→pass | 9,551 | 3,311 | -65% | 1 | 1 | 0% | 1,424 | 1,588 | +12% | 0 | 0 | — |
case-11 | fail→pass | 13,465 | 8,616 | -36% | 1 | 1 | 0% | 2,082 | 2,208 | +6% | 0 | 0 | — |
case-12 | fail→fail | 15,970 | 10,363 | -35% | 1 | 1 | 0% | 2,548 | 2,570 | +1% | 0 | 0 | — |
case-13 | pass→pass | 12,769 | 10,033 | -21% | 1 | 1 | 0% | 1,806 | 2,866 | +59% | 0 | 0 | — |
case-14 | fail→pass | 11,871 | 11,331 | -5% | 1 | 1 | 0% | 1,950 | 2,655 | +36% | 0 | 0 | — |
case-15 | fail→pass | 14,867 | 6,534 | -56% | 1 | 1 | 0% | 2,236 | 2,232 | -0% | 0 | 0 | — |
case-16 | fail→pass | 15,453 | 7,750 | -50% | 1 | 1 | 0% | 2,417 | 2,363 | -2% | 0 | 0 | — |
case-17 | fail→pass | 12,378 | 8,906 | -28% | 1 | 1 | 0% | 2,131 | 2,542 | +19% | 0 | 0 | — |
case-18 | pass→pass | 13,275 | 9,449 | -29% | 1 | 1 | 0% | 2,142 | 2,645 | +23% | 0 | 0 | — |
case-19 | pass→fail | 12,313 | 7,804 | -37% | 1 | 1 | 0% | 1,571 | 2,194 | +40% | 0 | 0 | — |
case-20 | fail→pass | 15,210 | 14,650 | -4% | 1 | 1 | 0% | 2,356 | 3,236 | +37% | 0 | 0 | — |
case-21 | fail→pass | 16,611 | 10,568 | -36% | 1 | 1 | 0% | 2,815 | 3,029 | +8% | 0 | 0 | — |
case-22 | pass→pass | 11,696 | 8,244 | -30% | 1 | 1 | 0% | 2,113 | 2,565 | +21% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 20 counted toward the lift figure. The other 2 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +50 percentage points is the difference between those two pass rates over the 20 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.