test: cover no-proxy undici startup

This commit is contained in:
Shakker
2026-05-06 01:02:48 +01:00
committed by Shakker
parent 85ed972217
commit 95652d5867
3 changed files with 99 additions and 39 deletions

View File

@@ -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<string, unknown>)[TEST_UNDICI_RUNTIME_DEPS_KEY] = {
@@ -1168,7 +1166,6 @@ describe("fetchWithSsrFGuard hardening", () => {
});
await result.release();
} finally {
setGlobalDispatcher(previousDispatcher);
resetGlobalUndiciStreamTimeoutsForTests();
}
});

View File

@@ -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 () => {

View File

@@ -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<string, unknown>) {}
@@ -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<string, unknown> };
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<string, unknown> };
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<string, unknown> };
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<string, unknown> };
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);
});