test: raw test servers listen on port 0 (#440)

http/Fastify servers in daemon-control, deepseek-status-shim, session-input-wait and the three tui tests listen on 0 and read address().port. The two "nothing listens here" ports (a fixed 3243, and the server's port + 1) come from closedPort(): bind 0, read the port, close.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
RandalixandClaude Opus 5.5 committed 2026-10-09 16:29:01 +02:00
1 parent 6f87186deb
commit eceaecf076
6 files changed
+81 -46

No files matched your search

+19 -6
View File
@@ -6,6 +6,7 @@
import { describe, it, expect, afterAll, beforeAll } from 'vitest'; import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import http from 'node:http'; import http from 'node:http';
import type { AddressInfo } from 'node:net';
import { import {
buildBaseUrl, buildBaseUrl,
buildStatusUrl, buildStatusUrl,
@@ -16,7 +17,17 @@ import {
probeServer, probeServer,
} from '../src/daemon-control.js'; } from '../src/daemon-control.js';
const PORT = 3216; /**
* A port nothing listens on: bind 0, read what the OS handed out, close. Free at the
* moment of use, unlike "the server's port + 1", which anything may hold.
*/
async function closedPort(): Promise<number> {
const probe = http.createServer();
await new Promise<void>((resolve) => probe.listen(0, '127.0.0.1', resolve));
const { port: free } = probe.address() as AddressInfo;
await new Promise<void>((resolve) => probe.close(() => resolve()));
return free;
}
describe('buildWebArgs', () => { describe('buildWebArgs', () => {
it('always passes host and port through explicitly', () => { it('always passes host and port through explicitly', () => {
@@ -143,6 +154,7 @@ describe('isProcessAlive', () => {
describe('probeServer', () => { describe('probeServer', () => {
let server: http.Server; let server: http.Server;
let port: number;
beforeAll(async () => { beforeAll(async () => {
server = http.createServer((req, res) => { server = http.createServer((req, res) => {
@@ -157,7 +169,8 @@ describe('probeServer', () => {
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, data: { version: '9.9.9' } })); res.end(JSON.stringify({ success: true, data: { version: '9.9.9' } }));
}); });
await new Promise<void>((resolve) => server.listen(PORT, '127.0.0.1', resolve)); await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
port = (server.address() as AddressInfo).port;
}); });
afterAll(async () => { afterAll(async () => {
@@ -165,23 +178,23 @@ describe('probeServer', () => {
}); });
it('reports up and reads the version back', async () => { it('reports up and reads the version back', async () => {
const result = await probeServer(`http://127.0.0.1:${PORT}/api/status`); const result = await probeServer(`http://127.0.0.1:${port}/api/status`);
expect(result.up).toBe(true); expect(result.up).toBe(true);
expect(result.version).toBe('9.9.9'); expect(result.version).toBe('9.9.9');
}); });
it('counts a 401 as up, because auth being active proves a server is there', async () => { it('counts a 401 as up, because auth being active proves a server is there', async () => {
const result = await probeServer(`http://127.0.0.1:${PORT}/unauthorized`); const result = await probeServer(`http://127.0.0.1:${port}/unauthorized`);
expect(result.up).toBe(true); expect(result.up).toBe(true);
}); });
it('does not mistake an unrelated service squatting on the port for Codeman', async () => { it('does not mistake an unrelated service squatting on the port for Codeman', async () => {
const result = await probeServer(`http://127.0.0.1:${PORT}/foreign`); const result = await probeServer(`http://127.0.0.1:${port}/foreign`);
expect(result.up).toBe(false); expect(result.up).toBe(false);
}); });
it('reports down when nothing is listening', async () => { it('reports down when nothing is listening', async () => {
const result = await probeServer(`http://127.0.0.1:${PORT + 1}/api/status`, 1000); const result = await probeServer(`http://127.0.0.1:${await closedPort()}/api/status`, 1000);
expect(result.up).toBe(false); expect(result.up).toBe(false);
}); });
+7 -4
View File
@@ -16,6 +16,7 @@
import { describe, expect, it, beforeEach, beforeAll, afterAll } from 'vitest'; import { describe, expect, it, beforeEach, beforeAll, afterAll } from 'vitest';
import { execFileSync, spawn } from 'node:child_process'; import { execFileSync, spawn } from 'node:child_process';
import { createServer, type Server } from 'node:http'; import { createServer, type Server } from 'node:http';
import type { AddressInfo } from 'node:net';
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, chmodSync } from 'node:fs'; import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, chmodSync } from 'node:fs';
import { dirname } from 'node:path'; import { dirname } from 'node:path';
import { import {
@@ -25,8 +26,6 @@ import {
DEEPSEEK_STATE_TO_HOOK_EVENT, DEEPSEEK_STATE_TO_HOOK_EVENT,
} from '../src/deepseek-status-shim.js'; } from '../src/deepseek-status-shim.js';
const PORT = 3251;
describe('DeepSeek status shim: provisioning', () => { describe('DeepSeek status shim: provisioning', () => {
beforeEach(() => { beforeEach(() => {
resetDeepSeekStatusShimForTest(); resetDeepSeekStatusShimForTest();
@@ -73,6 +72,7 @@ describe('DeepSeek status shim: provisioning', () => {
describe('DeepSeek status shim: the supervisor contract', () => { describe('DeepSeek status shim: the supervisor contract', () => {
let server: Server | undefined; let server: Server | undefined;
let port: number;
const received: Array<{ body: unknown; secret: string | undefined }> = []; const received: Array<{ body: unknown; secret: string | undefined }> = [];
let status = 200; let status = 200;
@@ -96,7 +96,10 @@ describe('DeepSeek status shim: the supervisor contract', () => {
res.end('{}'); res.end('{}');
}); });
}); });
server.listen(PORT, '127.0.0.1', resolve); server.listen(0, '127.0.0.1', () => {
port = (server!.address() as AddressInfo).port;
resolve();
});
}); });
beforeAll(() => listen()); beforeAll(() => listen());
@@ -120,7 +123,7 @@ describe('DeepSeek status shim: the supervisor contract', () => {
const child = spawn(process.execPath, [path, ...args], { const child = spawn(process.execPath, [path, ...args], {
env: { env: {
...process.env, ...process.env,
CODEMAN_API_URL: `http://127.0.0.1:${PORT}`, CODEMAN_API_URL: `http://127.0.0.1:${port}`,
CODEMAN_SESSION_ID: 'sess-from-env', CODEMAN_SESSION_ID: 'sess-from-env',
...env, ...env,
}, },
+2 -3
View File
@@ -451,13 +451,12 @@ describe('POST /api/sessions/:id/input: delivered reports the write, not just th
* exactly why this regression needs a POST and a real socket to catch. * exactly why this regression needs a POST and a real socket to catch.
*/ */
describe('POST /api/sessions/:id/input over real HTTP: hang-up handling', () => { describe('POST /api/sessions/:id/input over real HTTP: hang-up handling', () => {
const PORT = 3181; let base: string;
const base = `http://127.0.0.1:${PORT}`;
let app: FastifyInstance; let app: FastifyInstance;
beforeAll(async () => { beforeAll(async () => {
app = (await harness()).app; app = (await harness()).app;
await app.listen({ port: PORT, host: '127.0.0.1' }); base = await app.listen({ port: 0, host: '127.0.0.1' });
}); });
afterAll(async () => { afterAll(async () => {
+15 -12
View File
@@ -9,10 +9,11 @@
*/ */
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'; import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
import http from 'node:http'; import http from 'node:http';
import type { AddressInfo } from 'node:net';
import { TuiClient, type TuiApprovalEvent, type TuiSseStatusDetail } from '../../src/tui/tui-client.js'; import { TuiClient, type TuiApprovalEvent, type TuiSseStatusDetail } from '../../src/tui/tui-client.js';
const PORT = 3242; let port: number;
const BASE_URL = `http://127.0.0.1:${PORT}`; let baseUrl: string;
interface Connection { interface Connection {
url: string; url: string;
@@ -57,7 +58,9 @@ beforeAll(async () => {
res.flushHeaders(); res.flushHeaders();
connections.push({ url: req.url, headers: req.headers, res }); connections.push({ url: req.url, headers: req.headers, res });
}); });
await new Promise<void>((resolve) => server.listen(PORT, '127.0.0.1', resolve)); await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
port = (server.address() as AddressInfo).port;
baseUrl = `http://127.0.0.1:${port}`;
}); });
afterAll(async () => { afterAll(async () => {
@@ -82,7 +85,7 @@ describe('subscribeEvents', () => {
let planUsage: unknown = null; let planUsage: unknown = null;
let init: unknown = null; let init: unknown = null;
client = new TuiClient({ baseUrl: BASE_URL, password: 's3cret' }); client = new TuiClient({ baseUrl, password: 's3cret' });
client.subscribeEvents({ client.subscribeEvents({
onInit: (state) => { onInit: (state) => {
init = state; init = state;
@@ -117,7 +120,7 @@ describe('subscribeEvents', () => {
}); });
it('suppresses the terminal firehose by default and carries the auth header', async () => { it('suppresses the terminal firehose by default and carries the auth header', async () => {
client = new TuiClient({ baseUrl: BASE_URL, password: 's3cret' }); client = new TuiClient({ baseUrl, password: 's3cret' });
client.subscribeEvents({}); client.subscribeEvents({});
await until(() => connections.length === 1); await until(() => connections.length === 1);
expect(connections[0].url).toBe('/api/events?sessions=tui-no-terminal'); expect(connections[0].url).toBe('/api/events?sessions=tui-no-terminal');
@@ -126,7 +129,7 @@ describe('subscribeEvents', () => {
}); });
it('subscribes to the terminal stream of named sessions when asked', async () => { it('subscribes to the terminal stream of named sessions when asked', async () => {
client = new TuiClient({ baseUrl: BASE_URL }); client = new TuiClient({ baseUrl });
client.subscribeEvents({}, { sessionIds: ['a', 'b'] }); client.subscribeEvents({}, { sessionIds: ['a', 'b'] });
await until(() => connections.length === 1); await until(() => connections.length === 1);
expect(connections[0].url).toBe('/api/events?sessions=a%2Cb'); expect(connections[0].url).toBe('/api/events?sessions=a%2Cb');
@@ -134,7 +137,7 @@ describe('subscribeEvents', () => {
it('reconnects when the stream ends', async () => { it('reconnects when the stream ends', async () => {
const statuses: Array<[string, TuiSseStatusDetail]> = []; const statuses: Array<[string, TuiSseStatusDetail]> = [];
client = new TuiClient({ baseUrl: BASE_URL }); client = new TuiClient({ baseUrl });
const stream = client.subscribeEvents( const stream = client.subscribeEvents(
{ onStatus: (status, detail) => statuses.push([status, detail]) }, { onStatus: (status, detail) => statuses.push([status, detail]) },
{ baseBackoffMs: 10, maxBackoffMs: 20 } { baseBackoffMs: 10, maxBackoffMs: 20 }
@@ -148,7 +151,7 @@ describe('subscribeEvents', () => {
}); });
it('reconnects when a live stream goes silent, which no socket error reports', async () => { it('reconnects when a live stream goes silent, which no socket error reports', async () => {
client = new TuiClient({ baseUrl: BASE_URL }); client = new TuiClient({ baseUrl });
client.subscribeEvents({}, { staleTimeoutMs: 150, checkIntervalMs: 25, baseBackoffMs: 10, maxBackoffMs: 20 }); client.subscribeEvents({}, { staleTimeoutMs: 150, checkIntervalMs: 25, baseBackoffMs: 10, maxBackoffMs: 20 });
await until(() => connections.length === 1); await until(() => connections.length === 1);
@@ -161,7 +164,7 @@ describe('subscribeEvents', () => {
it('recommends polling once connecting keeps failing', async () => { it('recommends polling once connecting keeps failing', async () => {
refuse = true; refuse = true;
const details: TuiSseStatusDetail[] = []; const details: TuiSseStatusDetail[] = [];
client = new TuiClient({ baseUrl: BASE_URL }); client = new TuiClient({ baseUrl });
const stream = client.subscribeEvents( const stream = client.subscribeEvents(
{ onStatus: (_status, detail) => details.push(detail) }, { onStatus: (_status, detail) => details.push(detail) },
{ baseBackoffMs: 10, maxBackoffMs: 20, pollingAfterFailures: 2 } { baseBackoffMs: 10, maxBackoffMs: 20, pollingAfterFailures: 2 }
@@ -175,7 +178,7 @@ describe('subscribeEvents', () => {
}); });
it('stops reconnecting after close, so the process can exit', async () => { it('stops reconnecting after close, so the process can exit', async () => {
client = new TuiClient({ baseUrl: BASE_URL }); client = new TuiClient({ baseUrl });
const stream = client.subscribeEvents({}, { baseBackoffMs: 10, maxBackoffMs: 20 }); const stream = client.subscribeEvents({}, { baseBackoffMs: 10, maxBackoffMs: 20 });
await until(() => connections.length === 1); await until(() => connections.length === 1);
@@ -187,7 +190,7 @@ describe('subscribeEvents', () => {
}); });
it('closes every stream the client opened', async () => { it('closes every stream the client opened', async () => {
client = new TuiClient({ baseUrl: BASE_URL }); client = new TuiClient({ baseUrl });
client.subscribeEvents({}); client.subscribeEvents({});
client.subscribeEvents({}); client.subscribeEvents({});
await until(() => connections.length === 2); await until(() => connections.length === 2);
@@ -200,7 +203,7 @@ describe('subscribeEvents', () => {
}); });
it('refuses to subscribe before the client knows where the server is', () => { it('refuses to subscribe before the client knows where the server is', () => {
const disconnected = new TuiClient({ port: 3999 }); const disconnected = new TuiClient({ port });
expect(() => disconnected.subscribeEvents({})).toThrow(/connect\(\)/); expect(() => disconnected.subscribeEvents({})).toThrow(/connect\(\)/);
}); });
}); });
+33 -17
View File
@@ -11,6 +11,7 @@
*/ */
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import http from 'node:http'; import http from 'node:http';
import type { AddressInfo } from 'node:net';
import { mkdirSync, writeFileSync } from 'node:fs'; import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname } from 'node:path'; import { dirname } from 'node:path';
import { dataPath } from '../../src/config/instance.js'; import { dataPath } from '../../src/config/instance.js';
@@ -32,10 +33,22 @@ import {
type TuiExecFile, type TuiExecFile,
} from '../../src/tui/tui-client.js'; } from '../../src/tui/tui-client.js';
const PORT = 3241; /**
/** Nothing ever listens here: the "no server" path. */ * A port nothing listens on: bind 0, read what the OS handed out, close. Free at the
const DEAD_PORT = 3243; * moment of use, unlike "the server's port + 1", which anything may hold.
const BASE_URL = `http://127.0.0.1:${PORT}`; */
async function closedPort(): Promise<number> {
const probe = http.createServer();
await new Promise<void>((resolve) => probe.listen(0, '127.0.0.1', resolve));
const { port: free } = probe.address() as AddressInfo;
await new Promise<void>((resolve) => probe.close(() => resolve()));
return free;
}
let port: number;
/** Nothing listens one above the bound port: the "no server" path. */
let deadPort: number;
let baseUrl: string;
interface Recorded { interface Recorded {
method: string; method: string;
@@ -115,7 +128,7 @@ const defaultResponder: Responder = (req, res) => {
}; };
function client(overrides: Record<string, unknown> = {}): TuiClient { function client(overrides: Record<string, unknown> = {}): TuiClient {
return new TuiClient({ baseUrl: BASE_URL, timeoutMs: 4000, ...overrides }); return new TuiClient({ baseUrl, timeoutMs: 4000, ...overrides });
} }
let server: http.Server; let server: http.Server;
@@ -140,7 +153,10 @@ beforeAll(async () => {
(responder ?? defaultResponder)(req, res, body); (responder ?? defaultResponder)(req, res, body);
}); });
}); });
await new Promise<void>((resolve) => server.listen(PORT, '127.0.0.1', resolve)); await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
port = (server.address() as AddressInfo).port;
deadPort = await closedPort();
baseUrl = `http://127.0.0.1:${port}`;
}); });
afterAll(async () => { afterAll(async () => {
@@ -391,8 +407,8 @@ describe('TuiClient remaining API surface', () => {
describe('TuiClient.connect', () => { describe('TuiClient.connect', () => {
it('discovers the loopback server and reports its identity', async () => { it('discovers the loopback server and reports its identity', async () => {
const info = await new TuiClient({ port: PORT, probeTimeoutMs: 1000 }).connect(); const info = await new TuiClient({ port, probeTimeoutMs: 1000 }).connect();
expect(info?.baseUrl).toBe(BASE_URL); expect(info?.baseUrl).toBe(baseUrl);
expect(info?.version).toBe('9.9.9'); expect(info?.version).toBe('9.9.9');
expect(info?.hostname).toBeTruthy(); expect(info?.hostname).toBeTruthy();
expect(info?.authRequired).toBeUndefined(); expect(info?.authRequired).toBeUndefined();
@@ -403,17 +419,17 @@ describe('TuiClient.connect', () => {
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="codeman"' }); res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="codeman"' });
res.end('Unauthorized'); res.end('Unauthorized');
}; };
const info = await new TuiClient({ port: PORT, probeTimeoutMs: 1000 }).connect(); const info = await new TuiClient({ port, probeTimeoutMs: 1000 }).connect();
expect(info?.baseUrl).toBe(BASE_URL); expect(info?.baseUrl).toBe(baseUrl);
expect(info?.authRequired).toBe(true); expect(info?.authRequired).toBe(true);
}); });
it('returns null when nothing answers', async () => { it('returns null when nothing answers', async () => {
await expect(new TuiClient({ port: DEAD_PORT, probeTimeoutMs: 500 }).connect()).resolves.toBeNull(); await expect(new TuiClient({ port: deadPort, probeTimeoutMs: 500 }).connect()).resolves.toBeNull();
}); });
it('refuses to talk to an unconnected client', async () => { it('refuses to talk to an unconnected client', async () => {
await expect(new TuiClient({ port: DEAD_PORT }).fetchApprovals()).rejects.toThrow(/not connected/); await expect(new TuiClient({ port: deadPort }).fetchApprovals()).rejects.toThrow(/not connected/);
}); });
}); });
@@ -501,7 +517,7 @@ describe('degraded-mode tmux enumeration', () => {
describe('attach window sizing', () => { describe('attach window sizing', () => {
/** A client that only ever needs its injected exec: none of this talks to a server. */ /** A client that only ever needs its injected exec: none of this talks to a server. */
function sizingClient(exec: TuiExecFile): TuiClient { function sizingClient(exec: TuiExecFile): TuiClient {
return new TuiClient({ baseUrl: BASE_URL, socket: 'codeman-beta', exec }); return new TuiClient({ baseUrl, socket: 'codeman-beta', exec });
} }
it('parses the sizing format, and rejects a window tmux could not measure', () => { it('parses the sizing format, and rejects a window tmux could not measure', () => {
@@ -615,7 +631,7 @@ describe('TuiClient.bindSwitchKey', () => {
calls.push([...args]); calls.push([...args]);
return { stdout: '', stderr: '' }; return { stdout: '', stderr: '' };
}; };
const client = new TuiClient({ baseUrl: BASE_URL, socket: 'codeman-beta', exec }); const client = new TuiClient({ baseUrl, socket: 'codeman-beta', exec });
return client.bindSwitchKey('M-2', 'codeman-aaaa1111').then(() => { return client.bindSwitchKey('M-2', 'codeman-aaaa1111').then(() => {
const bind = calls.find((args) => args.includes('bind-key')); const bind = calls.find((args) => args.includes('bind-key'));
expect(bind).toBeDefined(); expect(bind).toBeDefined();
@@ -672,7 +688,7 @@ describe('TuiClient.clearLeakedAttachBanners', () => {
} }
return { stdout: '', stderr: '' }; return { stdout: '', stderr: '' };
}; };
return { client: new TuiClient({ baseUrl: BASE_URL, socket: 'codeman-beta', exec }), calls }; return { client: new TuiClient({ baseUrl, socket: 'codeman-beta', exec }), calls };
} }
it('takes down a bar a killed TUI left behind, and puts status back off', async () => { it('takes down a bar a killed TUI left behind, and puts status back off', async () => {
@@ -713,7 +729,7 @@ describe('TuiClient.clearLeakedAttachBanners', () => {
const exec: TuiExecFile = async () => { const exec: TuiExecFile = async () => {
throw new Error('no server running on /tmp/tmux-1000/codeman-beta'); throw new Error('no server running on /tmp/tmux-1000/codeman-beta');
}; };
const client = new TuiClient({ baseUrl: BASE_URL, socket: 'codeman-beta', exec }); const client = new TuiClient({ baseUrl, socket: 'codeman-beta', exec });
await expect(client.clearLeakedAttachBanners()).resolves.toBe(0); await expect(client.clearLeakedAttachBanners()).resolves.toBe(0);
}); });
}); });
@@ -761,7 +777,7 @@ describe('parseDetachKey', () => {
describe('attach status bar options', () => { describe('attach status bar options', () => {
function optionsClient(exec: TuiExecFile): TuiClient { function optionsClient(exec: TuiExecFile): TuiClient {
return new TuiClient({ baseUrl: BASE_URL, socket: 'codeman-beta', exec }); return new TuiClient({ baseUrl, socket: 'codeman-beta', exec });
} }
const SHOW = [ const SHOW = [
+5 -4
View File
@@ -25,6 +25,7 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import http from 'node:http'; import http from 'node:http';
import type { AddressInfo } from 'node:net';
import { mkdtempSync, rmSync } from 'node:fs'; import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path'; import { join, resolve } from 'node:path';
@@ -35,8 +36,7 @@ import type { SearchResponseData } from '../../src/types/search.js';
import type { ApprovalItem } from '../../src/web/approval-inbox.js'; import type { ApprovalItem } from '../../src/web/approval-inbox.js';
import type { AwayDigestResponse } from '../../src/web/away-digest.js'; import type { AwayDigestResponse } from '../../src/web/away-digest.js';
const PORT = 3244; let baseUrl: string;
const BASE_URL = `http://127.0.0.1:${PORT}`;
const ROOT = resolve(import.meta.dirname, '..', '..'); const ROOT = resolve(import.meta.dirname, '..', '..');
const COLS = 100; const COLS = 100;
const ROWS = 30; const ROWS = 30;
@@ -234,7 +234,7 @@ function childEnv(): Record<string, string> {
delete env.CODEMAN_PORT; delete env.CODEMAN_PORT;
return { return {
...env, ...env,
CODEMAN_API_URL: BASE_URL, CODEMAN_API_URL: baseUrl,
CODEMAN_DATA_DIR: dataDir, CODEMAN_DATA_DIR: dataDir,
CODEMAN_TMUX_SOCKET: 'codeman-tui-e2e', CODEMAN_TMUX_SOCKET: 'codeman-tui-e2e',
CODEMAN_TUI_GLYPHS: 'ascii', CODEMAN_TUI_GLYPHS: 'ascii',
@@ -347,7 +347,8 @@ beforeAll(async () => {
res.writeHead(404, { 'Content-Type': 'application/json' }); res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: 'no route', errorCode: 'NOT_FOUND' })); res.end(JSON.stringify({ success: false, error: 'no route', errorCode: 'NOT_FOUND' }));
}); });
await new Promise<void>((done) => server.listen(PORT, '127.0.0.1', done)); await new Promise<void>((done) => server.listen(0, '127.0.0.1', done));
baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
}); });
afterAll(async () => { afterAll(async () => {