test: stabilize gateway ports and timers
This commit is contained in:
@@ -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<{
|
||||
|
||||
Reference in New Issue
Block a user