mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-04 08:13:15 +00:00
fix(cliproxy): guard unsupported Qwen account auth
This commit is contained in:
1 parent
5b6c3e87e9
commit
b6ef4e5782
11 files changed
+177
-9
No files matched your search
@@ -61,11 +61,14 @@ CCS gives you one stable command surface while letting you switch between:
|
||||
|
||||
- multiple runtimes such as Claude Code, Factory Droid, and Codex CLI
|
||||
- multiple Claude subscriptions and isolated account contexts
|
||||
- OAuth providers like Codex, Kiro, Claude, Qwen, Kimi, and more, with legacy
|
||||
- OAuth providers like Codex, Kiro, Claude, Kimi, and more, with legacy
|
||||
Copilot compatibility for existing setups
|
||||
- API and local-model profiles like GLM, Kimi, OpenRouter, Ollama, llama.cpp,
|
||||
Novita, Fireworks AI, and Alibaba Coding Plan
|
||||
|
||||
Qwen Code account linking is not available in the bundled CLIProxy runtime yet;
|
||||
use an API-key Qwen profile such as Alibaba Coding Plan for Qwen models.
|
||||
|
||||
The goal is simple: stop rewriting config files, stop breaking active sessions,
|
||||
and move between providers in seconds.
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
getDeviceCodeVerificationProviders,
|
||||
getOAuthCallbackPort,
|
||||
getOAuthFlowType,
|
||||
getUnsupportedAuthStartReason,
|
||||
PROVIDER_CAPABILITIES,
|
||||
getProviderDisplayName,
|
||||
getProvidersByOAuthFlow,
|
||||
@@ -134,10 +135,20 @@ describe('provider-capabilities', () => {
|
||||
expect(getOAuthCallbackPort('gitlab')).toBe(17171);
|
||||
expect(getOAuthCallbackPort('gemini')).toBe(8085);
|
||||
expect(PROVIDER_CAPABILITIES.gemini.refreshOwnership).toBe('cliproxy');
|
||||
expect(PROVIDER_CAPABILITIES.qwen.refreshOwnership).toBe('unsupported');
|
||||
expect(getProviderDisplayName('agy')).toBe('Antigravity');
|
||||
expect(getProviderDisplayName('kilo')).toBe('Kilo AI');
|
||||
});
|
||||
|
||||
it('exposes auth start support separately from OAuth flow type', () => {
|
||||
expect(getOAuthFlowType('qwen')).toBe('device_code');
|
||||
expect(getUnsupportedAuthStartReason('qwen')).toContain(
|
||||
'Qwen account linking is not supported'
|
||||
);
|
||||
expect(getUnsupportedAuthStartReason('kiro')).toBeNull();
|
||||
expect(getUnsupportedAuthStartReason('qoder')).toBeNull();
|
||||
});
|
||||
|
||||
it('throws when provider aliases collide across providers', () => {
|
||||
const capabilitiesWithCollision = {
|
||||
...PROVIDER_CAPABILITIES,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { afterEach, describe, expect, it, spyOn } from 'bun:test';
|
||||
import { triggerOAuth } from '../oauth-handler';
|
||||
|
||||
describe('triggerOAuth unsupported providers', () => {
|
||||
const previousDisableBanWarnings = process.env.CCS_DISABLE_BAN_WARNINGS;
|
||||
|
||||
afterEach(() => {
|
||||
if (previousDisableBanWarnings === undefined) {
|
||||
delete process.env.CCS_DISABLE_BAN_WARNINGS;
|
||||
} else {
|
||||
process.env.CCS_DISABLE_BAN_WARNINGS = previousDisableBanWarnings;
|
||||
}
|
||||
});
|
||||
|
||||
it('fails Qwen account linking before preparing CLIProxy auth args', async () => {
|
||||
process.env.CCS_DISABLE_BAN_WARNINGS = '1';
|
||||
const logSpy = spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
const account = await triggerOAuth('qwen');
|
||||
|
||||
expect(account).toBeNull();
|
||||
expect(logSpy.mock.calls.some(([message]) => String(message).includes('--qwen-login'))).toBe(
|
||||
false
|
||||
);
|
||||
expect(
|
||||
logSpy.mock.calls.some(([message]) =>
|
||||
String(message).includes('Qwen account linking is not supported')
|
||||
)
|
||||
).toBe(true);
|
||||
} finally {
|
||||
logSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -79,6 +79,7 @@ import {
|
||||
import { maybeOfferPoolRouting } from '../routing/pool-opt-in-prompt';
|
||||
import { checkCrossLaneEmailOverlap } from '../accounts/account-safety-cross-lane';
|
||||
import { ensureCliAntigravityResponsibility } from '../auth/antigravity-responsibility';
|
||||
import { getUnsupportedAuthStartReason } from '../provider-capabilities';
|
||||
import { InteractivePrompt } from '../../utils/prompt';
|
||||
import { getCcsDir } from '../../utils/config-manager';
|
||||
import { generateSessionId } from './project-selection-handler';
|
||||
@@ -616,6 +617,11 @@ function buildOAuthArgs(
|
||||
kiroIDCFlow?: OAuthOptions['kiroIDCFlow'];
|
||||
} = {}
|
||||
): string[] {
|
||||
const unsupportedReason = getUnsupportedAuthStartReason(provider);
|
||||
if (unsupportedReason) {
|
||||
throw new AuthError(unsupportedReason, provider);
|
||||
}
|
||||
|
||||
const args = ['--config', configPath];
|
||||
|
||||
if (provider === 'kiro') {
|
||||
@@ -1091,6 +1097,12 @@ export async function triggerOAuth(
|
||||
options: OAuthOptions = {}
|
||||
): Promise<AccountInfo | null> {
|
||||
const oauthConfig = getOAuthConfig(provider);
|
||||
const unsupportedReason = getUnsupportedAuthStartReason(provider);
|
||||
if (unsupportedReason) {
|
||||
console.log(fail(unsupportedReason));
|
||||
return null;
|
||||
}
|
||||
|
||||
warnOAuthBanRisk(provider);
|
||||
const oauthStartedAt = Date.now();
|
||||
logger.stage('auth', 'cliproxy.oauth.start', 'Triggering OAuth flow', {
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
* Exports refresh functions for each OAuth provider.
|
||||
*
|
||||
* Refresh responsibility:
|
||||
* - CLIProxy-delegated: gemini, codex, agy, kiro, ghcp, qwen, iflow, kimi
|
||||
* - CLIProxy-delegated: gemini, codex, agy, kiro, ghcp, iflow, kimi
|
||||
* (CLIProxyAPIPlus handles refresh automatically in background)
|
||||
* - Unsupported account linking: qwen
|
||||
* - Not implemented: claude
|
||||
*/
|
||||
|
||||
|
||||
@@ -497,9 +497,10 @@ export function displayAuthStatus(): void {
|
||||
*
|
||||
* Refresh responsibility:
|
||||
* - gemini: CCS refreshes directly via Google OAuth
|
||||
* - codex, agy, kiro, ghcp, qwen, iflow: CLIProxyAPIPlus handles refresh
|
||||
* - codex, agy, kiro, ghcp, iflow: CLIProxyAPIPlus handles refresh
|
||||
* automatically in background (e.g. kiro refreshes every 1 min).
|
||||
* CCS only checks if token file exists (authentication state).
|
||||
* - qwen: account linking is unsupported by the bundled CLIProxy runtime
|
||||
* - claude: not yet implemented
|
||||
*
|
||||
* @param provider The CLIProxy provider
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { CLIProxyProvider } from './types';
|
||||
import { ConfigError } from '../errors/error-types';
|
||||
|
||||
export type OAuthFlowType = 'authorization_code' | 'device_code';
|
||||
export type TokenRefreshOwnership = 'ccs' | 'cliproxy' | 'unsupported';
|
||||
export type AuthStartSupport = 'cliproxy-cli' | 'unsupported';
|
||||
|
||||
export interface ProviderCapabilities {
|
||||
displayName: string;
|
||||
@@ -14,6 +16,10 @@ export interface ProviderCapabilities {
|
||||
authUrlProviderName: string;
|
||||
/** Who owns token refresh logic for this provider. */
|
||||
refreshOwnership: TokenRefreshOwnership;
|
||||
/** Whether CCS can start account linking through the bundled CLIProxy binary. */
|
||||
authStartSupport: AuthStartSupport;
|
||||
/** User-facing reason when account linking cannot be started. */
|
||||
authStartUnsupportedReason?: string;
|
||||
/** Filename prefixes used to identify auth tokens for this provider. */
|
||||
authFilePrefixes: readonly string[];
|
||||
/** Token JSON "type" values accepted for this provider. */
|
||||
@@ -34,6 +40,7 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
||||
callbackProviderName: 'gemini',
|
||||
authUrlProviderName: 'gemini-cli',
|
||||
refreshOwnership: 'cliproxy',
|
||||
authStartSupport: 'cliproxy-cli',
|
||||
authFilePrefixes: ['gemini-', 'google-'],
|
||||
tokenTypeValues: ['gemini'],
|
||||
aliases: ['gemini-cli'],
|
||||
@@ -46,6 +53,7 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
||||
callbackProviderName: 'codex',
|
||||
authUrlProviderName: 'codex',
|
||||
refreshOwnership: 'cliproxy',
|
||||
authStartSupport: 'cliproxy-cli',
|
||||
authFilePrefixes: ['codex-', 'openai-'],
|
||||
tokenTypeValues: ['codex'],
|
||||
aliases: [],
|
||||
@@ -58,6 +66,7 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
||||
callbackProviderName: 'antigravity',
|
||||
authUrlProviderName: 'antigravity',
|
||||
refreshOwnership: 'cliproxy',
|
||||
authStartSupport: 'cliproxy-cli',
|
||||
authFilePrefixes: ['antigravity-', 'agy-'],
|
||||
tokenTypeValues: ['antigravity'],
|
||||
aliases: ['antigravity'],
|
||||
@@ -69,7 +78,10 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
||||
callbackPort: null,
|
||||
callbackProviderName: 'qwen',
|
||||
authUrlProviderName: 'qwen',
|
||||
refreshOwnership: 'cliproxy',
|
||||
refreshOwnership: 'unsupported',
|
||||
authStartSupport: 'unsupported',
|
||||
authStartUnsupportedReason:
|
||||
'Alibaba Qwen account linking is not supported by the bundled CLIProxy runtime. Use an API-key Qwen profile; CLIProxyAPI does not expose Qwen OAuth yet.',
|
||||
authFilePrefixes: ['qwen-'],
|
||||
tokenTypeValues: ['qwen'],
|
||||
aliases: [],
|
||||
@@ -82,6 +94,7 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
||||
callbackProviderName: 'iflow',
|
||||
authUrlProviderName: 'iflow',
|
||||
refreshOwnership: 'cliproxy',
|
||||
authStartSupport: 'cliproxy-cli',
|
||||
authFilePrefixes: ['iflow-'],
|
||||
tokenTypeValues: ['iflow'],
|
||||
aliases: [],
|
||||
@@ -94,6 +107,7 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
||||
callbackProviderName: 'kiro',
|
||||
authUrlProviderName: 'kiro',
|
||||
refreshOwnership: 'cliproxy',
|
||||
authStartSupport: 'cliproxy-cli',
|
||||
authFilePrefixes: ['kiro-', 'aws-', 'codewhisperer-'],
|
||||
tokenTypeValues: ['kiro', 'codewhisperer'],
|
||||
aliases: ['codewhisperer'],
|
||||
@@ -106,6 +120,7 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
||||
callbackProviderName: 'copilot',
|
||||
authUrlProviderName: 'github',
|
||||
refreshOwnership: 'cliproxy',
|
||||
authStartSupport: 'cliproxy-cli',
|
||||
authFilePrefixes: ['github-copilot-', 'copilot-', 'gh-'],
|
||||
tokenTypeValues: ['github-copilot', 'copilot'],
|
||||
aliases: ['github-copilot', 'copilot'],
|
||||
@@ -118,6 +133,7 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
||||
callbackProviderName: 'anthropic',
|
||||
authUrlProviderName: 'anthropic',
|
||||
refreshOwnership: 'unsupported',
|
||||
authStartSupport: 'cliproxy-cli',
|
||||
authFilePrefixes: ['claude-', 'anthropic-'],
|
||||
tokenTypeValues: ['claude', 'anthropic'],
|
||||
aliases: ['anthropic'],
|
||||
@@ -130,6 +146,7 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
||||
callbackProviderName: 'kimi',
|
||||
authUrlProviderName: 'kimi',
|
||||
refreshOwnership: 'cliproxy',
|
||||
authStartSupport: 'cliproxy-cli',
|
||||
authFilePrefixes: ['kimi-'],
|
||||
tokenTypeValues: ['kimi'],
|
||||
aliases: ['moonshot'],
|
||||
@@ -142,6 +159,7 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
||||
callbackProviderName: 'cursor',
|
||||
authUrlProviderName: 'cursor',
|
||||
refreshOwnership: 'cliproxy',
|
||||
authStartSupport: 'cliproxy-cli',
|
||||
authFilePrefixes: ['cursor.', 'cursor-'],
|
||||
tokenTypeValues: ['cursor'],
|
||||
aliases: [],
|
||||
@@ -154,6 +172,7 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
||||
callbackProviderName: 'gitlab',
|
||||
authUrlProviderName: 'gitlab',
|
||||
refreshOwnership: 'cliproxy',
|
||||
authStartSupport: 'cliproxy-cli',
|
||||
authFilePrefixes: ['gitlab-'],
|
||||
tokenTypeValues: ['gitlab'],
|
||||
aliases: ['gitlab-duo'],
|
||||
@@ -166,6 +185,7 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
||||
callbackProviderName: 'codebuddy',
|
||||
authUrlProviderName: 'codebuddy',
|
||||
refreshOwnership: 'cliproxy',
|
||||
authStartSupport: 'cliproxy-cli',
|
||||
authFilePrefixes: ['codebuddy-'],
|
||||
tokenTypeValues: ['codebuddy'],
|
||||
aliases: ['tencent'],
|
||||
@@ -178,6 +198,7 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
||||
callbackProviderName: 'kilo',
|
||||
authUrlProviderName: 'kilo',
|
||||
refreshOwnership: 'unsupported',
|
||||
authStartSupport: 'cliproxy-cli',
|
||||
authFilePrefixes: ['kilo-'],
|
||||
tokenTypeValues: ['kilo'],
|
||||
aliases: [],
|
||||
@@ -190,6 +211,7 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
|
||||
callbackProviderName: 'qoder',
|
||||
authUrlProviderName: 'qoder',
|
||||
refreshOwnership: 'unsupported',
|
||||
authStartSupport: 'cliproxy-cli',
|
||||
authFilePrefixes: ['qoder-'],
|
||||
tokenTypeValues: ['qoder'],
|
||||
aliases: [],
|
||||
@@ -254,7 +276,7 @@ export function buildProviderAliasMap(
|
||||
|
||||
const existingProvider = aliasMap.get(normalized);
|
||||
if (existingProvider && existingProvider !== provider) {
|
||||
throw new Error(
|
||||
throw new ConfigError(
|
||||
`Provider alias collision for "${normalized}": ${existingProvider} and ${provider}`
|
||||
);
|
||||
}
|
||||
@@ -330,6 +352,17 @@ export function isRefreshDelegatedToCLIProxy(provider: CLIProxyProvider): boolea
|
||||
return PROVIDER_CAPABILITIES[provider].refreshOwnership === 'cliproxy';
|
||||
}
|
||||
|
||||
export function getUnsupportedAuthStartReason(provider: CLIProxyProvider): string | null {
|
||||
const capabilities = PROVIDER_CAPABILITIES[provider];
|
||||
if (capabilities.authStartSupport !== 'unsupported') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
capabilities.authStartUnsupportedReason ??
|
||||
`${capabilities.displayName} account linking is not supported by the bundled CLIProxy runtime.`
|
||||
);
|
||||
}
|
||||
|
||||
export function getProviderAuthFilePrefixes(provider: CLIProxyProvider): readonly string[] {
|
||||
return PROVIDER_CAPABILITIES[provider].authFilePrefixes;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
|
||||
import * as http from 'http';
|
||||
import { ToolSanitizationProxy } from '../tool-sanitization-proxy';
|
||||
import { CodexReasoningProxy } from '../../ai-providers/codex-reasoning-proxy';
|
||||
|
||||
// Mock upstream server that echoes requests
|
||||
let mockUpstream: http.Server;
|
||||
let mockUpstreamPort: number;
|
||||
let lastRequest: { body: unknown; headers: http.IncomingHttpHeaders } | null = null;
|
||||
let lastRequest: { path: string; body: unknown; headers: http.IncomingHttpHeaders } | null = null;
|
||||
|
||||
// Track response to send back
|
||||
let mockResponse: { status: number; body: unknown; stream?: boolean } = {
|
||||
@@ -28,6 +29,7 @@ beforeAll(async () => {
|
||||
req.on('end', () => {
|
||||
const body = Buffer.concat(chunks).toString('utf8');
|
||||
lastRequest = {
|
||||
path: req.url || '',
|
||||
body: body ? JSON.parse(body) : null,
|
||||
headers: req.headers,
|
||||
};
|
||||
@@ -184,6 +186,50 @@ describe('ToolSanitizationProxy Integration', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('normalizes codex effort aliases through the provider-scoped local proxy chain', async () => {
|
||||
const toolProxy = new ToolSanitizationProxy({
|
||||
upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`,
|
||||
});
|
||||
const toolPort = await toolProxy.start();
|
||||
const reasoningProxy = new CodexReasoningProxy({
|
||||
upstreamBaseUrl: `http://127.0.0.1:${toolPort}`,
|
||||
modelMap: {
|
||||
defaultModel: 'gpt-5.5-high',
|
||||
opusModel: 'gpt-5.5-xhigh',
|
||||
sonnetModel: 'gpt-5.5-high',
|
||||
haikuModel: 'gpt-5.5-mini-medium',
|
||||
},
|
||||
defaultEffort: 'medium',
|
||||
});
|
||||
const reasoningPort = await reasoningProxy.start();
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://127.0.0.1:${reasoningPort}/api/provider/codex/v1/messages`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-5.5-high',
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(lastRequest).not.toBeNull();
|
||||
expect(lastRequest!.path).toBe('/api/provider/codex/v1/messages');
|
||||
expect((lastRequest!.body as Record<string, unknown>).model).toBe('gpt-5.5');
|
||||
expect(
|
||||
((lastRequest!.body as Record<string, unknown>).reasoning as Record<string, unknown>)
|
||||
.effort
|
||||
).toBe('high');
|
||||
} finally {
|
||||
reasoningProxy.stop();
|
||||
toolProxy.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it('normalizes dotted Claude thinking model IDs for root/composite routes', async () => {
|
||||
const proxy = new ToolSanitizationProxy({
|
||||
upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`,
|
||||
|
||||
@@ -201,7 +201,7 @@ export const BUILTIN_PROVIDER_SHORTCUTS: readonly ShortcutEntry[] = CLIPROXY_PRO
|
||||
gemini: 'Google Gemini via CLIProxy OAuth',
|
||||
codex: 'OpenAI Codex via CLIProxy OAuth',
|
||||
agy: 'Antigravity via CLIProxy OAuth',
|
||||
qwen: 'Qwen Code via CLIProxy OAuth',
|
||||
qwen: 'Qwen Code via CLIProxy; account linking unsupported',
|
||||
iflow: 'iFlow via CLIProxy OAuth',
|
||||
kiro: 'Kiro via CLIProxy OAuth',
|
||||
ghcp: 'Deprecated GitHub Copilot via CLIProxy OAuth',
|
||||
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
} from '../../cliproxy/auth/auth-types';
|
||||
import {
|
||||
getOAuthFlowType,
|
||||
getUnsupportedAuthStartReason,
|
||||
isBrowserUrlAuthProvider,
|
||||
mapExternalProviderName,
|
||||
} from '../../cliproxy/provider-capabilities';
|
||||
@@ -285,6 +286,11 @@ export function getStartUrlUnsupportedReason(
|
||||
provider: CLIProxyProvider,
|
||||
options?: { kiroMethod?: KiroAuthMethod }
|
||||
): string | null {
|
||||
const unsupportedAuthStartReason = getStartAuthUnsupportedReason(provider);
|
||||
if (unsupportedAuthStartReason) {
|
||||
return unsupportedAuthStartReason;
|
||||
}
|
||||
|
||||
if (provider === 'kiro') {
|
||||
const kiroMethod = options?.kiroMethod ?? normalizeKiroAuthMethod();
|
||||
if (kiroMethod === 'idc') {
|
||||
@@ -316,6 +322,10 @@ export function getStartAuthFailureMessage(provider: CLIProxyProvider): string {
|
||||
return 'Authentication failed or was cancelled';
|
||||
}
|
||||
|
||||
export function getStartAuthUnsupportedReason(provider: CLIProxyProvider): string | null {
|
||||
return getUnsupportedAuthStartReason(provider);
|
||||
}
|
||||
|
||||
function getManualCallbackRegistrationError(provider: CLIProxyProvider): string {
|
||||
if (PROVIDERS_WITHOUT_EMAIL.includes(provider)) {
|
||||
return 'Authenticated token could not be matched to a new account. Retry the flow and choose a different nickname if needed.';
|
||||
@@ -683,6 +693,12 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
||||
}
|
||||
|
||||
const localProvider = provider as CLIProxyProvider;
|
||||
const unsupportedReason = getStartAuthUnsupportedReason(localProvider);
|
||||
if (unsupportedReason) {
|
||||
res.status(400).json({ error: unsupportedReason, code: 'AUTH_START_UNSUPPORTED' });
|
||||
return;
|
||||
}
|
||||
|
||||
const existingAccounts = getProviderAccounts(localProvider);
|
||||
const reauthTarget = getReauthAccountTarget(accountId, existingAccounts);
|
||||
if (reauthTarget.error) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
getReauthAccountTarget,
|
||||
getStartAuthFailureMessage,
|
||||
getStartAuthNicknameError,
|
||||
getStartAuthUnsupportedReason,
|
||||
getStartUrlUnsupportedReason,
|
||||
} from '../../../src/web-server/routes/cliproxy-auth-routes';
|
||||
|
||||
@@ -13,12 +14,14 @@ describe('cliproxy-auth-routes start-url guard', () => {
|
||||
"Kiro method 'aws' uses Device Code flow"
|
||||
);
|
||||
expect(getStartUrlUnsupportedReason('ghcp')).toContain("Provider 'ghcp' uses Device Code flow");
|
||||
expect(getStartUrlUnsupportedReason('qwen')).toContain("Provider 'qwen' uses Device Code flow");
|
||||
expect(getStartUrlUnsupportedReason('qwen')).toContain('Qwen account linking is not supported');
|
||||
expect(getStartUrlUnsupportedReason('codebuddy')).toContain(
|
||||
"Provider 'codebuddy' uses Device Code flow"
|
||||
);
|
||||
expect(getStartUrlUnsupportedReason('kilo')).toContain("Provider 'kilo' uses Device Code flow");
|
||||
expect(getStartUrlUnsupportedReason('qoder')).toContain("Provider 'qoder' uses Device Code flow");
|
||||
expect(getStartUrlUnsupportedReason('qoder')).toContain(
|
||||
"Provider 'qoder' uses Device Code flow"
|
||||
);
|
||||
});
|
||||
|
||||
it('allows Cursor browser URL auth on start-url', () => {
|
||||
@@ -89,6 +92,13 @@ describe('cliproxy-auth-routes Kiro IDC start validation', () => {
|
||||
});
|
||||
|
||||
describe('cliproxy-auth-routes start failure messaging', () => {
|
||||
it('returns a clear unsupported message for Qwen account linking', () => {
|
||||
expect(getStartAuthUnsupportedReason('qwen')).toContain(
|
||||
'Qwen account linking is not supported'
|
||||
);
|
||||
expect(getStartAuthUnsupportedReason('kiro')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns ghcp-specific guidance for Copilot verification failures', () => {
|
||||
expect(getStartAuthFailureMessage('ghcp')).toContain(
|
||||
'GitHub Copilot verification did not complete'
|
||||
|
||||
Reference in new issue
Block a user