skills/next-cache-components-optimizer/reference/red-test-robustness.md
The C-gate of the workflow. A RED that is red for the wrong reason sends you optimizing a route that was never broken: the route becomes instant, the test stays red (or the code is contorted to satisfy a broken assertion), and the effort lands on the wrong problem. The prevention is cheap: spend a few minutes verifying the RED is trustworthy first.
Does the marker render WITHOUT the lock, as the test user?
Everything below serves answering that question honestly.
With Cache Components, a top-level blocking read can fail the build before the
test can produce a RED. Add export const instant = false to the target route
as part of the temporary RED scaffold. It lets the known blocker build without
making the navigation instant. Remove the opt-out with the fix, and include it
when reverting the fix for the differential.
Do not manufacture a RED with cookies() or a session read alone. The testing
lock restricts the navigation to its shell; it does not make request cookies
unavailable. Use the route's real blocking uncached data, and prefer the
self-validating test variant when deferred content exists.
next dev), running as the test user, not only in the author's own logged-in session.next dev.data-testid on a known static-shell node, not a guessed role/name.display:none, off-screen, or inside a hover overlay; for lists,
target .filter({ visible: true }).first().Any of these makes a RED untrustworthy. None of them is "the navigation isn't instant."
| Wrong reason | How it occurs | How to rule it out |
|---|---|---|
| Selector matches nothing | a guessed getByRole('button', { name: 'Folder' }) | grep the component for the real accessible name; add a data-testid |
| Conditional redirect | the route redirect()s for the test user (flag/role), so the marker page is never reached | check the page's top-level branches; assert at the real destination, or pin the flag |
| Flag / plan / role gate | the author has the flag or plan; the test user does not | run the unlocked baseline as the test user; pin flags via the project's override mechanism |
| Empty state | the marker only exists when data does; the CI account is empty | pick a marker present in the empty state (a layout element such as the page header), or seed data |
| Timeout / flake | a slow API or transient infrastructure error | re-run; separate infrastructure flake from a real signal |
| Streamed marker | the marker is behind <Suspense>, so it is never in the shell | choose a sync shell element; verify it sits outside every <Suspense> |
| Auth redirect | unauthenticated → /login | confirm login succeeded before the navigation |
| Stale deployment | the test ran against the previous build (the URL under test still serves the prior deploy) | poll the deployment for a marker from the latest commit before trusting any verdict |
| Hidden / off-screen | the testid is on a hover-overlay or off-screen list item | put the marker on an always-visible node; .filter({ visible: true }).first() for lists |
These are illustrative failures from real optimization runs; each was red for a wrong reason, and none was an instant-navigation problem. One app's drift surface might be dominated by feature flags and plans; another's by auth state, an empty database, or locale. The taxonomy lists every wrong reason; the rig file's DRIFT list says which rows apply to your app.
data-testid on a real static-shell node.hidden sm:block hover-overlay link, then on an
off-screen carousel card; Playwright resolved the element but reported it hidden. → check 10.
Fix: an always-visible node; for lists, .filter({ visible: true }).first().The strongest evidence that the RED measured the property:
1. on the fixed branch → GREEN
2. revert ONLY the fix (the <Suspense> push-down) → RED
3. re-apply → GREEN
4. confirm no other change moves it
Link the two runs (or include the toggle diff and results) in the PR description. A reviewer who sees the differential knows the test measures the property.
instant() is not a stopwatchSee: instant().
The test does not measure how fast a navigation is. instant() gates dynamic data so the content
of the static shell can be asserted; the signal is presence, not speed. Under the lock, an instant
route's shell is present, and a blocking route's content never commits, regardless of wait time.
Therefore:
await expect(SHELL_MARKER).toBeVisible() under the lock. Do not add a
custom timeout or a painted boolean.3000) implies a race against a clock that does not exist. It adds
nothing to the verdict and invites false REDs on an instant route whose commit lands a microtask
late.locator.isVisible({ timeout }) as a soft wait: Playwright deprecated and ignores
that timeout; the call returns immediately.instant() guards need no retries and no prefetch warmingAn instant() guard is deterministic. Do not configure retries on one, and do not hover-warm to
help a prefetch land in time. Under the lock, the router initiates the route prefetch and awaits it
before committing (even for a prefetch={false} link, even for a route already in the prefetch
cache), so the committed shell does not depend on any prior render, hover, or menu-open prefetch.
A flaky guard has a real cause: a marker that is not a sync node of the destination's shell, a
flag/role/empty-state gap for the test user, or a genuinely blocking route. The fix is in the page or
the marker; a retry masks the regression the guard exists to catch. The only legitimate
.hover()/menu-open is when the trigger element itself is not in the DOM until hovered or opened.
instant() works by setting a cookie (next-instant-navigation-testing) that lock code inside the
build reads. It does not throw when that lock code is absent; it only throws on nested calls or an
unknown base URL. If the build was produced without the testing API
(experimental.exposeTestingApiInProductionBuild), the cookie is ignored, the navigation runs
normally, and the instant() test passes vacuously. A green instant() test is only meaningful if
the lock engaged.
Two defenses; use both:
Confirm the API is exposed on the target. Wire the flag to the platform's preview/staging condition or an explicit environment variable; the rig file records the project's spelling (SKILL.md phases 0 and A). Do not trust a pass from a build where it is not set.
Make the test self-validating: for any route with deferred content, also assert that the
deferred content is gated under the lock, not only that the shell is present
(test-template.md, self-validating variant). If the lock did not engage, the content is
already present and toHaveCount(0) fails.
The gated half holds under the lock for both navigation types regardless of warm state: the soft-nav client lock gates dynamic-data writes, and on an initial load the server honors the cookie on the document request and suspends dynamic data. A vacuous pass is only possible with a build produced WITHOUT the testing API, which defense #1 above covers.
next dev; SKILL.md phase A owns this invariant and
its rationale.