mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-26 14:50:55 +02:00
* feat: bind shared-code review advice to source and branch * feat: add shared-code extraction audit and scoped review checks * test: recognize complete source reads and explicit coverage legends * chore: bump version and changelog (v1.88.0.0) Co-Authored-By: OpenAI Codex <noreply@openai.com> * test: capture native review questions and retain public evidence Capture the actual first public native question with strict ownership and display matching. Preserve terminal failures and raw evidence, and retain SDK completion checks. * test: recognize verified review evidence and complete fixtures Recognize complete source and diagram evidence, concrete design and developer-experience decisions, and the complete planted scenario contracts. Preserve negative controls and grading thresholds. * fix: preserve decision brief structure in native questions Keep the required pros-and-cons heading and final Net field in native question text. Regenerate host outputs and document the release and evaluation repairs. Co-Authored-By: OpenAI Codex <noreply@openai.com> * docs: update project documentation for v1.88.0.0 Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix: correct eval retry accounting and ship workflow gates * fix: capture native eval evidence and stabilize CI fixtures * fix: keep shared-code eval skips read-only Choose explicit no-change answers instead of mixed fix/preservation options. Reuse the bounded revalidation prompt for path fixtures so required review metadata is available without repeated discovery. Preserve source checks, retry limits, and failed native terminal outcomes. Add captured-question and callback regressions, plus evaluation selection coverage for the affected fixtures. --------- Co-authored-by: OpenAI Codex <noreply@openai.com>
233 lines
8.4 KiB
TypeScript
233 lines
8.4 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() {
|
|
// Ask the OS for an available test port. A random port may already
|
|
// belong to another parallel test or local service.
|
|
const port = await new Promise((resolve, reject) => {
|
|
const srv = net.createServer();
|
|
srv.once('error', reject);
|
|
srv.listen(0, '127.0.0.1', () => {
|
|
const allocatedPort = srv.address().port;
|
|
srv.close((error) => error ? reject(error) : resolve(allocatedPort));
|
|
});
|
|
});
|
|
|
|
// 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().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|
|
`], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 });
|
|
|
|
expect(result.exitCode, result.stderr.toString()).toBe(0);
|
|
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]);
|
|
});
|
|
});
|