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.

Suite

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 revalidate window 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.