From 587f8c5668cb62ea2ae963600cad6562ed945fe1 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Sat, 30 May 2026 18:04:44 +0800 Subject: [PATCH] fix(acp): drop discontinued Qwen OAuth method --- .../cli/src/acp-integration/acpAgent.test.ts | 74 ++++++++++++++++++- packages/cli/src/acp-integration/acpAgent.ts | 48 ++---------- .../acp-integration/acpAgent.worktree.test.ts | 15 +++- .../src/acp-integration/authMethods.test.ts | 30 ++++++++ .../cli/src/acp-integration/authMethods.ts | 28 ++----- 5 files changed, 129 insertions(+), 66 deletions(-) create mode 100644 packages/cli/src/acp-integration/authMethods.test.ts diff --git a/packages/cli/src/acp-integration/acpAgent.test.ts b/packages/cli/src/acp-integration/acpAgent.test.ts index 59596c3d530..c8218abb978 100644 --- a/packages/cli/src/acp-integration/acpAgent.test.ts +++ b/packages/cli/src/acp-integration/acpAgent.test.ts @@ -145,7 +145,20 @@ vi.mock('./runtimeOutputDirContext.js', () => ({ ), })); -vi.mock('./authMethods.js', () => ({ buildAuthMethods: vi.fn() })); +vi.mock('./authMethods.js', () => { + const buildAuthMethods = vi.fn(); + return { + buildAuthMethods, + pickAuthMethodsForAuthRequired: vi.fn((selectedType?: string) => { + const authMethods = buildAuthMethods(); + if (!selectedType) return authMethods; + const matched = authMethods.filter( + (method: { id: string }) => method.id === selectedType, + ); + return matched.length ? matched : authMethods; + }), + }; +}); vi.mock('./service/filesystem.js', () => ({ AcpFileSystemService: vi.fn(), })); @@ -195,6 +208,7 @@ import { loadSettings } from '../config/settings.js'; import { loadCliConfig } from '../config/config.js'; import { Session, buildAvailableCommandsSnapshot } from './session/Session.js'; import { SERVE_STATUS_EXT_METHODS } from '../serve/status.js'; +import { buildAuthMethods } from './authMethods.js'; describe('runAcpAgent shutdown cleanup', () => { let processExitSpy: MockInstance; @@ -792,6 +806,64 @@ describe('QwenAgent MCP SSE/HTTP support', () => { await agentPromise; }); + it('does not return discontinued qwen-oauth as the only ACP auth option', async () => { + vi.mocked(buildAuthMethods).mockReturnValue([ + { + id: 'openai', + name: 'Use OpenAI API key', + description: 'Requires setting OPENAI_API_KEY', + }, + ]); + + const innerConfig = makeInnerConfig(); + vi.mocked(innerConfig.getModelsConfig).mockReturnValue({ + getCurrentAuthType: vi.fn().mockReturnValue('qwen-oauth'), + } as unknown as ReturnType); + vi.mocked(innerConfig.refreshAuth).mockRejectedValue( + new Error('qwen-oauth token expired'), + ); + vi.mocked(loadSettings).mockReturnValue(makeSessionSettings()); + vi.mocked(loadCliConfig).mockResolvedValue( + innerConfig as unknown as Config, + ); + + vi.mocked(Session).mockImplementation( + () => + ({ + getId: vi.fn().mockReturnValue('test-session-id'), + getConfig: vi.fn().mockReturnValue(innerConfig), + sendAvailableCommandsUpdate: vi.fn().mockResolvedValue(undefined), + replayHistory: vi.fn().mockResolvedValue(undefined), + installRewriter: vi.fn(), + }) as unknown as InstanceType, + ); + + const agentPromise = runAcpAgent( + mockConfig, + makeSessionSettings(), + mockArgv, + ); + await vi.waitFor(() => expect(capturedAgentFactory).toBeDefined()); + const agent = capturedAgentFactory!({ + get closed() { + return mockConnectionState.promise; + }, + }) as AgentLike; + + await expect( + agent.newSession({ cwd: '/tmp', mcpServers: [] }), + ).rejects.toMatchObject({ + authMethods: [ + expect.objectContaining({ + id: 'openai', + }), + ], + }); + + mockConnectionState.resolve(); + await agentPromise; + }); + function makeInnerConfig() { return { initialize: vi.fn().mockResolvedValue(undefined), diff --git a/packages/cli/src/acp-integration/acpAgent.ts b/packages/cli/src/acp-integration/acpAgent.ts index 4a36b20cd57..02588aa2a6d 100644 --- a/packages/cli/src/acp-integration/acpAgent.ts +++ b/packages/cli/src/acp-integration/acpAgent.ts @@ -40,7 +40,6 @@ import type { Content } from '@google/genai'; import type { Agent, AuthenticateRequest, - AuthMethod, CancelNotification, ClientCapabilities, InitializeRequest, @@ -69,7 +68,10 @@ import type { SetSessionModeRequest, SetSessionModeResponse, } from '@agentclientprotocol/sdk'; -import { buildAuthMethods } from './authMethods.js'; +import { + buildAuthMethods, + pickAuthMethodsForAuthRequired, +} from './authMethods.js'; import { AcpFileSystemService } from './service/filesystem.js'; import { Readable, Writable } from 'node:stream'; import type { LoadedSettings } from '../config/settings.js'; @@ -1936,7 +1938,7 @@ class QwenAgent implements Agent { const selectedType = config.getModelsConfig().getCurrentAuthType(); if (!selectedType) { throw RequestError.authRequired( - { authMethods: this.pickAuthMethodsForAuthRequired() }, + { authMethods: pickAuthMethodsForAuthRequired() }, 'Use Qwen Code CLI to authenticate first.', ); } @@ -1947,51 +1949,13 @@ class QwenAgent implements Agent { debugLogger.error(`Authentication failed: ${e}`); throw RequestError.authRequired( { - authMethods: this.pickAuthMethodsForAuthRequired(selectedType, e), + authMethods: pickAuthMethodsForAuthRequired(selectedType), }, 'Authentication failed: ' + (e as Error).message, ); } } - private pickAuthMethodsForAuthRequired( - selectedType?: AuthType | string, - error?: unknown, - ): AuthMethod[] { - const authMethods = buildAuthMethods(); - const errorMessage = this.extractErrorMessage(error); - if ( - errorMessage?.includes('qwen-oauth') || - errorMessage?.includes('Qwen OAuth') - ) { - const qwenOAuthMethods = authMethods.filter( - (m) => m.id === AuthType.QWEN_OAUTH, - ); - return qwenOAuthMethods.length ? qwenOAuthMethods : authMethods; - } - - if (selectedType) { - const matched = authMethods.filter((m) => m.id === selectedType); - return matched.length ? matched : authMethods; - } - - return authMethods; - } - - private extractErrorMessage(error?: unknown): string | undefined { - if (error instanceof Error) return error.message; - if ( - typeof error === 'object' && - error != null && - 'message' in error && - typeof error.message === 'string' - ) { - return error.message; - } - if (typeof error === 'string') return error; - return undefined; - } - private setupFileSystem(config: Config): void { if (!this.clientCapabilities?.fs) return; diff --git a/packages/cli/src/acp-integration/acpAgent.worktree.test.ts b/packages/cli/src/acp-integration/acpAgent.worktree.test.ts index a0e22ece45c..ef9da4a49d1 100644 --- a/packages/cli/src/acp-integration/acpAgent.worktree.test.ts +++ b/packages/cli/src/acp-integration/acpAgent.worktree.test.ts @@ -125,7 +125,20 @@ vi.mock('./runtimeOutputDirContext.js', () => ({ ), })); -vi.mock('./authMethods.js', () => ({ buildAuthMethods: vi.fn() })); +vi.mock('./authMethods.js', () => { + const buildAuthMethods = vi.fn(); + return { + buildAuthMethods, + pickAuthMethodsForAuthRequired: vi.fn((selectedType?: string) => { + const authMethods = buildAuthMethods(); + if (!selectedType) return authMethods; + const matched = authMethods.filter( + (method: { id: string }) => method.id === selectedType, + ); + return matched.length ? matched : authMethods; + }), + }; +}); vi.mock('./service/filesystem.js', () => ({ AcpFileSystemService: vi.fn(), })); diff --git a/packages/cli/src/acp-integration/authMethods.test.ts b/packages/cli/src/acp-integration/authMethods.test.ts new file mode 100644 index 00000000000..4f76df3fc91 --- /dev/null +++ b/packages/cli/src/acp-integration/authMethods.test.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, expect, it } from 'vitest'; +import { AuthType } from '@qwen-code/qwen-code-core'; +import { + buildAuthMethods, + pickAuthMethodsForAuthRequired, +} from './authMethods.js'; + +describe('ACP auth methods', () => { + it('does not advertise discontinued Qwen OAuth', () => { + const authMethods = buildAuthMethods(); + + expect(authMethods.map((method) => method.id)).toEqual([ + AuthType.USE_OPENAI, + ]); + }); + + it('falls back to working methods for a stored discontinued Qwen OAuth selection', () => { + const authMethods = pickAuthMethodsForAuthRequired('qwen-oauth'); + + expect(authMethods.map((method) => method.id)).toEqual([ + AuthType.USE_OPENAI, + ]); + }); +}); diff --git a/packages/cli/src/acp-integration/authMethods.ts b/packages/cli/src/acp-integration/authMethods.ts index 04d6c797866..75132391aed 100644 --- a/packages/cli/src/acp-integration/authMethods.ts +++ b/packages/cli/src/acp-integration/authMethods.ts @@ -18,33 +18,17 @@ export function buildAuthMethods(): AuthMethod[] { args: ['--auth-type=openai'], }, }, - { - id: AuthType.QWEN_OAUTH, - name: 'Qwen OAuth', - description: 'Qwen OAuth (free tier discontinued 2026-04-15)', - _meta: { - type: 'terminal', - args: ['--auth-type=qwen-oauth'], - }, - }, ]; } -export function filterAuthMethodsById( - authMethods: AuthMethod[], - authMethodId: string, +export function pickAuthMethodsForAuthRequired( + selectedType?: AuthType | string, ): AuthMethod[] { - return authMethods.filter((method) => method.id === authMethodId); -} - -export function pickAuthMethodsForDetails(details?: string): AuthMethod[] { const authMethods = buildAuthMethods(); - if (!details) { - return authMethods; - } - if (details.includes('qwen-oauth') || details.includes('Qwen OAuth')) { - const narrowed = filterAuthMethodsById(authMethods, AuthType.QWEN_OAUTH); - return narrowed.length ? narrowed : authMethods; + if (selectedType) { + const matched = authMethods.filter((method) => method.id === selectedType); + return matched.length ? matched : authMethods; } + return authMethods; }