Merge remote-tracking branch 'origin/main' into feat/debug-logging-refactor

This commit is contained in:
tanzhenxin 2026-02-05 20:23:48 +08:00
commit 4abec5c055
331 changed files with 19546 additions and 7771 deletions

View file

@ -0,0 +1,42 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { AuthType } from '@qwen-code/qwen-code-core';
import {
formatAcpModelId,
parseAcpBaseModelId,
parseAcpModelOption,
} from './acpModelUtils.js';
describe('acpModelUtils', () => {
it('formats modelId(authType)', () => {
expect(formatAcpModelId('qwen3', AuthType.QWEN_OAUTH)).toBe(
`qwen3(${AuthType.QWEN_OAUTH})`,
);
});
it('extracts base model id when string ends with parentheses', () => {
expect(parseAcpBaseModelId(`qwen3(${AuthType.USE_OPENAI})`)).toBe('qwen3');
});
it('does not strip when parentheses are not a trailing suffix', () => {
expect(parseAcpBaseModelId('qwen3(x) y')).toBe('qwen3(x) y');
});
it('parses modelId and validates authType', () => {
expect(parseAcpModelOption(` qwen3(${AuthType.USE_OPENAI}) `)).toEqual({
modelId: 'qwen3',
authType: AuthType.USE_OPENAI,
});
});
it('returns trimmed input as modelId when authType is invalid', () => {
expect(parseAcpModelOption('qwen3(not-a-real-auth)')).toEqual({
modelId: 'qwen3(not-a-real-auth)',
});
});
});

View file

@ -0,0 +1,61 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { AuthType } from '@qwen-code/qwen-code-core';
import { z } from 'zod';
/**
* ACP model IDs are represented as `${modelId}(${authType})` in the ACP protocol.
*/
export function formatAcpModelId(modelId: string, authType: AuthType): string {
return `${modelId}(${authType})`;
}
/**
* Extracts the base model id from an ACP model id string.
*
* If the string ends with `(...)`, the suffix is removed; otherwise returns the
* trimmed input as-is.
*/
export function parseAcpBaseModelId(value: string): string {
const trimmed = value.trim();
const closeIdx = trimmed.lastIndexOf(')');
const openIdx = trimmed.lastIndexOf('(');
if (openIdx >= 0 && closeIdx === trimmed.length - 1 && openIdx < closeIdx) {
return trimmed.slice(0, openIdx);
}
return trimmed;
}
/**
* Parses an ACP model option string into `{ modelId, authType? }`.
*
* Supports the following formats:
* - `${modelId}(${authType})` - Standard registry model (e.g., "gpt-4(USE_OPENAI)")
* - `${snapshotId}(${authType})` - Runtime model snapshot (e.g., "$runtime|USE_OPENAI|gpt-4(USE_OPENAI)")
* where snapshotId is in format `$runtime|${authType}|${modelId}`
* - Plain model ID - Returns as-is with no authType
*
* If the string ends with `(...)` and `...` is a valid `AuthType`, returns both;
* otherwise returns the trimmed input as `modelId` only.
*/
export function parseAcpModelOption(input: string): {
modelId: string;
authType?: AuthType;
} {
const trimmed = input.trim();
const closeIdx = trimmed.lastIndexOf(')');
const openIdx = trimmed.lastIndexOf('(');
if (openIdx >= 0 && closeIdx === trimmed.length - 1 && openIdx < closeIdx) {
const maybeModelId = trimmed.slice(0, openIdx);
const maybeAuthType = trimmed.slice(openIdx + 1, closeIdx);
const parsedAuthType = z.nativeEnum(AuthType).safeParse(maybeAuthType);
if (parsedAuthType.success) {
return { modelId: maybeModelId, authType: parsedAuthType.data };
}
}
return { modelId: trimmed };
}

View file

@ -65,7 +65,7 @@ describe('handleAutoUpdate', () => {
mockSettings = {
merged: {
general: {
disableAutoUpdate: false,
enableAutoUpdate: true,
},
},
} as LoadedSettings;
@ -94,32 +94,29 @@ describe('handleAutoUpdate', () => {
expect(mockSpawn).not.toHaveBeenCalled();
});
it('should do nothing if update nag is disabled', () => {
mockSettings.merged.general!.disableUpdateNag = true;
handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);
expect(mockGetInstallationInfo).not.toHaveBeenCalled();
expect(mockUpdateEventEmitter.emit).not.toHaveBeenCalled();
expect(mockSpawn).not.toHaveBeenCalled();
});
it('should emit "update-received" but not update if auto-updates are disabled', () => {
mockSettings.merged.general!.disableAutoUpdate = true;
it('should show manual update message when enableAutoUpdate is false', () => {
// When enableAutoUpdate is false, gemini.tsx won't call checkForUpdates(),
// but if handleAutoUpdate is still called, it should show a manual update message.
mockSettings.merged.general!.enableAutoUpdate = false;
mockGetInstallationInfo.mockReturnValue({
updateCommand: 'npm i -g @qwen-code/qwen-code@latest',
updateMessage: 'Please update manually.',
updateMessage:
'Please run npm i -g @qwen-code/qwen-code@latest to update',
isGlobal: true,
packageManager: PackageManager.NPM,
});
handleAutoUpdate(mockUpdateInfo, mockSettings, '/root', mockSpawn);
expect(mockUpdateEventEmitter.emit).toHaveBeenCalledTimes(1);
// Should still emit update-received with manual update message
expect(mockUpdateEventEmitter.emit).toHaveBeenCalledWith(
'update-received',
{
message: 'An update is available!\nPlease update manually.',
message:
'An update is available!\nPlease run npm i -g @qwen-code/qwen-code@latest to update',
},
);
// Should NOT spawn update when enableAutoUpdate is false
expect(mockSpawn).not.toHaveBeenCalled();
});

View file

@ -24,13 +24,14 @@ export function handleAutoUpdate(
return;
}
if (settings.merged.general?.disableUpdateNag) {
return;
}
// enableAutoUpdate is checked in gemini.tsx before calling this function,
// so if we get here, auto-update is enabled (or undefined, which defaults to enabled).
const isAutoUpdateEnabled =
settings.merged.general?.enableAutoUpdate !== false;
const installationInfo = getInstallationInfo(
projectRoot,
settings.merged.general?.disableAutoUpdate ?? false,
isAutoUpdateEnabled,
);
let combinedMessage = info.message;
@ -42,10 +43,8 @@ export function handleAutoUpdate(
message: combinedMessage,
});
if (
!installationInfo.updateCommand ||
settings.merged.general?.disableAutoUpdate
) {
// Don't automatically run the update if auto-update is disabled or no update command
if (!installationInfo.updateCommand || !isAutoUpdateEnabled) {
return;
}
const isNightly = info.update.latest.includes('nightly');

View file

@ -180,13 +180,15 @@ describe('getInstallationInfo', () => {
throw new Error('Command failed');
});
const info = getInstallationInfo(projectRoot, false);
// isAutoUpdateEnabled = true -> "Attempting to automatically update"
const info = getInstallationInfo(projectRoot, true);
expect(info.packageManager).toBe(PackageManager.PNPM);
expect(info.isGlobal).toBe(true);
expect(info.updateCommand).toBe('pnpm add -g @qwen-code/qwen-code@latest');
expect(info.updateMessage).toContain('Attempting to automatically update');
const infoDisabled = getInstallationInfo(projectRoot, true);
// isAutoUpdateEnabled = false -> "Please run..."
const infoDisabled = getInstallationInfo(projectRoot, false);
expect(infoDisabled.updateMessage).toContain('Please run pnpm add');
});
@ -198,7 +200,8 @@ describe('getInstallationInfo', () => {
throw new Error('Command failed');
});
const info = getInstallationInfo(projectRoot, false);
// isAutoUpdateEnabled = true -> "Attempting to automatically update"
const info = getInstallationInfo(projectRoot, true);
expect(info.packageManager).toBe(PackageManager.YARN);
expect(info.isGlobal).toBe(true);
expect(info.updateCommand).toBe(
@ -206,7 +209,8 @@ describe('getInstallationInfo', () => {
);
expect(info.updateMessage).toContain('Attempting to automatically update');
const infoDisabled = getInstallationInfo(projectRoot, true);
// isAutoUpdateEnabled = false -> "Please run..."
const infoDisabled = getInstallationInfo(projectRoot, false);
expect(infoDisabled.updateMessage).toContain('Please run yarn global add');
});
@ -218,13 +222,15 @@ describe('getInstallationInfo', () => {
throw new Error('Command failed');
});
const info = getInstallationInfo(projectRoot, false);
// isAutoUpdateEnabled = true -> "Attempting to automatically update"
const info = getInstallationInfo(projectRoot, true);
expect(info.packageManager).toBe(PackageManager.BUN);
expect(info.isGlobal).toBe(true);
expect(info.updateCommand).toBe('bun add -g @qwen-code/qwen-code@latest');
expect(info.updateMessage).toContain('Attempting to automatically update');
const infoDisabled = getInstallationInfo(projectRoot, true);
// isAutoUpdateEnabled = false -> "Please run..."
const infoDisabled = getInstallationInfo(projectRoot, false);
expect(infoDisabled.updateMessage).toContain('Please run bun add');
});
@ -303,7 +309,8 @@ describe('getInstallationInfo', () => {
throw new Error('Command failed');
});
const info = getInstallationInfo(projectRoot, false);
// isAutoUpdateEnabled = true -> "Attempting to automatically update"
const info = getInstallationInfo(projectRoot, true);
expect(info.packageManager).toBe(PackageManager.NPM);
expect(info.isGlobal).toBe(true);
expect(info.updateCommand).toBe(
@ -311,7 +318,8 @@ describe('getInstallationInfo', () => {
);
expect(info.updateMessage).toContain('Attempting to automatically update');
const infoDisabled = getInstallationInfo(projectRoot, true);
// isAutoUpdateEnabled = false -> "Please run..."
const infoDisabled = getInstallationInfo(projectRoot, false);
expect(infoDisabled.updateMessage).toContain('Please run npm install');
});
});

View file

@ -32,7 +32,7 @@ export interface InstallationInfo {
export function getInstallationInfo(
projectRoot: string,
isAutoUpdateDisabled: boolean,
isAutoUpdateEnabled: boolean,
): InstallationInfo {
const cliPath = process.argv[1];
if (!cliPath) {
@ -101,9 +101,9 @@ export function getInstallationInfo(
packageManager: PackageManager.PNPM,
isGlobal: true,
updateCommand,
updateMessage: isAutoUpdateDisabled
? `Please run ${updateCommand} to update`
: 'Installed with pnpm. Attempting to automatically update now...',
updateMessage: isAutoUpdateEnabled
? 'Installed with pnpm. Attempting to automatically update now...'
: `Please run ${updateCommand} to update`,
};
}
@ -114,9 +114,9 @@ export function getInstallationInfo(
packageManager: PackageManager.YARN,
isGlobal: true,
updateCommand,
updateMessage: isAutoUpdateDisabled
? `Please run ${updateCommand} to update`
: 'Installed with yarn. Attempting to automatically update now...',
updateMessage: isAutoUpdateEnabled
? 'Installed with yarn. Attempting to automatically update now...'
: `Please run ${updateCommand} to update`,
};
}
@ -134,9 +134,9 @@ export function getInstallationInfo(
packageManager: PackageManager.BUN,
isGlobal: true,
updateCommand,
updateMessage: isAutoUpdateDisabled
? `Please run ${updateCommand} to update`
: 'Installed with bun. Attempting to automatically update now...',
updateMessage: isAutoUpdateEnabled
? 'Installed with bun. Attempting to automatically update now...'
: `Please run ${updateCommand} to update`,
};
}
@ -167,9 +167,9 @@ export function getInstallationInfo(
packageManager: PackageManager.NPM,
isGlobal: true,
updateCommand,
updateMessage: isAutoUpdateDisabled
? `Please run ${updateCommand} to update`
: 'Installed with npm. Attempting to automatically update now...',
updateMessage: isAutoUpdateEnabled
? 'Installed with npm. Attempting to automatically update now...'
: `Please run ${updateCommand} to update`,
};
} catch (error) {
debugLogger.error('Failed to detect installation info:', error);

View file

@ -121,7 +121,7 @@ describe('SettingsUtils', () => {
description: 'Accessibility settings.',
showInDialog: false,
properties: {
disableLoadingPhrases: {
enableLoadingPhrases: {
type: 'boolean',
label: 'Disable Loading Phrases',
category: 'UI',
@ -285,14 +285,14 @@ describe('SettingsUtils', () => {
it('should handle nested settings correctly', () => {
const settings = makeMockSettings({
ui: { accessibility: { disableLoadingPhrases: true } },
ui: { accessibility: { enableLoadingPhrases: true } },
});
const mergedSettings = makeMockSettings({
ui: { accessibility: { disableLoadingPhrases: false } },
ui: { accessibility: { enableLoadingPhrases: false } },
});
const value = getEffectiveValue(
'ui.accessibility.disableLoadingPhrases',
'ui.accessibility.enableLoadingPhrases',
settings,
mergedSettings,
);
@ -316,7 +316,7 @@ describe('SettingsUtils', () => {
it('should return all setting keys', () => {
const keys = getAllSettingKeys();
expect(keys).toContain('test');
expect(keys).toContain('ui.accessibility.disableLoadingPhrases');
expect(keys).toContain('ui.accessibility.enableLoadingPhrases');
});
});
@ -343,9 +343,9 @@ describe('SettingsUtils', () => {
describe('isValidSettingKey', () => {
it('should return true for valid setting keys', () => {
expect(isValidSettingKey('ui.requiresRestart')).toBe(true);
expect(
isValidSettingKey('ui.accessibility.disableLoadingPhrases'),
).toBe(true);
expect(isValidSettingKey('ui.accessibility.enableLoadingPhrases')).toBe(
true,
);
});
it('should return false for invalid setting keys', () => {
@ -358,7 +358,7 @@ describe('SettingsUtils', () => {
it('should return correct category for valid settings', () => {
expect(getSettingCategory('ui.requiresRestart')).toBe('UI');
expect(
getSettingCategory('ui.accessibility.disableLoadingPhrases'),
getSettingCategory('ui.accessibility.enableLoadingPhrases'),
).toBe('UI');
});
@ -392,7 +392,7 @@ describe('SettingsUtils', () => {
const uiSettings = categories['UI'];
const uiKeys = uiSettings.map((s) => s.key);
expect(uiKeys).toContain('ui.requiresRestart');
expect(uiKeys).toContain('ui.accessibility.disableLoadingPhrases');
expect(uiKeys).toContain('ui.accessibility.enableLoadingPhrases');
expect(uiKeys).not.toContain('ui.theme'); // This is now marked false
});
@ -422,7 +422,7 @@ describe('SettingsUtils', () => {
const keys = booleanSettings.map((s) => s.key);
expect(keys).toContain('ui.requiresRestart');
expect(keys).toContain('ui.accessibility.disableLoadingPhrases');
expect(keys).toContain('ui.accessibility.enableLoadingPhrases');
expect(keys).not.toContain('privacy.usageStatisticsEnabled');
expect(keys).not.toContain('security.auth.selectedType'); // Advanced setting
expect(keys).not.toContain('security.auth.useExternal'); // Advanced setting
@ -455,7 +455,7 @@ describe('SettingsUtils', () => {
expect(dialogKeys).toContain('ui.requiresRestart');
// Should include nested settings marked for dialog
expect(dialogKeys).toContain('ui.accessibility.disableLoadingPhrases');
expect(dialogKeys).toContain('ui.accessibility.enableLoadingPhrases');
// Should NOT include settings marked as hidden
expect(dialogKeys).not.toContain('ui.theme'); // Hidden
@ -602,14 +602,14 @@ describe('SettingsUtils', () => {
it('should return true when value differs from default', () => {
expect(isSettingModified('ui.requiresRestart', true)).toBe(true);
expect(
isSettingModified('ui.accessibility.disableLoadingPhrases', true),
isSettingModified('ui.accessibility.enableLoadingPhrases', true),
).toBe(true);
});
it('should return false when value matches default', () => {
expect(isSettingModified('ui.requiresRestart', false)).toBe(false);
expect(
isSettingModified('ui.accessibility.disableLoadingPhrases', false),
isSettingModified('ui.accessibility.enableLoadingPhrases', false),
).toBe(false);
});
});
@ -629,11 +629,11 @@ describe('SettingsUtils', () => {
it('should return true for nested settings that exist', () => {
const settings = makeMockSettings({
ui: { accessibility: { disableLoadingPhrases: true } },
ui: { accessibility: { enableLoadingPhrases: true } },
});
expect(
settingExistsInScope(
'ui.accessibility.disableLoadingPhrases',
'ui.accessibility.enableLoadingPhrases',
settings,
),
).toBe(true);
@ -643,7 +643,7 @@ describe('SettingsUtils', () => {
const settings = makeMockSettings({});
expect(
settingExistsInScope(
'ui.accessibility.disableLoadingPhrases',
'ui.accessibility.enableLoadingPhrases',
settings,
),
).toBe(false);
@ -653,7 +653,7 @@ describe('SettingsUtils', () => {
const settings = makeMockSettings({ ui: { accessibility: {} } });
expect(
settingExistsInScope(
'ui.accessibility.disableLoadingPhrases',
'ui.accessibility.enableLoadingPhrases',
settings,
),
).toBe(false);
@ -675,25 +675,25 @@ describe('SettingsUtils', () => {
it('should set nested setting value', () => {
const pendingSettings = makeMockSettings({});
const result = setPendingSettingValue(
'ui.accessibility.disableLoadingPhrases',
'ui.accessibility.enableLoadingPhrases',
true,
pendingSettings,
);
expect(result.ui?.accessibility?.disableLoadingPhrases).toBe(true);
expect(result.ui?.accessibility?.enableLoadingPhrases).toBe(true);
});
it('should preserve existing nested settings', () => {
const pendingSettings = makeMockSettings({
ui: { accessibility: { disableLoadingPhrases: false } },
ui: { accessibility: { enableLoadingPhrases: false } },
});
const result = setPendingSettingValue(
'ui.accessibility.disableLoadingPhrases',
'ui.accessibility.enableLoadingPhrases',
true,
pendingSettings,
);
expect(result.ui?.accessibility?.disableLoadingPhrases).toBe(true);
expect(result.ui?.accessibility?.enableLoadingPhrases).toBe(true);
});
it('should not mutate original settings', () => {
@ -1030,7 +1030,7 @@ describe('SettingsUtils', () => {
const settings = makeMockSettings({}); // nested setting doesn't exist
const result = isDefaultValue(
'ui.accessibility.disableLoadingPhrases',
'ui.accessibility.enableLoadingPhrases',
settings,
);
expect(result).toBe(true);
@ -1038,11 +1038,11 @@ describe('SettingsUtils', () => {
it('should return false when nested setting exists in scope', () => {
const settings = makeMockSettings({
ui: { accessibility: { disableLoadingPhrases: true } },
ui: { accessibility: { enableLoadingPhrases: true } },
}); // nested setting exists
const result = isDefaultValue(
'ui.accessibility.disableLoadingPhrases',
'ui.accessibility.enableLoadingPhrases',
settings,
);
expect(result).toBe(false);
@ -1080,14 +1080,14 @@ describe('SettingsUtils', () => {
it('should return false for nested settings that exist in scope', () => {
const settings = makeMockSettings({
ui: { accessibility: { disableLoadingPhrases: true } },
ui: { accessibility: { enableLoadingPhrases: true } },
});
const mergedSettings = makeMockSettings({
ui: { accessibility: { disableLoadingPhrases: true } },
ui: { accessibility: { enableLoadingPhrases: true } },
});
const result = isValueInherited(
'ui.accessibility.disableLoadingPhrases',
'ui.accessibility.enableLoadingPhrases',
settings,
mergedSettings,
);
@ -1097,11 +1097,11 @@ describe('SettingsUtils', () => {
it('should return true for nested settings that do not exist in scope', () => {
const settings = makeMockSettings({});
const mergedSettings = makeMockSettings({
ui: { accessibility: { disableLoadingPhrases: true } },
ui: { accessibility: { enableLoadingPhrases: true } },
});
const result = isValueInherited(
'ui.accessibility.disableLoadingPhrases',
'ui.accessibility.enableLoadingPhrases',
settings,
mergedSettings,
);