mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-14 17:05:28 +02:00
spawnSync/execSync/Bun.spawnSync BLOCK the main thread, so bun's in-process per-test timeout can never fire while one waits — a hung child (stdin read, network probe, dead daemon) wedges the whole shard until the runner's external wall-clock SIGKILL. This exact class reached main: free-tests run 33262077256, test/gstack-memory-ingest.test.ts (normally 2.3s) held shard 2 at the 360s wall while its five siblings finished in ~65s. Mechanical sweep in two waves (12 + 4 fan-out agents, every edit verified against its call site): default timeout: 30_000 (matches the free runner's per-test budget), 120_000 for genuinely slow ops (installs, builds, playwright, provider CLIs), helper wrappers fixed ONCE where call sites route through them. Sites that only LOOK like calls (string fixtures, grep needles, comments) were skipped with reasons — the enforcement commit that follows marks them exempt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
234 lines
8.3 KiB
TypeScript
234 lines
8.3 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import * as net from 'net';
|
|
import * as path from 'path';
|
|
import { __testInternals__ } from '../src/server';
|
|
|
|
const polyfillPath = path.resolve(import.meta.dir, '../src/bun-polyfill.cjs');
|
|
|
|
// Helper: bind a port and hold it open, returning a cleanup function
|
|
function occupyPort(port: number): Promise<() => Promise<void>> {
|
|
return new Promise((resolve, reject) => {
|
|
const srv = net.createServer();
|
|
srv.once('error', reject);
|
|
srv.listen(port, '127.0.0.1', () => {
|
|
resolve(() => new Promise<void>((r) => srv.close(() => r())));
|
|
});
|
|
});
|
|
}
|
|
|
|
// Helper: find a known-free port by binding to 0
|
|
function getFreePort(): Promise<number> {
|
|
return new Promise((resolve, reject) => {
|
|
const srv = net.createServer();
|
|
srv.once('error', reject);
|
|
srv.listen(0, '127.0.0.1', () => {
|
|
const port = (srv.address() as net.AddressInfo).port;
|
|
srv.close(() => resolve(port));
|
|
});
|
|
});
|
|
}
|
|
|
|
describe('findPort / isPortAvailable', () => {
|
|
test('explicit BROWSE_PORT diagnostic distinguishes bind denial from occupied port', () => {
|
|
const blocked = __testInternals__.formatExplicitPortUnavailableError(34567, {
|
|
available: false,
|
|
code: 'EPERM',
|
|
message: 'operation not permitted',
|
|
}).message;
|
|
|
|
expect(blocked).toContain('Cannot bind BROWSE_PORT=34567');
|
|
expect(blocked).toContain('localhost port binding is blocked');
|
|
expect(blocked).toContain('not that the port is occupied');
|
|
|
|
const occupied = __testInternals__.formatExplicitPortUnavailableError(34567, {
|
|
available: false,
|
|
code: 'EADDRINUSE',
|
|
message: 'address already in use',
|
|
}).message;
|
|
|
|
expect(occupied).toBe('[browse] Port 34567 (from BROWSE_PORT env) is in use');
|
|
});
|
|
|
|
test('random port diagnostic calls out sandbox-style bind denial', () => {
|
|
const message = __testInternals__.formatRandomPortUnavailableError([
|
|
{ port: 11001, result: { available: false, code: 'EADDRINUSE', message: 'address already in use' } },
|
|
{ port: 12002, result: { available: false, code: 'EPERM', message: 'operation not permitted' } },
|
|
]).message;
|
|
|
|
expect(message).toContain('Cannot bind localhost ports after 2 attempts');
|
|
expect(message).toContain('Last error: 12002 (EPERM: operation not permitted)');
|
|
expect(message).toContain('not that every sampled port is occupied');
|
|
expect(message).toContain('set BROWSE_PORT to an approved port');
|
|
});
|
|
|
|
test('random port diagnostic preserves old busy-port meaning when all attempts are occupied', () => {
|
|
const message = __testInternals__.formatRandomPortUnavailableError([
|
|
{ port: 11001, result: { available: false, code: 'EADDRINUSE', message: 'address already in use' } },
|
|
{ port: 12002, result: { available: false, code: 'EADDRINUSE', message: 'address already in use' } },
|
|
]).message;
|
|
|
|
expect(message).toContain('No available port after 5 attempts');
|
|
expect(message).toContain('every sampled port was already in use');
|
|
});
|
|
|
|
test('isPortAvailable returns true for a free port', async () => {
|
|
// Use the same isPortAvailable logic from server.ts
|
|
const port = await getFreePort();
|
|
|
|
const available = await new Promise<boolean>((resolve) => {
|
|
const srv = net.createServer();
|
|
srv.once('error', () => resolve(false));
|
|
srv.listen(port, '127.0.0.1', () => {
|
|
srv.close(() => resolve(true));
|
|
});
|
|
});
|
|
|
|
expect(available).toBe(true);
|
|
});
|
|
|
|
test('isPortAvailable returns false for an occupied port', async () => {
|
|
const port = await getFreePort();
|
|
const release = await occupyPort(port);
|
|
|
|
try {
|
|
const available = await new Promise<boolean>((resolve) => {
|
|
const srv = net.createServer();
|
|
srv.once('error', () => resolve(false));
|
|
srv.listen(port, '127.0.0.1', () => {
|
|
srv.close(() => resolve(true));
|
|
});
|
|
});
|
|
|
|
expect(available).toBe(false);
|
|
} finally {
|
|
await release();
|
|
}
|
|
});
|
|
|
|
test('port is actually free after isPortAvailable returns true', async () => {
|
|
// This is the core race condition test: after isPortAvailable says
|
|
// a port is free, can we IMMEDIATELY bind to it?
|
|
const port = await getFreePort();
|
|
|
|
// Simulate isPortAvailable
|
|
const isFree = await new Promise<boolean>((resolve) => {
|
|
const srv = net.createServer();
|
|
srv.once('error', () => resolve(false));
|
|
srv.listen(port, '127.0.0.1', () => {
|
|
srv.close(() => resolve(true));
|
|
});
|
|
});
|
|
|
|
expect(isFree).toBe(true);
|
|
|
|
// Now immediately try to bind — this would fail with the old
|
|
// Bun.serve() polyfill approach because the test server's
|
|
// listen() would still be pending
|
|
const canBind = await new Promise<boolean>((resolve) => {
|
|
const srv = net.createServer();
|
|
srv.once('error', () => resolve(false));
|
|
srv.listen(port, '127.0.0.1', () => {
|
|
srv.close(() => resolve(true));
|
|
});
|
|
});
|
|
|
|
expect(canBind).toBe(true);
|
|
});
|
|
|
|
test('polyfill Bun.serve stop() is fire-and-forget (async)', async () => {
|
|
// Verify that the polyfill's stop() does NOT wait for the socket
|
|
// to actually close — this is the root cause of the race condition.
|
|
// On macOS/Linux the OS reclaims the port fast enough that the race
|
|
// rarely manifests, but on Windows TIME_WAIT makes it 100% repro.
|
|
const result = Bun.spawnSync(['node', '-e', `
|
|
require('${polyfillPath}');
|
|
const net = require('net');
|
|
|
|
async function test() {
|
|
const port = 10000 + Math.floor(Math.random() * 50000);
|
|
|
|
const testServer = Bun.serve({
|
|
port,
|
|
hostname: '127.0.0.1',
|
|
fetch: () => new Response('ok'),
|
|
});
|
|
|
|
// stop() returns undefined — it does NOT return a Promise,
|
|
// so callers cannot await socket teardown
|
|
const retval = testServer.stop();
|
|
console.log(typeof retval === 'undefined' ? 'FIRE_AND_FORGET' : 'AWAITABLE');
|
|
}
|
|
|
|
test();
|
|
`], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 });
|
|
|
|
const output = result.stdout.toString().trim();
|
|
// Confirms the polyfill's stop() is fire-and-forget — callers
|
|
// cannot wait for the port to be released, hence the race
|
|
expect(output).toBe('FIRE_AND_FORGET');
|
|
});
|
|
|
|
test('net.createServer approach does not have the race condition', async () => {
|
|
// Prove the fix: net.createServer with proper async bind/close
|
|
// releases the port cleanly
|
|
const result = Bun.spawnSync(['node', '-e', ` // timeout in trailing options
|
|
const net = require('net');
|
|
|
|
async function testFix() {
|
|
const port = 10000 + Math.floor(Math.random() * 50000);
|
|
|
|
// Simulate the NEW isPortAvailable: proper async bind/close
|
|
const isFree = await new Promise((resolve) => {
|
|
const srv = net.createServer();
|
|
srv.once('error', () => resolve(false));
|
|
srv.listen(port, '127.0.0.1', () => {
|
|
srv.close(() => resolve(true));
|
|
});
|
|
});
|
|
|
|
if (!isFree) {
|
|
console.log('PORT_BUSY');
|
|
return;
|
|
}
|
|
|
|
// Immediately try to bind — should succeed because close()
|
|
// completed before the Promise resolved
|
|
const canBind = await new Promise((resolve) => {
|
|
const srv = net.createServer();
|
|
srv.once('error', () => resolve(false));
|
|
srv.listen(port, '127.0.0.1', () => {
|
|
srv.close(() => resolve(true));
|
|
});
|
|
});
|
|
|
|
console.log(canBind ? 'FIX_WORKS' : 'FIX_BROKEN');
|
|
}
|
|
|
|
testFix();
|
|
`], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 });
|
|
|
|
const output = result.stdout.toString().trim();
|
|
expect(output).toBe('FIX_WORKS');
|
|
});
|
|
|
|
test('isPortAvailable handles rapid sequential checks', async () => {
|
|
// Stress test: check the same port multiple times in sequence
|
|
const port = await getFreePort();
|
|
const results: boolean[] = [];
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
const available = await new Promise<boolean>((resolve) => {
|
|
const srv = net.createServer();
|
|
srv.once('error', () => resolve(false));
|
|
srv.listen(port, '127.0.0.1', () => {
|
|
srv.close(() => resolve(true));
|
|
});
|
|
});
|
|
results.push(available);
|
|
}
|
|
|
|
// All 5 checks should succeed — no leaked sockets
|
|
expect(results).toEqual([true, true, true, true, true]);
|
|
});
|
|
});
|