mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 00:19:18 +00:00
Phase 03 of #1162. Splits proxy + binary resolution out of the orchestrator into a focused module: - src/cliproxy/executor/proxy-resolver.ts (196 LOC): ResolvedProxy + ResolveExecutorProxyContext interfaces, resolveExecutorProxy function. Encapsulates proxy config resolution, port mutation, remote reachability check, fallback prompt, local backend selection, and binary acquisition. - src/cliproxy/executor/__tests__/proxy-resolver.test.ts: 10 unit tests covering local/remote/fallback paths. index.ts: 1168 -> 1045 LOC (-123). Removes 9 now-unused imports. Browser flag handling intentionally left in index.ts for Phase 04. Behavior unchanged; full suite passes 1824/1824. Refs #1162
258 lines
8.5 KiB
TypeScript
258 lines
8.5 KiB
TypeScript
/**
|
|
* Unit tests for proxy-resolver.ts (Phase 03 extraction)
|
|
*
|
|
* Tests cover the proxy resolution + remote reachability + binary acquisition
|
|
* logic extracted from executor/index.ts.
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, it, jest } from 'bun:test';
|
|
import type { ResolveExecutorProxyContext } from '../proxy-resolver';
|
|
import type { ExecutorConfig } from '../../types';
|
|
import type { UnifiedConfig } from '../../../config/schemas/unified-config';
|
|
|
|
// ── Module mocks ──────────────────────────────────────────────────────────────
|
|
|
|
const mockEnsureCLIProxyBinary = jest.fn().mockResolvedValue('/usr/local/bin/cliproxy');
|
|
const mockGetConfiguredBackend = jest.fn().mockReturnValue('original');
|
|
const mockGetPlusBackendUnavailableMessage = jest.fn().mockReturnValue('Plus backend unavailable');
|
|
|
|
jest.mock('../../binary-manager', () => ({
|
|
ensureCLIProxyBinary: mockEnsureCLIProxyBinary,
|
|
getConfiguredBackend: mockGetConfiguredBackend,
|
|
getPlusBackendUnavailableMessage: mockGetPlusBackendUnavailableMessage,
|
|
}));
|
|
|
|
const mockCheckRemoteProxy = jest.fn();
|
|
jest.mock('../../services/remote-proxy-client', () => ({
|
|
checkRemoteProxy: mockCheckRemoteProxy,
|
|
}));
|
|
|
|
jest.mock('../retry-handler', () => ({
|
|
isNetworkError: jest.fn().mockReturnValue(false),
|
|
handleNetworkError: jest.fn(),
|
|
}));
|
|
|
|
const mockResolveProxyConfig = jest.fn();
|
|
jest.mock('../../proxy/proxy-config-resolver', () => ({
|
|
resolveProxyConfig: mockResolveProxyConfig,
|
|
}));
|
|
|
|
jest.mock('../../config/config-generator', () => ({
|
|
CLIPROXY_DEFAULT_PORT: 8317,
|
|
validatePort: jest.fn((port: number | undefined) => port ?? 8317),
|
|
}));
|
|
|
|
// ── Import after mocks ────────────────────────────────────────────────────────
|
|
|
|
const { resolveExecutorProxy } = await import('../proxy-resolver');
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
function makeMinimalUnifiedConfig(): UnifiedConfig {
|
|
return {
|
|
cliproxy_server: undefined,
|
|
} as unknown as UnifiedConfig;
|
|
}
|
|
|
|
function makeBaseCfg(): ExecutorConfig {
|
|
return {
|
|
port: 8317,
|
|
timeout: 5000,
|
|
verbose: false,
|
|
pollInterval: 100,
|
|
};
|
|
}
|
|
|
|
function makeContext(
|
|
overrides: Partial<ResolveExecutorProxyContext> = {}
|
|
): ResolveExecutorProxyContext {
|
|
return {
|
|
unifiedConfig: makeMinimalUnifiedConfig(),
|
|
allProviders: ['gemini'],
|
|
verbose: false,
|
|
cfg: makeBaseCfg(),
|
|
log: jest.fn(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/** Mock resolveProxyConfig to return a local-mode config */
|
|
function mockLocalProxyConfig(remainingArgs: string[] = []): void {
|
|
mockResolveProxyConfig.mockReturnValue({
|
|
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 ─────────────────────────────────────────────────────────────────────
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
mockEnsureCLIProxyBinary.mockResolvedValue('/usr/local/bin/cliproxy');
|
|
mockGetConfiguredBackend.mockReturnValue('original');
|
|
});
|
|
|
|
describe('resolveExecutorProxy — local mode', () => {
|
|
it('returns useRemoteProxy=false and correct binary for local mode', async () => {
|
|
mockLocalProxyConfig(['--verbose']);
|
|
const result = await resolveExecutorProxy(['--verbose'], makeContext());
|
|
|
|
expect(result.useRemoteProxy).toBe(false);
|
|
expect(result.localBackend).toBe('original');
|
|
expect(result.binaryPath).toBe('/usr/local/bin/cliproxy');
|
|
expect(result.argsWithoutProxy).toEqual(['--verbose']);
|
|
});
|
|
|
|
it('strips proxy flags and passes remainingArgs through', async () => {
|
|
mockLocalProxyConfig(['clean-arg']);
|
|
const result = await resolveExecutorProxy(['--local-proxy', 'clean-arg'], makeContext());
|
|
|
|
expect(result.argsWithoutProxy).toEqual(['clean-arg']);
|
|
expect(result.useRemoteProxy).toBe(false);
|
|
});
|
|
|
|
it('does not call checkRemoteProxy in local mode', async () => {
|
|
mockLocalProxyConfig();
|
|
await resolveExecutorProxy([], makeContext());
|
|
|
|
expect(mockCheckRemoteProxy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('resolveExecutorProxy — remote mode reachable', () => {
|
|
it('returns useRemoteProxy=true when remote proxy is reachable', async () => {
|
|
mockRemoteProxyConfig();
|
|
mockCheckRemoteProxy.mockResolvedValue({ reachable: true, latencyMs: 12, error: undefined });
|
|
|
|
const result = await resolveExecutorProxy([], makeContext());
|
|
|
|
expect(result.useRemoteProxy).toBe(true);
|
|
});
|
|
|
|
it('skips binary acquisition when remote proxy is reachable', async () => {
|
|
mockRemoteProxyConfig();
|
|
mockCheckRemoteProxy.mockResolvedValue({ reachable: true, latencyMs: 5, error: undefined });
|
|
|
|
const result = await resolveExecutorProxy([], makeContext());
|
|
|
|
expect(result.binaryPath).toBeUndefined();
|
|
expect(mockEnsureCLIProxyBinary).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('resolveExecutorProxy — remote mode unreachable', () => {
|
|
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' });
|
|
|
|
await expect(resolveExecutorProxy([], makeContext())).rejects.toThrow(
|
|
'Remote proxy unreachable and --remote-only specified'
|
|
);
|
|
});
|
|
|
|
it('throws when fallback disabled and remote is unreachable', async () => {
|
|
mockResolveProxyConfig.mockReturnValue({
|
|
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' });
|
|
|
|
await expect(resolveExecutorProxy([], makeContext())).rejects.toThrow(
|
|
'Remote proxy unreachable and fallback disabled'
|
|
);
|
|
});
|
|
|
|
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' });
|
|
mockEnsureCLIProxyBinary.mockResolvedValue('/usr/local/bin/cliproxy');
|
|
|
|
const result = await resolveExecutorProxy([], makeContext());
|
|
|
|
expect(result.useRemoteProxy).toBe(false);
|
|
expect(result.binaryPath).toBe('/usr/local/bin/cliproxy');
|
|
expect(mockEnsureCLIProxyBinary).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('resolveExecutorProxy — proxyConfig propagated in result', () => {
|
|
it('returns the resolved proxyConfig object', async () => {
|
|
mockLocalProxyConfig();
|
|
|
|
const result = await resolveExecutorProxy([], makeContext());
|
|
|
|
expect(result.proxyConfig).toBeDefined();
|
|
expect(result.proxyConfig.mode).toBe('local');
|
|
expect(result.proxyConfig.port).toBe(8317);
|
|
});
|
|
|
|
it('returns mutated cfg with validated port', async () => {
|
|
mockLocalProxyConfig();
|
|
const ctx = makeContext();
|
|
|
|
const result = await resolveExecutorProxy([], ctx);
|
|
|
|
// cfg is mutated in place and also returned
|
|
expect(result.cfg).toBe(ctx.cfg);
|
|
expect(result.cfg.port).toBe(8317);
|
|
});
|
|
});
|