test: stabilize gateway ports and timers

This commit is contained in:
Peter Steinberger
2026-01-18 04:29:52 +00:00
parent cf8b3ed988
commit 1a0d1cb7b2
7 changed files with 137 additions and 135 deletions

View File

@@ -3,7 +3,7 @@ import { type AddressInfo, createServer } from "node:net";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, expect } from "vitest";
import { afterEach, beforeEach, expect, vi } from "vitest";
import { WebSocket } from "ws";
import { resolveMainSessionKeyFromConfig, type SessionEntry } from "../config/sessions.js";
@@ -12,6 +12,7 @@ import { drainSystemEvents, peekSystemEvents } from "../infra/system-events.js";
import { rawDataToString } from "../infra/ws.js";
import { resetLogger, setLoggerOverride } from "../logging.js";
import { DEFAULT_AGENT_ID, toAgentStoreSessionKey } from "../routing/session-key.js";
import { getDeterministicFreePortBlock } from "../test-utils/ports.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
import { PROTOCOL_VERSION } from "./protocol/index.js";
@@ -29,6 +30,9 @@ import {
} from "./test-helpers.mocks.js";
let previousHome: string | undefined;
let previousUserProfile: string | undefined;
let previousStateDir: string | undefined;
let previousConfigPath: string | undefined;
let tempHome: string | undefined;
let tempConfigRoot: string | undefined;
@@ -60,10 +64,18 @@ export async function writeSessionStore(params: {
export function installGatewayTestHooks() {
beforeEach(async () => {
// Some tests intentionally use fake timers; ensure they don't leak into gateway suites.
vi.useRealTimers();
setLoggerOverride({ level: "silent", consoleLevel: "silent" });
previousHome = process.env.HOME;
previousUserProfile = process.env.USERPROFILE;
previousStateDir = process.env.CLAWDBOT_STATE_DIR;
previousConfigPath = process.env.CLAWDBOT_CONFIG_PATH;
tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gateway-home-"));
process.env.HOME = tempHome;
process.env.USERPROFILE = tempHome;
process.env.CLAWDBOT_STATE_DIR = path.join(tempHome, ".clawdbot");
delete process.env.CLAWDBOT_CONFIG_PATH;
tempConfigRoot = path.join(tempHome, ".clawdbot-test");
setTestConfigRoot(tempConfigRoot);
sessionStoreSaveDelayMs.value = 0;
@@ -101,8 +113,16 @@ export function installGatewayTestHooks() {
}, 60_000);
afterEach(async () => {
vi.useRealTimers();
resetLogger();
process.env.HOME = previousHome;
if (previousHome === undefined) delete process.env.HOME;
else process.env.HOME = previousHome;
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
else process.env.USERPROFILE = previousUserProfile;
if (previousStateDir === undefined) delete process.env.CLAWDBOT_STATE_DIR;
else process.env.CLAWDBOT_STATE_DIR = previousStateDir;
if (previousConfigPath === undefined) delete process.env.CLAWDBOT_CONFIG_PATH;
else process.env.CLAWDBOT_CONFIG_PATH = previousConfigPath;
if (tempHome) {
await fs.rm(tempHome, {
recursive: true,
@@ -116,42 +136,8 @@ export function installGatewayTestHooks() {
});
}
let nextTestPortOffset = 0;
export async function getFreePort(): Promise<number> {
const workerIdRaw = process.env.VITEST_WORKER_ID ?? process.env.VITEST_POOL_ID ?? "";
const workerId = Number.parseInt(workerIdRaw, 10);
const shard = Number.isFinite(workerId) ? Math.max(0, workerId) : Math.abs(process.pid);
// Avoid flaky "get a free port then bind later" races by allocating from a
// deterministic per-worker port range. Still probe for EADDRINUSE to avoid
// collisions with external processes.
const rangeSize = 1000;
const shardCount = 30;
const base = 30_000 + (Math.abs(shard) % shardCount) * rangeSize; // <= 59_999
for (let attempt = 0; attempt < rangeSize; attempt++) {
const port = base + (nextTestPortOffset++ % rangeSize);
// eslint-disable-next-line no-await-in-loop
const ok = await new Promise<boolean>((resolve) => {
const server = createServer();
server.once("error", () => resolve(false));
server.listen(port, "127.0.0.1", () => {
server.close(() => resolve(true));
});
});
if (ok) return port;
}
// Fallback: let the OS pick a port.
return await new Promise((resolve, reject) => {
const server = createServer();
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
const port = (server.address() as AddressInfo).port;
server.close((err) => (err ? reject(err) : resolve(port)));
});
});
return await getDeterministicFreePortBlock({ offsets: [0, 1, 2, 3, 4] });
}
export async function occupyPort(): Promise<{