xt-test-utils'
import { isNextDeploy, isNextDev, nextTestSetup } from 'e2e-utils'
import { createFakeCDN } from './server.mjs'

describe('segment cache (CDN cache busting)', () => {
  if (isNextDev || isNextDeploy) {
    test('should not run during dev or deploy test runs', () => {})
    return
  }

  const { next } = nextTestSetup({
    files: __dirname,
    skipStart: true,
  })

  // TODO(runtime-ppr): add tests for runtime prefetches

  // To debug these tests locally, run:
  //   node start.mjs
  //

 let nextExit: Promise<any> | undefined
  let cdnServer: Server
  let port: number

  beforeAll(async () => {
    await next.build()
    const nextPort = await findPort()
    const proxyPort = (port = await findPort())

    let resolveReady!: () => void

// This will start the Next app and also a proxy server that simulates a CDN.
  // Like certain real-world CDNs, our fake CDN doesn't respect the Vary header.
  // It only uses the URL.
  let nextChild: ChildProcess | undefined


from 'e2e-utils'
import type * as Playwright from 'playwright'
import { createRouterAct } from 'router-act'

describe('segment cache - root params segment prefetch', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

  if (isNextDev) {
    test('prefetching is disabled in dev mode', () => {})
    return
  }

  it('does not encode root param placeholders in segment-prefetch responses', async () => {
    let act: ReturnType<typeof createRouterAct>
    const segmentPrefetchBodies

 response.request()
          if (request.headers()['next-router-segment-prefetch']) {
            segmentPrefetchBodies.push(response.text().catch(() => ''))
          }
        })
      },
    })

    await act(
      async () => {
        const toggle =

: Array<Promise<string>> = []
    const browser = await next.browser('/root-params', {
      beforePageLoad(p: Playwright.Page) {
        act = createRouterAct(p)
        p.on('response', (response) => {
          const request =


s of features and edge cases.
 */
describe('segment cache - vary params', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

  if (isNextDev) {
    test('prefetching is disabled in dev mode', () => {})
    return
  }

  it('renders cached loading state instantly during navigation', async () => {
    // Setup: All links share category='electronics' but different itemId values.
    // Layout only accesses 'category', page renders itemId dynamically.
    let act: ReturnType<typeo

wait act(
      async () => {
        const toggle = await browser.elementByCss(
          'input[data-link-accordion="/instant-loading/electronics/phone"]'
        )
        await toggle.click()
      },
      { includes: 'Category: electronics' }
    )

f createRouterAct>
    const browser = await next.browser('/instant-loading', {
      beforePageLoad(p: Playwright.Page) {
        act = createRouterAct(p)
      },
    })

    // Prefetch the first link - layout is fetched
    a


port { LinkAccordion } from '../../../components/link-accordion'

export default function RuntimePrefetchLayoutSplitIndexPage() {
  return (

Runtime Prefetch - Layout/Page Param Split

          <LinkAccordion href="/runtime-prefetch-layout-split/electronics/phone">
            Electronics: Phone
          </LinkAccordion>

          <LinkAccordion href="/runtime-prefetch-layout-split/electroni

          Electronics: Tablet
          </LinkAccordion>
        </li>
        <li>
          <LinkAccordion href="/runtime-prefetch-layout-split/clothing/shirt">
            Clothing: Shirt
          </LinkAccordion>
        </li>
      </ul>
    </div>


 } from 'react'

/**
 * Layout that accesses both category and itemId params inside Suspense.
 *
 * Since this layout accesses both params, it varies on both — changing
 * either param requires re-fetching this layout segment.
 */
type Params = { category: string; itemId: string }

export default async function RuntimePrefetchLayoutSplitLayout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Promise<Params>
}) {
  return (
    <div data-layout-split-layout="true">
      <Suspense fallback=

nt params={params} />
      </Suspense>
      {children}
    </div>
  )
}

async function LayoutContent({ params }: { params: Promise<Params> }) {
  const { category, itemId } = await params
  return <div data-layout-content="true">{`Layout: ${category}/${

{
Loading layout...
}>
        <LayoutConte


'react'
import { connection } from 'next/server'

/**
 * Page that accesses only category (not itemId) in the static portion.
 *
 * Combined with the layout (which accesses both params), this tests that
 * vary params are tracked per-segment in runtime prefetches:
 * - Layout varies on both category AND itemId → re-fetched when either changes
 * - Page varies only on category → cached when only itemId changes
 */
export const unstable_instant: {
  unstable_samples: Array<{ params: { category: string; itemId

ms = { category: string; itemId: string }

export default async function RuntimePrefetchLayoutSplitPage({
  params,
}: {
  params: Promise<Params>
}) {
  return (

      <Suspense fallback={
Loading page

: string } }>
} = {
  unstable_samples: [
    { params: { category: 'electronics', itemId: 'phone' } },
    { params: { category: 'clothing', itemId: 'shirt' } },
  ],
}
export const unstable_prefetch = 'force-runtime'

type Para


from '../../../components/link-accordion'

/**
 * Index page for the instant loading state test. This is the canonical test
 * for the vary params feature — it demonstrates the core user-facing benefit:
 * instant loading feedback when navigating to routes that share cached segments.
 *
 * All links share category='electronics' but have different itemId values.
 * Since the layout only accesses 'category', the layout segment is cached after
 * the first prefetch and reused for all subsequent links.
 *
 * Th

 * 1. Click the checkbox for "phone" — triggers prefetch, layout is fetched
 * 2. Click the checkbox for "tablet" — cache hit, no request
 * 3. Click the checkbox for "laptop" — cache hit, no request
 * 4. Click the "headphones" link to navigate — loading

e page component renders itemId dynamically (not in generateStaticParams),
 * so when navigating, the cached layout/loading shell renders instantly while
 * the dynamic page content loads in the background.
 *
 * Manual testing:


/**
 * Layout for the [category] segment. This layout:
 * - Accesses only 'category' param → cached across different itemIds
 * - Has generateStaticParams for 'electronics' → this segment is static
 *
 * The child [itemId] segment is dynamic (no generateStaticParams), so when
 * navigating to different itemIds within the same category, this layout
 * renders instantly from cache while the page loads dynamically.
 */
export async function generateStaticParams(): Promise<Params[]> {
  return [{ category: 'el

ntLoadingCategoryLayout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Promise<Params>
}) {
  const { category } = await params

  return (

{`Catego

ectronics' }]
}

export default async function Insta


s = { category: string; itemId: string }

/**
 * Page component that renders the itemId param. This segment has NO
 * generateStaticParams, so itemId is fully dynamic and loads behind
 * the Suspense boundary (loading.tsx).
 *
 * When navigating to this page:
 * 1. The parent [category] layout renders instantly from cache (it's static)
 * 2. The loading.tsx shows while this page loads dynamically
 * 3. This page replaces the loading state once the dynamic request completes
 */
export default async function

Page({
  params,
}: {
  params: Promise<Params>
}) {
  const { category, itemId } = await params
  return (

        Item: {itemId} (in category: {category})


kAccordion } from '../../../components/link-accordion'

/**
 * Index page for the metadata vary params test.
 *
 * This tests that param access in generateMetadata is tracked in the head
 * segment's varyParams, separate from body segment tracking.
 *
 * Setup:
 * - [slug]/page.tsx has generateMetadata that accesses params
 * - The page body does NOT access params
 * - Links go to /metadata/aaa, /metadata/bbb, /metadata/ccc
 *
 * Expected behavior:
 * - First prefetch fetches the head segment (varies on slu

or "bbb" - cache miss, new prefetch (slug changed)
 * 3. Click checkbox for "ccc" - cache miss, new prefetch (slug changed)
 */
export default function MetadataIndexPage() {
  return (

Metadata Vary Params Test

g)
 * - Subsequent prefetches with different slugs require new fetches
 *   because metadata accesses the slug param
 *
 * Manual testing:
 * 1. Click checkbox for "aaa" - triggers prefetch, content fetched
 * 2. Click checkbox f


'react'
import { connection } from 'next/server'

type Params = { slug: string }

/**
 * Page where generateMetadata accesses params but the body does NOT.
 *
 * This tests that metadata param access is tracked separately from page body.
 * When the slug changes:
 * - Head segment should be re-fetched (metadata accesses slug)
 * - Body segment should be cached (body does NOT access slug)
 *
 * The body contains a self-contained dynamic marker (`<DynamicContent />`
 * inside Suspense). This is the documented

refetch is still cacheable across
 * slug changes.
 */
export async function generateStaticParams(): Promise<Params[]> {
  return [{ slug: 'aaa' }, { slug: 'bbb' }, { slug: 'ccc' }]
}

export async function generateMetadata({
  params,
}: {
  params: Promi

 mitigation for dynamic
 * `generateMetadata` on an otherwise-static body — it makes the body partially
 * dynamic via PPR, which is what we actually want here: the body doesn't depend
 * on `slug`, so the static portion of the p


pense } from 'react'
import { connection } from 'next/server'

/**
 * Runtime prefetch page where generateMetadata accesses params but the
 * page body does NOT.
 *
 * This tests that head vary params are tracked separately from segment body
 * in the runtime prefetch pipeline. When slug changes:
 * - Head segment should be re-fetched (metadata accesses slug)
 * - Body segment should be cached (body does NOT access slug)
 */
export const unstable_instant: {
  unstable_samples: Array<{ params: { slug: string

 {
  params: Promise<Params>
}) {
  const { slug } = await params
  return { title: `Runtime Metadata: ${slug}` }
}

export default async function RuntimePrefetchMetadataPage() {
  // Intentionally NOT accessing params in the body
  return (
    <div id="r

 } }>
} = {
  unstable_samples: [{ params: { slug: 'aaa' } }, { params: { slug: 'bbb' } }],
}
export const unstable_prefetch = 'force-runtime'

type Params = { slug: string }

export async function generateMetadata({
  params,
}:


link-accordion'

/**
 * Index page for the search params access tracking test.
 *
 * This tests that when a page accesses searchParams, separate cache entries
 * are created for each unique search param value.
 *
 * Two sub-routes:
 * - /search-params/target-page?foo=X - Page accesses searchParams, NO cache reuse
 * - /search-params/static-target?foo=X - Page does NOT access searchParams, cache reuse
 *
 * Manual testing:
 * 1. Click checkbox for "?foo=1" target link - triggers prefetch
 * 2. Click checkbox

ault function SearchParamsIndexPage() {
  return (

Search Params Access Tracking Test

        Tests that accessing searchParams creates separate cache entries for
        each unique search param

 for "?foo=2" target link - should trigger NEW prefetch (no cache)
 * 3. Click checkbox for "?foo=1" static-target link - triggers prefetch
 * 4. Click checkbox for "?foo=2" static-target link - should be cache hit
 */
export def


Act } from 'router-act'
import { findPort } from 'next-test-utils'
import { isNextStart, nextTestSetup } from 'e2e-utils'
import { join } from 'path'
import { createExportServer } from './server.mjs'

describe('segment cache (output: "export")', () => {
  if (!isNextStart) {
    test('build test should not run during dev test run', () => {})
    return
  }

  const { next } = nextTestSetup({
    files: __dirname,
    skipStart: true,
    disableAutoSkewProtection: true,
  })

  // To debug these tests local

r: Server

  beforeAll(async () => {
    await next.build()
    port = await findPort()
    server = createExportServer(join(next.testDir, 'out'))
    server.listen(port)
  })

  afterAll(() => {
    server?.close()
  })

  it('basic prefetch in output: "e

ly, first build the app, then run:
  //
  // node start.mjs
  //
  // This will serve the static `/out` directory, and also set up a server-side
  // rewrite, which some of the tests below rely on.

  let port: number
  let serve


ent cache (metadata)', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })
  if (isNextDev) {
    test('disabled in development', () => {})
    return
  }
  describe("regression: prefetch the head if it's missing even if all other data is cached", () => {
    it('pages with dynamic content and dynamic metadata, using a full prefetch', async () => {
      let act: ReturnType<typeof createRouterAct>
      const browser = await next.browser('/', {
        beforePageLoad(p) {

        )
        await checkbox.click()
      }, [
        {
          includes: 'Target page',
        },
        // Because the link is prefetched with prefetch={true},
        // we should be able to prefetch the title, even though it's dynamic.

    act = createRouterAct(p)
        },
      })

      // Fully prefetch a page
      await act(async () => {
        const checkbox = await browser.elementByCss(
          'input[data-link-accordion="/page-with-dynamic-head"]'


sure', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })
  if (isNextDev) {
    test('disabled in development', () => {})
    return
  }

  it('evicts least recently used prefetch data once cache size exceeds limit', async () => {
    let act: ReturnType<typeof createRouterAct>
    const browser = await next.browser('/memory-pressure', {
      beforePageLoad(page) {
        act = createRouterAct(page)
      },
    })

    const switchToTab1 = await browser.elementByCss(

ync () => {
        await switchToTab1.click()
      },
      { includes: 'Page 0.' }
    )

    // Switching to tab 2 causes the cache to overflow, evicting the prefetch
    // for the Page 0 link.
    await act(
      async () => {
        await switchTo

'input[type="radio"][value="1"]'
    )
    const switchToTab2 = await browser.elementByCss(
      'input[type="radio"][value="2"]'
    )

    // Switch to tab 1 to kick off a prefetch for a link to Page 0.
    await act(
      as


useState } from 'react'
import { useRouter } from 'next/navigation'

function Tab0() {
  return
Intentionally empty

}

function Tab1() {
  return <Link href="/memory-pressure/0">Link 0</Link>
}

function Tab2() {
  const links = []
  for (let i = 1; i < 60; i++) {
    links.push(<Link href={'/memory-pressure/' + i}>Link {i}</Link>)
  }
  return links
}

export default function MemoryPressure() {
  const router = useRouter()
  const [selectedTab, setSelectedTab] = useState('0')

  c

b1 />
      break
    case '2':
      tab = <Tab2 />
      break
    default:
      tab = null
      break
  }

  return (
    <form>

Memory pressure

        Tests that prefetch data is evicted once the cache size grows too large,

onst handlePageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setSelectedTab(event.target.value)
  }

  let tab
  switch (selectedTab) {
    case '0':
      tab = <Tab0 />
      break
    case '1':
      tab = <Ta


ture for max prefetch inlining. With Infinity thresholds, all
// segments are bundled into a single response per route, similar to how
// prefetching worked before the Segment Cache (pre-Next 16).
import { LinkAccordion } from '../components/link-accordion'

export default function Home() {
  return (

Home

          <LinkAccordion href="/shared/a/b/c">Route A</LinkAccordion>

          <LinkAccordion href="/shared/a/d/e">Route B</Lin

nkAccordion href="/shared/a/b/c" id="duplicate-a">
            Route A (duplicate)
          </LinkAccordion>
        </li>
        <li>
          <LinkAccordion href="/dynamic/hello">Dynamic Route</LinkAccordion>
        </li>
      </ul>
    </div>
  )
}

kAccordion>
        </li>
        <li>
          <Li


kAccordion } from '../components/link-accordion'

export default function Page() {
  return (
    <>

        This page is used to test various scenarios related to prefetch cache
        staleness. In the corresponding e2e test, the links below are prefetched
        (by toggling their visibility), time is elapsed, and then prefetched
        again to check whether a new network request is made.

          <LinkAccordion href="/stale-2-minutes">
            Page w

i>
          <LinkAccordion href="/runtime-stale-2-minutes">
            Page whose runtime prefetch has a stale time of 2 minutes
          </LinkAccordion>
        </li>
        <li>
          <LinkAccordion href="/runtime-stale-4-minutes">
            P

ith stale time of 2 minutes
          </LinkAccordion>
        </li>
        <li>
          <LinkAccordion href="/stale-4-minutes">
            Page with stale time of 4 minutes
          </LinkAccordion>
        </li>
        <l


/link-accordion'

export default function Page() {
  return (
    <>

        This page tests per-page dynamic stale time configuration via{' '}
        export const unstable_dynamicStaleTime.

          <LinkAccordion href="/per-page-config/dynamic-stale-60">
            Dynamic page with stale time of 60 seconds
          </LinkAccordion>

          <LinkAccordion href="/per-page-config/dynamic-stale-10">
            Dynamic

Accordion>
        </li>
        <li>
          <LinkAccordion href="/per-page-config/parallel-slots">
            Parallel routes with slots having different stale times (60s and
            15s)
          </LinkAccordion>
        </li>
      </ul>
    </

 page with stale time of 10 seconds
          </Link


e TreePrefetch as an ASCII tree showing inlining decisions.
 * Segments marked with "⇣ inlined" have their data included in a descendant's
 * response instead of being fetched separately. Validates that parent/child
 * hints are consistent (every InlinedIntoChild parent must have a child with
 * ParentInlinedIntoSelf, and vice versa).
 */
// "outlined ■" is the fixed-width tag (10 chars). Other tags are right-aligned
// to match.
const OUTLINED_TAG = 'outlined \u25A0'
const INLINED_TAG = '\u21E3'.padStart(O

tring[] = []
  const isHeadOutlined = (tree.prefetchHints & HeadOutlined) !== 0
  collectNodes(tree, '', !isHeadOutlined, false, lines)
  if (isHeadOutlined) {
    // Metadata is not inlined into any page — render as a standalone sibling.
    lines.push(`$

UTLINED_TAG.length)
const RUNTIME_TAG = 'runtime \u25FB'.padStart(OUTLINED_TAG.length)
const DYNAMIC_TAG = 'dynamic \u25FB'.padStart(OUTLINED_TAG.length)

function renderInliningTree(tree: TreePrefetch): string {
  const lines: s


Runtime layout with parallel routes. The parent's data should pass through
// this layout into one child slot only — not into both. This tests that the
// pass-through behavior respects the "parent inlines into one child" rule
// even when the pass-through segment has multiple child slots.
import { ReactNode, Suspense } from 'react'
import { cookies } from 'next/headers'

export const unstable_instant = {
  unstable_samples: [{ cookies: [{ name: 'theme', value: 'default' }] }],
}
export const unstable_prefe

/p>
}

export default function RuntimeParallelLayout({
  children,
  sidebar,
}: {
  children: ReactNode
  sidebar: ReactNode
}) {
  return (

      <Suspense fallback={
Loading layout...
}>
        <DynamicContent />
      </Suspense>

tch = 'force-runtime'

async function DynamicContent() {
  const cookieStore = await cookies()
  const theme = cookieStore.get('theme')?.value ?? 'default'
  return <p id="layout-runtime-parallel">Runtime layout (theme: {theme})<


s prefetching disabled because it performs uncached dynamic
// I/O (connection()). Static parents above it should be able to inline through
// it and into the static child below, because this layout acts as a transparent
// pass-through — its slot in the bundle is null but the chain isn't broken.
import { ReactNode, Suspense } from 'react'
import { connection } from 'next/server'

export const unstable_instant = false
export const unstable_prefetch = 'force-disabled'

async function DynamicContent() {
  awa

-passthrough">Dynamic layout

}

export default function InstantFalsePassthroughLayout({
  children,
}: {
  children: ReactNode
}) {
  return (

      <Suspense fallback={
Loading...
}>
        <DynamicContent />
      </Suspense>
      {c

it connection()
  return <p id="layout-instant-false


 { ReactNode, Suspense } from 'react'
import { cookies } from 'next/headers'

// This layout uses runtime prefetching and sits ABOVE the [item] param.
// It's shared between /test-independent-head/a and /test-independent-head/b.
// Once cached from the first prefetch, a subsequent prefetch to a sibling
// page won't need a runtime request for this layout — it's already cached.
export const unstable_instant = {
  unstable_samples: [
    {
      cookies: [{ name: 'theme', value: 'default' }],
      searchPara

me = cookieStore.get('theme')?.value ?? 'default'
  return (

Shared layout (theme: {theme})

      {children}

  )
}

export default function Layout({ children }: { children: ReactNode }) {
  ret

ms: { q: null },
      params: { item: 'a' },
    },
  ],
}
export const unstable_prefetch = 'force-runtime'

async function LayoutContent({ children }: { children: ReactNode }) {
  const cookieStore = await cookies()
  const the


mistic routing - route cache keying regression', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

  if (isNextDev) {
    test('skipped in dev mode', () => {})
    return
  }

  // Regression test for https://github.com/vercel/next.js/pull/88863
  //
  // When navigating to a route that was not previously prefetched (e.g. via
  // a Link with prefetch={false}, or router.push()), the client creates a
  // route cache entry from the server response so it can be reused by future

was always set to null, even though the rest of the
  // system uses the real nextUrl value when looking up entries. This meant
  // every subsequent cache lookup missed, and the client would make
  // redundant requests for route data it already had.
  //

  // navigations and prefetches to the same URL.
  //
  // A bug caused these entries to be stored with an incorrect cache key: the
  // "nextUrl" dimension (which tracks the referring page for interception
  // route purposes)


 { connection } from 'next/server'
import { Suspense } from 'react'
import Link from 'next/link'

async function PhotoContent({ params }: { params: Promise<{ id: string }> }) {
  // connection() opts this page into dynamic rendering. This is important
  // because the test uses staleTimes.dynamic to control when cache entries
  // expire. Without dynamic rendering, the page would use the static stale
  // time (which is long by default and would mask the bug).
  await connection()
  const { id } = await par

<PhotoContent params={params} />
      </Suspense>

Step 2: Navigate back

        Click this link to go back to the feed page. The route and segment data
        from this page remain in the

ams
  return
Photo {id}

}

export default function PhotoPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  return (

      <Suspense fallback={
Loading photo...
}>


from 'next/link'
import { LinkAccordion } from '../../components/link-accordion'

export default function FeedPage() {
  return (

Feed page

Step 1: Navigate without prefetch

        Click this link to navigate to the photo page. Since prefetching is
        disabled, the client has no cached data for this route and will fetch
        everything from the server. The response is then stored in the route

k

        After navigating to the photo page and back, toggle this checkbox to
        reveal a prefetched link to the same URL. The prefetch system should
        find the route data already in the ca

cache for future use.

        <Link href="/photo/1" prefetch={false} id="link-no-prefetch">
          Go to photo 1 (no prefetch)
        </Link>

Step 3: Reveal a prefetched lin


port { LinkAccordion } from '../../components/link-accordion'

// TODO: Once the appropriate API exists/is implemented, configure the param to
// be statically generated on demand but not at build time (`dynamicParams =
// true` isn't supported when `cacheComponents` is enabled.) For now this test case
// seems to work without extra configuration but it might not in the future.

export default function LazilyGeneratedParamsStartPage() {
  return (
    <>
      <p>
        Demonstrates that we can prefetch p

ot generated at build
        time but is lazily generated on demand

          <LinkAccordion href="/lazily-generated-params/some-param-value">
            Target
          </LinkAccordion>

    <


efault function OutputExport() {
  return (
    <>

        Demonstrates that per-segment prefetching works in{' '}
        output: export mode.

          <LinkAccordion href="/target-page">Target</LinkAccordion>

        The following link is rewritten on the server to the same page as the
        link above:

          <LinkAccordion href="/rewrite-to-target-page">
            Rewr

        The following link is redirected on the server to the same page as the
        link above:

          <LinkAccordion href="/redirect-to-target-page">
            Redirect to target pag

ite to target page
          </LinkAccordion>


ort { Suspense } from 'react'

type Params = { step: string }

async function Content({ params }: { params: Promise<Params> }) {
  const { step } = await params
  return (

{`Page ${step}.`}

      {/* Render a large string such that a prefetch of this segment is roughly
          1MB over the network */}

{'a'.repeat(1024 * 1024)}

  )
}

export default async function Page({ params }: { params: Promise<Params> }) {
  return (
    <

gger the
  // default LRU limit used by the client prefetch cache.
  // TODO: Once we add a config option to set the prefetch limit, we can set a
  // smaller number, to speed up testing iteration (and CI).
  const result: Array<Params> = []
  for (let i =

Suspense fallback="Loading...">
      <Content params={params} />
    </Suspense>
  )
}

export async function generateStaticParams(): Promise<Array<Params>> {
  // Generate some number of steps. This should be just enough to tri


from 'e2e-utils'
import { build, start } from './servers.mjs'

describe('segment cache (deployment skew)', () => {
  if (isNextDev) {
    test('should not run during dev', () => {})
    return
  }

  const { next } = nextTestSetup({
    files: __dirname,
    skipStart: true,
    env: {
      // rely on skew protection when deployed
      NEXT_DEPLOYMENT_ID: isNextDeploy ? undefined : 'test-deployment-id',
    },
    disableAutoSkewProtection: true,
  })

  // To debug these tests locally, first build the ap

// based on the request information.

  if (isNextStart) {
    let cleanup: () => Promise<void>
    let port: number

    describe('with BUILD_ID', () => {
      beforeAll(async () => {
        build('BUILD_ID')
        const proxyPort = (port = await find

p:
  //   node build.mjs
  //
  // Then start:
  //   node start.mjs
  //
  // This will build two versions of the same app on different ports, then
  // start a proxy server that rewrites incoming requests to one or the other


ccordion'

export default function Page() {
  return (

Prefetch inlining home

          <LinkAccordion href="/test-small-chain">Small chain</LinkAccordion>

          <LinkAccordion href="/test-deep/a/b/c">Deep chain</LinkAccordion>

          <LinkAccordion href="/test-outlined">Outlined</LinkAccordion>

          <LinkAccordion href="/test-restart/large-middle/after">

c</LinkAccordion>

          <LinkAccordion href="/test-runtime-bailout">
            Runtime bailout
          </LinkAccordion>

          <LinkAccordion href="/test-stale-hints/nested/deep">

Restart
          </LinkAccordion>

          <LinkAccordion href="/test-parallel">Parallel</LinkAccordion>

          <LinkAccordion href="/test-dynamic/hello">Dynami


mport { ReactNode } from 'react'
import { NoInline } from '../../../components/no-inline'

// This layout renders large content that depends on the route param.
// In a fallback shell render, `await params` causes postponement so
// the segment output is small. In a concrete render the full content
// appears, pushing the segment above the 2KB gzip inlining threshold.
// If hints were incorrectly based on the fallback shell, this layout
// would appear small and get inlined — the test catches that.
export d

unction Layout({
  children,
  params,
}: {
  children: ReactNode
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  return (

      <NoInline />

{`Dynamic layout for: ${slug}`}

      {children}

  )


ort { LinkAccordion } from '../../../components/link-accordion'

// The page segment itself is fully static — it doesn't access searchParams
// or cookies. Only the metadata accesses searchParams (making the head
// depend on runtime data) and the [item] param (so it differs per route).
// The page segment content is the same for all param values.
export async function generateStaticParams() {
  return [{ item: 'a' }, { item: 'b' }]
}

export async function generateMetadata({
  params,
  searchParams,
}: {

xport default async function Page({
  params,
}: {
  params: Promise<{ item: string }>
}) {
  const { item } = await params
  const sibling = item === 'a' ? 'b' : 'a'
  return (

Independent head page

  params: Promise<{ item: string }>
  searchParams: Promise<{ q?: string }>
}) {
  const { item } = await params
  const { q } = await searchParams
  return { title: `Independent Head Title: ${item}${q ? ` (q=${q})` : ''}` }
}

e


Link from 'next/link'
import { connection } from 'next/server'

export default async function SamePageNav() {
  // Treat the page as dynamic so we can detect when it's refreshed
  await connection()

  return (
    <>

        Demonstrates that when navigating to the exact same URL as the current
        location, we refresh the page segments.

        Observe that the random number below changes if you click the same link
        multiple times, but not when you switch between

<Link href="/same-page-nav">Link to current page</Link>

          <Link href="/same-page-nav#hash-a">
            Link to current page with hash fragment #hash-a
          </Link>

links.

        Random number (changes on each refresh):{' '}

          {Math.floor(Math.random() * 1_000_000_000)}


spense } from 'react'
import { cacheLife } from 'next/cache'

export default function Page() {
  return (

        Caches that use the 'seconds' profile will be omitted from static
        prerenders, making this essentially dynamic. If we omit it, it should
        not affect the cache life of the prerender.

      <Suspense fallback="Loading...">
        <ShortLivedContent />
      </Suspense>

Longer lived caches should still affect the cache life of the

)
}

async function ShortLivedContent() {
  'use cache'
  cacheLife('seconds')
  return
Short-lived cached content

}

async function LongerLivedContent() {
  'use cache'
  cacheLife('minutes')
  return
Longer-lived cached content

}

 page.</p>
      <LongerLivedContent />
    </main>


eRouterAct } from 'router-act'

// Tests prefetch inlining with maxSize and maxBundleSize set to Infinity,
// which inlines all segments into a single response per route. This
// approximates the behavior of pre-Segment Cache (pre-Next 16) prefetching,
// where all prefetch data was bundled into one response. The tradeoff is that
// you lose the benefits of per-layout deduplication across routes.
//
// This is a special case of the general prefetch inlining feature tested in
// the sibling `prefetch-inlinin

, () => {})
    return
  }

  it('bundles all segment data into a single request per route', async () => {
    // The test app has two routes that are 5 segments deep:
    //   /shared/a/b/c  and  /shared/a/d/e
    // Without inlining, prefetching each rou

g` directory. We may consolidate these tests
// in the future.
describe('max prefetch inlining', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })
  if (isNextDev) {
    it('disabled in development'


t should
// be able to inline through it and into the static child below, because this
// layout acts as a transparent pass-through — its slot in the bundle is null
// but the chain isn't broken.
import { ReactNode, Suspense } from 'react'
import { cookies } from 'next/headers'

export const unstable_instant = {
  unstable_samples: [{ cookies: [{ name: 'theme', value: 'default' }] }],
}
export const unstable_prefetch = 'force-runtime'

async function DynamicContent() {
  const cookieStore = await cookies()

default'
  return
Runtime layout (theme: {theme})

}

export default function RuntimePassthroughLayout({
  children,
}: {
  children: ReactNode
}) {
  return (

      <Suspense fallback={
Loading layout...</

const theme = cookieStore.get('theme')?.value ?? '


default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (

Route Cache Keying Regression Test

            Reproduces a bug where navigating to an unprefetched route stored
            the route cache entry with an incorrect key, causing subsequent
            prefetches to the same URL to miss the cache and ma

taleTimes feature to keep route cache
            entries alive across navigations. The client cache currently only
            writes segment data during prefetches, not navigations, so
            staleTimes is needed to preserve the entries for reuse. O

ke redundant
            requests. See{' '}
            #88863.

            This test relies on the s


kAccordion } from '../../../components/link-accordion'

export default function InPageLoadingBoundaryIndexPage() {
  return (

In-Page Loading Boundary Test

        Verifies that a page with an in-page Suspense boundary (instead of a
        separate loading.tsx) produces a shareable prefetch. Since there is no
        generateStaticParams, the segment prefetch is generated from a prerender
        where params are unresolved (hanging). T

LinkAccordion href="/in-page-loading-boundary/phone">
            Phone
          </LinkAccordion>
        </li>
        <li>
          <LinkAccordion href="/in-page-loading-boundary/tablet">
            Tablet
          </LinkAccordion>
        </li>

he child component that awaits
        params suspends, so the prefetch contains only the Suspense fallback
        with empty varyParams — making it reusable across all slug values.

          <


Suspense } from 'react'
import { DebugRenderKind } from '../../shared'

export const unstable_instant = true
export const unstable_prefetch = 'force-runtime'
export default async function Layout({ children }) {
  return (

Shared layout

        <DebugRenderKind />

          This shared layout uses cookies and no uncached IO, so it should be
          completely runtime-prefetchable.

        <Suspense fallback="Loadin

Suspense>

      {children}

  )
}

async function RuntimePrefetchable() {
  await cookies()
  return (

      Runtime-prefetchable content from shared layout

g ...">
          <RuntimePrefetchable />
        </


next/headers'
import { DebugLinkAccordion } from '../../../components/link-accordion'
import { Suspense } from 'react'

// No `export const unstable_instant = ...` is needed, we default to static
// (but see page description for more on the actual behavior)

export default function Page() {
  return (

Child of a runtime prefetchable layout

          When this page is prefetched from outside of the{' '}
          /segment-config/runtime-prefetchable</c

<Suspense fallback="Loading...">
          <RuntimePrefetchable />
        </Suspense>

          A page that shares some layouts with this page, and should use a
          static prefetch:

ode> segment, the page
          contents will be runtime-prefetched (despite this segment not being
          marked as runtime-prefetchable) because the parent layout is marked as
          runtime-prefetchable.
        </p>


{ cookies } from 'next/headers'
import { connection } from 'next/server'
import { Suspense } from 'react'

// Technically, no `export const unstable_instant = ...` is needed, because we default to static,
// this is just to make sure that we excercise the codepaths for it
export const unstable_instant = true
export default function Page() {
  return (

Statically prefetchable

        This page is a child of a runtime-pre

   <Suspense fallback="Loading...">
        <DynamicContent />
      </Suspense>
    </main>
  )
}

async function RuntimePrefetchable() {
  await cookies()
  return (
    <div id="runtime-prefetchable-content-page">
      Runtime-prefetchable content from

fetchable layout that is not
        configured as runtime-prefetchable. We should not use a runtime prefetch
        for it.
      </p>
      <Suspense fallback="Loading...">
        <RuntimePrefetchable />
      </Suspense>


import { cookies } from 'next/headers'
import { Suspense } from 'react'
import { DebugRenderKind } from '../../../shared'

// No `export const unstable_instant = ...` is needed, we default to static

export default async function SubLayout({ children }) {
  return (

Sub-layout

        <DebugRenderKind />

          This sub-layout uses cookies, but did not specify that it should be
          runtime prefetchable, so it should

ble />
        </Suspense>

      {children}

  )
}

async function RuntimePrefetchable() {
  await cookies()
  return (

      Runtime-prefetchable content from sub-

be prefetched statically by
          default.

        <Suspense
          fallback={

Loading ...

          }
        >
          <RuntimePrefetcha


mport { cookies } from 'next/headers'
import { Suspense } from 'react'

export const unstable_instant = true
export const unstable_prefetch = 'force-runtime'

export default function Page() {
  return (

        A runtime-prefetchable child of a runtime-prefetchable layout

        This page has two relevant parent layouts:

        1. A runtime-prefetchable shared layout layout.

tatically.
      </p>
      <Suspense fallback="Loading...">
        <RuntimePrefetchable />
      </Suspense>
    </main>
  )
}

async function RuntimePrefetchable() {
  await cookies()
  return (
    <div id="runtime-prefetchable-content-page">
      Run

2. A statically-prefetchable sub-layout not shared with other pages.

        We should use a runtime prefetch for it without any overrides on Link,
        but its sub-layout should be prefetched s


kies } from 'next/headers'
import { Suspense } from 'react'
import { cachedDelay, DebugRenderKind, uncachedIO } from '../shared'
import { connection } from 'next/server'

export default async function Layout({ children }) {
  return (

Shared layout

        <DebugRenderKind />

          This shared layout uses cookies and some uncached IO, so parts of it
          should be runtime-prefetchable.

        <Suspense

ieStore = await cookies()
  const cookieValue = cookieStore.get('testCookie')?.value ?? null
  await cachedDelay([__filename, cookieValue])
  return (

{`Cook

fallback={
Loading 1...
}>
          <RuntimePrefetchable />
        </Suspense>

      {children}

  )
}

async function RuntimePrefetchable() {
  const cook


ort { Suspense } from 'react'
import { cachedDelay, uncachedIO } from '../../shared'
import { cookies } from 'next/headers'

export default function Page() {
  return (

Page two

      <Suspense fallback={
Loading 1...
}>
        <RuntimePrefetchable />
      </Suspense>

  )
}

async function RuntimePrefetchable() {
  const cookieStore = await cookies()
  const cookieValue = cookieStore.get('testCookie')?.valu

ack={
Loading 2...
}>
        <Dynamic />
      </Suspense>

  )
}

async function Dynamic() {
  await uncachedIO()
  return (

      <div id="dyna

e ?? null
  await cachedDelay([__filename, cookieValue])
  return (

{`Cookie from page: ${cookieValue}`}

      <Suspense fallb


ort { Suspense } from 'react'
import { cachedDelay, uncachedIO } from '../../shared'
import { cookies } from 'next/headers'

export default function Page() {
  return (

Page one

      <Suspense fallback={
Loading 1...
}>
        <RuntimePrefetchable />
      </Suspense>

  )
}

async function RuntimePrefetchable() {
  const cookieStore = await cookies()
  const cookieValue = cookieStore.get('testCookie')?.val

back={
Loading 2...
}>
        <Dynamic />
      </Suspense>

  )
}

async function Dynamic() {
  await uncachedIO()
  return (

      <div id="dyn

ue ?? null
  await cachedDelay([__filename, cookieValue])
  return (

{`Cookie from page: ${cookieValue}`}

      <Suspense fall


 () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })
  if (isNextDev) {
    test('disabled in development', () => {})
    return
  }

  it('router.refresh() refreshes both cached and dynamic data', async () => {
    // Load the main Dashboard page. This will render the nav bar into the
    // @navbar slot.
    let page: Playwright.Page
    const browser = await next.browser('/dashboard', {
      beforePageLoad(p: Playwright.Page) {
        page = p
      },
    })
    const act

    const toggleAnalyticsLink = await browser.elementByCss(
        'input[data-link-accordion="/dashboard/analytics"]'
      )
      await toggleAnalyticsLink.click()
      const link = await browser.elementByCss('a[href="/dashboard/analytics"]')
      aw

= createRouterAct(page)

    // Navigate to the Analytics page. The analytics page does not match the
    // @navbar slot, so the client reuses the one that was rendered by the
    // previous page.
    await act(async () => {


ort { LinkAccordion } from '../../components/link-accordion'
import { ClientRefreshButton } from './client'
import { refresh } from 'next/cache'

function ServerRefreshButton() {
  return (
    <form
      action={async () => {
        'use server'
        refresh()
      }}
    >
      <button id="server-refresh-button">Server refresh</button>
    </form>
  )
}

export default function DashboardLayout({
  navbar,
  main,
}: {
  navbar: React.ReactNode
  main: React.ReactNode
}) {
  return (
    <div>

n href="/dashboard">Dashboard Home</LinkAccordion>

            <LinkAccordion href="/dashboard/analytics">Analytics</LinkAccordion>

            <LinkAccordion href="/docs">Docs</LinkAccordion>

        {navbar}

          <ClientRefreshButton />
          <ServerRefreshButton />

            <LinkAccordio


ction } from 'next/server'
import { RenderCounterClient } from './client'

async function DynamicRenderCounter(): Promise<React.ReactNode> {
  // Renders a count of the number of times the client receives new dynamic data
  // from the server. The count is computed on the client and stored in React
  // state, so it gets reset if the state of the tree is reset.
  await connection()
  const uuid = crypto.randomUUID()
  return <RenderCounterClient uuid={uuid} />
}

export default function DashboardNavbarLandi

<Suspense fallback={
Loading...
}>

          Navbar dynamic render counter:{' '}

            <DynamicRenderCounter />

      </Suspense>


retry } from 'next-test-utils'

describe('segment cache (MPA navigations)', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })
  if (isNextDev) {
    test('ppr is disabled', () => {})
    return
  }

  type ElementWithExpando = Element & { __expando?: boolean }

  it('triggers MPA navigation when navigating to a different root layout', async () => {
    const browser = await next.browser('/')

    // Set an expando on the html element so we can detect if the page
    // gets un

lementByCss(`a[href="/foo"]`)
    await link.click()

    // The expando should not be present because we did a full-page navigation.
    await retry(async () => {
      const htmlAfterNav = await browser.elementByCss('html')
      expect(
        await ht

loaded.
    const html = await browser.elementByCss('html')

    await html.evaluate((el) => ((el as ElementWithExpando).__expando = true))

    // Navigate to a page with a different root layout.
    const link = await browser.e


enerateStaticParams() {
  return [{ rootParam: 'foo' }, { rootParam: 'bar' }]
}

export default async function Page(props: {
  params: Promise<{ rootParam: string }>
}) {
  const { rootParam } = await props.params
  const otherRootParam = rootParam === 'foo' ? 'bar' : 'foo'
  return (
    <>

          This link should not trigger an MPA navigation, because it
          only changes a root param:

        <Link href={`/${otherRootParam}`}>/{otherRootParam}</Link>

em>should</em> trigger an MPA navigation, because in
          addition to changing a root param, it also navigates to a different
          root layout:
        </p>
        <Link href={`/${otherRootParam}/inner`}>/{otherRootParam}/inner</Link>
      </p>


eRouterAct } from 'router-act'

describe('App Shell prefetching', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })
  if (isNextDev) {
    it('is skipped', () => {})
    return
  }

  it('reuses the app shell across different param values so navigation to an unprefetched route is instant', async () => {
    let page: Playwright.Page
    const browser = await next.browser('/', {
      beforePageLoad(p: Playwright.Page) {
        page = p
      },
    })
    const act = createRo

posts" substring appears in two responses: one
    // for the App Shell itself and one for the per-link prefetch of
    // /posts/1 (which, for a dynamic page, also includes the shell
    // content above its inner Suspense fallback).
    await act(async (

uterAct(page)

    // Reveal the LinkAccordion for /posts/1. This caches the App Shell
    // for the route — the param-independent content of the page that's
    // reusable for any /posts/[id].
    //
    // The "App shell for


import Link from 'next/link'
import { LinkAccordion } from '../components/link-accordion'

export default function Page() {
  return (

Home

Dynamic posts

        These posts read request-time data (cookies). Their App Shell is the
        part of the page that doesn't depend on the URL params, so it can
        be cached once and reused for any post.

          <LinkAccordion href="/posts/1">Post 1</LinkAccordion>

posts/124" prefetch={false}>
            Unprefetched post
          </Link>
        </li>
        <li>
          <Link href="/posts/125?foo=bar" prefetch={false}>
            Unprefetched post with search params
          </Link>
        </li>
      </ul>

          <LinkAccordion href="/posts/2">Post 2</LinkAccordion>

          <LinkAccordion href="/posts/3">Post 3</LinkAccordion>

          <Link href="/


'react'
import { cookies } from 'next/headers'
import { connection } from 'next/server'

type Params = { id: string }

export const unstable_prefetch = 'force-runtime'

export default function Page({ params }: { params: Promise<Params> }) {
  return (

      {/* Cookies are part of the request context, so they're available
          when the App Shell is prerendered. The result is included in
          the cached shell. */}
      <Suspense fallback={
Loading cookie...
}>

aramsDependent params={params} />
      </Suspense>
    </main>
  )
}

async function CookieDependent() {
  const cookieStore = await cookies()
  const value = cookieStore.get('testCookie')?.value ?? 'none'
  return <p id="cookie-value">{`Cookie: ${value}`

<CookieDependent />
      </Suspense>
      {/* The fallback is the App Shell — the part of the page that
          doesn't depend on params. */}
      <Suspense fallback={
App shell for posts
}>
        <P


layout-max'
  | 'tag-layout-legacy'
  | 'server-action-tag-layout-expireNow'
  | 'server-action-tag-layout-max'
  | 'server-action-tag-layout-legacy'

type SegmentPrefetchResponse = {
  body: string
  requestPathname: string
  requestSegmentPath: string
  segmentPrefetchPath: string
  status: number
}

describe('segment cache - vary params base dynamic', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })

  if (isNextDev) {
    test('prefetching is disabled in dev mode', () =>

l',
    '/acme/dashboard/settings':
      'Project settings overview content - team: acme, project: dashboard',
    '/globex/portal/settings':
      'Project settings overview content - team: globex, project: portal',
    '/acme/dashboard/settings/domains'

{})
    return
  }

  const expectedTextByHref: Record<string, string> = {
    '/acme/dashboard': 'Team project content - team: acme, project: dashboard',
    '/globex/portal': 'Team project content - team: globex, project: porta


lidateLayoutByTagLegacyAction,
  revalidateLayoutByTagMaxAction,
} from '../app/revalidate-actions'

type ApiRevalidateMode =
  | 'tag-layout-expireNow'
  | 'tag-layout-max'
  | 'tag-layout-legacy'
  | 'path-root-layout'
  | 'path-team-layout'
  | 'path-team-page'
type ServerActionMode =
  | 'server-action-tag-layout-expireNow'
  | 'server-action-tag-layout-max'
  | 'server-action-tag-layout-legacy'

export function RevalidateControls() {
  const [result, setResult] = useState('idle')
  const [isPending, se

    })
      const data = await response.json()
      setResult(JSON.stringify(data))
    } catch (error) {
      setResult(
        JSON.stringify({
          error: error instanceof Error ? error.message : 'unknown error',
          mode,
        })

tIsPending] = useState(false)

  const runApiRevalidate = async (mode: ApiRevalidateMode) => {
    setIsPending(true)
    try {
      const response = await fetch(`/api/revalidate-layout?mode=${mode}`, {
        method: 'GET',


nk'
import { LinkAccordion } from '../components/link-accordion'
import { RevalidateControls } from '../components/revalidate-controls'

export default function HomePage() {
  return (

Root Dynamic Route Vary Params

        Prefetch dynamic team/project routes and validate segment payload
        params.

          <Link href="/acme/dashboard" data-nav-link="/acme/dashboard">
            Navigate: a

         href="/acme/dashboard/settings/domains"
            data-nav-link="/acme/dashboard/settings/domains"
          >
            Navigate: acme/dashboard/settings/domains
          </Link>
        </li>
        <li>
          <Link
            href="/

cme/dashboard
          </Link>

          <Link href="/globex/portal" data-nav-link="/globex/portal">
            Navigate: globex/portal
          </Link>

          <Link


from 'next/link'
import { Suspense } from 'react'
import { LinkAccordion } from '../../../components/link-accordion'

type Params = { teamSlug: string; project: string }

export default function TeamProjectPage({
  params,
}: {
  params: Promise<Params>
}) {
  return (

      <Suspense
        fallback={
Loading team/project route...
}
      >
        <TeamProjectContent params={params} />
      </Suspense>

           Navigate: acme/dashboard
            </Link>
          </li>
          <li>
            <Link href="/globex/portal" data-nav-link="/globex/portal">
              Navigate: globex/portal
            </Link>
          </li>
          <li>

            <Link href="/" data-nav-link="/">
              Navigate: home
            </Link>

            <Link href="/acme/dashboard" data-nav-link="/acme/dashboard">


from 'next/link'
import { Suspense } from 'react'
import { LinkAccordion } from '../../../../components/link-accordion'

type Params = { teamSlug: string; project: string }

export default function ProjectSettingsPage({
  params,
}: {
  params: Promise<Params>
}) {
  return (

      <Suspense
        fallback={
Loading settings page...
}
      >
        <ProjectSettingsContent params={params} />
      </Suspense>
      <nav data-nav-link-

/domains"
              data-nav-link="/acme/dashboard/settings/domains"
            >
              Navigate: acme/dashboard/settings/domains
            </Link>
          </li>
          <li>
            <Link
              href="/globex/portal/settings/

list="true">

            <Link href="/" data-nav-link="/">
              Navigate: home
            </Link>

            <Link
              href="/acme/dashboard/settings


from 'next/link'
import { Suspense } from 'react'
import { LinkAccordion } from '../../../../../../components/link-accordion'

type Params = { teamSlug: string; project: string }

export default function ProjectDomainsSettingsPage({
  params,
}: {
  params: Promise<Params>
}) {
  return (

      <Suspense
        fallback={

            Loading project settings domains page...

        }
      >
        <Proj

ashboard/settings"
            >
              Navigate: acme/dashboard/settings
            </Link>
          </li>
          <li>
            <Link
              href="/globex/portal/settings"
              data-nav-link="/globex/portal/settings"

ectDomainsSettingsContent params={params} />
      </Suspense>

            <Link
              href="/acme/dashboard/settings"
              data-nav-link="/acme/d


revalidateTag } from 'next/cache'
import { NextRequest } from 'next/server'

type RevalidateMode =
  | 'tag-layout-expireNow'
  | 'tag-layout-max'
  | 'tag-layout-legacy'
  | 'path-root-layout'
  | 'path-team-layout'
  | 'path-team-page'

function isRevalidateMode(value: string): value is RevalidateMode {
  return (
    value === 'tag-layout-expireNow' ||
    value === 'tag-layout-max' ||
    value === 'tag-layout-legacy' ||
    value === 'path-root-layout' ||
    value === 'path-team-layout' ||
    value =

ry
      : 'tag-layout-expireNow'

  switch (mode) {
    case 'tag-layout-expireNow':
      revalidateTag('_N_T_/layout', 'expireNow')
      break
    case 'tag-layout-max':
      revalidateTag('_N_T_/layout', 'max')
      break
    case 'tag-layout-legacy

== 'path-team-page'
  )
}

export async function GET(request: NextRequest) {
  const modeQuery = request.nextUrl.searchParams.get('mode')
  const mode: RevalidateMode =
    modeQuery && isRevalidateMode(modeQuery)
      ? modeQue


/dynamic-params/123"]`
        )
        await linkToggle.click()
      }, [
        // Should allow reading dynamic params
        {
          includes: 'Param: 123',
        },
        // Should not prefetch the dynamic content
        {
          includes: 'Dynamic content',
          block: 'reject',
        },
      ])

      // Reveal the link to trigger a runtime prefetch for a different value of the dynamic param
      await act(async () => {
        const linkToggle = await browser.elementByCss(

    // Should not prefetch the dynamic content
        {
          includes: 'Dynamic content',
          block: 'reject',
        },
      ])

      // Navigate to the page
      await act(async () => {
        await act(
          async () => {

        `input[data-link-accordion="/${prefix}/dynamic-params/456"]`
        )
        await linkToggle.click()
      }, [
        // Should allow reading dynamic params
        {
          includes: 'Param: 456',
        },


import Link, { type LinkProps } from 'next/link'
import { ComponentProps, useState } from 'react'

export function LinkAccordion({
  href,
  children,
  prefetch,
}: {
  href: string
  children: React.ReactNode
  prefetch?: LinkProps['prefetch']
}) {
  const [isVisible, setIsVisible] = useState(false)
  return (
    <>
      <input
        type="checkbox"
        checked={isVisible}
        onChange={() => setIsVisible(!isVisible)}
        data-link-accordion={href}
        data-prefetch={getPrefetchKind(

cordion({
  href,
  prefetch,
}: Omit<ComponentProps<typeof LinkAccordion>, 'children'>) {
  const prefetchKind = getPrefetchKind(prefetch)
  return (
    <LinkAccordion href={href} prefetch={prefetch} data-prefetch={prefetch}>
      {href} ({prefetchKind}

prefetch)}
      />
      {isVisible ? (
        <Link href={href} prefetch={prefetch}>
          {children}
        </Link>
      ) : (
        <>{children} (link is hidden)</>
      )}
    </>
  )
}

export function DebugLinkAc


import { cookies } from 'next/headers'
import { Suspense } from 'react'
import { cachedDelay, DebugRenderKind } from '../shared'

export default async function Layout({ children }) {
  return (

Shared layout

        <DebugRenderKind />

          This shared layout uses cookies and no uncached IO, so it should be
          completely runtime-prefetchable.

        <Suspense fallback={

alue = cookieStore.get('testCookie')?.value ?? null
  await cachedDelay([__filename, cookieValue])
  return (

{`Cookie from layout: ${cookieValue}`}

Loading 1...
}>
          <RuntimePrefetchable />
        </Suspense>

      {children}

  )
}

async function RuntimePrefetchable() {
  const cookieStore = await cookies()
  const cookieV


{ Suspense } from 'react'
import { cachedDelay } from '../../shared'
import { cookies } from 'next/headers'

export default function Page() {
  return (

Page one

      <Suspense fallback={
Loading 1...
}>
        <RuntimePrefetchable />
      </Suspense>

  )
}

async function RuntimePrefetchable() {
  const cookieStore = await cookies()
  const cookieValue = cookieStore.get('testCookie')?.value ?? null
  aw

refetched layout that had no holes itself will still be considered partial
        if any other segment in the response is partial, because we don't track partiality per-segment,
        so if we want to test that full prefetches can reuse layouts from run

ait cachedDelay([__filename, cookieValue])
  return (

{`Cookie from page: ${cookieValue}`}

      {/*
        TODO: a runtime-p


pense } from 'react'

/**
 * Page that accesses searchParams with runtime prefetching.
 *
 * By awaiting searchParams, this page's cache key varies by search params.
 * Different search param values will NOT share cached prefetch data.
 *
 * The searchParams access must be inside a Suspense boundary for runtime
 * prefetching to work - the page shell is static, the searchParams-dependent
 * content is runtime-prefetchable.
 *
 * Expected behavior:
 * - Prefetching /target?foo=1 fetches the segment with foo=

e SearchParams = { foo?: string }

export default async function SearchParamsTargetPage({
  searchParams,
}: {
  searchParams: Promise<SearchParams>
}) {
  return (

      <Suspense fallback={
Loading search para

1 content
 * - Prefetching /target?foo=2 fetches the segment AGAIN (no cache hit)
 */
export const unstable_instant = {
  unstable_samples: [{ searchParams: { foo: '1' } }],
}
export const unstable_prefetch = 'force-runtime'

typ


etch page that does NOT access searchParams.
 *
 * Since searchParams are not accessed, the '?' sentinel is not added to
 * varyParams, and different search param values share the cached segment.
 */
export const unstable_instant = {
  unstable_samples: [{ searchParams: { q: '1' } }],
}
export const unstable_prefetch = 'force-runtime'

export default async function RuntimePrefetchSearchParamsTargetPage() {
  // Intentionally NOT accessing searchParams
  return (
    <div id="runtime-prefetch-search-params-t

t - searchParams not accessed

      <Suspense fallback={
Loading...
}>
        <DynamicContent />
      </Suspense>

  )
}

async function DynamicContent() {
  await connection()
  return
Dynamic co

arget">
      <div data-content="true">Static conten


from '../../../components/link-accordion'

/**
 * Index page for the page segment reuse test.
 *
 * This test demonstrates a non-obvious case: a page segment can be reused when
 * it doesn't access a param that the layout DOES access. This is the inverse
 * of the typical pattern where layouts are reused while pages vary.
 *
 * Setup:
 * - Layout accesses BOTH `category` AND `item` → varies on both
 * - Page accesses ONLY `category` → varies only on category
 *
 * When navigating between routes that share t

 *
 * Manual testing:
 * 1. Click checkbox for "phone" — layout and page both fetched
 * 2. Click checkbox for "tablet" — layout re-fetched, page is cache hit
 */
export default function PageReuseIndexPage() {
  return (
    <div id="page-reuse-index">

he same category but different
 * items (e.g., /electronics/phone → /electronics/tablet), the layout must be
 * re-fetched (it accesses `item`), but the page is a cache hit (it only
 * accesses `category`, which hasn't changed).


t that accesses ONLY the `category` param, NOT `item`.
 *
 * This is the key to this test: even though the URL includes both params,
 * this page only accesses `category`. Therefore:
 * - When navigating between /electronics/phone and /electronics/tablet,
 *   this page segment is a cache hit (category unchanged)
 *
 * The layout accesses both params, so it must be re-fetched when `item`
 * changes. This demonstrates that param access tracking is per-segment,
 * allowing fine-grained cache reuse.
 */
export

 {
  params: Promise<Params>
}) {
  // NOTE: Only accessing `category`, NOT `item`
  // This is intentional to demonstrate page reuse when item changes
  const { category } = await params

  return (
    <div id="page-reuse-page">
      <div data-page-cate

 default async function PageReusePage({
  params,
}:


pense } from 'react'
import { connection } from 'next/server'

/**
 * Runtime prefetch page where NO params are accessed in the static portion.
 *
 * Both category and itemId are accessed only after connection(), so they
 * are NOT tracked in varyParams. This means ALL param combinations should
 * share the same cached loading shell (empty vary params set = max sharing).
 */
export const unstable_instant: {
  unstable_samples: Array<{ params: { category: string; itemId: string } }>
} = {
  unstable_samples:

}

export default async function RuntimePrefetchNoVaryPage({
  params,
}: {
  params: Promise<Params>
}) {
  return (

      <Suspense
        fallback={

Loading all content dy

 [
    { params: { category: 'electronics', itemId: 'phone' } },
    { params: { category: 'clothing', itemId: 'shirt' } },
  ],
}
export const unstable_prefetch = 'force-runtime'

type Params = { category: string; itemId: string


kAccordion } from '../../../components/link-accordion'

/**
 * Index page for the metadata-no-params vary params test.
 *
 * This is the inverse of the metadata test: generateMetadata does NOT access
 * params, so the head segment should be cacheable across different param values.
 *
 * Setup:
 * - [slug]/page.tsx has generateMetadata that does NOT access params
 * - The page body does NOT access params either
 * - Links go to /metadata-no-params/aaa, /metadata-no-params/bbb
 *
 * Expected behavior:
 * - Fi

ontent fetched
 * 2. Click checkbox for "bbb" - cache hit, no new request
 */
export default function MetadataNoParamsIndexPage() {
  return (

Metadata No Params Test

        Tests that when

rst prefetch fetches head and body segments
 * - Subsequent prefetches with different slugs are cache hits
 *   (neither metadata nor body access params)
 *
 * Manual testing:
 * 1. Click checkbox for "aaa" - triggers prefetch, c


'react'
import { connection } from 'next/server'

/**
 * Runtime prefetch page where ALL params are accessed in the static portion.
 *
 * Both category and itemId are accessed before connection(), so they
 * are both tracked in varyParams. Every unique combination of (category, itemId)
 * requires its own prefetch — no cache sharing.
 */
export const unstable_instant: {
  unstable_samples: Array<{ params: { category: string; itemId: string } }>
} = {
  unstable_samples: [
    { params: { category: 'electron

imePrefetchAllVaryPage({
  params,
}: {
  params: Promise<Params>
}) {
  return (

      <Suspense fallback={
Loading...
}>
        <StaticContent params={params} />
      </Suspense>

  )
}

ics', itemId: 'phone' } },
    { params: { category: 'clothing', itemId: 'shirt' } },
  ],
}
export const unstable_prefetch = 'force-runtime'

type Params = { category: string; itemId: string }

export default async function Runt


from 'e2e-utils'
import type * as Playwright from 'playwright'
import { createRouterAct } from 'router-act'

describe('<Link prefetch="auto">', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
  })
  if (isNextDev) {
    it('disabled in development / deployment', () => {})
    return
  }

  // NOTE: Since "auto" is just an alias for the default, I'm not bothering to
  // write tests for every default prefetching behavior; that's covered by a
  // bunch of other test suites. This i

 page
    let page: Playwright.Page
    const browser = await next.browser('/', {
      beforePageLoad(p: Playwright.Page) {
        page = p
      },
    })
    const act = createRouterAct(page)

    // Reveal the link to trigger a prefetch
    await act(

s just a quick test to confirm that the
  // alias exists.

  it('<Link prefetch="auto"> works the same as if prefetch were undefined or null', async () => {
    // Test that the link only prefetches the static part of the target


n (

Home

        Links with prefetch=false

          <Link href="/partially-static" prefetch={false}>
            Go to partially static page
          </Link>

          <Link href="/fully-static" prefetch={false}>
            Go to fully static page
          </Link>

          <Link href="/with-static-params/foo" prefetch={false}>
            Go to page with static par

       <Link href="/with-fallback-params/foo" prefetch={false}>
            Go to page with fallback params
          </Link>
        </li>
        <li>
          <Link href="/runtime-prefetchable" prefetch={false}>
            Go to runtime-prefetchable p

ams
          </Link>
        </li>
        <li>


kies, headers } from 'next/headers'
import { connection } from 'next/server'
import { Suspense } from 'react'

export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ q?: string }>
}) {
  return (

      <CachedContent />

        <Suspense fallback={
Loading search params...
}>
          <SearchParamsContent searchParams={searchParams} />
        </Suspense>

        <Suspense fal

pense>

        <Suspense fallback={
Loading connection...
}>
          <ConnectionContent />
        </Suspense>

  )
}

async function CachedContent() {
  'use cache'
  cacheLi

lback={
Loading cookies...
}>
          <CookiesContent />
        </Suspense>

        <Suspense fallback={
Loading headers...
}>
          <HeadersContent />
        </Sus


import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { ReactNode } from 'react'
import { LinkAccordion } from '../../components/link-accordion'

export default function ParamsLayout({ children }: { children: ReactNode }) {
  const { bfcacheId } = useRouter()

  return (
    <>

          <Link href="/with-fallback-params" prefetch={false}>
            /with-fallback-params
          </Link>

          <Link href="/with-fallback-pa

ink>
        </li>
        <li>
          <LinkAccordion
            key={bfcacheId}
            href="/with-fallback-params/foo"
            prefetch={true}
          >
            /with-fallback-params/foo (prefetch=true)
          </LinkAccordion>

rams/foo" prefetch={false}>
            /with-fallback-params/foo
          </Link>
        </li>
        <li>
          <Link href="/with-fallback-params/bar" prefetch={false}>
            /with-fallback-params/bar
          </L


 opt-in has been removed. These tests need to be updated to use the segment configuration.
  it.skip('runtime prefetches should omit layouts that were already prefetched with a runtime prefetch', async () => {
    // Prefetches should re-use results from previous prefetches with the same fetch strategy.

    let page: Playwright.Page
    const browser = await next.browser('/', {
      beforePageLoad(p: Playwright.Page) {
        page = p
      },
    })
    // Clear cookies after the test. This currently do

er a runtime prefetch for page one
    await act(async () => {
      const linkToggle = await browser.elementByCss(
        `input[data-prefetch="runtime"][data-link-accordion="/shared-layout/one"]`
      )
      await linkToggle.click()
    }, [
      //

esn't happen automatically.
    await using _ = defer(() => browser.deleteCookies())

    const act = createRouterAct(page)

    await browser.addCookie({ name: 'testCookie', value: 'testValue' })

    // Reveal the link to trigg


he'
import { setTimeout } from 'timers/promises'

export async function uncachedIO() {
  await setTimeout(0)
}

export async function cachedDelay(key: any) {
  'use cache'
  cacheLife('minutes')
  await setTimeout(1)
}

export function DebugRenderKind() {
  const { workUnitAsyncStorage } =
    require('next/dist/server/app-render/work-unit-async-storage.external') as typeof import('next/dist/server/app-render/work-unit-async-storage.external')
  const workUnitStore = workUnitAsyncStorage.getStore()!
  retur

re.type}
      {(() => {
        switch (workUnitStore.type) {
          case 'prerender':
            return '(static prefetch)'
          case 'prerender-runtime':
            return '(runtime prefetch)'
          default:
            return null

n (
    <div>
      workUnitStore.type: {workUnitSto


pe Params = { slug?: string[] }

/**
 * Optional catch-all page that checks for the param using the `in` operator.
 *
 * Using `'slug' in params` should cause the segment to vary on the optional
 * catch-all param, even when the param has no value.
 */
export async function generateStaticParams(): Promise<Params[]> {
  return [{ slug: [] }, { slug: ['aaa'] }, { slug: ['bbb'] }]
}

export default async function OptionalCatchallHasPage({
  params,
}: {
  params: Promise<Params>
}) {
  const resolvedParams = a

const hasSlug = 'slug' in resolvedParams
  const slug = hasSlug ? resolvedParams.slug : undefined
  const slugDisplay = `Slug: ${slug ? slug.join('/') : 'none'}`
  return (

{slugDisplay}


n } from 'next/server'

/**
 * Runtime prefetch page - canonical test for runtime prefetching.
 *
 * This is the runtime prefetch equivalent of the instant-loading test:
 * - `category` is accessed in the static/cached portion → enables cache sharing
 * - `itemId` is accessed only in the dynamic portion → not relevant for caching
 *
 * This allows cache reuse across different itemId values (same category),
 * providing instant loading feedback when navigating.
 */
export const unstable_instant: {
  unstable

const unstable_prefetch = 'force-runtime'

type Params = { category: string; itemId: string }

export default async function RuntimePrefetchPage({
  params,
}: {
  params: Promise<Params>
}) {
  return (
    <div id="runtime-prefetch-page">
      {/* Outer

_samples: Array<{ params: { category: string; itemId: string } }>
} = {
  unstable_samples: [
    { params: { category: 'electronics', itemId: 'phone' } },
    { params: { category: 'clothing', itemId: 'shirt' } },
  ],
}
export


 time (30s)
    // but under the static cache's stale time (120s). If the stale time sent to
    // the client incorrectly used the runtime cache's value, the cached
    // segments would have expired and the second navigation wouldn't be
    // instant.
    await page.clock.fastForward(60_000)

    // Second navigation — cached static data should show immediately
    await act(async () => {
      await act(
        async () => {
          await browser.elementByCss('a[href="/partially-static"]').click()

          block: true,
        }
      )

      // The static/cached part should be visible while the dynamic
      // request is still blocked
      expect(await browser.elementById('cached-content').text()).toContain(
        'Cached content'
      )

      },
        {
          // Block the dynamic request. The cached/prefetchable content
          // should still be visible even though the dynamic data hasn't
          // arrived yet.
          includes: 'Dynamic content',


import { connection } from 'next/server'
import { Suspense } from 'react'

export async function generateStaticParams() {
  return [{ slug: 'foo' }]
}

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  return (

      <CachedContent />

Param: {slug}

        <Suspense fallback={
Loading connection...
}>
          <ConnectionConten

)
}

async function CachedContent() {
  'use cache'
  return
Cached content ({new Date().toISOString()})

}

async function ConnectionContent() {
  await connection()
  return
Dynamic content ({new Date().toISOString()})

}

t />
        </Suspense>
      </div>
    </main>


ugLinkAccordion } from '../components/link-accordion'
import { cacheLife } from 'next/cache'

export default async function Page() {
  'use cache'
  cacheLife('minutes')
  return (

shared layout prefetching - layout with cookies and dynamic data

          <DebugLinkAccordion href="/shared-layout/one" prefetch={true} />

          <DebugLinkAccordion href="/shared-layout/two" prefetch={'auto'} />

n
            href="/runtime-prefetchable-layout/one"
            prefetch={true}
          />
        </li>
        <li>
          <DebugLinkAccordion
            href="/runtime-prefetchable-layout/two"
            prefetch={'auto'}
          />
        <

          <DebugLinkAccordion href="/shared-layout/two" prefetch={true} />

shared layout prefetching - layout with cookies

          <DebugLinkAccordio


link-accordion'

/**
 * Index page for the runtime prefetch vary params test.
 *
 * This tests vary params tracking with runtime prefetching enabled. Unlike
 * static prefetching, runtime prefetching allows dynamic params to be rendered
 * as part of the prefetch (rather than being omitted). This enables testing
 * more specific combinations:
 *
 * - The page renders `category` in the static portion → tracked in varyParams
 * - The page renders `itemId` in the dynamic portion → NOT tracked in varyParams
 *

 checkbox for "phone" — triggers prefetch, page content fetched
 * 2. Click checkbox for "tablet" and "laptop" — cache hits (same category)
 * 3. Navigate to "headphones" — loading state instant, then dynamic content loads
 */
export default function Runti

 * Since `itemId` is only accessed in the dynamic section (inside Suspense after
 * connection()), the page should only vary on `category`, allowing cache reuse
 * across different itemId values.
 *
 * Manual testing:
 * 1. Click


arams = { category: string; item: string }

/**
 * Layout for the [category]/[item] segment. This layout:
 * - Accesses BOTH 'category' AND 'item' → must be re-fetched when either changes
 *
 * This is the inverse of the typical pattern. Usually the layout accesses fewer
 * params than the page, enabling layout reuse. Here, the layout accesses MORE
 * params than the page, which means:
 * - When `item` changes, the layout must be re-fetched
 * - But the page (which only accesses `category`) can be reused
 *

  { category: 'electronics', item: 'tablet' },
  ]
}

export default async function PageReuseLayout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Promise<Params>
}) {
  const { category, item } = await params

  return (
    <div data-

 * This tests that param access is tracked at the segment level, not just at
 * the route level.
 */
export async function generateStaticParams(): Promise<Params[]> {
  return [
    { category: 'electronics', item: 'phone' },


kies, headers } from 'next/headers'
import { connection } from 'next/server'
import { Suspense } from 'react'

export const unstable_instant = {
  unstable_samples: [
    {
      cookies: [{ name: 'testCookie', value: 'testValue' }],
      headers: [['x-test-header', 'test']],
      searchParams: {},
    },
  ],
}
export const unstable_prefetch = 'force-runtime'

export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ q?: string }>
}) {
  return (
    <main>
      <CachedContent

ry">
        <Suspense fallback={
Loading cookies...
}>
          <CookiesContent />
        </Suspense>

        <Suspense fallback={
Loading headers...
}>
          <HeadersContent />
        </Susp

/>

        <Suspense fallback={
Loading search params...
}>
          <SearchParamsContent searchParams={searchParams} />
        </Suspense>

      <div id="cookies-bounda


ort { Suspense } from 'react'
import { cachedDelay, uncachedIO } from '../../shared'
import { cookies } from 'next/headers'

export default function Page() {
  return (

Page two

      <Suspense fallback={
Loading 1...
}>
        <RuntimePrefetchable />
      </Suspense>

  )
}

async function RuntimePrefetchable() {
  const cookieStore = await cookies()
  const cookieValue = cookieStore.get('testCookie')?.valu

ack={
Loading 2...
}>
        <Dynamic />
      </Suspense>

  )
}

async function Dynamic() {
  await uncachedIO()
  return (

      <div id="dyna

e ?? null
  await cachedDelay([__filename, cookieValue])
  return (

{`Cookie from page: ${cookieValue}`}

      <Suspense fallb


 layout for the root params test.
 *
 * This layout is the first layout in its route tree (no app/layout.tsx above it),
 * which makes `rootParam` a "root param" accessible via `next/root-params`.
 *
 * The layout accesses rootParam via the next/root-params API to verify that
 * root param access is properly tracked in varyParams.
 */
export async function generateStaticParams(): Promise<Params[]> {
  return [{ rootParam: 'aaa' }, { rootParam: 'bbb' }]
}

export default async function RootParamsLayout({
  c

st param = await rootParam()

  return (

            {`Root param layout - param: ${param}`}

          {children}

hildren,
}: {
  children: React.ReactNode
}) {
  con


kAccordion } from '../../../components/link-accordion'

/**
 * Index page for the root params vary test.
 *
 * This page links to routes that use the rootParams() API from next/navigation.
 * The target routes are at /aaa and /bbb which have their own root layout
 * (no shared app/layout.tsx above them), making `rootParam` a true "root param".
 *
 * Setup:
 * - /aaa and /bbb have layouts that call rootParams()
 * - The rootParam value is accessed in both layout and page
 *
 * Expected behavior:
 * - Prefetc

d
 * 2. Click checkbox for "bbb" — should also fetch (not cached)
 */
export default function RootParamsIndexPage() {
  return (

Root Params Vary Test

        Tests that root param access via rootPa

hing /aaa fetches the content
 * - Prefetching /bbb triggers a NEW request (not a cache hit)
 *   because rootParam is tracked in varyParams
 *
 * Manual testing:
 * 1. Click checkbox for "aaa" — triggers prefetch, content fetche


{ cacheLife } from 'next/cache'
import { connection } from 'next/server'
import { Suspense } from 'react'

export default function Page({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  return (

      <CachedContent />

        <Suspense fallback={
Loading params...
}>
          <ParamsContent params={params} />
        </Suspense>

        <Suspense fallback={
Loading connection...
}>

}

async function ParamsContent({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  // When the param is a fallback param (not in generateStaticParams), the
  // await is deferred to the runtime stage, so this content won't appear
  // in the stati

<ConnectionContent />
        </Suspense>

  )
}

async function CachedContent() {
  'use cache'
  cacheLife({ stale: 120 })
  return
Cached content ({new Date().toISOString()})


'react'
import { cachedDelay, DebugRenderKind, uncachedIO } from '../../../../shared'
import { connection } from 'next/server'
import { lang } from 'next/root-params'

export const unstable_instant = {
  unstable_samples: [{ params: { lang: 'en' } }],
}
export const unstable_prefetch = 'force-runtime'

export default async function Page() {
  return (
    <main>
      <DebugRenderKind />
      <p>
        This page uses root params (inside a private cache) and some uncached
        IO. Private caches are on

<RuntimePrefetchable />
      </Suspense>

  )
}

async function RuntimePrefetchable() {
  const currentLang = await privateCache()
  return (

{`Lan

ly rendered during runtime prefetches and
        navigation requests, so they won't be part of a static prefetch, but
        they should be part of a runtime prefetch.
      </p>
      <Suspense fallback="Loading 1...">


'react'
import { cachedDelay, DebugRenderKind, uncachedIO } from '../../../../shared'
import { connection } from 'next/server'
import { lang } from 'next/root-params'

export const unstable_instant = {
  unstable_samples: [{ params: { lang: 'en' } }],
}
export const unstable_prefetch = 'force-runtime'

export default async function Page() {
  return (
    <main>
      <DebugRenderKind />
      <p>
        This page uses root params (passed to a private cache) and some uncached
        IO. Root params should

hable />
      </Suspense>

  )
}

async function RuntimePrefetchable() {
  const currentLang = await publicCache(lang())
  return (

{`Lang: ${curren

 always be available in static prerenders, so a
        runtime prefetch should have them too, and they should not be considered
        a hanging input.
      </p>
      <Suspense fallback="Loading 1...">
        <RuntimePrefetc


pense } from 'react'
import { cachedDelay, DebugRenderKind, uncachedIO } from '../../../../shared'
import { connection } from 'next/server'
import { lang } from 'next/root-params'
import { cookies } from 'next/headers'

export const unstable_instant = {
  unstable_samples: [{ params: { lang: 'en' } }],
}
export const unstable_prefetch = 'force-runtime'

export default async function Page() {
  return (
    <main>
      <DebugRenderKind />
      <p>
        This page uses root params and some uncached IO. Ro

in>
  )
}

async function RuntimePrefetchable() {
  await cookies()

  const currentLang = await lang()
  await cachedDelay([__filename, currentLang])
  return (

      <div id="root-param-value

ot params should
        always be available in static prerenders, so a runtime prefetch should
        have them too.
      </p>
      <Suspense fallback="Loading 1...">
        <RuntimePrefetchable />
      </Suspense>
    </ma


          cookies + dynamic content

              <DebugLinkAccordion href="/in-page/cookies" />

          headers + dynamic content

              <DebugLinkAccordion href="/in-page/headers" />

          search params + dynamic content

              <DebugLinkAccordion href="/in-page/s

params + dynamic content

              <DebugLinkAccordion href="/in-page/dynamic-params/123" />

              <DebugLinkAccordion href="/in-page/dynamic-params/456" />

earch-params?searchParam=123" />

              <DebugLinkAccordion href="/in-page/search-params?searchParam=456" />

          dynamic


t/root-params'
import { DebugLinkAccordion } from '../../../components/link-accordion'

export default async function Page() {
  const currentLang = await lang()
  const otherLang = currentLang === 'en' ? 'de' : 'en'
  return (

Home - with root param ({currentLang})

directly in a page

          root params + dynamic content

              <DebugLinkAccordion
                href={`/with-root-param/${currentLang}

        use cache: private

          root params + dynamic content

              <DebugLinkAccordion
                href={`/with-r

/in-page/root-params`}
              />
            </li>
            <li>
              <DebugLinkAccordion
                href={`/with-root-param/${otherLang}/in-page/root-params`}
              />
            </li>


import { Suspense } from 'react'

type Params = {
  pageNumber: string
}

export async function generateViewport({
  params,
}: {
  params: Promise<Params>
}) {
  const { pageNumber } = await params
  return {
    // Put the page number into the media query. This is just a trick to allow
    // the test to detect when the viewport for this page has been prefetched.
    themeColor: [{ media: `(min-width: ${pageNumber}px)`, color: 'light' }],
  }
}

async function Content({ params }: { params: Promise<Params>

oading...">
      <Content params={params} />
    </Suspense>
  )
}

export async function generateStaticParams(): Promise<Array<Params>> {
  const result: Array<Params> = []
  for (let n = 1; n <= 10; n++) {
    result.push({ pageNumber: n.toString() })

 }) {
  const { pageNumber } = await params
  return 'Content of page ' + pageNumber
}

export default async function LinkCancellationTargetPage({
  params,
}: {
  params: Promise<Params>
}) {
  return (
    <Suspense fallback="L


ould throw, without swallowing the error.
 *
 * It calls `callback` and checks if it throws.
 * If it did, we rethrow the error.
 * If it didn't throw, we throw a AssertionError/ClientAssertionError.
 * */
export function ensureThrows(callback: () => any, message?: string): never {
  let thrown: { value: unknown } | null = null
  try {
    callback()
  } catch (err) {
    thrown = { value: err }
  }

  if (thrown) {
    // The callback threw, as expected.
    throw thrown.value
  } else {
    // The callbac

ail(message)
    } else {
      throw new Error('This code should never run in the browser')
    }
  }

  // For some reason, typescript doesn't believe that all codepaths above throw
  throw new Error('Unreachable')
}

/**
 * This helper is used for testi

k didn't throw like it should have.
    message ??= 'Expected callback to throw'
    if (typeof window === 'undefined') {
      const { fail } =
        require('node:assert/strict') as typeof import('node:assert/strict')
      f


describe('invalid - missing suspense around runtime', () => {
      it('should fail build when cookies are outside Suspense', async () => {
        const result = await prerender(
          '/(default)/invalid-missing-suspense-around-runtime'
        )
        expect(extractBuildValidationError(result.cliOutput))
          .toMatchInlineSnapshot(`
         "Error: Route "/invalid-missing-suspense-around-runtime": Next.js encountered uncached data during prerendering or a navigation.

         \`fetch(...)\`

access with \`"use cache"\`
           - Provide a placeholder with \`<Suspense fallback={...}>\` around the data access
           - Set \`export const instant = false\` to allow a blocking route

         Learn more: https://nextjs.org/docs/messages/bloc

 or \`connection()\` accessed outside of \`<Suspense>\` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience.

         Ways to fix this:
           - Cache the data


import { lang } from 'next/root-params'
import { ensureRejects } from '../../../../ensure-error'

export const unstable_instant: Instant = {
  level: 'experimental-error',
  // no samples
  unstable_samples: [{}],
}
export const unstable_prefetch = 'force-runtime'

export default async function Page() {
  // Guard behind cookies() so that the rest of this component only runs during validation
  await cookies()

  try {
    await ensureRejects(
      () => lang(),
      `Expected lang() to error if sample is

he error. It should still be reported and fail the validation.
  }

  return (
    <main>
      <p>
        This page uses a root param. It's missing a sample for it, so we can't
        validate it. It catches the error thrown by the rootparam call, but

 not provided`
    )
  } catch {
    // We swallow t


} from 'next'
import { cookies } from 'next/headers'
import { lang } from 'next/root-params'
import assert from 'node:assert/strict'

export const unstable_instant: Instant = {
  level: 'experimental-error',
  unstable_samples: [{ params: { lang: 'en-from-samples' } }],
}
export const unstable_prefetch = 'force-runtime'

export default async function Page() {
  // Guard behind cookies() so that the rest of this component only runs during validation
  await cookies()

  // We'll only get here during validati

es`
  const currentLang = await lang()
  assert.equal(currentLang, 'en-from-samples', `Unexpected \`lang()\``)

  return (
    <main>
      <p>
        This page uses a root param. It should get it from samples, not
        generateStaticParams.
      </p>

on, so we should only observe the `lang` from `sampl


mport assert from 'node:assert/strict'
import { Suspense } from 'react'

// No unstable_instant here — should inherit samples from the layout.

export default async function Page({
  params,
}: {
  params: Promise<Record<string, string>>
}) {
  return (

        This page does not define unstable_instant samples, so it should inherit
        the samples from the parent layout.

      <Suspense fallback={
Loading...
}>
        <TestParams params={params} />
      </Susp

in>
  )
}

async function TestParams({
  params,
}: {
  params: Promise<Record<string, string>>
}) {
  const p = await params
  assert.equal(
    p.slug,
    'from-layout',
    `Expected param 'slug' to be 'from-layout', got '${p.slug}'`
  )
  return null


import assert from 'node:assert/strict'
import { Suspense } from 'react'

export const unstable_instant: Instant = {
  level: 'experimental-error',
  unstable_samples: [
    {
      params: {
        slug: 'from-page',
      },
    },
  ],
}

export default async function Page({
  params,
}: {
  params: Promise<Record<string, string>>
}) {
  return (
    <main>
      <p>
        This page defines its own unstable_instant samples, which should
        override the layout samples (no merging).
      </p>

   <TestParams params={params} />
      </Suspense>
    </main>
  )
}

async function TestParams({
  params,
}: {
  params: Promise<Record<string, string>>
}) {
  const p = await params
  assert.equal(
    p.slug,
    'from-page',
    `Expected param 'slug


rom 'next'
import assert from 'node:assert/strict'

import { Suspense } from 'react'

export const unstable_instant: Instant = {
  level: 'experimental-error',
  unstable_samples: [
    {
      searchParams: {
        // TODO(instant-validation-build): specify and test escaping behavior for spaces etc
        single: 'test',
        multiple: ['a', 'b'],
        missing: null,
      },
    },
  ],
}
export const unstable_prefetch = 'force-runtime'

type SearchParams = Record<string, string | string[]>

expo

ecified in the sample.

      <Suspense fallback={
Loading...
}>
        <TestSearchParams searchParams={searchParams} />
      </Suspense>

  )
}

async function TestSearchParams({
  searchParams,
}: {
  searchParams: Promise

rt default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ foo?: string | string[] }>
}) {
  return (
    <main>
      <p>
        When validated in build, the page should receive the search params
        sp


rict'

export const unstable_instant: Instant = {
  level: 'experimental-error',
  unstable_samples: [
    {
      searchParams: {
        single: 'test',
        multiple: ['a', 'b'],
      },
    },
  ],
}
export const unstable_prefetch = 'force-runtime'

export default async function Page({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[]>>
}) {
  return (
    <main>
      <CachedChild searchParams={await searchParams} />
    </main>
  )
}

async function CachedChild({
  sear

 | string[]>
}) {
  'use cache'
  assert.equal(
    searchParams.single,
    'test',
    `Expected searchParams.single to be 'test', got '${searchParams.single}'`
  )
  assert.deepStrictEqual(
    searchParams.multiple,
    ['a', 'b'],
    `Unexpected valu

chParams,
}: {
  searchParams: Record<string, string


rict'

import { ClientChild } from './client'

export const unstable_instant: Instant = {
  level: 'experimental-error',
  unstable_samples: [
    {
      searchParams: {
        single: 'test',
        multiple: ['a', 'b'],
      },
    },
  ],
}
export const unstable_prefetch = 'force-runtime'

export default async function Page({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[]>>
}) {
  return (
    <main>
      <TestSearchParams searchParams={searchParams} />
    </main>
  )

,
}: {
  searchParams: Promise<Record<string, string | string[]>>
}) {
  const sp = await searchParams
  assert.equal(
    sp.single,
    'test',
    `Expected 'single' to be 'test', got '${sp.single}'`
  )
  assert.deepStrictEqual(
    sp.multiple,
    ['

}

async function TestSearchParams({
  searchParams


e-error'

export const unstable_instant = {
  level: 'experimental-error',
  unstable_samples: [{ searchParams: { q: 'test' } }],
}
export const unstable_prefetch = 'force-runtime'

export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ q?: string; undeclared?: string }>
}) {
  return (
    <main>
      <p>
        This page reads a searchParam that is not declared in the sample, so it
        should fail validation with an exhaustiveness error.
      </p>
      <SearchResult se

c function SearchResult({
  searchParams,
}: {
  searchParams: Promise<{ q?: string; undeclared?: string }>
}) {
  const sp = await searchParams
  ensureThrows(
    () => sp.undeclared,
    `Expected accessing an undeclared search param to throw`
  )
  ret

archParams={searchParams} />
    </main>
  )
}

asyn


 { ensureThrows } from '../../../../ensure-error'

export const unstable_instant = {
  level: 'experimental-error',
  unstable_samples: [{ searchParams: { q: 'test' } }],
}
export const unstable_prefetch = 'force-runtime'

export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ q?: string; undeclared?: string }>
}) {
  return (
    <main>
      <p>
        This page reads a searchParam that is not declared in the sample, so it
        should fail validation with an exhaustiveness

rchParams,
}: {
  searchParams: Promise<{ q?: string; undeclared?: string }>
}) {
  const sp = await searchParams

  try {
    ensureThrows(
      () => sp.undeclared,
      `Expected accessing an undeclared search param to throw`
    )
  } catch (err) {

 error. It catches the
        error thrown by the searchParam access, but validation should still
        fail.
      </p>
      <SearchResult searchParams={searchParams} />
    </main>
  )
}

async function SearchResult({
  sea


{ Instant } from 'next'
import assert from 'node:assert/strict'
import { ensureThrows } from '../../../../../../ensure-error'

export const unstable_instant: Instant = {
  level: 'experimental-error',
  unstable_samples: [
    {
      params: {
        one: '123',
        // two: <missing>
      },
    },
  ],
}
export const unstable_prefetch = 'force-runtime'

export default async function Page({
  params,
}: {
  params: Promise<Record<string, string>>
}) {
  return (
    <main>
      <p>
        This page

stParams params={params} />
    </main>
  )
}

async function TestParams({
  params,
}: {
  params: Promise<Record<string, string>>
}) {
  const p = await params

  assert.equal(p.one, '123', `Unexpected value for param 'one'`)

  try {
    // We're not al

 reads a param that is not declared in the sample, so it should
        fail validation with an exhaustiveness error. It catches the error
        thrown by the param access, but validation should still fail.
      </p>
      <Te


ort type { Instant } from 'next'
import assert from 'node:assert/strict'

export const unstable_instant: Instant = {
  level: 'experimental-error',
  unstable_samples: [
    {
      params: {
        one: '123',
        two: '456',
      },
    },
  ],
}
export const unstable_prefetch = 'force-runtime'

export default async function Page({
  params,
}: {
  params: Promise<Record<string, string>>
}) {
  return (
    <main>
      <p>
        When validated in build, the page should receive the params specifie

 p

  assert.equal(one, '123', `Unexpected value for param 'one'`)
  assert.equal(two, '456', `Unexpected value for param 'two'`)
  assert.equal('three' in p, false)
  assert.equal(p.three, undefined)

  assert.deepStrictEqual(
    { ...p },
    { one: '12

d in
        the sample.
      </p>
      <TestParams params={params} />
    </main>
  )
}

async function TestParams({
  params,
}: {
  params: Promise<Record<string, string>>
}) {
  const p = await params
  const { one, two } =


 params: Promise<Record<string, string>>
}) {
  const p = await params

  assert.equal(p.one, '123', `Unexpected value for param 'one'`)

  // We're allowed to access names that don't correspond to a param.
  assert.equal('three' in p, false)
  assert.equa

 reads a param that is not declared in the sample, so it should
        fail validation with an exhaustiveness error.
      </p>
      <TestParams params={params} />
    </main>
  )
}

async function TestParams({
  params,
}: {


'../../../../client-assertion-error'

/**
 * Asserts that client hooks receive sample params during instant validation,
 * not the values from generateStaticParams. This component is inside a Suspense
 * boundary gated by `await cookies()`, so it only renders during validation.
 */
export function AssertParamsClient() {
  const params = useParams()
  const pathname = usePathname()

  // During validation, useParams() should return the sample param ('hello'),
  // not a value from generateStaticParams ('foo'

   `Expected useParams().slug === 'hello' (from sample), got '${params.slug}'`
  )

  // usePathname() should reflect the sample param value too.
  assert(
    pathname === '/gsp/hello',
    `Expected usePathname() === '/gsp/hello', got '${pathname}'`
  )

 or 'bar').
  assert(
    params.slug === 'hello',


rom 'next'
import assert from 'node:assert/strict'
import { cookies } from 'next/headers'
import { Suspense } from 'react'
import { AssertParamsClient } from './client'

// samples use 'hello', but generateStaticParams uses 'foo'/'bar'.
// During validation, the sample params should be used, not the GSP values.
export const unstable_instant: Instant = {
  level: 'experimental-error',
  unstable_samples: [
    {
      params: {
        slug: 'hello',
      },
    },
  ],
}
export const unstable_prefetch = 'f

<Suspense fallback={
Loading...
}>
        <AssertSampleParams params={params} />
      </Suspense>

  )
}

async function AssertSampleParams({
  params,
}: {
  params: Promise<Record<string, string>>
}) {
  // Gate behind cookies() so

orce-runtime'

export function generateStaticParams() {
  return [{ slug: 'foo' }, { slug: 'bar' }]
}

export default async function Page({
  params,
}: {
  params: Promise<Record<string, string>>
}) {
  return (
    <main>