mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
308 lines
9.9 KiB
TypeScript
308 lines
9.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
|
|
import * as fs from 'fs/promises'
|
|
import * as os from 'os'
|
|
import * as path from 'path'
|
|
import {
|
|
DEFAULT_AI_REQUEST_TIMEOUT_MS,
|
|
MAX_AI_REQUEST_TIMEOUT_MS,
|
|
MIN_AI_REQUEST_TIMEOUT_MS,
|
|
SYSTEM_PROXY_ERROR_ENV,
|
|
SYSTEM_PROXY_URL_ENV,
|
|
getManualNetworkProxyUrl,
|
|
buildNetworkEnvironment,
|
|
getNetworkProxyFetchOptions,
|
|
getNetworkProxyUrl,
|
|
loadNetworkSettings,
|
|
normalizeNetworkSettings,
|
|
} from '../services/networkSettings.js'
|
|
import { resetSettingsCache } from '../../utils/settings/settingsCache.js'
|
|
|
|
let tmpDir: string
|
|
let originalConfigDir: string | undefined
|
|
const PROXY_ENV_KEYS = [
|
|
'HTTP_PROXY',
|
|
'HTTPS_PROXY',
|
|
'http_proxy',
|
|
'https_proxy',
|
|
'ALL_PROXY',
|
|
'all_proxy',
|
|
'NO_PROXY',
|
|
'no_proxy',
|
|
SYSTEM_PROXY_URL_ENV,
|
|
SYSTEM_PROXY_ERROR_ENV,
|
|
] as const
|
|
let originalProxyEnv: Partial<Record<typeof PROXY_ENV_KEYS[number], string>>
|
|
|
|
async function setup() {
|
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'network-settings-test-'))
|
|
originalConfigDir = process.env.CLAUDE_CONFIG_DIR
|
|
originalProxyEnv = {}
|
|
for (const key of PROXY_ENV_KEYS) {
|
|
if (process.env[key] !== undefined) originalProxyEnv[key] = process.env[key]
|
|
delete process.env[key]
|
|
}
|
|
process.env.CLAUDE_CONFIG_DIR = tmpDir
|
|
resetSettingsCache()
|
|
}
|
|
|
|
async function teardown() {
|
|
if (originalConfigDir !== undefined) {
|
|
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
|
|
} else {
|
|
delete process.env.CLAUDE_CONFIG_DIR
|
|
}
|
|
for (const key of PROXY_ENV_KEYS) {
|
|
const originalValue = originalProxyEnv[key]
|
|
if (originalValue === undefined) delete process.env[key]
|
|
else process.env[key] = originalValue
|
|
}
|
|
resetSettingsCache()
|
|
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
}
|
|
|
|
describe('network settings', () => {
|
|
beforeEach(setup)
|
|
afterEach(teardown)
|
|
|
|
it('normalizes missing and legacy-invalid settings to the 600s system-proxy default', () => {
|
|
expect(normalizeNetworkSettings({})).toEqual({
|
|
aiRequestTimeoutMs: DEFAULT_AI_REQUEST_TIMEOUT_MS,
|
|
proxy: {
|
|
mode: 'system',
|
|
url: '',
|
|
},
|
|
})
|
|
expect(normalizeNetworkSettings({
|
|
network: { proxy: { mode: 'legacy', url: 'http://stale.example:8080' } },
|
|
}).proxy).toEqual({
|
|
mode: 'system',
|
|
url: '',
|
|
})
|
|
})
|
|
|
|
it('preserves explicit direct mode and clears inherited HTTP, HTTPS, and ALL proxy environment', () => {
|
|
const settings = normalizeNetworkSettings({
|
|
network: {
|
|
proxy: {
|
|
mode: 'direct',
|
|
url: '',
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(buildNetworkEnvironment(settings, {
|
|
HTTP_PROXY: 'http://127.0.0.1:1181',
|
|
HTTPS_PROXY: 'http://127.0.0.1:1181',
|
|
http_proxy: 'http://127.0.0.1:1181',
|
|
https_proxy: 'http://127.0.0.1:1181',
|
|
ALL_PROXY: 'socks5://127.0.0.1:1182',
|
|
all_proxy: 'socks5://127.0.0.1:1182',
|
|
})).toMatchObject({
|
|
HTTP_PROXY: '',
|
|
HTTPS_PROXY: '',
|
|
http_proxy: '',
|
|
https_proxy: '',
|
|
ALL_PROXY: '',
|
|
all_proxy: '',
|
|
})
|
|
expect(getNetworkProxyUrl(settings, {
|
|
[SYSTEM_PROXY_URL_ENV]: 'http://127.0.0.1:1183',
|
|
})).toBeNull()
|
|
process.env.HTTP_PROXY = 'http://127.0.0.1:1181'
|
|
expect(getNetworkProxyFetchOptions(settings, 'https://api.example.com/v1/messages').proxy).toBeUndefined()
|
|
})
|
|
|
|
it('uses only the explicit system-proxy bridge for system provider requests', () => {
|
|
const settings = normalizeNetworkSettings({
|
|
network: {
|
|
proxy: {
|
|
mode: 'system',
|
|
url: '',
|
|
},
|
|
},
|
|
})
|
|
|
|
const bridgeUrl = 'http://127.0.0.1:1183'
|
|
const baseEnv = {
|
|
HTTP_PROXY: 'http://inherited.example:8080',
|
|
ALL_PROXY: 'socks5://inherited.example:1080',
|
|
NO_PROXY: '.corp.local',
|
|
[SYSTEM_PROXY_URL_ENV]: ` ${bridgeUrl} `,
|
|
}
|
|
expect(getNetworkProxyUrl(settings, baseEnv)).toBe(bridgeUrl)
|
|
expect(buildNetworkEnvironment(settings, baseEnv)).toEqual({
|
|
API_TIMEOUT_MS: String(DEFAULT_AI_REQUEST_TIMEOUT_MS),
|
|
HTTP_PROXY: bridgeUrl,
|
|
HTTPS_PROXY: bridgeUrl,
|
|
http_proxy: bridgeUrl,
|
|
https_proxy: bridgeUrl,
|
|
ALL_PROXY: bridgeUrl,
|
|
all_proxy: bridgeUrl,
|
|
NO_PROXY: '.corp.local,localhost,127.0.0.1,::1',
|
|
no_proxy: '.corp.local,localhost,127.0.0.1,::1',
|
|
})
|
|
|
|
process.env.HTTP_PROXY = 'http://inherited.example:8080'
|
|
process.env[SYSTEM_PROXY_URL_ENV] = bridgeUrl
|
|
expect(getNetworkProxyFetchOptions(settings, 'https://api.example.com/v1/messages').proxy)
|
|
.toBe(bridgeUrl)
|
|
})
|
|
|
|
it('uses inherited process proxy for non-Electron system-mode servers', () => {
|
|
const settings = normalizeNetworkSettings({
|
|
network: { proxy: { mode: 'system', url: '' } },
|
|
})
|
|
process.env.HTTP_PROXY = 'http://inherited.example:8080'
|
|
|
|
expect(getNetworkProxyUrl(settings)).toBe('http://inherited.example:8080')
|
|
expect(buildNetworkEnvironment(settings, process.env)).toMatchObject({
|
|
HTTP_PROXY: 'http://inherited.example:8080',
|
|
HTTPS_PROXY: 'http://inherited.example:8080',
|
|
http_proxy: 'http://inherited.example:8080',
|
|
https_proxy: 'http://inherited.example:8080',
|
|
ALL_PROXY: 'http://inherited.example:8080',
|
|
all_proxy: 'http://inherited.example:8080',
|
|
})
|
|
expect(getNetworkProxyFetchOptions(settings, 'https://api.example.com/v1/messages').proxy)
|
|
.toBe('http://inherited.example:8080')
|
|
})
|
|
|
|
it('surfaces a host bridge startup failure instead of silently going direct', () => {
|
|
const settings = normalizeNetworkSettings({
|
|
network: { proxy: { mode: 'system', url: '' } },
|
|
})
|
|
const env = {
|
|
[SYSTEM_PROXY_ERROR_ENV]: 'local bridge could not bind',
|
|
}
|
|
|
|
expect(() => getNetworkProxyUrl(settings, env))
|
|
.toThrow('local bridge could not bind')
|
|
expect(() => buildNetworkEnvironment(settings, env))
|
|
.toThrow('local bridge could not bind')
|
|
|
|
expect(getNetworkProxyUrl(normalizeNetworkSettings({
|
|
network: { proxy: { mode: 'direct', url: '' } },
|
|
}), env)).toBeNull()
|
|
expect(getNetworkProxyUrl(normalizeNetworkSettings({
|
|
network: { proxy: { mode: 'manual', url: 'http://127.0.0.1:7890' } },
|
|
}), env)).toBe('http://127.0.0.1:7890')
|
|
})
|
|
|
|
it('clamps AI request timeouts and trims manual proxy URLs', () => {
|
|
expect(normalizeNetworkSettings({
|
|
network: {
|
|
aiRequestTimeoutMs: 9_999_999,
|
|
proxy: {
|
|
mode: 'manual',
|
|
url: ' http://127.0.0.1:7890 ',
|
|
},
|
|
},
|
|
})).toEqual({
|
|
aiRequestTimeoutMs: MAX_AI_REQUEST_TIMEOUT_MS,
|
|
proxy: {
|
|
mode: 'manual',
|
|
url: 'http://127.0.0.1:7890',
|
|
},
|
|
})
|
|
|
|
expect(normalizeNetworkSettings({
|
|
network: {
|
|
aiRequestTimeoutMs: 100,
|
|
},
|
|
}).aiRequestTimeoutMs).toBe(MIN_AI_REQUEST_TIMEOUT_MS)
|
|
})
|
|
|
|
it('loads persisted user network settings for provider requests', async () => {
|
|
await fs.writeFile(
|
|
path.join(tmpDir, 'settings.json'),
|
|
JSON.stringify({
|
|
network: {
|
|
aiRequestTimeoutMs: 180_000,
|
|
proxy: {
|
|
mode: 'manual',
|
|
url: ' http://127.0.0.1:7890 ',
|
|
},
|
|
},
|
|
}),
|
|
'utf-8',
|
|
)
|
|
|
|
const settings = await loadNetworkSettings()
|
|
|
|
expect(settings.aiRequestTimeoutMs).toBe(180_000)
|
|
expect(getManualNetworkProxyUrl(settings)).toBe('http://127.0.0.1:7890')
|
|
expect(buildNetworkEnvironment(settings)).toEqual({
|
|
API_TIMEOUT_MS: '180000',
|
|
HTTP_PROXY: 'http://127.0.0.1:7890',
|
|
HTTPS_PROXY: 'http://127.0.0.1:7890',
|
|
http_proxy: 'http://127.0.0.1:7890',
|
|
https_proxy: 'http://127.0.0.1:7890',
|
|
ALL_PROXY: 'http://127.0.0.1:7890',
|
|
all_proxy: 'http://127.0.0.1:7890',
|
|
NO_PROXY: 'localhost,127.0.0.1,::1',
|
|
no_proxy: 'localhost,127.0.0.1,::1',
|
|
})
|
|
})
|
|
|
|
it('preserves custom no_proxy entries while adding loopback bypasses for manual proxies', () => {
|
|
const settings = normalizeNetworkSettings({
|
|
network: {
|
|
proxy: {
|
|
mode: 'manual',
|
|
url: 'http://proxy.example:8080',
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(buildNetworkEnvironment(settings, { no_proxy: '.corp.local,10.0.0.0/8' })).toMatchObject({
|
|
NO_PROXY: '.corp.local,10.0.0.0/8,localhost,127.0.0.1,::1',
|
|
no_proxy: '.corp.local,10.0.0.0/8,localhost,127.0.0.1,::1',
|
|
})
|
|
})
|
|
|
|
it('preserves authenticated manual proxy URLs for provider requests', () => {
|
|
const settings = normalizeNetworkSettings({
|
|
network: {
|
|
proxy: {
|
|
mode: 'manual',
|
|
url: ' https://user:p%40ss@proxy.example.com:8443 ',
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(getManualNetworkProxyUrl(settings)).toBe('https://user:p%40ss@proxy.example.com:8443')
|
|
expect(getNetworkProxyUrl(settings, {
|
|
[SYSTEM_PROXY_URL_ENV]: 'http://system.example:8080',
|
|
})).toBe('https://user:p%40ss@proxy.example.com:8443')
|
|
expect(buildNetworkEnvironment(settings)).toMatchObject({
|
|
HTTP_PROXY: 'https://user:p%40ss@proxy.example.com:8443',
|
|
HTTPS_PROXY: 'https://user:p%40ss@proxy.example.com:8443',
|
|
ALL_PROXY: 'https://user:p%40ss@proxy.example.com:8443',
|
|
all_proxy: 'https://user:p%40ss@proxy.example.com:8443',
|
|
})
|
|
process.env.HTTP_PROXY = 'http://inherited.example:8080'
|
|
process.env[SYSTEM_PROXY_URL_ENV] = 'http://system.example:8080'
|
|
expect(getNetworkProxyFetchOptions(settings, 'https://api.example.com/v1/messages').proxy)
|
|
.toBe('https://user:p%40ss@proxy.example.com:8443')
|
|
})
|
|
|
|
it('fails closed to direct when a corrupted manual setting has no URL', () => {
|
|
const settings = normalizeNetworkSettings({
|
|
network: { proxy: { mode: 'manual', url: ' ' } },
|
|
})
|
|
|
|
expect(getNetworkProxyUrl(settings)).toBeNull()
|
|
expect(buildNetworkEnvironment(settings, {
|
|
HTTP_PROXY: 'http://inherited.example:8080',
|
|
ALL_PROXY: 'socks5://inherited.example:1080',
|
|
})).toMatchObject({
|
|
HTTP_PROXY: '',
|
|
HTTPS_PROXY: '',
|
|
http_proxy: '',
|
|
https_proxy: '',
|
|
ALL_PROXY: '',
|
|
all_proxy: '',
|
|
})
|
|
})
|
|
})
|