diff --git a/src/cliproxy/cliproxy-executor.ts b/src/cliproxy/cliproxy-executor.ts index bea75a25..c74453f5 100644 --- a/src/cliproxy/cliproxy-executor.ts +++ b/src/cliproxy/cliproxy-executor.ts @@ -498,7 +498,14 @@ export async function execClaudeWithCLIProxy( } // 3. Ensure OAuth completed (if provider requires it) - if (providerConfig.requiresOAuth) { + // Skip local OAuth check when using remote proxy with auth token + // The remote proxy has its own OAuth sessions and handles authentication + const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + if (skipLocalAuth) { + log(`Using remote proxy authentication (skipping local OAuth)`); + } + + if (providerConfig.requiresOAuth && !skipLocalAuth) { log(`Checking authentication for ${provider}`); if (forceAuth || !isAuthenticated(provider)) { @@ -549,7 +556,8 @@ export async function execClaudeWithCLIProxy( // 3b. Preflight quota check - auto-switch to account with quota before launch // Uses quota-manager for caching, tier priority, and cooldown support - if (provider === 'agy') { + // Skip for remote proxy - quota is managed on the remote server + if (provider === 'agy' && !skipLocalAuth) { const preflight = await preflightCheck(provider); if (!preflight.proceed) { @@ -571,23 +579,27 @@ export async function execClaudeWithCLIProxy( // 4. First-run model configuration (interactive) // For supported providers, prompt user to select model on first run // Pass customSettingsPath for CLIProxy variants - if (supportsModelConfig(provider)) { + // Skip for remote proxy - model is configured on the remote server + if (supportsModelConfig(provider) && !skipLocalAuth) { await configureProviderModel(provider, false, cfg.customSettingsPath); // false = only if not configured } // 5. Check for known broken models and warn user - const currentModel = getCurrentModel(provider, cfg.customSettingsPath); - if (currentModel && isModelBroken(provider, currentModel)) { - const modelEntry = findModel(provider, currentModel); - const issueUrl = getModelIssueUrl(provider, currentModel); - console.error(''); - console.error(warn(`${modelEntry?.name || currentModel} has known issues with Claude Code`)); - console.error(' Tool calls will fail. Use "gemini-3-pro-preview" instead.'); - if (issueUrl) { - console.error(` Tracking: ${issueUrl}`); + // Skip for remote proxy - model selection is on the remote server + if (!skipLocalAuth) { + const currentModel = getCurrentModel(provider, cfg.customSettingsPath); + if (currentModel && isModelBroken(provider, currentModel)) { + const modelEntry = findModel(provider, currentModel); + const issueUrl = getModelIssueUrl(provider, currentModel); + console.error(''); + console.error(warn(`${modelEntry?.name || currentModel} has known issues with Claude Code`)); + console.error(' Tool calls will fail. Use "gemini-3-pro-preview" instead.'); + if (issueUrl) { + console.error(` Tracking: ${issueUrl}`); + } + console.error(` Run "ccs ${provider} --config" to change model.`); + console.error(''); } - console.error(` Run "ccs ${provider} --config" to change model.`); - console.error(''); } // 6. Ensure user settings file exists (creates from defaults if not) diff --git a/tests/unit/cliproxy/skip-local-auth.test.ts b/tests/unit/cliproxy/skip-local-auth.test.ts new file mode 100644 index 00000000..e840e032 --- /dev/null +++ b/tests/unit/cliproxy/skip-local-auth.test.ts @@ -0,0 +1,213 @@ +/** + * Unit tests for skip-local-auth functionality when using remote proxy with auth token + * + * When --proxy-host and --proxy-auth-token are provided together, the system should + * skip local OAuth checks because the remote proxy handles authentication. + */ +import { describe, it, expect } from 'bun:test'; + +describe('skip-local-auth logic', () => { + describe('skipLocalAuth flag determination', () => { + it('should skip local auth when both useRemoteProxy and authToken are truthy', () => { + const useRemoteProxy = true; + const proxyConfig = { authToken: 'test-token-123' }; + + const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + + expect(skipLocalAuth).toBeTruthy(); + }); + + it('should NOT skip local auth when useRemoteProxy is false', () => { + const useRemoteProxy = false; + const proxyConfig = { authToken: 'test-token-123' }; + + const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + + expect(skipLocalAuth).toBeFalsy(); + }); + + it('should NOT skip local auth when authToken is undefined', () => { + const useRemoteProxy = true; + const proxyConfig = { authToken: undefined }; + + const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + + expect(skipLocalAuth).toBeFalsy(); + }); + + it('should NOT skip local auth when authToken is empty string', () => { + const useRemoteProxy = true; + const proxyConfig = { authToken: '' }; + + const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + + expect(skipLocalAuth).toBeFalsy(); + }); + + it('should NOT skip local auth when both are falsy', () => { + const useRemoteProxy = false; + const proxyConfig = { authToken: undefined }; + + const skipLocalAuth = useRemoteProxy && proxyConfig.authToken; + + expect(skipLocalAuth).toBeFalsy(); + }); + }); + + describe('OAuth check bypass scenarios', () => { + it('should document that OAuth is skipped for remote proxy with auth', () => { + // This test documents the expected behavior: + // When using remote proxy with auth token, the remote server + // already has its own OAuth sessions, so local OAuth is unnecessary + const scenario = { + useRemoteProxy: true, + authToken: 'bearer-token', + providerRequiresOAuth: true, + }; + + const skipLocalAuth = scenario.useRemoteProxy && scenario.authToken; + const shouldTriggerLocalOAuth = scenario.providerRequiresOAuth && !skipLocalAuth; + + expect(skipLocalAuth).toBeTruthy(); + expect(shouldTriggerLocalOAuth).toBe(false); + }); + + it('should document that OAuth runs when no remote proxy', () => { + const scenario = { + useRemoteProxy: false, + authToken: undefined, + providerRequiresOAuth: true, + }; + + const skipLocalAuth = scenario.useRemoteProxy && scenario.authToken; + const shouldTriggerLocalOAuth = scenario.providerRequiresOAuth && !skipLocalAuth; + + expect(skipLocalAuth).toBeFalsy(); + expect(shouldTriggerLocalOAuth).toBe(true); + }); + + it('should document that OAuth runs when remote proxy has no auth token', () => { + // Edge case: remote proxy configured but no auth token + // This should fall back to local OAuth + const scenario = { + useRemoteProxy: true, + authToken: undefined, + providerRequiresOAuth: true, + }; + + const skipLocalAuth = scenario.useRemoteProxy && scenario.authToken; + const shouldTriggerLocalOAuth = scenario.providerRequiresOAuth && !skipLocalAuth; + + expect(skipLocalAuth).toBeFalsy(); + expect(shouldTriggerLocalOAuth).toBe(true); + }); + }); + + describe('preflight quota check bypass', () => { + it('should skip preflight for agy provider when using remote proxy with auth', () => { + const provider = 'agy'; + const skipLocalAuth = true; + + const shouldRunPreflight = provider === 'agy' && !skipLocalAuth; + + expect(shouldRunPreflight).toBe(false); + }); + + it('should run preflight for agy provider when using local mode', () => { + const provider = 'agy'; + const skipLocalAuth = false; + + const shouldRunPreflight = provider === 'agy' && !skipLocalAuth; + + expect(shouldRunPreflight).toBe(true); + }); + + it('should not run preflight for non-agy providers regardless of mode', () => { + const providers = ['gemini', 'codex', 'copilot', 'kiro']; + + for (const provider of providers) { + const shouldRunPreflight = provider === 'agy' && !false; + expect(shouldRunPreflight).toBe(false); + } + }); + }); + + describe('model configuration bypass', () => { + it('should skip model config when using remote proxy with auth', () => { + const supportsModelConfig = true; + const skipLocalAuth = true; + + const shouldConfigureModel = supportsModelConfig && !skipLocalAuth; + + expect(shouldConfigureModel).toBe(false); + }); + + it('should run model config when using local mode', () => { + const supportsModelConfig = true; + const skipLocalAuth = false; + + const shouldConfigureModel = supportsModelConfig && !skipLocalAuth; + + expect(shouldConfigureModel).toBe(true); + }); + }); + + describe('broken model warning bypass', () => { + it('should skip broken model warning when using remote proxy with auth', () => { + const skipLocalAuth = true; + const currentModel = 'some-broken-model'; + const isModelBroken = true; + + // Logic: only warn if NOT skipping local auth + const shouldWarn = !skipLocalAuth && currentModel && isModelBroken; + + expect(shouldWarn).toBe(false); + }); + + it('should show broken model warning when using local mode', () => { + const skipLocalAuth = false; + const currentModel = 'some-broken-model'; + const isModelBroken = true; + + const shouldWarn = !skipLocalAuth && currentModel && isModelBroken; + + expect(shouldWarn).toBe(true); + }); + }); + + describe('CI/CD workflow scenarios', () => { + it('should enable headless CI workflow with remote proxy', () => { + // Simulate GitHub Actions workflow configuration + const workflowConfig = { + headless: true, + proxyHost: 'proxy.example.com', + proxyPort: 443, + proxyProtocol: 'https', + proxyAuthToken: 'github-secret-token', + remoteOnly: true, + }; + + // Determine if this configuration should skip local OAuth + const useRemoteProxy = !!workflowConfig.proxyHost; + const skipLocalAuth = useRemoteProxy && !!workflowConfig.proxyAuthToken; + + expect(useRemoteProxy).toBe(true); + expect(skipLocalAuth).toBe(true); + }); + + it('should require local OAuth when no proxy configured', () => { + // Simulate local development without proxy + const localConfig = { + headless: false, + proxyHost: undefined, + proxyAuthToken: undefined, + }; + + const useRemoteProxy = !!localConfig.proxyHost; + const skipLocalAuth = useRemoteProxy && !!localConfig.proxyAuthToken; + + expect(useRemoteProxy).toBe(false); + expect(skipLocalAuth).toBe(false); + }); + }); +});