Files
vercel__next.js/test/unit/prepare-latest-upgrade.test.ts
Jiwon Choi d3d30cf88a Add future defaults upgrade coverage for next upgrade --ai (#98643)
Stacked on #98640.

This PR adds `next upgrade --ai="future"` flag, which is targeted to
help users leverage agents to upgrade their app to adopt the future
defaults when available. Just like latest version upgrade, it covers
running codemods and a migration checklist for major-to-major upgrades
to support breaking changes more reliably.

This PR currently covers Cache Components only for the future default.
2026-09-18 00:52:21 +02:00

172 lines
4.9 KiB
TypeScript

import { mkdtemp, mkdir, rm, writeFile } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { prepareUpgrade } from 'next/dist/lib/upgrade/prepare-upgrade'
import loadConfig from 'next/dist/server/config'
jest.mock('next/dist/server/config', () => ({
__esModule: true,
default: jest.fn(),
}))
describe('prepare latest upgrade', () => {
const directories: string[] = []
const originalFetch = global.fetch
async function createApp(version: string): Promise<string> {
const directory = await mkdtemp(join(tmpdir(), 'next-latest-upgrade-'))
directories.push(directory)
await mkdir(join(directory, 'node_modules/next'), { recursive: true })
await writeFile(join(directory, 'package.json'), '{}')
await writeFile(
join(directory, 'node_modules/next/package.json'),
JSON.stringify({ version, engines: { node: '>=18' } })
)
return directory
}
function mockLatestVersion(version: string) {
global.fetch = jest.fn().mockResolvedValue(
new Response(
JSON.stringify({
version,
engines: { node: '>=18' },
}),
{ status: 200 }
)
)
}
function mockFutureMetadata(vulnerableVersions: string) {
global.fetch = jest.fn(async (input) => {
const url = String(input)
if (url === 'https://registry.npmjs.org/next/latest') {
return Response.json({
version: '16.4.0',
engines: { node: '>=18' },
})
}
if (url.startsWith('https://api.github.com/advisories?')) {
return new Response(null, { status: 500 })
}
if (url === 'https://registry.npmjs.org/next') {
return Response.json({
versions: {
'16.2.0': { version: '16.2.0' },
'16.4.0': { version: '16.4.0' },
},
})
}
if (
url === 'https://registry.npmjs.org/-/npm/v1/security/advisories/bulk'
) {
return Response.json({
next: [
{
id: 1,
url: 'https://github.com/advisories/GHSA-next-test',
severity: 'high',
vulnerable_versions: vulnerableVersions,
},
],
})
}
throw new Error(`Unexpected request: ${url}`)
})
}
beforeEach(() => {
jest.mocked(loadConfig).mockResolvedValue({
cacheComponents: false,
} as never)
})
afterEach(async () => {
global.fetch = originalFetch
await Promise.all(
directories
.splice(0)
.map((directory) => rm(directory, { recursive: true, force: true }))
)
})
it('selects the exact latest stable release', async () => {
const directory = await createApp('16.2.1')
mockLatestVersion('17.1.0')
await expect(prepareUpgrade(directory, 'latest')).resolves.toEqual(
expect.objectContaining({
status: 'ready',
installedVersion: '16.2.1',
targetVersion: '17.1.0',
references: ['https://registry.npmjs.org/next/latest'],
})
)
expect(global.fetch).toHaveBeenCalledTimes(1)
expect(jest.mocked(global.fetch).mock.calls[0][0]).toBe(
'https://registry.npmjs.org/next/latest'
)
})
it('does nothing when the app already uses latest', async () => {
const directory = await createApp('17.1.0')
mockLatestVersion('17.1.0')
await expect(prepareUpgrade(directory, 'latest')).resolves.toEqual({
status: 'unaffected',
reason: 'Next.js 17.1.0 is already the latest stable release.',
})
})
it('does not downgrade an app newer than latest', async () => {
const directory = await createApp('18.0.0')
mockLatestVersion('17.1.0')
await expect(prepareUpgrade(directory, 'latest')).resolves.toEqual({
status: 'unaffected',
reason: 'Next.js 18.0.0 is newer than the latest stable release 17.1.0.',
})
})
it('allows a Future target outside npm fallback advisory ranges', async () => {
const directory = await createApp('16.2.0')
mockFutureMetadata('<16.3.0')
await expect(prepareUpgrade(directory, 'future')).resolves.toEqual(
expect.objectContaining({
status: 'ready',
installedVersion: '16.2.0',
targetVersion: '16.4.0',
})
)
})
it('blocks a Future target inside npm fallback advisory ranges', async () => {
const directory = await createApp('16.2.0')
mockFutureMetadata('<=16.4.0')
await expect(prepareUpgrade(directory, 'future')).rejects.toThrow(
'Next.js 16.4.0 is affected by an active advisory.'
)
})
it('uses the adapter to detect an adopted Future Default', async () => {
const directory = await createApp('16.4.0')
jest.mocked(loadConfig).mockResolvedValue({
cacheComponents: true,
} as never)
mockFutureMetadata('<16.3.0')
await expect(prepareUpgrade(directory, 'future')).resolves.toEqual({
status: 'unaffected',
reason:
'Next.js 16.4.0 is current and all available Future Defaults are enabled.',
})
})
})