Summary
packages/server/test/start.test.ts has a flaky test:
"retries on port+1 and updates the lock when the requested port is held by a third party"
It assumes that port + 1 remains free between the helper releasing it and startServer binding it. Under vitest's concurrent workers, another test can grab port + 1 in that window, causing startServer to bind to port + 2 or higher and the assertion to fail.
Reproduction
Run the packages/server suite repeatedly:
for i in {1..10}; do pnpm vitest run packages/server; done
In my environment this fails ~80% of the time with an error like:
Expected: "http://127.0.0.1:55398"
Received: "http://127.0.0.1:55399"
❯ test/start.test.ts:183:25
expect(r.address).toBe(`http://127.0.0.1:${String(next)}`);
Running start.test.ts in isolation always passes, which confirms the failure is caused by concurrent port allocation from other vitest workers.
Root cause
allocateAdjacentFreePair() finds port and port + 1, then immediately releases both. The test occupies port to simulate a third-party listener, but leaves port + 1 unprotected. While the test prepares to start the server, another worker can bind port + 1, so startServer advances to port + 2 and the strict assertion fails.
Possible fixes
-
Relax the integration assertion (recommended)
Verify that startServer binds to a port >= port + 1 and that the lock file records the actual bound port. Keep the exact port + 1 retry strategy covered by the existing listenWithPortRetry unit tests, which already use a fake gateway to assert 5000 -> 5001 -> 5002.
-
Hold port + 1 until right before binding
Have allocateAdjacentFreePair() return a releaseNext() callback that is awaited immediately before startServer() runs. This shrinks the race window but does not fully eliminate it; in my testing it reduced the failure rate from ~80% to ~20%.
-
Run the test sequentially
Mark the test or describe block as sequential so it does not race with other workers. This avoids the issue but slows down the suite and does not address the underlying fragility.
-
Introduce cross-process locking
Use a file lock or similar mechanism so only one process allocates ports in this helper at a time. This is heavier and adds complexity.
I am happy to open a PR for whichever approach the maintainers prefer. I have already verified option 1 locally: after relaxing the assertion, 10 consecutive full packages/server runs passed for start.test.ts.
Summary
packages/server/test/start.test.tshas a flaky test:It assumes that
port + 1remains free between the helper releasing it andstartServerbinding it. Under vitest's concurrent workers, another test can grabport + 1in that window, causingstartServerto bind toport + 2or higher and the assertion to fail.Reproduction
Run the
packages/serversuite repeatedly:In my environment this fails ~80% of the time with an error like:
Running
start.test.tsin isolation always passes, which confirms the failure is caused by concurrent port allocation from other vitest workers.Root cause
allocateAdjacentFreePair()findsportandport + 1, then immediately releases both. The test occupiesportto simulate a third-party listener, but leavesport + 1unprotected. While the test prepares to start the server, another worker can bindport + 1, sostartServeradvances toport + 2and the strict assertion fails.Possible fixes
Relax the integration assertion (recommended)
Verify that
startServerbinds to a port>= port + 1and that the lock file records the actual bound port. Keep the exactport + 1retry strategy covered by the existinglistenWithPortRetryunit tests, which already use a fake gateway to assert5000 -> 5001 -> 5002.Hold
port + 1until right before bindingHave
allocateAdjacentFreePair()return areleaseNext()callback that is awaited immediately beforestartServer()runs. This shrinks the race window but does not fully eliminate it; in my testing it reduced the failure rate from ~80% to ~20%.Run the test sequentially
Mark the test or
describeblock as sequential so it does not race with other workers. This avoids the issue but slows down the suite and does not address the underlying fragility.Introduce cross-process locking
Use a file lock or similar mechanism so only one process allocates ports in this helper at a time. This is heavier and adds complexity.
I am happy to open a PR for whichever approach the maintainers prefer. I have already verified option 1 locally: after relaxing the assertion, 10 consecutive full
packages/serverruns passed forstart.test.ts.