Repository: vercel/next.js
Pull request: [test] Fix `edge-async-local-storage` deploy test timeout & race condition

The test was calling `createNext` inside each `it.each` test body, which meant deployment setup time counted against the 60-second per-test timeout introduced in #89929. In deploy mode, `createNext` deploys to Vercel which can exceed 60 seconds. When the test timed out, the `next` local variable was never assigned (the `await createNext(...)` hadn't resolved), so the `afterEach` hook would throw a `TypeError` on `next.destroy()`. This left the module-level `nextInstance` variable pointing at the stale, half-initialized deploy instance. When the next test case started, `createNext` saw the leftover `nextInstance` and threw `"createNext called without destroying previous instance"`.

The test now uses `nextTestSetup`, which runs `createNext` in `beforeAll` where it has the longer 120-second `setupTimeout`. Both API route variants (`/api/single` and `/api/multiple`) are real fixture files served from a single instance, so deployment only happens once.

Changed files:
filename: test/e2e/edge-async-local-storage/index.test.ts
patch: @@ -1,105 +1,51 @@
-import { createNext } from 'e2e-utils'
-import { NextInstance } from 'e2e-utils'
+import { nextTestSetup } from 'e2e-utils'
 import { fetchViaHTTP } from 'next-test-utils'
 
 describe('edge api can use async local storage', () => {
-  let next: NextInstance
+  const { next } = nextTestSetup({
+    files: __dirname,
+  })
 
   const cases = [
     {
       title: 'a single instance',
-      code: `
-        export const config = { runtime: 'edge' }
-        const storage = new AsyncLocalStorage()
-  
-        export default async function handler(request) {
-          const id = request.headers.get('req-id')
-          return storage.run({ id }, async () => {
-            await getSomeData()
-            return Response.json(storage.getStore())
-          })
-        }
-  
-        async function getSomeData() {
-          try {
-            const response = await fetch('https://example.vercel.sh')
-            await response.text()
-          } finally {
-            return true
-          }
-        }
-      `,
-      expectResponse: (response, id) =>
+      route: '/api/single',
+      expectResponse: (response: any, id: string) =>
         expect(response).toMatchObject({ status: 200, json: { id } }),
     },
     {
       title: 'multiple instances',
-      code: `
-        export const config = { runtime: 'edge' }
-        const topStorage = new AsyncLocalStorage()
-  
-        export default async function handler(request) {
-          const id = request.headers.get('req-id')
-          return topStorage.run({ id }, async () => {
-            const nested = await getSomeData(id)
-            return Response.json({ ...nested, ...topStorage.getStore() })
-          })
-        }
-  
-        async function getSomeData(id) {
-          const nestedStorage = new AsyncLocalStorage()
-          return nestedStorage.run('nested-' + id, async () => {
-            try {
-              const response = await fetch('https://example.vercel.sh')
-              await response.text()
-            } finally {
-              return { nestedId: nestedStorage.getStore() }
-            }
-          })
-        }
-      `,
-      expectResponse: (response, id) =>
+      route: '/api/multiple',
+      expectResponse: (response: any, id: string) =>
         expect(response).toMatchObject({
           status: 200,
           json: { id: id, nestedId: `nested-${id}` },
         }),
     },
   ]
 
-  afterEach(() => next.destroy())
-
   it.each(cases)(
-    'cans use $title per request',
-    async ({ code, expectResponse }) => {
-      next = await createNext({
-        files: {
-          'pages/index.js': `
-            export default function () { return <div>Hello, world!</div> }
-          `,
-          'pages/api/async.js': code,
-        },
-      })
+    'can use $title per request',
+    async ({ route, expectResponse }) => {
       const ids = Array.from({ length: 100 }, (_, i) => `req-${i}`)
 
       const responses = await Promise.all(
         ids.map((id) =>
-          fetchViaHTTP(
-            next.url,
-            '/api/async',
-            {},
-            { headers: { 'req-id': id } }
-          ).then((response) =>
-            response.headers.get('content-type')?.startsWith('application/json')
-              ? response.json().then((json) => ({
-                  status: response.status,
-                  json,
-                  text: null,
-                }))
-              : response.text().then((text) => ({
-                  status: response.status,
-                  json: null,
-                  text,
-                }))
+          fetchViaHTTP(next.url, route, {}, { headers: { 'req-id': id } }).then(
+            (response) =>
+              response.headers
+                .get('content-type')
+                ?.startsWith('application/json')
+                ? response.json().then((json) => ({
+                    status: response.status,
+                    json,
+                    text: null,
+                  }))
+                : response.text().then((text) => ({
+                    status: response.status,
+                    json: null,
+                    text,
+                  }))
           )
         )
       )

filename: test/e2e/edge-async-local-storage/pages/api/multiple.js
patch: @@ -0,0 +1,24 @@
+export const config = { runtime: 'edge' }
+// eslint-disable-next-line no-undef
+const topStorage = new AsyncLocalStorage()
+
+export default async function handler(request) {
+  const id = request.headers.get('req-id')
+  return topStorage.run({ id }, async () => {
+    const nested = await getSomeData(id)
+    return Response.json({ ...nested, ...topStorage.getStore() })
+  })
+}
+
+async function getSomeData(id) {
+  // eslint-disable-next-line no-undef
+  const nestedStorage = new AsyncLocalStorage()
+  return nestedStorage.run('nested-' + id, async () => {
+    try {
+      const response = await fetch('https://example.vercel.sh')
+      await response.text()
+    } finally {
+      return { nestedId: nestedStorage.getStore() }
+    }
+  })
+}

filename: test/e2e/edge-async-local-storage/pages/api/single.js
patch: @@ -0,0 +1,20 @@
+export const config = { runtime: 'edge' }
+// eslint-disable-next-line no-undef
+const storage = new AsyncLocalStorage()
+
+export default async function handler(request) {
+  const id = request.headers.get('req-id')
+  return storage.run({ id }, async () => {
+    await getSomeData()
+    return Response.json(storage.getStore())
+  })
+}
+
+async function getSomeData() {
+  try {
+    const response = await fetch('https://example.vercel.sh')
+    await response.text()
+  } finally {
+    return true
+  }
+}