test: update proxy resolver coverage

This commit is contained in:
Tam Nhu Tran committed 2026-05-30 15:14:34 -04:00
1 parent 8114ff016e
commit 1f1cc056ef
3 files changed
+77 -108

No files matched your search

@@ -5,7 +5,7 @@
* logic extracted from executor/index.ts. * logic extracted from executor/index.ts.
*/ */
import { beforeEach, describe, expect, it, jest } from 'bun:test'; import { afterEach, beforeEach, describe, expect, it, jest } from 'bun:test';
import type { ResolveExecutorProxyContext } from '../proxy-resolver'; import type { ResolveExecutorProxyContext } from '../proxy-resolver';
import type { ExecutorConfig } from '../../types'; import type { ExecutorConfig } from '../../types';
import type { UnifiedConfig } from '../../../config/schemas/unified-config'; import type { UnifiedConfig } from '../../../config/schemas/unified-config';
@@ -15,11 +15,29 @@ import type { UnifiedConfig } from '../../../config/schemas/unified-config';
const mockEnsureCLIProxyBinary = jest.fn().mockResolvedValue('/usr/local/bin/cliproxy'); const mockEnsureCLIProxyBinary = jest.fn().mockResolvedValue('/usr/local/bin/cliproxy');
const mockGetConfiguredBackend = jest.fn().mockReturnValue('original'); const mockGetConfiguredBackend = jest.fn().mockReturnValue('original');
const mockGetPlusBackendUnavailableMessage = jest.fn().mockReturnValue('Plus backend unavailable'); const mockGetPlusBackendUnavailableMessage = jest.fn().mockReturnValue('Plus backend unavailable');
const mockInstallCliproxyVersion = jest.fn().mockResolvedValue(undefined);
const mockFetchLatestCliproxyVersion = jest.fn().mockResolvedValue('test-version');
const mockCheckCliproxyUpdate = jest.fn().mockResolvedValue({ available: false });
jest.mock('../../binary-manager', () => ({ jest.mock('../../binary-manager', () => ({
ensureCLIProxyBinary: mockEnsureCLIProxyBinary, ensureCLIProxyBinary: mockEnsureCLIProxyBinary,
getConfiguredBackend: mockGetConfiguredBackend, getConfiguredBackend: mockGetConfiguredBackend,
getPlusBackendUnavailableMessage: mockGetPlusBackendUnavailableMessage, getPlusBackendUnavailableMessage: mockGetPlusBackendUnavailableMessage,
getStoredConfiguredBackend: mockGetConfiguredBackend,
getCLIProxyPath: jest.fn().mockReturnValue('/usr/local/bin/cliproxy'),
getInstalledCliproxyVersion: jest.fn().mockReturnValue('test-version'),
isCLIProxyInstalled: jest.fn().mockReturnValue(true),
resolveLocalBackend: mockGetConfiguredBackend,
syncPlusFallbackStateIfNeeded: jest.fn(),
installCliproxyVersion: mockInstallCliproxyVersion,
fetchLatestCliproxyVersion: mockFetchLatestCliproxyVersion,
checkCliproxyUpdate: mockCheckCliproxyUpdate,
getPinnedVersion: jest.fn().mockReturnValue(null),
savePinnedVersion: jest.fn(),
clearPinnedVersion: jest.fn(),
isVersionPinned: jest.fn().mockReturnValue(false),
getVersionPinPath: jest.fn().mockReturnValue('/tmp/cliproxy-version-pin'),
BinaryManager: class {},
})); }));
const mockCheckRemoteProxy = jest.fn(); const mockCheckRemoteProxy = jest.fn();
@@ -30,21 +48,28 @@ jest.mock('../../services/remote-proxy-client', () => ({
jest.mock('../retry-handler', () => ({ jest.mock('../retry-handler', () => ({
isNetworkError: jest.fn().mockReturnValue(false), isNetworkError: jest.fn().mockReturnValue(false),
handleNetworkError: jest.fn(), handleNetworkError: jest.fn(),
})); handleTokenExpiration: jest.fn(),
handleQuotaCheck: jest.fn(),
const mockResolveProxyConfig = jest.fn(); PROVIDER_ERROR_PATTERNS: [],
jest.mock('../../proxy/proxy-config-resolver', () => ({ detectFailedTier: jest.fn().mockReturnValue(null),
resolveProxyConfig: mockResolveProxyConfig, isProviderError: jest.fn().mockReturnValue(false),
}));
jest.mock('../../config/config-generator', () => ({
CLIPROXY_DEFAULT_PORT: 8317,
validatePort: jest.fn((port: number | undefined) => port ?? 8317),
})); }));
// ── Import after mocks ──────────────────────────────────────────────────────── // ── Import after mocks ────────────────────────────────────────────────────────
const { resolveExecutorProxy } = await import('../proxy-resolver'); const { resolveExecutorProxy, resolveExecutorProxyConfig } = await import('../proxy-resolver');
const PROXY_ENV_KEYS = [
'CCS_PROXY_HOST',
'CCS_PROXY_PORT',
'CCS_PROXY_PROTOCOL',
'CCS_PROXY_AUTH_TOKEN',
'CCS_PROXY_TIMEOUT',
'CCS_PROXY_FALLBACK_ENABLED',
'CCS_ALLOW_SELF_SIGNED',
] as const;
let proxyEnvSnapshot: Record<string, string | undefined> = {};
// ── Helpers ─────────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────────
@@ -76,51 +101,37 @@ function makeContext(
}; };
} }
/** Mock resolveProxyConfig to return a local-mode config */ async function resolveProxyForTest(args: string[], context = makeContext()) {
function mockLocalProxyConfig(remainingArgs: string[] = []): void { const resolvedConfig = resolveExecutorProxyConfig(args, context);
mockResolveProxyConfig.mockReturnValue({ return resolveExecutorProxy(resolvedConfig, context);
config: {
mode: 'local',
port: 8317,
protocol: 'http',
fallbackEnabled: false,
autoStartLocal: false,
remoteOnly: false,
forceLocal: true,
},
remainingArgs,
});
}
/** Mock resolveProxyConfig to return a remote-mode config */
function mockRemoteProxyConfig(remainingArgs: string[] = []): void {
mockResolveProxyConfig.mockReturnValue({
config: {
mode: 'remote',
host: '192.168.1.100',
port: 8317,
protocol: 'http',
fallbackEnabled: false,
autoStartLocal: false,
remoteOnly: false,
forceLocal: false,
},
remainingArgs,
});
} }
// ── Tests ───────────────────────────────────────────────────────────────────── // ── Tests ─────────────────────────────────────────────────────────────────────
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
proxyEnvSnapshot = Object.fromEntries(PROXY_ENV_KEYS.map((key) => [key, process.env[key]]));
for (const key of PROXY_ENV_KEYS) {
delete process.env[key];
}
mockEnsureCLIProxyBinary.mockResolvedValue('/usr/local/bin/cliproxy'); mockEnsureCLIProxyBinary.mockResolvedValue('/usr/local/bin/cliproxy');
mockGetConfiguredBackend.mockReturnValue('original'); mockGetConfiguredBackend.mockReturnValue('original');
}); });
afterEach(() => {
for (const key of PROXY_ENV_KEYS) {
const value = proxyEnvSnapshot[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
});
describe('resolveExecutorProxy — local mode', () => { describe('resolveExecutorProxy — local mode', () => {
it('returns useRemoteProxy=false and correct binary for local mode', async () => { it('returns useRemoteProxy=false and correct binary for local mode', async () => {
mockLocalProxyConfig(['--verbose']); const result = await resolveProxyForTest(['--local-proxy', '--verbose']);
const result = await resolveExecutorProxy(['--verbose'], makeContext());
expect(result.useRemoteProxy).toBe(false); expect(result.useRemoteProxy).toBe(false);
expect(result.localBackend).toBe('original'); expect(result.localBackend).toBe('original');
@@ -129,16 +140,14 @@ describe('resolveExecutorProxy — local mode', () => {
}); });
it('strips proxy flags and passes remainingArgs through', async () => { it('strips proxy flags and passes remainingArgs through', async () => {
mockLocalProxyConfig(['clean-arg']); const result = await resolveProxyForTest(['--local-proxy', 'clean-arg']);
const result = await resolveExecutorProxy(['--local-proxy', 'clean-arg'], makeContext());
expect(result.argsWithoutProxy).toEqual(['clean-arg']); expect(result.argsWithoutProxy).toEqual(['clean-arg']);
expect(result.useRemoteProxy).toBe(false); expect(result.useRemoteProxy).toBe(false);
}); });
it('does not call checkRemoteProxy in local mode', async () => { it('does not call checkRemoteProxy in local mode', async () => {
mockLocalProxyConfig(); await resolveProxyForTest(['--local-proxy']);
await resolveExecutorProxy([], makeContext());
expect(mockCheckRemoteProxy).not.toHaveBeenCalled(); expect(mockCheckRemoteProxy).not.toHaveBeenCalled();
}); });
@@ -146,19 +155,17 @@ describe('resolveExecutorProxy — local mode', () => {
describe('resolveExecutorProxy — remote mode reachable', () => { describe('resolveExecutorProxy — remote mode reachable', () => {
it('returns useRemoteProxy=true when remote proxy is reachable', async () => { it('returns useRemoteProxy=true when remote proxy is reachable', async () => {
mockRemoteProxyConfig();
mockCheckRemoteProxy.mockResolvedValue({ reachable: true, latencyMs: 12, error: undefined }); mockCheckRemoteProxy.mockResolvedValue({ reachable: true, latencyMs: 12, error: undefined });
const result = await resolveExecutorProxy([], makeContext()); const result = await resolveProxyForTest(['--proxy-host', '192.168.1.100']);
expect(result.useRemoteProxy).toBe(true); expect(result.useRemoteProxy).toBe(true);
}); });
it('skips binary acquisition when remote proxy is reachable', async () => { it('skips binary acquisition when remote proxy is reachable', async () => {
mockRemoteProxyConfig();
mockCheckRemoteProxy.mockResolvedValue({ reachable: true, latencyMs: 5, error: undefined }); mockCheckRemoteProxy.mockResolvedValue({ reachable: true, latencyMs: 5, error: undefined });
const result = await resolveExecutorProxy([], makeContext()); const result = await resolveProxyForTest(['--proxy-host', '192.168.1.100']);
expect(result.binaryPath).toBeUndefined(); expect(result.binaryPath).toBeUndefined();
expect(mockEnsureCLIProxyBinary).not.toHaveBeenCalled(); expect(mockEnsureCLIProxyBinary).not.toHaveBeenCalled();
@@ -167,65 +174,27 @@ describe('resolveExecutorProxy — remote mode reachable', () => {
describe('resolveExecutorProxy — remote mode unreachable', () => { describe('resolveExecutorProxy — remote mode unreachable', () => {
it('throws expected message when remoteOnly=true and remote is unreachable', async () => { it('throws expected message when remoteOnly=true and remote is unreachable', async () => {
mockResolveProxyConfig.mockReturnValue({
config: {
mode: 'remote',
host: '192.168.1.100',
port: 8317,
protocol: 'http',
fallbackEnabled: false,
autoStartLocal: false,
remoteOnly: true,
forceLocal: false,
},
remainingArgs: [],
});
mockCheckRemoteProxy.mockResolvedValue({ reachable: false, error: 'Connection refused' }); mockCheckRemoteProxy.mockResolvedValue({ reachable: false, error: 'Connection refused' });
await expect(resolveExecutorProxy([], makeContext())).rejects.toThrow( await expect(
'Remote proxy unreachable and --remote-only specified' resolveProxyForTest(['--proxy-host', '192.168.1.100', '--remote-only'])
); ).rejects.toThrow('Remote proxy unreachable and --remote-only specified');
}); });
it('throws when fallback disabled and remote is unreachable', async () => { it('throws when fallback disabled and remote is unreachable', async () => {
mockResolveProxyConfig.mockReturnValue({ process.env.CCS_PROXY_FALLBACK_ENABLED = '0';
config: {
mode: 'remote',
host: '192.168.1.100',
port: 8317,
protocol: 'http',
fallbackEnabled: false,
autoStartLocal: false,
remoteOnly: false,
forceLocal: false,
},
remainingArgs: [],
});
mockCheckRemoteProxy.mockResolvedValue({ reachable: false, error: 'Timeout' }); mockCheckRemoteProxy.mockResolvedValue({ reachable: false, error: 'Timeout' });
await expect(resolveExecutorProxy([], makeContext())).rejects.toThrow( await expect(resolveProxyForTest(['--proxy-host', '192.168.1.100'])).rejects.toThrow(
'Remote proxy unreachable and fallback disabled' 'Remote proxy unreachable and fallback disabled'
); );
}); });
it('falls back to local and acquires binary when autoStartLocal=true', async () => { it('falls back to local and acquires binary when autoStartLocal=true', async () => {
mockResolveProxyConfig.mockReturnValue({
config: {
mode: 'remote',
host: '192.168.1.100',
port: 8317,
protocol: 'http',
fallbackEnabled: true,
autoStartLocal: true,
remoteOnly: false,
forceLocal: false,
},
remainingArgs: [],
});
mockCheckRemoteProxy.mockResolvedValue({ reachable: false, error: 'Timeout' }); mockCheckRemoteProxy.mockResolvedValue({ reachable: false, error: 'Timeout' });
mockEnsureCLIProxyBinary.mockResolvedValue('/usr/local/bin/cliproxy'); mockEnsureCLIProxyBinary.mockResolvedValue('/usr/local/bin/cliproxy');
const result = await resolveExecutorProxy([], makeContext()); const result = await resolveProxyForTest(['--proxy-host', '192.168.1.100']);
expect(result.useRemoteProxy).toBe(false); expect(result.useRemoteProxy).toBe(false);
expect(result.binaryPath).toBe('/usr/local/bin/cliproxy'); expect(result.binaryPath).toBe('/usr/local/bin/cliproxy');
@@ -235,9 +204,7 @@ describe('resolveExecutorProxy — remote mode unreachable', () => {
describe('resolveExecutorProxy — proxyConfig propagated in result', () => { describe('resolveExecutorProxy — proxyConfig propagated in result', () => {
it('returns the resolved proxyConfig object', async () => { it('returns the resolved proxyConfig object', async () => {
mockLocalProxyConfig(); const result = await resolveProxyForTest(['--local-proxy']);
const result = await resolveExecutorProxy([], makeContext());
expect(result.proxyConfig).toBeDefined(); expect(result.proxyConfig).toBeDefined();
expect(result.proxyConfig.mode).toBe('local'); expect(result.proxyConfig.mode).toBe('local');
@@ -245,10 +212,9 @@ describe('resolveExecutorProxy — proxyConfig propagated in result', () => {
}); });
it('returns mutated cfg with validated port', async () => { it('returns mutated cfg with validated port', async () => {
mockLocalProxyConfig();
const ctx = makeContext(); const ctx = makeContext();
const result = await resolveExecutorProxy([], ctx); const result = await resolveProxyForTest(['--local-proxy'], ctx);
// cfg is mutated in place and also returned // cfg is mutated in place and also returned
expect(result.cfg).toBe(ctx.cfg); expect(result.cfg).toBe(ctx.cfg);
+6 -4
View File
@@ -483,8 +483,9 @@ export async function reconcileExhaustedRotationAccounts(
return []; return [];
} }
const { pauseAccountForQuotaCooldown, restoreExpiredQuotaPauses } = const { pauseAccountForQuotaCooldown, restoreExpiredQuotaPauses } = await import(
await import('../accounts/account-safety'); '../accounts/account-safety'
);
restoreExpiredQuotaPauses(); restoreExpiredQuotaPauses();
const config = loadOrCreateUnifiedConfig(); const config = loadOrCreateUnifiedConfig();
@@ -592,8 +593,9 @@ export async function preflightCheck(provider: CLIProxyProvider): Promise<Prefli
return { proceed: true, accountId: defaultAccount?.id || '' }; return { proceed: true, accountId: defaultAccount?.id || '' };
} }
const { pauseAccountForQuotaCooldown, restoreExpiredQuotaPauses } = const { pauseAccountForQuotaCooldown, restoreExpiredQuotaPauses } = await import(
await import('../accounts/account-safety'); '../accounts/account-safety'
);
restoreExpiredQuotaPauses(); restoreExpiredQuotaPauses();
const config = loadOrCreateUnifiedConfig(); const config = loadOrCreateUnifiedConfig();
@@ -112,8 +112,9 @@ export async function runImageAnalysisCheck(results: HealthCheck): Promise<void>
* Fix image analysis configuration issues * Fix image analysis configuration issues
*/ */
export async function fixImageAnalysisConfig(): Promise<boolean> { export async function fixImageAnalysisConfig(): Promise<boolean> {
const { updateConfig, loadOrCreateUnifiedConfig } = const { updateConfig, loadOrCreateUnifiedConfig } = await import(
await import('../../config/config-loader-facade'); '../../config/config-loader-facade'
);
const config = loadOrCreateUnifiedConfig(); const config = loadOrCreateUnifiedConfig();
let fixed = false; let fixed = false;