diff --git a/src/gateway/server-discovery-runtime.ts b/src/gateway/server-discovery-runtime.ts index e2179391a..22f56621f 100644 --- a/src/gateway/server-discovery-runtime.ts +++ b/src/gateway/server-discovery-runtime.ts @@ -17,7 +17,12 @@ export async function startGatewayDiscovery(params: { logDiscovery: { info: (msg: string) => void; warn: (msg: string) => void }; }) { let bonjourStop: (() => Promise) | null = null; - const tailnetDns = await resolveTailnetDnsHint(); + const bonjourEnabled = + process.env.CLAWDBOT_DISABLE_BONJOUR !== "1" && + process.env.NODE_ENV !== "test" && + !process.env.VITEST; + const needsTailnetDns = bonjourEnabled || params.wideAreaDiscoveryEnabled; + const tailnetDns = needsTailnetDns ? await resolveTailnetDnsHint() : undefined; const sshPortEnv = process.env.CLAWDBOT_SSH_PORT?.trim(); const sshPortParsed = sshPortEnv ? Number.parseInt(sshPortEnv, 10) : NaN; const sshPort = Number.isFinite(sshPortParsed) && sshPortParsed > 0 ? sshPortParsed : undefined; diff --git a/src/gateway/test-helpers.server.ts b/src/gateway/test-helpers.server.ts index 24e71d829..526eed1e1 100644 --- a/src/gateway/test-helpers.server.ts +++ b/src/gateway/test-helpers.server.ts @@ -29,6 +29,10 @@ import { testTailnetIPv4, } from "./test-helpers.mocks.js"; +// Preload the gateway server module once per worker. +// Important: `test-helpers.mocks` must run before importing the server so vi.mock hooks apply. +const serverModulePromise = import("./server.js"); + let previousHome: string | undefined; let previousUserProfile: string | undefined; let previousStateDir: string | undefined; @@ -105,7 +109,7 @@ export function installGatewayTestHooks() { embeddedRunMock.waitResults.clear(); drainSystemEvents(resolveMainSessionKeyFromConfig()); resetAgentRunContextForTest(); - const mod = await import("./server.js"); + const mod = await serverModulePromise; mod.__resetModelCatalogCacheForTest(); piSdkMock.enabled = false; piSdkMock.discoverCalls = 0; @@ -184,7 +188,7 @@ export function onceMessage( } export async function startGatewayServer(port: number, opts?: GatewayServerOptions) { - const mod = await import("./server.js"); + const mod = await serverModulePromise; return await mod.startGatewayServer(port, opts); } diff --git a/src/infra/machine-name.ts b/src/infra/machine-name.ts index 04c0825a1..b6a6f3a75 100644 --- a/src/infra/machine-name.ts +++ b/src/infra/machine-name.ts @@ -31,6 +31,9 @@ function fallbackHostName() { export async function getMachineDisplayName(): Promise { if (cachedPromise) return cachedPromise; cachedPromise = (async () => { + if (process.env.VITEST || process.env.NODE_ENV === "test") { + return fallbackHostName(); + } if (process.platform === "darwin") { const computerName = await tryScutil("ComputerName"); if (computerName) return computerName; diff --git a/src/test-utils/ports.ts b/src/test-utils/ports.ts index e3d6c145e..2bf6e6734 100644 --- a/src/test-utils/ports.ts +++ b/src/test-utils/ports.ts @@ -1,4 +1,5 @@ import { type AddressInfo, createServer } from "node:net"; +import { isMainThread, threadId } from "node:worker_threads"; async function isPortFree(port: number): Promise { if (!Number.isFinite(port) || port <= 0 || port > 65535) return false; @@ -45,7 +46,11 @@ export async function getDeterministicFreePortBlock(params?: { 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); + const shard = Number.isFinite(workerId) + ? Math.max(0, workerId) + : isMainThread + ? Math.abs(process.pid) + : Math.abs(threadId); const rangeSize = 1000; const shardCount = 30; @@ -79,4 +84,3 @@ export async function getDeterministicFreePortBlock(params?: { throw new Error("failed to acquire a free port block"); } - diff --git a/test/test-env.ts b/test/test-env.ts index 7560aa0cf..172085521 100644 --- a/test/test-env.ts +++ b/test/test-env.ts @@ -62,10 +62,16 @@ export function installTestEnv(): { cleanup: () => void; tempHome: string } { { key: "XDG_CACHE_HOME", value: process.env.XDG_CACHE_HOME }, { key: "CLAWDBOT_STATE_DIR", value: process.env.CLAWDBOT_STATE_DIR }, { key: "CLAWDBOT_CONFIG_PATH", value: process.env.CLAWDBOT_CONFIG_PATH }, + { key: "CLAWDBOT_GATEWAY_PORT", value: process.env.CLAWDBOT_GATEWAY_PORT }, + { key: "CLAWDBOT_BRIDGE_ENABLED", value: process.env.CLAWDBOT_BRIDGE_ENABLED }, + { key: "CLAWDBOT_BRIDGE_HOST", value: process.env.CLAWDBOT_BRIDGE_HOST }, + { key: "CLAWDBOT_BRIDGE_PORT", value: process.env.CLAWDBOT_BRIDGE_PORT }, + { key: "CLAWDBOT_CANVAS_HOST_PORT", value: process.env.CLAWDBOT_CANVAS_HOST_PORT }, { key: "CLAWDBOT_TEST_HOME", value: process.env.CLAWDBOT_TEST_HOME }, { key: "COPILOT_GITHUB_TOKEN", value: process.env.COPILOT_GITHUB_TOKEN }, { key: "GH_TOKEN", value: process.env.GH_TOKEN }, { key: "GITHUB_TOKEN", value: process.env.GITHUB_TOKEN }, + { key: "NODE_OPTIONS", value: process.env.NODE_OPTIONS }, ]; const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-test-home-")); @@ -78,10 +84,18 @@ export function installTestEnv(): { cleanup: () => void; tempHome: string } { delete process.env.CLAWDBOT_CONFIG_PATH; // Prefer deriving state dir from HOME so nested tests that change HOME also isolate correctly. delete process.env.CLAWDBOT_STATE_DIR; + // Prefer test-controlled ports over developer overrides (avoid port collisions across tests/workers). + delete process.env.CLAWDBOT_GATEWAY_PORT; + delete process.env.CLAWDBOT_BRIDGE_ENABLED; + delete process.env.CLAWDBOT_BRIDGE_HOST; + delete process.env.CLAWDBOT_BRIDGE_PORT; + delete process.env.CLAWDBOT_CANVAS_HOST_PORT; // Avoid leaking real GitHub/Copilot tokens into non-live test runs. delete process.env.COPILOT_GITHUB_TOKEN; delete process.env.GH_TOKEN; delete process.env.GITHUB_TOKEN; + // Avoid leaking local dev tooling flags into tests (e.g. --inspect). + delete process.env.NODE_OPTIONS; // Windows: prefer the legacy default state dir so auth/profile tests match real paths. if (process.platform === "win32") {