Self-test — is caching working?
Press the button. This requests every rendering and caching route from the server twice, compares the execution stamp of every panel on it, and reports whether the movement — or the lack of it — was the correct answer for that route.
It is the same set of assertions scripts/cachecheck makes from outside the app, run from inside it and against its own origin. What each check expected, the evidence behind the verdict, and a ranked list of what would explain a failure are all on the card: open one to read it.
What to check
A red result is not always your bug. The first four checks exist to rule out the usual impostors before you go looking: whether this is a dev server, whether the cache handler is storing anything at all, and whether a CDN in front of the app is answering instead of the app.
Run it in pnpm build && pnpm start for a real verdict. Under next dev nothing is prerendered and nothing is prefetched, so the App Shell checks are skipped and build-dependent failures are reported as INFO rather than FAIL.
quick ≈ 15 checks, full ≈ 30, each one two or more requests with a 250ms gap. The first run after a deploy or a restart is the slow one: every cache entry has to be filled from its upstream before there is anything to hit.
How a verdict is reached
- PASS
- every panel did what the route requires — stamps held where a cache hit was guaranteed, and moved where nothing was cached.
- FAIL
- a panel broke a guarantee. The load-bearing one: inside a profile’s
revalidatewindow Cache Components guarantees a hit, so a scope that re-runs there is a real fault rather than timing luck. - INFO
- the answer was legal either way — usually a panel already past its revalidate window, where regenerating is ordinary background work. Re-run to get a reading inside the window.
- SKIP
- the check needs a production build to mean anything.
Two things will make this lie to you, both documented in TESTING.md. A run is itself a visit, so it is the run that triggers an App Shell upgrade — check a cold URL with curl if you want to see the first-visit path. And a revalidation window expiring mid-run looks exactly like a blast radius, which is why every panel is judged against its own age rather than against the clock.
From a shell
The same run streams as newline-delimited JSON, so it works in CI or a terminal:
curl -N localhost:3000/api/selftest?suite=quick
curl -N 'localhost:3000/api/selftest?suite=full' | jq -c 'select(.type=="result") | {v:.result.verdict, t:.result.title}'
# the invalidation scenarios mutate shared cache state, so they need a POST
curl -N -X POST 'localhost:3000/api/selftest?suite=quick&confirm=mutate-cache'GET never invalidates anything, and the POST is refused cross-site. The suite only requests a fixed list of paths, and only on loopback or a hostname listed in SELFTEST_ALLOWED_HOSTS — an unrecognised Host header falls back to this process’s own port rather than being followed. Set SELFTEST_DISABLED=1 to turn the endpoint off entirely, and SELFTEST_SHOW_ENV=1 to un-redact configuration values, which are reported only as set or not set outside dev.