Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/cli/src/commands/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ const initializeI18n = vi.fn();
const resolveLanguageSetting = vi.fn((language?: string) => language || 'auto');

vi.mock('../config/settings.js', () => ({ loadSettings }));
vi.mock('../ui/utils/updateCheck.js', () => ({ checkForUpdatesDetailed }));
vi.mock('../ui/utils/updateCheck.js', async (importOriginal) => ({
...(await importOriginal<typeof import('../ui/utils/updateCheck.js')>()),
checkForUpdatesDetailed,
}));
vi.mock('../utils/installationInfo.js', () => ({
formatUpdateInstructions,
getInstallationInfo,
Expand Down Expand Up @@ -235,7 +238,7 @@ describe('update command', () => {
await updateCommand.handler(updateArgs);

expect(writeStderrLine).toHaveBeenCalledWith(
'Failed to check for updates. Please check your network or registry configuration.',
'Failed to check for updates (registry error). Please check your network or registry configuration.',
);
expect(process.exitCode).toBe(1);
expect(getInstallationInfo).not.toHaveBeenCalled();
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const updateCommand: CommandModule = {
handler: async () => {
const [
{ loadSettings },
{ checkForUpdatesDetailed },
{ checkForUpdatesDetailed, describeUpdateCheckFailure },
installationInfoModule,
standaloneUpdate,
stdioHelpers,
Expand Down Expand Up @@ -54,7 +54,8 @@ export const updateCommand: CommandModule = {
if (updateCheck.status === 'error') {
writeStderrLine(
t(
'Failed to check for updates. Please check your network or registry configuration.',
'Failed to check for updates ({{reason}}). Please check your network or registry configuration.',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The report in #7049 is about the non-blocking startup check. This changes the explicit qwen update failure contract as well, and the same propagation appears in /update and the relaunch path. Could we leave those paths unchanged in this PR and keep the change at the startup boundary?

{ reason: describeUpdateCheckFailure(updateCheck.error) },
),
);
process.exitCode = 1;
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/i18n/locales/ca.js
Original file line number Diff line number Diff line change
Expand Up @@ -2098,8 +2098,6 @@ export default {
'Hi ha una versió nova de Qwen Code disponible! {{current}} → {{latest}}',
'Qwen Code {{version}} is up to date!':
'Qwen Code {{version}} està actualitzat!',
'Failed to check for updates. Please check your network or registry configuration.':
"No s'han pogut comprovar les actualitzacions. Comproveu la xarxa o la configuració del registre.",
'Unable to check for updates: {{reason}}':
'No es poden comprovar les actualitzacions: {{reason}}',
'Update successful! The new version will be used on your next run.':
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/i18n/locales/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -2146,8 +2146,6 @@ export default {
'A new version of Qwen Code is available! {{current}} → {{latest}}':
'Eine neue Version von Qwen Code ist verfügbar! {{current}} → {{latest}}',
'Qwen Code {{version}} is up to date!': 'Qwen Code {{version}} ist aktuell!',
'Failed to check for updates. Please check your network or registry configuration.':
'Suche nach Updates fehlgeschlagen. Bitte Netzwerk- oder Registry-Konfiguration prüfen.',
'Unable to check for updates: {{reason}}':
'Updates können nicht geprüft werden: {{reason}}',
'Update successful! The new version will be used on your next run.':
Expand Down
10 changes: 8 additions & 2 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -2604,8 +2604,14 @@ export default {
'A new version of Qwen Code is available! {{current}} → {{latest}}',
'Qwen Code {{version}} is up to date!':
'Qwen Code {{version}} is up to date!',
'Failed to check for updates. Please check your network or registry configuration.':
'Failed to check for updates. Please check your network or registry configuration.',
'Failed to check for updates ({{reason}}). Please check your network or registry configuration.':
'Failed to check for updates ({{reason}}). Please check your network or registry configuration.',
'Update check skipped ({{reason}}) — run /update to retry.':
'Update check skipped ({{reason}}) — run /update to retry.',
'registry did not respond within {{seconds}}s':
'registry did not respond within {{seconds}}s',
'registry unreachable': 'registry unreachable',
'registry error': 'registry error',
'Unable to check for updates: {{reason}}':
'Unable to check for updates: {{reason}}',
'Update successful! The new version will be used on your next run.':
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/i18n/locales/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -2148,8 +2148,6 @@ export default {
'A new version of Qwen Code is available! {{current}} → {{latest}}':
'Une nouvelle version de Qwen Code est disponible ! {{current}} → {{latest}}',
'Qwen Code {{version}} is up to date!': 'Qwen Code {{version}} est à jour !',
'Failed to check for updates. Please check your network or registry configuration.':
'Échec de la vérification des mises à jour. Vérifiez votre réseau ou la configuration du registre.',
'Unable to check for updates: {{reason}}':
'Impossible de vérifier les mises à jour : {{reason}}',
'Update successful! The new version will be used on your next run.':
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/i18n/locales/ja.js
Original file line number Diff line number Diff line change
Expand Up @@ -1912,8 +1912,6 @@ export default {
'A new version of Qwen Code is available! {{current}} → {{latest}}':
'Qwen Code の新しいバージョンがあります!{{current}} → {{latest}}',
'Qwen Code {{version}} is up to date!': 'Qwen Code {{version}} は最新です!',
'Failed to check for updates. Please check your network or registry configuration.':
'アップデートの確認に失敗しました。ネットワークまたはレジストリ設定を確認してください。',
'Unable to check for updates: {{reason}}':
'アップデートを確認できません: {{reason}}',
'Update successful! The new version will be used on your next run.':
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/i18n/locales/pt.js
Original file line number Diff line number Diff line change
Expand Up @@ -2133,8 +2133,6 @@ export default {
'Uma nova versão do Qwen Code está disponível! {{current}} → {{latest}}',
'Qwen Code {{version}} is up to date!':
'Qwen Code {{version}} está atualizado!',
'Failed to check for updates. Please check your network or registry configuration.':
'Falha ao verificar atualizações. Verifique sua rede ou configuração do registro.',
'Unable to check for updates: {{reason}}':
'Não foi possível verificar atualizações: {{reason}}',
'Update successful! The new version will be used on your next run.':
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/i18n/locales/ru.js
Original file line number Diff line number Diff line change
Expand Up @@ -2120,8 +2120,6 @@ export default {
'A new version of Qwen Code is available! {{current}} → {{latest}}':
'Доступна новая версия Qwen Code! {{current}} → {{latest}}',
'Qwen Code {{version}} is up to date!': 'Qwen Code {{version}} актуален!',
'Failed to check for updates. Please check your network or registry configuration.':
'Не удалось проверить обновления. Проверьте сеть или настройки registry.',
'Unable to check for updates: {{reason}}':
'Невозможно проверить обновления: {{reason}}',
'Update successful! The new version will be used on your next run.':
Expand Down
10 changes: 8 additions & 2 deletions packages/cli/src/i18n/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -2197,8 +2197,14 @@ export default {
'A new version of Qwen Code is available! {{current}} → {{latest}}':
'Qwen Code 有新版本可用!{{current}} → {{latest}}',
'Qwen Code {{version}} is up to date!': 'Qwen Code {{version}} 已是最新!',
'Failed to check for updates. Please check your network or registry configuration.':
'檢查更新失敗。請檢查網路或 registry 設定。',
'Failed to check for updates ({{reason}}). Please check your network or registry configuration.':
'檢查更新失敗({{reason}})。請檢查網路或 registry 設定。',
'Update check skipped ({{reason}}) — run /update to retry.':
'已略過更新檢查({{reason}})— 可執行 /update 重試。',
'registry did not respond within {{seconds}}s':
'registry 在 {{seconds}} 秒內未回應',
'registry unreachable': 'registry 無法連線',
'registry error': 'registry 錯誤',
'Unable to check for updates: {{reason}}': '無法檢查更新:{{reason}}',
'Update successful! The new version will be used on your next run.':
'更新成功!新版本將在下次執行時生效。',
Expand Down
10 changes: 8 additions & 2 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -2399,8 +2399,14 @@ export default {
'A new version of Qwen Code is available! {{current}} → {{latest}}':
'Qwen Code 有新版本可用!{{current}} → {{latest}}',
'Qwen Code {{version}} is up to date!': 'Qwen Code {{version}} 已是最新!',
'Failed to check for updates. Please check your network or registry configuration.':
'检查更新失败。请检查网络或 registry 配置。',
'Failed to check for updates ({{reason}}). Please check your network or registry configuration.':
'检查更新失败({{reason}})。请检查网络或 registry 配置。',
'Update check skipped ({{reason}}) — run /update to retry.':
'已跳过更新检查({{reason}})— 可运行 /update 重试。',
'registry did not respond within {{seconds}}s':
'registry 在 {{seconds}} 秒内未响应',
'registry unreachable': 'registry 无法连接',
'registry error': 'registry 错误',
'Unable to check for updates: {{reason}}': '无法检查更新:{{reason}}',
'Update successful! The new version will be used on your next run.':
'更新成功!新版本将在下次运行时生效。',
Expand Down
60 changes: 53 additions & 7 deletions packages/cli/src/startup/startup-prefetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ vi.mock('../utils/startupProfiler.js', () => ({
recordStartupEvent: (...args: unknown[]) => mockRecordStartupEvent(...args),
}));

vi.mock('../ui/utils/updateCheck.js', () => ({
vi.mock('../ui/utils/updateCheck.js', async (importOriginal) => ({
...(await importOriginal<typeof import('../ui/utils/updateCheck.js')>()),
checkForUpdatesDetailed: (...args: unknown[]) =>
mockCheckForUpdatesDetailed(...args),
}));
Expand All @@ -78,7 +79,12 @@ vi.mock('../utils/updateEventEmitter.js', () => ({
}));

vi.mock('../i18n/index.js', () => ({
t: (key: string) => key,
t: (key: string, params?: Record<string, string | number>) =>
params
? key.replace(/\{\{(\w+)\}\}/g, (match, name) =>
name in params ? String(params[name]) : match,
)
: key,
}));

vi.mock('../core/initializer.js', () => ({
Expand Down Expand Up @@ -415,7 +421,7 @@ describe('startupPrefetch', () => {
expect(mockConnectIdeForStartup).toHaveBeenCalledWith(config);
});

it('surfaces update check errors through the update event emitter', async () => {
it('surfaces update check errors as a warning through the update event emitter', async () => {
const config = makeConfig();
mockCheckForUpdatesDetailed.mockResolvedValue({
status: 'error',
Expand All @@ -427,12 +433,52 @@ describe('startupPrefetch', () => {
await vi.dynamicImportSettled();

expect(mockUpdateEventEmit).toHaveBeenCalledWith('update-failed', {
message:
'Failed to check for updates. Please check your network or registry configuration.',
message: 'Update check skipped (registry error) — run /update to retry.',
severity: 'warning',
});
expect(mockRequestUpdateOnExit).not.toHaveBeenCalled();
});

it('reports a timeout-specific reason when the update check times out', async () => {
const config = makeConfig();
const { UpdateCheckTimeoutError, FETCH_TIMEOUT_MS } = await import(
'../ui/utils/updateCheck.js'
);
mockCheckForUpdatesDetailed.mockResolvedValue({
status: 'error',
error: new UpdateCheckTimeoutError(FETCH_TIMEOUT_MS, 'latest'),
});

startPostRenderPrefetches(config, makeSettings());

await vi.dynamicImportSettled();

expect(mockUpdateEventEmit).toHaveBeenCalledWith('update-failed', {
message: `Update check skipped (registry did not respond within ${Math.round(FETCH_TIMEOUT_MS / 1000)}s) — run /update to retry.`,
severity: 'warning',
});
});

it('reports an offline-specific reason when the registry is unreachable', async () => {
const config = makeConfig();
const error = new Error('getaddrinfo failed') as NodeJS.ErrnoException;
error.code = 'ENOTFOUND';
mockCheckForUpdatesDetailed.mockResolvedValue({
status: 'error',
error,
});

startPostRenderPrefetches(config, makeSettings());

await vi.dynamicImportSettled();

expect(mockUpdateEventEmit).toHaveBeenCalledWith('update-failed', {
message:
'Update check skipped (registry unreachable) — run /update to retry.',
severity: 'warning',
});
});

it('requires connectIde option before connecting IDE', async () => {
const config = makeConfig();
const { statuses, stop } = captureIdeConnectionStatuses();
Expand Down Expand Up @@ -657,8 +703,8 @@ describe('startupPrefetch', () => {

expect(mockWarn).toHaveBeenCalledWith('update_check failed:', error);
expect(mockUpdateEventEmit).toHaveBeenCalledWith('update-failed', {
message:
'Failed to check for updates. Please check your network or registry configuration.',
message: 'Update check skipped (registry error) — run /update to retry.',
severity: 'warning',
});
});

Expand Down
18 changes: 12 additions & 6 deletions packages/cli/src/startup/startup-prefetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export function startPostRenderPrefetches(
) {
runDeferredTask('update_check', async () => {
const [
{ checkForUpdatesDetailed },
{ checkForUpdatesDetailed, describeUpdateCheckFailure },
{ handleAutoUpdate },
{ getInstallationInfo },
{ updateEventEmitter },
Expand All @@ -171,9 +171,13 @@ export function startPostRenderPrefetches(
import('../utils/updateEventEmitter.js'),
import('../i18n/index.js'),
]);
const updateFailedMessage = t(
'Failed to check for updates. Please check your network or registry configuration.',
);
// The startup check is best-effort background work: surface failures as
// a soft warning with the concrete reason instead of an alarming error
// (#7049), while keeping the failure visible (#6857).
const updateCheckSkippedMessage = (error: unknown) =>
t('Update check skipped ({{reason}}) — run /update to retry.', {
reason: describeUpdateCheckFailure(error),
});
try {
const result = await checkForUpdatesDetailed();
if (result.status === 'update') {
Expand Down Expand Up @@ -222,12 +226,14 @@ export function startPostRenderPrefetches(
}
} else if (result.status === 'error') {
updateEventEmitter.emit('update-failed', {
message: updateFailedMessage,
message: updateCheckSkippedMessage(result.error),
severity: 'warning',
});
}
} catch (error) {
updateEventEmitter.emit('update-failed', {
message: updateFailedMessage,
message: updateCheckSkippedMessage(error),
severity: 'warning',
});
throw error;
}
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/src/ui/commands/update-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ const formatUpdateInstructions = vi.fn(
return ['Manual update required. Please reinstall Qwen Code.'];
},
);
vi.mock('../utils/updateCheck.js', () => ({ checkForUpdatesDetailed }));
vi.mock('../utils/updateCheck.js', async (importOriginal) => ({
...(await importOriginal<typeof import('../utils/updateCheck.js')>()),
checkForUpdatesDetailed,
}));
vi.mock('../../utils/processUtils.js', () => ({
CUSTOM_SANDBOX_IMAGE_ENV_VAR: 'QWEN_CODE_CUSTOM_SANDBOX_IMAGE',
HOST_UPDATE_RELAUNCH_ENV_VAR: 'QWEN_CODE_HOST_UPDATE_RELAUNCH',
Expand Down Expand Up @@ -376,7 +379,7 @@ describe('updateCommand', () => {
type: 'message',
messageType: 'error',
content:
'Failed to check for updates. Please check your network or registry configuration.',
'Failed to check for updates (registry error). Please check your network or registry configuration.',
});
expect(getInstallationInfo).not.toHaveBeenCalled();
});
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/ui/commands/update-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const updateCommand: SlashCommand = {
supportedModes: ['interactive', 'non_interactive', 'acp'] as const,
action: async (context) => {
const [
{ checkForUpdatesDetailed },
{ checkForUpdatesDetailed, describeUpdateCheckFailure },
{
CUSTOM_SANDBOX_IMAGE_ENV_VAR,
HOST_UPDATE_RELAUNCH_ENV_VAR,
Expand Down Expand Up @@ -54,7 +54,8 @@ export const updateCommand: SlashCommand = {
type: 'message' as const,
messageType: 'error' as const,
content: t(
'Failed to check for updates. Please check your network or registry configuration.',
'Failed to check for updates ({{reason}}). Please check your network or registry configuration.',
{ reason: describeUpdateCheckFailure(updateCheck.error) },
),
};
}
Expand Down
43 changes: 43 additions & 0 deletions packages/cli/src/ui/utils/updateCheck.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest';
import {
checkForUpdates,
checkForUpdatesDetailed,
classifyUpdateCheckError,
fetchGlobalNpmUpdateInfo,
FETCH_TIMEOUT_MS,
isGlobalNpmInstallation,
Expand Down Expand Up @@ -633,4 +634,46 @@ describe('checkForUpdates', () => {
}
});
});

describe('timeout budget (#7049)', () => {
it('allows at least 5 seconds for slow registries', () => {
expect(FETCH_TIMEOUT_MS).toBeGreaterThanOrEqual(5000);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be toBe(5000). >= 5000 would still pass if this accidentally became 50 seconds, and the other timeout tests advance by the same constant, so they would not catch that regression either.

});
});
});

describe('classifyUpdateCheckError', () => {
it('classifies UpdateCheckTimeoutError as timeout', () => {
expect(classifyUpdateCheckError(new UpdateCheckTimeoutError(5000))).toBe(
'timeout',
);
});

it.each([
'ENOTFOUND',
'ECONNREFUSED',
'EAI_AGAIN',
'ETIMEDOUT',
'ENETUNREACH',
])('classifies %s errors as offline', (code) => {
const error = new Error(`request failed`) as NodeJS.ErrnoException;
error.code = code;
expect(classifyUpdateCheckError(error)).toBe('offline');
});

it('classifies network codes embedded in the message as offline', () => {
// npm child-process failures surface the code inside stderr text only.
expect(
classifyUpdateCheckError(
new Error('npm error code ENOTFOUND\nnpm error network'),
),
).toBe('offline');
});

it('classifies other errors as registry', () => {
expect(classifyUpdateCheckError(new Error('404 Not Found'))).toBe(
'registry',
);
expect(classifyUpdateCheckError('not-an-error')).toBe('registry');
});
});
Loading
Loading