Files
next.js/test/e2e/app-dir/use-cache-default-profile-expire-zero/use-cache-default-profile-expire-zero.test.ts
Hendrik LiebauandGitHub b420536fe8 Fix false-positive nested-cache error for a short default profile (#95373)
Overriding the `default` `cacheLife` profile with a short cache life
(the built-in `expire` is `INFINITE_CACHE`) makes every `'use cache'`
without an inline `cacheLife()` inherit that short life. In this
configuration the nested-`'use cache'` error, in both its short-`expire`
and zero-`revalidate` variants, fired in two cases where it should not
have.

The error exists to catch a nested cache silently shortening an outer
cache that has no explicit `cacheLife`, degrading it to a dynamic hole
the developer did not choose. But with a short default profile, a single
non-nested `'use cache'` already resolves to a short life with nothing
nested, so the error fired with no nesting at all: in production this
killed the build, and in development it threw on the request. And even
for a genuinely nested cache, the warning is pointless here, because the
default profile already makes every cache a dynamic hole, so there is no
silent degradation to surprise the developer.

The throw condition keyed on a short life that was not set through an
inline `cacheLife()`, and a value inherited from the `default` profile
is non-explicit, so it matched. This change additionally requires that a
dynamic nested cache actually propagated its short life upward
(`dynamicNestedCacheError` is set) and that the `default` profile is
itself prerenderable. Otherwise the short-lived entry is omitted as a
dynamic hole instead, exactly as an inline short `cacheLife()` already
was. Genuine nesting under a prerenderable default profile still errors.

The nesting condition also subsumes the dev-only private-cache
exclusion: a private cache's `revalidate: 0` is forced by us rather than
by a nested cache and never carries a nested error, so the explicit
`kind !== 'private'` check is no longer needed.
2026-07-02 23:53:17 +02:00

49 lines
2.0 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
const isoDateRegExp = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
describe('use-cache-default-profile-expire-zero', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('treats a short default cacheLife profile as a dynamic hole, not a nested-cache error', async () => {
const browser = await next.browser('/')
const initialValue = await browser.elementById('value').text()
expect(initialValue).toMatch(isoDateRegExp)
// The short `expire` comes from the app's configured default profile, and
// the cache is not nested inside another one, so the nested-cache "no
// explicit cacheLife" error must never fire (in production it kills the
// build; in dev it throws on the request).
expect(next.cliOutput).not.toContain(
'nested-use-cache-no-explicit-cachelife'
)
// An `expire: 0` cache is a dynamic hole. In production it regenerates on
// every request; in dev it is served warm across reloads and re-warmed in
// the background. Either way, reloads converge to a fresh value.
await retry(async () => {
await browser.refresh()
const value = await browser.elementById('value').text()
expect(value).toMatch(isoDateRegExp)
expect(value).not.toEqual(initialValue)
})
})
it('does not error when a short-lived cache is nested under a dynamic default profile', async () => {
// A short-lived inner cache nested inside an outer cache without an
// explicit `cacheLife` normally errors during prerendering. But because the
// default profile is itself dynamic, the outer cache is omitted from
// prerenders by default, so there is no silent degradation to warn about
// and the nested cache renders as a dynamic hole instead.
const browser = await next.browser('/nested')
expect(await browser.elementById('value').text()).toMatch(isoDateRegExp)
expect(next.cliOutput).not.toContain(
'nested-use-cache-no-explicit-cachelife'
)
})
})