mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 22:14:22 +00:00
test: update proxy resolver coverage
This commit is contained in:
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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user