mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-09 04:02:35 +00:00
`proxyEnvironmentForChild()` hands a child the proxy values the user exported, including one this package refused — a SOCKS URL kept because `curl` reads it — and sets `NODE_USE_ENV_PROXY=1` so a child Node honors them. Node parses `HTTP_PROXY` and `HTTPS_PROXY` under that flag before running the program and exits on any scheme other than `http:` or `https:`. So a user with a usable `HTTP_PROXY` and `HTTPS_PROXY=socks4://…` lost every Node child — stdio MCP servers, subagent CLIs, `npm` in the bash tool — before its first line, while this process had reported only that the scheme stayed direct. Measured on Node 24.17: `socks4://`, `ftp://`, and a malformed value all exit 1; `socks5://` is accepted there and only there. The flag is now withheld whenever a value under the names Node parses is one `isSupportedProxyUrl` refuses. Such a child connects directly, which is what this process already said about that scheme, and `curl` still reads the value it was kept for. Node does not read `ALL_PROXY`, so a refused value there alone changes nothing. The socks5 case in `install.spec.ts` now asserts the flag absent; the ALL_PROXY fill case asserts it present; a new case spawns a real child Node under the overlay for each refused shape and asserts it starts.
459 lines
20 KiB
TypeScript
459 lines
20 KiB
TypeScript
import { spawnSync } from 'node:child_process'
|
|
import { createServer, type Server } from 'node:http'
|
|
import type { AddressInfo } from 'node:net'
|
|
import { afterEach, beforeAll, afterAll, describe, expect, it } from 'vitest'
|
|
import { getGlobalDispatcher } from 'undici'
|
|
import {
|
|
clearedProxyEnv,
|
|
installProxyFromEnvironment,
|
|
proxyEnvironmentForChild,
|
|
proxyRouteFor,
|
|
} from '../src/index.ts'
|
|
import { PROXY_ENV_NAMES } from '../src/policy.ts'
|
|
|
|
/** Absolute-form request targets the fake proxy received; a populated entry proves a request was tunnelled. */
|
|
let proxied: string[] = []
|
|
let proxy: Server
|
|
let origin: Server
|
|
let proxyUrl: string
|
|
let originUrl: string
|
|
|
|
/**
|
|
* The target for every assertion about a tunnelled hop. It is deliberately not loopback: no policy
|
|
* routes this machine through a proxy, so a loopback target could only ever prove a direct hop. The
|
|
* host never resolves — the client connects to the proxy, which answers the absolute-form request.
|
|
*/
|
|
const proxyTarget = 'http://origin.test/probe'
|
|
|
|
function listen(server: Server): Promise<AddressInfo> {
|
|
return new Promise((resolve) => {
|
|
server.listen(0, '127.0.0.1', () => { resolve(server.address() as AddressInfo) })
|
|
})
|
|
}
|
|
|
|
function close(server: Server): Promise<void> {
|
|
return new Promise((resolve) => { server.close(() => { resolve() }) })
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
proxy = createServer((request, response) => {
|
|
proxied.push(`${request.method} ${request.url}`)
|
|
response.writeHead(200, { 'content-type': 'text/plain' })
|
|
response.end('VIA-PROXY')
|
|
})
|
|
proxy.on('connect', (request, socket) => {
|
|
proxied.push(`CONNECT ${request.url ?? ''}`)
|
|
socket.end()
|
|
})
|
|
origin = createServer((_request, response) => { response.end('DIRECT') })
|
|
const [proxyAddress, originAddress] = await Promise.all([listen(proxy), listen(origin)])
|
|
proxyUrl = `http://127.0.0.1:${String(proxyAddress.port)}`
|
|
originUrl = `http://127.0.0.1:${String(originAddress.port)}/probe`
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await Promise.all([close(proxy), close(origin)])
|
|
})
|
|
|
|
afterEach(() => {
|
|
proxied = []
|
|
})
|
|
|
|
/** A second proxy URL, never dialed: it only has to differ from {@link proxyUrl} in an assertion. */
|
|
const nestedUrl = 'http://127.0.0.1:9'
|
|
|
|
/** A launch environment built from the names a user would export, in the casings they wrote. */
|
|
function env(values: Record<string, string>): { get(name: string): { value: string } | undefined } {
|
|
return { get: name => (name in values ? { value: values[name] as string } : undefined) }
|
|
}
|
|
|
|
/** The environment of a user who exported one proxy for both schemes. */
|
|
function proxyAll(noProxy?: string): { get(name: string): { value: string } | undefined } {
|
|
return env({ HTTP_PROXY: proxyUrl, HTTPS_PROXY: proxyUrl, ...noProxy === undefined ? {} : { NO_PROXY: noProxy } })
|
|
}
|
|
|
|
/** Install and collect whatever the resolution reported, so a case can assert on both. */
|
|
async function install(
|
|
lookup: { get(name: string): { value: string } | undefined },
|
|
): Promise<{ dispose: () => Promise<void>; reported: string[] }> {
|
|
const reported: string[] = []
|
|
const dispose = await installProxyFromEnvironment(lookup, (message) => { reported.push(message) })
|
|
return { dispose, reported }
|
|
}
|
|
|
|
/** Run one case from a known-empty proxy environment, then restore what the machine had. */
|
|
async function withCleanProxyEnv(run: () => Promise<void>): Promise<void> {
|
|
const saved = Object.fromEntries(PROXY_ENV_NAMES.map(name => [name, process.env[name]]))
|
|
for (const name of PROXY_ENV_NAMES) Reflect.deleteProperty(process.env, name)
|
|
try {
|
|
await run()
|
|
} finally {
|
|
for (const [name, value] of Object.entries(saved)) {
|
|
if (value === undefined) Reflect.deleteProperty(process.env, name)
|
|
else process.env[name] = value
|
|
}
|
|
}
|
|
}
|
|
|
|
describe('installProxyFromEnvironment', () => {
|
|
it('routes the built-in global fetch through the proxy', async () => {
|
|
const { dispose } = await install(proxyAll())
|
|
try {
|
|
await expect((await fetch(proxyTarget)).text()).resolves.toBe('VIA-PROXY')
|
|
expect(proxied).toEqual([`GET ${proxyTarget}`])
|
|
} finally {
|
|
await dispose()
|
|
}
|
|
})
|
|
|
|
it('connects directly when the bypass list covers the target', async () => {
|
|
const { dispose } = await install(env({ HTTP_PROXY: proxyUrl, NO_PROXY: 'origin.test' }))
|
|
try {
|
|
await expect(fetch(proxyTarget, { signal: AbortSignal.timeout(1500) })).rejects.toThrow()
|
|
expect(proxied).toEqual([])
|
|
} finally {
|
|
await dispose()
|
|
}
|
|
})
|
|
|
|
it('reports a value it cannot use and installs the rest', async () => {
|
|
const { dispose, reported } = await install(env({ HTTP_PROXY: proxyUrl, HTTPS_PROXY: 'socks5://127.0.0.1:1080' }))
|
|
try {
|
|
// A variable exported for another tool must not stop the agent from starting, and the user
|
|
// has to learn that this scheme stays direct rather than discover it from a failing request.
|
|
// The message names the variable, never its value: a proxy URL may carry `user:password`.
|
|
expect(reported).toHaveLength(1)
|
|
expect(reported[0]).toContain('HTTPS_PROXY')
|
|
expect(reported[0]).toContain('SOCKS')
|
|
expect(reported[0]).not.toContain('1080')
|
|
await expect((await fetch(proxyTarget)).text()).resolves.toBe('VIA-PROXY')
|
|
} finally {
|
|
await dispose()
|
|
}
|
|
})
|
|
|
|
it('publishes the policy through the proxy environment in both casings', async () => {
|
|
const { dispose } = await install(proxyAll('example.com'))
|
|
try {
|
|
expect(process.env.http_proxy).toBe(proxyUrl)
|
|
expect(process.env.HTTP_PROXY).toBe(proxyUrl)
|
|
expect(process.env.no_proxy).toContain('example.com')
|
|
expect(process.env.NO_PROXY).toContain('example.com')
|
|
} finally {
|
|
await dispose()
|
|
}
|
|
})
|
|
|
|
it('removes an environment name the policy leaves unset', async () => {
|
|
process.env.HTTPS_PROXY = 'http://stale.example'
|
|
// The user named no HTTPS proxy, so the policy derives one from HTTP — the name is rewritten,
|
|
// never left carrying a value from an earlier process.
|
|
const { dispose } = await install(env({ HTTP_PROXY: proxyUrl, HTTPS_PROXY: 'socks5://127.0.0.1:1080' }))
|
|
try {
|
|
expect(process.env.HTTPS_PROXY).toBeUndefined()
|
|
} finally {
|
|
await dispose()
|
|
expect(process.env.HTTPS_PROXY).toBe('http://stale.example')
|
|
delete process.env.HTTPS_PROXY
|
|
}
|
|
})
|
|
|
|
it('restores the dispatcher, the route, and the environment on disposal', async () => {
|
|
const before = getGlobalDispatcher()
|
|
const beforeEnv = process.env.HTTP_PROXY
|
|
const { dispose } = await install(proxyAll())
|
|
expect(getGlobalDispatcher()).not.toBe(before)
|
|
expect(proxyRouteFor(new URL(proxyTarget)).proxied).toBe(true)
|
|
await dispose()
|
|
expect(getGlobalDispatcher()).toBe(before)
|
|
expect(proxyRouteFor(new URL(proxyTarget)).proxied).toBe(false)
|
|
expect(process.env.HTTP_PROXY).toBe(beforeEnv)
|
|
await expect((await fetch(originUrl)).text()).resolves.toBe('DIRECT')
|
|
})
|
|
|
|
it('installs no dispatcher and touches no environment when the user exported none', async () => {
|
|
const before = getGlobalDispatcher()
|
|
process.env.HTTP_PROXY = 'http://untouched.example'
|
|
const { dispose, reported } = await install(env({}))
|
|
try {
|
|
expect(getGlobalDispatcher()).toBe(before)
|
|
expect(process.env.HTTP_PROXY).toBe('http://untouched.example')
|
|
expect(reported).toEqual([])
|
|
expect(proxyRouteFor(new URL(proxyTarget))).toEqual({ proxied: false })
|
|
} finally {
|
|
await dispose()
|
|
delete process.env.HTTP_PROXY
|
|
}
|
|
})
|
|
|
|
it('keeps a scheme direct when the policy refused the proxy the user named for it', async () => {
|
|
// What `HTTPS_PROXY=socks5://…` plus `HTTP_PROXY=http://p` resolves to: http proxied, https
|
|
// direct. undici's own EnvHttpProxyAgent cannot express this — with no HTTPS proxy present it
|
|
// reuses the HTTP one, tunnelling the scheme the diagnostic told the user stayed direct.
|
|
const { dispose } = await install(env({ HTTP_PROXY: proxyUrl, HTTPS_PROXY: 'socks5://127.0.0.1:1080' }))
|
|
try {
|
|
// The direct path here fails on a DNS miss whose latency is the machine's resolver to decide;
|
|
// the deadline bounds it. Either rejection proves the same thing — no CONNECT reached the
|
|
// proxy — and a proxied hop would have answered in milliseconds instead.
|
|
await expect(fetch('https://refused-scheme.invalid/', { signal: AbortSignal.timeout(1500) })).rejects.toThrow()
|
|
expect(proxied).toEqual([])
|
|
// The same policy still tunnels http, so the empty expectation above is not vacuous.
|
|
await expect((await fetch(proxyTarget)).text()).resolves.toBe('VIA-PROXY')
|
|
expect(proxied).toEqual([`GET ${proxyTarget}`])
|
|
} finally {
|
|
await dispose()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('proxyRouteFor', () => {
|
|
it('carries the dispatcher already routing, so a branch and its request agree', async () => {
|
|
const { dispose } = await install(proxyAll())
|
|
const route = proxyRouteFor(new URL(proxyTarget))
|
|
expect(route).toMatchObject({ proxied: true, proxy: proxyUrl })
|
|
if (!route.proxied) throw new Error('unreachable: asserted proxied above')
|
|
// One transport, not a copy: a caller that branched on this route sends its request through the
|
|
// very agent the branch described, so no second read can put the two on different routes.
|
|
expect(route.dispatcher).toBe(getGlobalDispatcher())
|
|
const undici = await import('undici')
|
|
// Disposing the install while a request is in flight: the shared dispatcher is closed, not
|
|
// destroyed, so the hop that already left finishes.
|
|
const inFlight = undici.fetch(proxyTarget, { dispatcher: route.dispatcher })
|
|
await dispose()
|
|
await expect((await inFlight).text()).resolves.toBe('VIA-PROXY')
|
|
expect(proxied).toEqual([`GET ${proxyTarget}`])
|
|
})
|
|
|
|
it('is direct for a bypassed URL, and direct with nothing installed', async () => {
|
|
const { dispose } = await install(proxyAll('origin.test'))
|
|
try {
|
|
expect(proxyRouteFor(new URL(proxyTarget))).toEqual({ proxied: false })
|
|
} finally {
|
|
await dispose()
|
|
}
|
|
expect(proxyRouteFor(new URL(proxyTarget))).toEqual({ proxied: false })
|
|
})
|
|
|
|
it('is direct for a loopback URL under a policy that proxies everything', async () => {
|
|
const { dispose } = await install(proxyAll())
|
|
try {
|
|
expect(proxyRouteFor(new URL(originUrl))).toEqual({ proxied: false })
|
|
} finally {
|
|
await dispose()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('proxyEnvironmentForChild', () => {
|
|
it('is empty when no policy is installed', () => {
|
|
expect(proxyEnvironmentForChild()).toEqual({})
|
|
})
|
|
|
|
it('is empty when the user exported none, so a child sees no flag it cannot use', async () => {
|
|
const { dispose } = await install(env({}))
|
|
try {
|
|
expect(proxyEnvironmentForChild()).toEqual({})
|
|
} finally {
|
|
await dispose()
|
|
}
|
|
})
|
|
|
|
it('hands a child the values the user exported, not this process\'s normalization', async () => {
|
|
await withCleanProxyEnv(async () => {
|
|
// A user who set only HTTP_PROXY, plus a SOCKS proxy this package refuses but `curl` uses.
|
|
process.env.HTTP_PROXY = proxyUrl
|
|
process.env.https_proxy = 'socks5://127.0.0.1:1080'
|
|
const { dispose } = await install(env({ HTTP_PROXY: proxyUrl, https_proxy: 'socks5://127.0.0.1:1080', NO_PROXY: 'example.com' }))
|
|
try {
|
|
const child = proxyEnvironmentForChild()
|
|
// The published policy derived an HTTPS proxy for this process; the child must not see it.
|
|
// Asserted over both casings rather than one: Windows folds the pair into a single variable,
|
|
// so which spelling carries the value is the platform's to decide — that it is the user's
|
|
// value and never the derived one is not.
|
|
const https = [child.https_proxy, child.HTTPS_PROXY]
|
|
expect(https).toContain('socks5://127.0.0.1:1080')
|
|
expect(https).not.toContain(proxyUrl)
|
|
expect(child.HTTP_PROXY).toBe(proxyUrl)
|
|
// The bypass list is the resolved one: it only adds entries to what the user wrote, and
|
|
// without the loopback ones the child sends its own localhost traffic to a proxy that
|
|
// cannot route it.
|
|
expect(child.no_proxy).toBe('example.com,localhost,127.0.0.1,::1,[::1]')
|
|
expect(child.NO_PROXY).toBe('example.com,localhost,127.0.0.1,::1,[::1]')
|
|
// The SOCKS value kept for `curl` is one Node would refuse at startup, so the flag that makes
|
|
// Node read it is withheld and a child Node connects directly rather than failing to start.
|
|
expect(child.NODE_USE_ENV_PROXY).toBeUndefined()
|
|
} finally {
|
|
await dispose()
|
|
}
|
|
})
|
|
})
|
|
|
|
it('fills a scheme the user named in neither casing, so a child Node is not left direct', async () => {
|
|
await withCleanProxyEnv(async () => {
|
|
// The user exported only ALL_PROXY. `NODE_USE_ENV_PROXY` never reads that name, so a child
|
|
// Node would connect directly while this process proxies — the seam this fill closes.
|
|
process.env.ALL_PROXY = proxyUrl
|
|
const { dispose } = await install(env({ ALL_PROXY: proxyUrl }))
|
|
try {
|
|
const child = proxyEnvironmentForChild()
|
|
expect(child.HTTP_PROXY).toBe(proxyUrl)
|
|
expect(child.http_proxy).toBe(proxyUrl)
|
|
expect(child.HTTPS_PROXY).toBe(proxyUrl)
|
|
expect(child.https_proxy).toBe(proxyUrl)
|
|
expect(child.NODE_USE_ENV_PROXY).toBe('1')
|
|
} finally {
|
|
await dispose()
|
|
}
|
|
})
|
|
})
|
|
|
|
it.each(['socks4://127.0.0.1:1080', 'ftp://p:1', 'not a url'])(
|
|
'withholds NODE_USE_ENV_PROXY when the child receives %s, so a child Node still starts',
|
|
async (refused) => {
|
|
await withCleanProxyEnv(async () => {
|
|
process.env.HTTP_PROXY = proxyUrl
|
|
process.env.HTTPS_PROXY = refused
|
|
const { dispose } = await install(env({ HTTP_PROXY: proxyUrl, HTTPS_PROXY: refused }))
|
|
try {
|
|
const child = proxyEnvironmentForChild()
|
|
// The value is still handed over — `curl` may read it — but Node, which parses these two
|
|
// names before running anything under the flag, must not be told to.
|
|
expect(child.HTTPS_PROXY).toBe(refused)
|
|
expect(child.HTTP_PROXY).toBe(proxyUrl)
|
|
expect(child).not.toHaveProperty('NODE_USE_ENV_PROXY')
|
|
// Proved on a real child rather than inferred: the same environment with the flag present
|
|
// exits before the program runs, on every Node this repository supports.
|
|
const childEnv: Record<string, string> = { PATH: process.env.PATH ?? '' }
|
|
for (const [name, value] of Object.entries(child)) if (value !== undefined) childEnv[name] = value
|
|
const run = spawnSync(process.execPath, ['-e', 'process.stdout.write("started")'], { env: childEnv, encoding: 'utf8' })
|
|
expect({ status: run.status, stdout: run.stdout }).toEqual({ status: 0, stdout: 'started' })
|
|
} finally {
|
|
await dispose()
|
|
}
|
|
})
|
|
},
|
|
)
|
|
|
|
it('keeps the outermost install\'s record of what the user exported across a nested one', async () => {
|
|
await withCleanProxyEnv(async () => {
|
|
// The user exported one name, in one casing.
|
|
process.env.HTTP_PROXY = proxyUrl
|
|
// The launcher installs first; a second `installProxyFromEnvironment` layers another policy over it.
|
|
const outer = await install(env({ HTTP_PROXY: proxyUrl, HTTPS_PROXY: proxyUrl, NO_PROXY: 'example.com' }))
|
|
try {
|
|
const inner = await install(env({ HTTP_PROXY: nestedUrl, HTTPS_PROXY: nestedUrl }))
|
|
try {
|
|
const child = proxyEnvironmentForChild()
|
|
// The user named no HTTPS proxy, so this scheme carries whichever policy is active. Reading
|
|
// the outer install's published environment as the user's would pin it to the outer proxy
|
|
// instead — the one discriminator that does not depend on how a platform cases names.
|
|
expect(child.https_proxy).toBe(nestedUrl)
|
|
expect(child.HTTPS_PROXY).toBe(nestedUrl)
|
|
} finally {
|
|
await inner.dispose()
|
|
}
|
|
// Unmounting the inner install must leave the outer one still able to describe that
|
|
// environment; clearing the record instead makes this an empty object, so every later child
|
|
// inherits the normalized values from `process.env` untouched.
|
|
expect(proxyEnvironmentForChild().HTTP_PROXY).toBe(proxyUrl)
|
|
expect(proxyEnvironmentForChild().https_proxy).toBe(proxyUrl)
|
|
} finally {
|
|
await outer.dispose()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('installing over an existing installation', () => {
|
|
it('stops proxying when the mounted policy proxies nothing', async () => {
|
|
const outer = await install(proxyAll())
|
|
try {
|
|
await expect((await fetch(proxyTarget)).text()).resolves.toBe('VIA-PROXY')
|
|
const off = await install(env({}))
|
|
try {
|
|
// `mode: 'off'` must actually stop proxying, not merely report a direct policy while the
|
|
// launcher's agent keeps tunnelling. A direct hop needs a host that answers, so this one
|
|
// reaches the real origin rather than the name only the proxy can resolve.
|
|
await expect((await fetch(originUrl)).text()).resolves.toBe('DIRECT')
|
|
expect(proxyRouteFor(new URL(proxyTarget))).toEqual({ proxied: false })
|
|
} finally {
|
|
await off.dispose()
|
|
}
|
|
// Disposing the direct policy restores the proxy the launcher installed.
|
|
await expect((await fetch(proxyTarget)).text()).resolves.toBe('VIA-PROXY')
|
|
expect(proxyRouteFor(new URL(proxyTarget)).proxied).toBe(true)
|
|
} finally {
|
|
await outer.dispose()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('the environment while a direct policy is layered over a proxied one', () => {
|
|
it('hands a child the user\'s own values, and the outer normalization again afterwards', async () => {
|
|
await withCleanProxyEnv(async () => {
|
|
// The user exported one usable proxy and one this package refuses.
|
|
process.env.HTTP_PROXY = proxyUrl
|
|
process.env.https_proxy = 'socks5://127.0.0.1:1080'
|
|
const outer = await install(env({ HTTP_PROXY: proxyUrl, https_proxy: 'socks5://127.0.0.1:1080' }))
|
|
try {
|
|
// The outer install published its policy: the refused scheme is removed in both casings.
|
|
expect([process.env.HTTPS_PROXY, process.env.https_proxy]).toEqual([undefined, undefined])
|
|
const off = await install(env({}))
|
|
try {
|
|
// A spawned child copies `process.env`, and `proxyEnvironmentForChild()` adds nothing under a
|
|
// direct policy — so what it copies has to be the user's own environment, not a normalization
|
|
// no active policy stands behind: the SOCKS value they set for `curl` is theirs again.
|
|
expect(process.env.HTTP_PROXY).toBe(proxyUrl)
|
|
expect([process.env.HTTPS_PROXY, process.env.https_proxy]).toContain('socks5://127.0.0.1:1080')
|
|
expect(proxyEnvironmentForChild()).toEqual({})
|
|
} finally {
|
|
await off.dispose()
|
|
}
|
|
// Ending the window re-applies what the outer install published.
|
|
expect([process.env.HTTPS_PROXY, process.env.https_proxy]).toEqual([undefined, undefined])
|
|
expect(process.env.HTTP_PROXY).toBe(proxyUrl)
|
|
} finally {
|
|
await outer.dispose()
|
|
}
|
|
})
|
|
})
|
|
|
|
it('touches no environment when the install underneath proxied nothing', async () => {
|
|
process.env.HTTP_PROXY = 'http://untouched.example'
|
|
const outer = await install(env({}))
|
|
const inner = await install(env({}))
|
|
try {
|
|
expect(process.env.HTTP_PROXY).toBe('http://untouched.example')
|
|
} finally {
|
|
await inner.dispose()
|
|
await outer.dispose()
|
|
expect(process.env.HTTP_PROXY).toBe('http://untouched.example')
|
|
delete process.env.HTTP_PROXY
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('the published environment', () => {
|
|
it('restores every name from one snapshot taken before any write', async () => {
|
|
process.env.http_proxy = 'http://before.example'
|
|
process.env.HTTP_PROXY = 'http://before.example'
|
|
const { dispose } = await install(proxyAll())
|
|
expect(process.env.HTTP_PROXY).toBe(proxyUrl)
|
|
await dispose()
|
|
// Reading the uppercase spelling after writing the lowercase one must not restore the value
|
|
// just written — the failure Windows's case-folded environment would produce.
|
|
expect(process.env.http_proxy).toBe('http://before.example')
|
|
expect(process.env.HTTP_PROXY).toBe('http://before.example')
|
|
delete process.env.http_proxy
|
|
delete process.env.HTTP_PROXY
|
|
})
|
|
})
|
|
|
|
describe('clearedProxyEnv', () => {
|
|
it('names every proxy variable for removal, so a replay reaches its own fixture server', () => {
|
|
const cleared = clearedProxyEnv()
|
|
expect(Object.keys(cleared).sort()).toEqual([...PROXY_ENV_NAMES].sort())
|
|
expect(Object.values(cleared).every(value => value === undefined)).toBe(true)
|
|
})
|
|
})
|