Files
Hendrik LiebauandGitHub d0abfdb98e [test] Retry the .next deletion to fix an ENOTEMPTY flake (#95307)
The `watch-distdir-deletion` dev test deletes `.next` to verify that the
dev server notices its distDir (`.next/dev`) being removed and restarts.
The deletion frequently failed in CI with `ENOTEMPTY: directory not
empty, rmdir '.../.next/dev'`.

The cause is a race between the test's recursive delete and the dev
server's own writes. After the warmup request returns, Turbopack keeps
flushing manifests into `.next/dev` through its background update
subscription, and `writeFileAtomic` does so by creating a transient
temporary file and renaming it over the target. `fs.rm` removes a
directory by reading its entries, unlinking them, and then calling
`rmdir`; when the server writes a new file into `.next/dev` between the
unlink pass and the `rmdir`, the `rmdir` throws `ENOTEMPTY`. The test
only passed `force: true`, which suppresses `ENOENT` but not
`ENOTEMPTY`, so the first such collision failed the test outright.
Resource contention on CI widens the window, which is why it failed so
often.

This change passes `maxRetries` and `retryDelay` to the same `fs.rm`
call so the failed `rmdir` is re-attempted across that brief window
until the flush settles, instead of failing on the first collision. The
deletion stays a plain recursive remove. Run twenty times concurrently
on an oversubscribed machine, the test went from a 45% failure rate to
passing every time, with the distDir-deletion watcher still firing in
every run.
2026-06-30 14:40:38 +02:00

56 lines
1.7 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
import fs from 'fs'
import path from 'path'
describe('app-dir watch-distdir-deletion', () => {
const { next } = nextTestSetup({
files: path.join(__dirname, 'fixture'),
})
it('should restart the server when distDir is deleted', async () => {
// Wait for the dev server to be ready
await retry(async () => {
expect(next.cliOutput).toMatch(/ready/i)
})
// Make a request first to ensure the dev server has compiled
const warmupRes = await next.fetch('/')
expect(warmupRes.status).toBe(200)
// Delete .next (which also removes .next/dev, the watched distDir). The dev
// server may still be flushing manifests into .next/dev from the warmup
// compilation, and those writes race the recursive delete: fs.rm unlinks
// the directory's contents and then rmdir's it, but a file written
// concurrently makes the rmdir throw ENOTEMPTY. maxRetries makes this
// single delete resilient to that brief window by re-attempting the failed
// rmdir until the flush settles. force ignores entries an earlier attempt
// already removed.
await fs.promises.rm(path.join(next.testDir, '.next'), {
recursive: true,
force: true,
maxRetries: 10,
retryDelay: 200,
})
// Wait for restart message and server to come back up
await retry(
async () => {
expect(next.cliOutput).toMatch(/The directory at .* was deleted/)
},
30_000,
1000
)
// Server should come back up and serve pages
await retry(
async () => {
const res = await next.fetch('/')
expect(res.status).toBe(200)
},
30_000,
1000
)
})
})