From 95652d5867b6fe99aa2c6e3a2b95bd552ef933e1 Mon Sep 17 00:00:00 2001 From: Shakker Date: Wed, 6 May 2026 01:02:48 +0100 Subject: [PATCH] test: cover no-proxy undici startup --- src/infra/net/fetch-guard.ssrf.test.ts | 3 - src/infra/net/proxy-fetch.test.ts | 19 +-- .../net/undici-global-dispatcher.test.ts | 116 +++++++++++++----- 3 files changed, 99 insertions(+), 39 deletions(-) diff --git a/src/infra/net/fetch-guard.ssrf.test.ts b/src/infra/net/fetch-guard.ssrf.test.ts index ada989e34ce..d66a59603e2 100644 --- a/src/infra/net/fetch-guard.ssrf.test.ts +++ b/src/infra/net/fetch-guard.ssrf.test.ts @@ -1139,8 +1139,6 @@ describe("fetchWithSsrFGuard hardening", () => { }); it("inherits the configured global stream timeout for guarded direct dispatchers", async () => { - const { getGlobalDispatcher, setGlobalDispatcher } = await import("undici"); - const previousDispatcher = getGlobalDispatcher(); try { ensureGlobalUndiciStreamTimeouts({ timeoutMs: 1_900_000 }); (globalThis as Record)[TEST_UNDICI_RUNTIME_DEPS_KEY] = { @@ -1168,7 +1166,6 @@ describe("fetchWithSsrFGuard hardening", () => { }); await result.release(); } finally { - setGlobalDispatcher(previousDispatcher); resetGlobalUndiciStreamTimeoutsForTests(); } }); diff --git a/src/infra/net/proxy-fetch.test.ts b/src/infra/net/proxy-fetch.test.ts index e6df13c2efd..2601dd5dd17 100644 --- a/src/infra/net/proxy-fetch.test.ts +++ b/src/infra/net/proxy-fetch.test.ts @@ -14,13 +14,13 @@ const ORIGINAL_PROXY_ENV = Object.fromEntries( ) as Record<(typeof PROXY_ENV_KEYS)[number], string | undefined>; const { - ProxyAgent, EnvHttpProxyAgent, MockUndiciFormData, undiciFetch, proxyAgentSpy, envAgentSpy, getLastAgent, + loadUndiciRuntimeDeps, } = vi.hoisted(() => { const undiciFetch = vi.fn(); const proxyAgentSpy = vi.fn(); @@ -53,6 +53,12 @@ const { envAgentSpy(options); } } + const loadUndiciRuntimeDeps = vi.fn(() => ({ + ProxyAgent, + EnvHttpProxyAgent, + FormData: MockUndiciFormData, + fetch: undiciFetch, + })); return { ProxyAgent, @@ -62,16 +68,14 @@ const { proxyAgentSpy, envAgentSpy, getLastAgent: () => ProxyAgent.lastCreated, + loadUndiciRuntimeDeps, }; }); -const mockedModuleIds = ["undici"] as const; +const mockedModuleIds = ["./undici-runtime.js"] as const; -vi.mock("undici", () => ({ - ProxyAgent, - EnvHttpProxyAgent, - FormData: MockUndiciFormData, - fetch: undiciFetch, +vi.mock("./undici-runtime.js", () => ({ + loadUndiciRuntimeDeps, })); let getProxyUrlFromFetch: typeof import("./proxy-fetch.js").getProxyUrlFromFetch; @@ -248,6 +252,7 @@ describe("resolveProxyFetchFromEnv", () => { it("returns undefined when no proxy env vars are set", () => { expect(resolveProxyFetchFromEnv({})).toBeUndefined(); + expect(loadUndiciRuntimeDeps).not.toHaveBeenCalled(); }); it("returns proxy fetch using EnvHttpProxyAgent when HTTPS_PROXY is set", async () => { diff --git a/src/infra/net/undici-global-dispatcher.test.ts b/src/infra/net/undici-global-dispatcher.test.ts index c69844174be..01db4cc5f00 100644 --- a/src/infra/net/undici-global-dispatcher.test.ts +++ b/src/infra/net/undici-global-dispatcher.test.ts @@ -1,14 +1,17 @@ +import { execFileSync } from "node:child_process"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const { Agent, EnvHttpProxyAgent, ProxyAgent, - getGlobalDispatcher, setGlobalDispatcher, setCurrentDispatcher, getCurrentDispatcher, getDefaultAutoSelectFamily, + loadUndiciGlobalDispatcherDeps, } = vi.hoisted(() => { class Agent { constructor(public readonly options?: Record) {} @@ -34,6 +37,12 @@ const { }; const getCurrentDispatcher = () => currentDispatcher; const getDefaultAutoSelectFamily = vi.fn(() => undefined as boolean | undefined); + const loadUndiciGlobalDispatcherDeps = vi.fn(() => ({ + Agent, + EnvHttpProxyAgent, + getGlobalDispatcher, + setGlobalDispatcher, + })); return { Agent, @@ -44,17 +53,11 @@ const { setCurrentDispatcher, getCurrentDispatcher, getDefaultAutoSelectFamily, + loadUndiciGlobalDispatcherDeps, }; }); -const mockedModuleIds = ["node:net", "undici", "./proxy-env.js", "../wsl.js"] as const; - -vi.mock("undici", () => ({ - Agent, - EnvHttpProxyAgent, - getGlobalDispatcher, - setGlobalDispatcher, -})); +const mockedModuleIds = ["node:net", "./proxy-env.js", "./undici-runtime.js", "../wsl.js"] as const; vi.mock("node:net", () => ({ getDefaultAutoSelectFamily, @@ -65,6 +68,10 @@ vi.mock("./proxy-env.js", () => ({ resolveEnvHttpProxyAgentOptions: vi.fn(() => undefined), })); +vi.mock("./undici-runtime.js", () => ({ + loadUndiciGlobalDispatcherDeps, +})); + vi.mock("../wsl.js", () => ({ isWSL2Sync: vi.fn(() => false), })); @@ -99,24 +106,53 @@ describe("ensureGlobalUndiciStreamTimeouts", () => { vi.mocked(resolveEnvHttpProxyAgentOptions).mockReturnValue(undefined); }); - it("replaces default Agent dispatcher with extended stream timeouts", () => { + it("records timeout bridge without importing undici when no env proxy is configured", () => { getDefaultAutoSelectFamily.mockReturnValue(true); ensureGlobalUndiciStreamTimeouts(); - expect(setGlobalDispatcher).toHaveBeenCalledTimes(1); - const next = getCurrentDispatcher() as { options?: Record }; - expect(next).toBeInstanceOf(Agent); - expect(next.options?.bodyTimeout).toBe(DEFAULT_UNDICI_STREAM_TIMEOUT_MS); - expect(next.options?.headersTimeout).toBe(DEFAULT_UNDICI_STREAM_TIMEOUT_MS); - expect(next.options?.connect).toEqual({ - autoSelectFamily: true, - autoSelectFamilyAttemptTimeout: 300, - }); + expect(loadUndiciGlobalDispatcherDeps).not.toHaveBeenCalled(); + expect(setGlobalDispatcher).not.toHaveBeenCalled(); + expect(undiciGlobalDispatcherModule._globalUndiciStreamTimeoutMs).toBe( + DEFAULT_UNDICI_STREAM_TIMEOUT_MS, + ); + }); + + it("does not initialize the undici global dispatcher in a no-proxy subprocess", () => { + const moduleUrl = pathToFileURL(path.resolve("src/infra/net/undici-global-dispatcher.ts")).href; + const source = ` + const dispatcherKey = Symbol.for("undici.globalDispatcher.1"); + const mod = await import(${JSON.stringify(moduleUrl)}); + mod.ensureGlobalUndiciStreamTimeouts({ timeoutMs: 1_900_000 }); + if (globalThis[dispatcherKey] !== undefined) { + throw new Error("undici global dispatcher was initialized"); + } + console.log("ok"); + `; + const env = { ...process.env }; + for (const key of [ + "HTTP_PROXY", + "HTTPS_PROXY", + "ALL_PROXY", + "http_proxy", + "https_proxy", + "all_proxy", + ]) { + delete env[key]; + } + + const output = execFileSync( + process.execPath, + ["--import", "tsx", "--input-type=module", "--eval", source], + { cwd: process.cwd(), encoding: "utf8", env }, + ); + + expect(output.trim()).toBe("ok"); }); it("replaces EnvHttpProxyAgent dispatcher while preserving env-proxy mode", () => { getDefaultAutoSelectFamily.mockReturnValue(false); + vi.mocked(hasEnvHttpProxyAgentConfigured).mockReturnValue(true); setCurrentDispatcher(new EnvHttpProxyAgent()); ensureGlobalUndiciStreamTimeouts(); @@ -133,6 +169,7 @@ describe("ensureGlobalUndiciStreamTimeouts", () => { }); it("preserves explicit env proxy options when replacing EnvHttpProxyAgent dispatcher", () => { + vi.mocked(hasEnvHttpProxyAgentConfigured).mockReturnValue(true); vi.mocked(resolveEnvHttpProxyAgentOptions).mockReturnValue({ httpProxy: "socks5://proxy.test:1080", httpsProxy: "socks5://proxy.test:1080", @@ -165,6 +202,8 @@ describe("ensureGlobalUndiciStreamTimeouts", () => { it("is idempotent for unchanged dispatcher kind and network policy", () => { getDefaultAutoSelectFamily.mockReturnValue(true); + vi.mocked(hasEnvHttpProxyAgentConfigured).mockReturnValue(true); + setCurrentDispatcher(new EnvHttpProxyAgent()); ensureGlobalUndiciStreamTimeouts(); ensureGlobalUndiciStreamTimeouts(); @@ -175,10 +214,11 @@ describe("ensureGlobalUndiciStreamTimeouts", () => { it("does not lower global stream timeouts below the default floor", () => { ensureGlobalUndiciStreamTimeouts({ timeoutMs: 15_000 }); - expect(setGlobalDispatcher).toHaveBeenCalledTimes(1); - const next = getCurrentDispatcher() as { options?: Record }; - expect(next.options?.bodyTimeout).toBe(DEFAULT_UNDICI_STREAM_TIMEOUT_MS); - expect(next.options?.headersTimeout).toBe(DEFAULT_UNDICI_STREAM_TIMEOUT_MS); + expect(loadUndiciGlobalDispatcherDeps).not.toHaveBeenCalled(); + expect(setGlobalDispatcher).not.toHaveBeenCalled(); + expect(undiciGlobalDispatcherModule._globalUndiciStreamTimeoutMs).toBe( + DEFAULT_UNDICI_STREAM_TIMEOUT_MS, + ); }); it("honors explicit global stream timeouts above the default floor", () => { @@ -186,13 +226,14 @@ describe("ensureGlobalUndiciStreamTimeouts", () => { ensureGlobalUndiciStreamTimeouts({ timeoutMs }); - expect(setGlobalDispatcher).toHaveBeenCalledTimes(1); - const next = getCurrentDispatcher() as { options?: Record }; - expect(next.options?.bodyTimeout).toBe(timeoutMs); - expect(next.options?.headersTimeout).toBe(timeoutMs); + expect(loadUndiciGlobalDispatcherDeps).not.toHaveBeenCalled(); + expect(setGlobalDispatcher).not.toHaveBeenCalled(); + expect(undiciGlobalDispatcherModule._globalUndiciStreamTimeoutMs).toBe(timeoutMs); }); it("re-applies when autoSelectFamily decision changes", () => { + vi.mocked(hasEnvHttpProxyAgentConfigured).mockReturnValue(true); + setCurrentDispatcher(new EnvHttpProxyAgent()); getDefaultAutoSelectFamily.mockReturnValue(true); ensureGlobalUndiciStreamTimeouts(); @@ -210,12 +251,14 @@ describe("ensureGlobalUndiciStreamTimeouts", () => { it("disables autoSelectFamily on WSL2 to avoid IPv6 connectivity issues", () => { getDefaultAutoSelectFamily.mockReturnValue(true); vi.mocked(isWSL2Sync).mockReturnValue(true); + vi.mocked(hasEnvHttpProxyAgentConfigured).mockReturnValue(true); + setCurrentDispatcher(new EnvHttpProxyAgent()); ensureGlobalUndiciStreamTimeouts(); expect(setGlobalDispatcher).toHaveBeenCalledTimes(1); const next = getCurrentDispatcher() as { options?: Record }; - expect(next).toBeInstanceOf(Agent); + expect(next).toBeInstanceOf(EnvHttpProxyAgent); expect(next.options?.connect).toEqual({ autoSelectFamily: false, autoSelectFamilyAttemptTimeout: 300, @@ -313,11 +356,26 @@ describe("forceResetGlobalDispatcher", () => { vi.mocked(resolveEnvHttpProxyAgentOptions).mockReturnValue(undefined); }); - it("replaces an EnvHttpProxyAgent with a direct Agent when proxy env is cleared", () => { + it("does not import undici when proxy env is cleared", () => { setCurrentDispatcher(new EnvHttpProxyAgent()); forceResetGlobalDispatcher(); + expect(loadUndiciGlobalDispatcherDeps).not.toHaveBeenCalled(); + expect(setGlobalDispatcher).not.toHaveBeenCalled(); + }); + + it("restores a direct Agent when clearing a proxy dispatcher installed by OpenClaw", () => { + vi.mocked(hasEnvHttpProxyAgentConfigured).mockReturnValue(true); + ensureGlobalUndiciEnvProxyDispatcher(); + expect(getCurrentDispatcher()).toBeInstanceOf(EnvHttpProxyAgent); + + vi.clearAllMocks(); + vi.mocked(hasEnvHttpProxyAgentConfigured).mockReturnValue(false); + + forceResetGlobalDispatcher(); + + expect(loadUndiciGlobalDispatcherDeps).toHaveBeenCalledTimes(1); expect(setGlobalDispatcher).toHaveBeenCalledTimes(1); expect(getCurrentDispatcher()).toBeInstanceOf(Agent); });