mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-12 16:08:59 +02:00
The terminal-agent bound `Bun.serve({ port: 0 })` and kept that OS-assigned
port for its whole (weeks-long) lifetime. `port: 0` draws from the OS
EPHEMERAL range (49152-65535 on macOS) — the exact pool every short-lived
`app.listen(0)` test server draws from — so the agent squatted ports that
test suites expected to receive and silently absorbed their traffic as
phantom 404s (two squatting daemons verified in the report).
Fix per decision 8: extract the main server's port allocation into
browse/src/port-allocator.ts (checkPortAvailable / isPortAvailable /
findAvailablePort + the 10000-60000 range constants and the actionable
sandbox-vs-occupied error formatters, all verbatim from server.ts) and make
BOTH long-lived listeners use it — server.ts's findPort is now a thin
findAvailablePort(BROWSE_PORT) wrapper, and terminal-agent's buildServer
takes a pre-allocated port from the same range. No terminal-port consumer
carries a range assumption (they read the port file), verified by grep.
Tests: terminal-agent-port-range (new — allocator stays inside
10000-60000 and below the 49152 ephemeral floor, explicit-port honored,
occupied-explicit throws, static tripwires pin no-port:0 in
terminal-agent.ts and the shared wrapper in server.ts) + findport +
terminal-agent-integration/session-routing/detach-reattach +
dual-listener: 67 pass, 0 fail.
Fixes #2314.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
79 lines
3.0 KiB
TypeScript
79 lines
3.0 KiB
TypeScript
/**
|
|
* #2314: the terminal-agent must allocate its port from the SAME fixed
|
|
* 10000-60000 scan range the main server uses (port-allocator.ts,
|
|
* decision 8) — never `port: 0`. Binding 0 drew from the OS ephemeral range
|
|
* (49152-65535 on macOS), where the weeks-lived agent squatted ports that
|
|
* short-lived `app.listen(0)` test servers expected to receive, absorbing
|
|
* their traffic as phantom 404s across every Node test suite on the machine.
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import * as fs from 'fs';
|
|
import * as net from 'net';
|
|
import * as path from 'path';
|
|
import {
|
|
findAvailablePort,
|
|
RANDOM_PORT_MIN,
|
|
RANDOM_PORT_MAX,
|
|
} from '../src/port-allocator';
|
|
|
|
const AGENT_TS = path.resolve(import.meta.dir, '..', 'src', 'terminal-agent.ts');
|
|
const SERVER_TS = path.resolve(import.meta.dir, '..', 'src', 'server.ts');
|
|
|
|
describe('shared port allocator (#2314)', () => {
|
|
test('allocates inside the fixed scan range, never the ephemeral range', async () => {
|
|
for (let i = 0; i < 5; i++) {
|
|
const port = await findAvailablePort();
|
|
expect(port).toBeGreaterThanOrEqual(RANDOM_PORT_MIN);
|
|
expect(port).toBeLessThan(RANDOM_PORT_MAX);
|
|
// The load-bearing property: below the ephemeral floor (49152).
|
|
expect(RANDOM_PORT_MAX).toBeLessThanOrEqual(60000);
|
|
expect(RANDOM_PORT_MIN).toBeGreaterThanOrEqual(1024);
|
|
}
|
|
});
|
|
|
|
test('explicit free port is honored', async () => {
|
|
// Find a free port by binding 0, then ask the allocator for exactly it.
|
|
const free = await new Promise<number>((resolve, reject) => {
|
|
const srv = net.createServer();
|
|
srv.once('error', reject);
|
|
srv.listen(0, '127.0.0.1', () => {
|
|
const p = (srv.address() as net.AddressInfo).port;
|
|
srv.close(() => resolve(p));
|
|
});
|
|
});
|
|
expect(await findAvailablePort(free)).toBe(free);
|
|
});
|
|
|
|
test('explicit occupied port throws an actionable error', async () => {
|
|
const srv = net.createServer();
|
|
await new Promise<void>((resolve, reject) => {
|
|
srv.once('error', reject);
|
|
srv.listen(0, '127.0.0.1', () => resolve());
|
|
});
|
|
const occupied = (srv.address() as net.AddressInfo).port;
|
|
try {
|
|
await expect(findAvailablePort(occupied)).rejects.toThrow(/in use/);
|
|
} finally {
|
|
await new Promise<void>((r) => srv.close(() => r()));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('terminal-agent uses the shared allocator (static tripwire)', () => {
|
|
test('terminal-agent.ts never binds port: 0', () => {
|
|
const src = fs.readFileSync(AGENT_TS, 'utf-8');
|
|
// Strip comments so the explanatory history above the bind doesn't trip.
|
|
const code = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
|
expect(code).not.toMatch(/port:\s*0\b/);
|
|
expect(src).toContain("from './port-allocator'");
|
|
expect(src).toContain('findAvailablePort');
|
|
});
|
|
|
|
test('server.ts routes findPort through the same allocator', () => {
|
|
const src = fs.readFileSync(SERVER_TS, 'utf-8');
|
|
expect(src).toContain("from './port-allocator'");
|
|
expect(src).toMatch(/findAvailablePort\(BROWSE_PORT\)/);
|
|
});
|
|
});
|