Skip to content

fix(cli): apply FETCH_TIMEOUT_MS to /update version check and log fetchInfo results (#6857) - #6887

Merged
wenshao merged 3 commits into
QwenLM:mainfrom
C0d3N1nja97342:fix/update-check-timeout-and-logging
Jul 15, 2026
Merged

fix(cli): apply FETCH_TIMEOUT_MS to /update version check and log fetchInfo results (#6857)#6887
wenshao merged 3 commits into
QwenLM:mainfrom
C0d3N1nja97342:fix/update-check-timeout-and-logging

Conversation

@C0d3N1nja97342

Copy link
Copy Markdown
Contributor

What this PR does

Wires the existing FETCH_TIMEOUT_MS = 2000 constant in packages/cli/src/ui/utils/updateCheck.ts up to update-notifier's fetchInfo() — the option didn't exist upstream, so the constant was dead code and /update had no bound. Race the fetch against the timer and surface a new UpdateCheckTimeoutError when it fires, so /update returns the existing 'error' status instead of silently reporting 'up to date'. Also log the fetchInfo return value under the UPDATE_CHECK debug tag so the next round of reports can distinguish "registry returned the wrong version" from "we compared it incorrectly" without more speculation.

Why it's needed

Users report /update saying "Qwen Code 0.19.9 is up to date!" while npm view @qwen-code/qwen-code@latest version on the same machine returns 0.19.10. The triage on #6857 confirmed checkForUpdatesDetailed() reaches return { status: 'up-to-date' } — meaning fetchInfo() either returned the current version or resolved late enough that a stale configstore cache was used, and the failure was swallowed silently.

Two things make the failure invisible today:

  • FETCH_TIMEOUT_MS was never applied. update-notifier@7.3.x's fetchInfo() takes no timeout option, and the exported FETCH_TIMEOUT_MS was never passed anywhere. On slow / unreachable registries (corporate proxies, offline networks, scoped .npmrc mirrors without auth), the check hangs long enough for the caller to drop it — but the 'error' code path never fires, so the user only sees the fallback message.
  • No signal about what actually came back. The existing debugLogger.warn runs only when an exception escapes, so a return of { latest: '0.19.9' } in the exact "up to date but shouldn't be" case leaves nothing to bisect.

This PR does the two smallest things that make future reports diagnosable and cut off the silent-hang failure mode. It intentionally does not replace update-notifier's fetchInfo() with a direct package-json / npm view call — that swap is a larger design change that the triage flagged as a separate option, and this PR keeps the fix scoped to plumbing an already-declared constant.

Reviewer Test Plan

How to verify

Unit coverage lives in packages/cli/src/ui/utils/updateCheck.test.ts and asserts both directions of the race:

  • returns a detailed error when fetchInfo does not resolve within FETCH_TIMEOUT_MS — mocks fetchInfo as a promise that never resolves, advances fake timers past 2000 ms, expects the result to be { status: 'error', error: UpdateCheckTimeoutError, currentVersion }.
  • still resolves the update path when fetchInfo returns before the timeout — guards against the timer accidentally firing on a healthy fast fetch (which would silently drop every real /update to error).

Full suites confirmed locally with npx vitest run:

  • packages/cli/src/ui/utils/updateCheck.test.ts — 17/17 pass (15 pre-existing + 2 new)
  • packages/cli/src/commands/update.test.ts — 10/10 pass
  • packages/cli/src/ui/commands/update-command.test.ts — 15/15 pass
  • packages/cli/src/startup/startup-prefetch.test.ts — 21/21 pass
  • packages/cli/src/utils/handleAutoUpdate.test.ts — 24/24 pass

For a manual smoke: set QWEN_DEBUG=UPDATE_CHECK, run /update on any current version, and confirm a UPDATE_CHECK DEBUG line reporting the fetchInfo return value now appears in the debug log; if run on a network that can't reach registry.npmjs.org within 2 s (e.g. block outbound to registry.npmjs.org), the result should now be error with an UpdateCheckTimeoutError, not up-to-date.

Evidence (Before & After)

N/A — non-user-visible behavior change (debug log line + failure-path routing).

Tested on

OS Status
🍏 macOS ⚠️
🪟 Windows
🐧 Linux ⚠️

Environment (optional)

npx vitest run for the affected suites. No live-registry probe.

Risk & Scope

  • Main risk or tradeoff: a legitimate fetchInfo() that resolves in more than 2 s on a slow-but-reachable network will now be reported as error instead of the previous hang / fallback. That is the intended behavior — the user gets a real error message instead of an invisible failure — but if the timeout turns out to be too tight in practice, tuning FETCH_TIMEOUT_MS is a one-line follow-up. Kept the existing 2 s value to avoid mixing the wiring fix with a value bump.
  • Not validated / out of scope: the underlying question of why fetchInfo() sometimes returns a stale latest in some environments (proxy, corporate .npmrc, package-json scoped-registry handling). The new debug log is the tool for that investigation; the swap to a direct registry call the triage sketched is a separate change.
  • Breaking changes / migration notes: none. Public exports are additive (UpdateCheckTimeoutError); the UpdateCheckResult union already had 'error'.

Linked Issues

Refs #6857

中文说明

这个 PR 做了什么

packages/cli/src/ui/utils/updateCheck.ts 里已有但从未被使用的 FETCH_TIMEOUT_MS = 2000 常量真正接上 update-notifierfetchInfo()——因为该库上游没有 timeout 选项,这个常量此前是死代码,/update 也就没有任何上限。用 Promise.race 与定时器竞速,超时时抛出新增的 UpdateCheckTimeoutError,让 /update 走已有的 'error' 状态而非静默报"up to date"。同时把 fetchInfo 的返回值记录到 UPDATE_CHECK debug tag 下,方便下次收到报告时区分"registry 返回错了版本"与"比较逻辑错了",而不必继续凭猜。

为什么需要

用户报告 /update 在 v0.19.9 上说"Qwen Code 0.19.9 is up to date!",但同一台机器 npm view @qwen-code/qwen-code@latest version 返回 0.19.10#6857 的 triage 确认代码走到了 return { status: 'up-to-date' }——要么 fetchInfo() 返回了当前版本号,要么响应太慢被上游用了陈旧的 configstore 缓存,且失败被静默吞掉。

现在有两点让这类失败不可见:

  • FETCH_TIMEOUT_MS 从没被真正应用。 update-notifier@7.3.xfetchInfo() 不接受 timeout 选项,导出的 FETCH_TIMEOUT_MS 也没传给任何地方。在慢的 / 不可达的 registry(企业代理、离线网络、.npmrc scoped mirror 未配置 auth)上,请求悬挂到调用方放弃——但 'error' 分支从未触发,用户只看到 fallback 提示。
  • 返回值没有任何信号可查。 现有 debugLogger.warn 只在异常逃出时打印,遇到 fetchInfo() 返回 { latest: '0.19.9' } 这种真正 "看似正常却错误" 的情况时无从 bisect。

本 PR 做两件把未来报告可诊断化 + 切断静默 hang 失败路径的最小改动。刻意update-notifierfetchInfo() 换成对 package-json / npm view 的直接调用——那是 triage 里另一个更大的设计变更,本 PR 把修复限定在把已声明的常量接上。

复审测试计划

如何验证

单元覆盖位于 packages/cli/src/ui/utils/updateCheck.test.ts,同时验证 race 的两个方向:

  • returns a detailed error when fetchInfo does not resolve within FETCH_TIMEOUT_MS —— mock fetchInfo 永远不 resolve,用 fake timer 推进 2000ms+,期望结果为 { status: 'error', error: UpdateCheckTimeoutError, currentVersion }
  • still resolves the update path when fetchInfo returns before the timeout —— 防止定时器在健康的快速请求上误触(那样每次真实 /update 都会被静默降级为 error)。

npx vitest run 本地全绿:

  • packages/cli/src/ui/utils/updateCheck.test.ts 17/17(原 15 + 新增 2)
  • packages/cli/src/commands/update.test.ts 10/10
  • packages/cli/src/ui/commands/update-command.test.ts 15/15
  • packages/cli/src/startup/startup-prefetch.test.ts 21/21
  • packages/cli/src/utils/handleAutoUpdate.test.ts 24/24

手工冒烟:设置 QWEN_DEBUG=UPDATE_CHECK,运行 /update 于任意版本,确认 debug log 里多了一条 UPDATE_CHECK DEBUG 行报告 fetchInfo 的返回值;如果限制 2s 内不可达 registry.npmjs.org,结果应变成 error 携带 UpdateCheckTimeoutError 而不是 up-to-date

证据(Before & After)

N/A——非用户可见的行为变更(debug 日志 + 失败路径路由)。

测试环境

系统 状态
🍏 macOS ⚠️
🪟 Windows
🐧 Linux ⚠️

环境(可选)

npx vitest run 相关测试集。未做真实 registry 探测。

风险与范围

  • 主要风险 / 取舍:一次合法但耗时超过 2s 的 fetchInfo() 现在会被报告为 error,而不是此前那种悬挂 / fallback。这就是刻意的行为改变——用户会看到真实错误提示而非隐形失败——如果实际使用中发现 2s 太紧,调整 FETCH_TIMEOUT_MS 只是一行 follow-up。本 PR 保留 2s 原值,避免把常量接线的修复与调值混在一起。
  • 未验证 / 范围之外:为什么 fetchInfo() 在某些环境下返回陈旧的 latest 这个更深层问题(代理、企业 .npmrcpackage-json 的 scoped registry 处理)。新增的 debug 日志正是为此提供工具;triage 里草拟的直接调用 registry 的方案是独立的改动。
  • 破坏性变更 / 迁移说明:无。公开导出为新增(UpdateCheckTimeoutError);UpdateCheckResult union 早已含 'error'

关联 Issue

Refs #6857

…chInfo results (QwenLM#6857)

The FETCH_TIMEOUT_MS = 2000 constant in updateCheck.ts was defined but
never wired up. update-notifier's fetchInfo() takes no timeout option,
so slow/unreachable registries (corporate proxies, offline networks,
scoped .npmrc mirrors without auth) would either hang the check or fall
back to whatever update-notifier internally decides — sometimes a stale
configstore cache, reported by users as '/update reports up-to-date on
0.19.9 when 0.19.10 is available.'

Race fetchInfo() against a bounded timer via Promise.race and surface a
new UpdateCheckTimeoutError when it fires, so '/update' returns the
existing 'error' status instead of silently reporting 'up to date.'

Also log the fetchInfo return value under the UPDATE_CHECK debug tag so
the next round of reports can distinguish 'registry returned the wrong
version' from 'we compared incorrectly' without adding more speculation.

Refs QwenLM#6857
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR! (Re-run after new commits)

Template looks good ✓

Problem: Observed bug with evidence. Users report /update saying "up to date" while a newer version exists (referenced in #6857). The FETCH_TIMEOUT_MS constant was dead code — never wired into fetchInfo() — and the failure path was silent. This is a real, user-reported issue.

Direction: Aligned. Making /update reliable and diagnosable is squarely within the CLI's core mission. This is the minimal fix for the plumbing gap — wire the existing timeout, add debug logging — while explicitly leaving the deeper fetchInfo() replacement for a follow-up. CHANGELOG: no direct reference, but update reliability is a recurring user concern.

Size: Not applicable — packages/cli/src/ui/utils/ is not a core module path. 67 production lines + 116 test lines across 2 files.

Approach: The scope is tight and well-scoped. Since the last review, the author added distTag tracking on UpdateCheckTimeoutError — the nightly path fires two concurrent fetches, and now the error message names which dist-tag stalled (for nightly vs for latest). Also added two more test cases covering nightly-specific timeout paths. No scope creep, no drive-by refactors. Moving on to code review. 🔍

中文说明

感谢贡献!(新提交后重新审查)

模板完整 ✓

问题:已观测到的 bug,有证据。用户报告 /update 显示"up to date"但实际有新版本(参见 #6857)。FETCH_TIMEOUT_MS 常量是死代码——从未接入 fetchInfo()——且失败路径是静默的。这是真实的用户报告问题。

方向:对齐。让 /update 可靠且可诊断完全在 CLI 核心使命范围内。这是对管道缺口的最小修复——接入已有超时、添加调试日志——同时明确将更深层的 fetchInfo() 替换留给后续。CHANGELOG 无直接引用,但更新可靠性是用户反复关注的问题。

规模:不适用——packages/cli/src/ui/utils/ 不是核心模块路径。2 个文件,67 行生产代码 + 116 行测试代码。

方案:范围紧凑且聚焦。自上次审查以来,作者在 UpdateCheckTimeoutError 上添加了 distTag 跟踪——nightly 路径同时发起两个并发 fetch,现在错误消息会指出哪个 dist-tag 超时(for nightly vs for latest)。还新增了 2 个覆盖 nightly 特定超时路径的测试用例。无范围蔓延,无顺手重构。进入代码审查 🔍

Qwen Code · qwen3.7-max

Reviewed at 51578836af550d71b67798c66ed52a4a81c7470f · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Code Review (re-run — new commit 5157883 since last review)

Independent proposal: I would wrap fetchInfo() with Promise.race against a bounded timer, throw a named error on timeout, and add debug logging of the return value at each call site. For the nightly path (two concurrent fetches via Promise.all), I'd carry the dist-tag on the error so oncall can tell which endpoint stalled. The existing FETCH_TIMEOUT_MS = 2000 constant was clearly dead code waiting to be wired up.

Comparison: The PR matches this proposal exactly — fetchInfoWithTimeout() helper with Promise.race, UpdateCheckTimeoutError class carrying distTag, finally block for timer cleanup, and debugLogger.debug at both the nightly and latest call sites. The new commit since last review adds exactly the distTag enrichment I would have suggested.

No critical issues found:

  • The Promise.race pattern is correct — Promise.resolve(notifier.fetchInfo()) handles both sync and async returns, and the finally block reliably clears the timer regardless of which race branch wins.
  • UpdateCheckTimeoutError is a proper named Error subclass with this.name set and an optional distTag field — makes it easy to catch by type and diagnose which endpoint stalled downstream.
  • Both call sites (nightly path's Promise.all and latest path's single call) are updated consistently, each passing the appropriate dist-tag.
  • Four timeout tests now cover all paths: latest timeout fires → error, fast fetch → update, nightly-only stalls → error naming nightly, both stall → error for either tag. The vi.advanceTimersByTimeAsync(FETCH_TIMEOUT_MS + 1) correctly exercises the timer.
  • Debug logging uses JSON.stringify on the UpdateInfo return and includes current=${version}, which gives enough context to diagnose "registry returned wrong version" vs "comparison bug" — exactly the diagnostic gap the PR description called out.

Real-Scenario Testing

This is a non-user-visible change (debug logging + failure-path routing). DEV mode (DEV=true) skips the update check entirely, so a before/after CLI comparison can't exercise this code path. The timeout behavior requires blocking network access to registry.npmjs.org, which isn't reproducible in a quick tmux test.

Unit test evidence from tmux session (all suites):

$ npx vitest run src/ui/utils/updateCheck.test.ts 2>&1 | tee /tmp/triage-6887-090932-test.log

 RUN  v3.2.4 packages/cli
      Coverage enabled with v8

 ✓ src/ui/utils/updateCheck.test.ts (19 tests) 15ms

 Test Files  1 passed (1)
      Tests  19 passed (19)
   Start at  09:09:33
   Duration  7.42s (transform 2.01s, setup 72ms, collect 3.01s, tests 15ms, environment 247ms, prepare 88ms)

Related suites also all green: update.test.ts (10/10), update-command.test.ts (15/15), startup-prefetch.test.ts (21/21), handleAutoUpdate.test.ts (24/24) — 89/89 total (up from 87 with 2 new nightly timeout tests).

中文说明

代码审查 (重新审查——自上次审查以来新提交 5157883)

独立方案: 我会用 Promise.racefetchInfo() 与有界定时器竞速,超时时抛出命名错误,并在每个调用点添加调试日志。对于 nightly 路径(通过 Promise.all 并发两个 fetch),我会在错误上携带 dist-tag,以便 oncall 能分辨哪个端点超时。已有的 FETCH_TIMEOUT_MS = 2000 常量明显是等待被接入的死代码。

对比: PR 与此方案完全一致——fetchInfoWithTimeout() 辅助函数用 Promise.raceUpdateCheckTimeoutError 类携带 distTagfinally 块清理定时器,以及在 nightly 和 latest 两个调用点都加了 debugLogger.debug。新提交添加的 distTag 正是我会建议的改进。

未发现关键问题:

  • Promise.race 模式正确——Promise.resolve(notifier.fetchInfo()) 处理同步和异步返回值,finally 块无论哪个分支胜出都可靠地清理定时器
  • UpdateCheckTimeoutError 是带 this.name 和可选 distTag 字段的正式命名 Error 子类——便于下游按类型捕获并诊断哪个端点超时
  • 两个调用点(nightly 路径的 Promise.all 和 latest 路径的单次调用)一致更新,各自传入对应的 dist-tag
  • 四个超时测试覆盖所有路径:latest 超时 → 错误,快速 fetch → 更新,仅 nightly 超时 → 错误命名 nightly,两者都超时 → 错误
  • 调试日志用 JSON.stringify 输出 UpdateInfo 返回值并包含 current=${version},足以诊断"registry 返回错误版本"vs"比较逻辑 bug"

真实场景测试

这是非用户可见的变更(调试日志 + 失败路径路由)。DEV 模式 (DEV=true) 完全跳过更新检查,因此 CLI before/after 对比无法触发此代码路径。超时行为需要阻断到 registry.npmjs.org 的网络访问,在快速 tmux 测试中无法复现。

单元测试证据:19/19 通过(含 4 个超时测试)。相关测试套件全绿:89/89 总计。

Qwen Code · qwen3.7-max

Reviewed at 51578836af550d71b67798c66ed52a4a81c7470f · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — Clean across every stage. Real problem, focused fix, proper tests, clean code.

This PR does exactly what it says: wires a dead constant into the actual code path and adds the debug logging needed to diagnose future reports. The FETCH_TIMEOUT_MS = 2000 was clearly waiting to be used — someone declared it with intent but never connected it — and this PR closes that gap with a minimal Promise.race wrapper and a named error type. The new commit since the last review strengthens the fix further: UpdateCheckTimeoutError now carries distTag, so when the nightly path's two concurrent fetches race, the error message tells oncall exactly which endpoint stalled (for nightly vs for latest). Two additional tests cover those nightly-specific paths.

The scope remains disciplined — two files, 67 production lines, no drive-by refactors, no scope creep. The author explicitly deferred the larger fetchInfo() replacement to a follow-up, which is the right call for keeping this reviewable. Tests are thorough: all four directions of the race are covered (latest timeout, fast fetch, nightly-only timeout, both-timeout), and all 89 tests across 5 related suites pass.

If there's a risk, it's that 2 seconds might be too tight for some corporate proxy environments. But that's a one-line follow-up (FETCH_TIMEOUT_MS = 5000) and not a reason to hold this back. The previous behavior — silently reporting "up to date" on a timeout — was strictly worse.

LGTM, looks ready to ship. ✅

中文说明

置信度:5/5 — 每个阶段都干净。真实问题、聚焦修复、完善测试、干净代码。

这个 PR 完全做到了它承诺的:将一个死常量接入实际代码路径,并添加诊断未来报告所需的调试日志。FETCH_TIMEOUT_MS = 2000 明显在等待被使用——有人带着意图声明了它但从未接入——本 PR 用最小的 Promise.race 包装和命名错误类型关闭了这个缺口。自上次审查以来的新提交进一步加强了修复:UpdateCheckTimeoutError 现在携带 distTag,当 nightly 路径的两个并发 fetch 竞速时,错误消息会精确告诉 oncall 哪个端点超时(for nightly vs for latest)。新增两个测试覆盖这些 nightly 特定路径。

范围依然严格自律——2 个文件、67 行生产代码、无顺手重构、无范围蔓延。作者明确将更大的 fetchInfo() 替换留给了后续 PR,这对保持可审查性是正确的选择。测试充分:race 的所有四个方向都覆盖了(latest 超时、快速 fetch、仅 nightly 超时、两者都超时),5 个相关套件共 89 个测试全部通过。

如果有风险,就是 2 秒对某些企业代理环境可能太紧。但那只是一个一行的后续修复(FETCH_TIMEOUT_MS = 5000),不是阻止本 PR 的理由。之前的行为——超时时静默报"up to date"——严格更差。

可以合入 ✅

Qwen Code · qwen3.7-max

Reviewed at 51578836af550d71b67798c66ed52a4a81c7470f · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

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.

LGTM, looks ready to ship. ✅

@C0d3N1nja97342

Copy link
Copy Markdown
Contributor Author

Heads up on the failing Test (ubuntu-latest, Node 22.x) and web-shell E2E Smoke — the failure is unrelated to this PR. Both jobs abort in Install dependencies because the @qwen-code/web-shell build (run from scripts/prepare.js during npm ci) trips a pre-existing TS2300 duplicate identifier error in packages/web-shell/client/components/ChatEditor.tsx:

client/components/ChatEditor.tsx(21,10): error TS2300: Duplicate identifier 'useWebShellPortalRoot'.
client/components/ChatEditor.tsx(46,10): error TS2300: Duplicate identifier 'useWebShellPortalRoot'.

PR #6872 added a second identical import next to a new call site without noticing the same specifier was already imported earlier in the file. This blocks CI on every open PR against main — none of my updateCheck.ts changes are touched here.

Filed the one-line fix separately in #6890. Once that merges I'll merge main into this branch so CI can actually exercise the diff.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

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.

Reviewed. Suggestions are inline. Not reviewed: chunk 1 — no agent reported covering these; nobody read them.

— qwen3.7-max via Qwen Code /review

Comment on lines +270 to +273
getPackageJson.mockResolvedValue({
name: 'test-package',
version: '1.0.0',
});

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.

[Suggestion] The timeout tests only exercise the non-nightly (single-fetch) path. The nightly branch uses Promise.all with two concurrent fetchInfoWithTimeout calls — a more complex interaction that has no timeout test. If a bug existed in the nightly path's Promise.all + timeout interaction, it would go undetected. — Concrete cost: the nightly Promise.all path is untested for timeout behavior despite being the more complex of the two code paths.

Add a test with version: '1.0.0-nightly.1' where one or both fetchInfo mocks return a never-resolving promise, advance timers past FETCH_TIMEOUT_MS, and assert status: 'error' with UpdateCheckTimeoutError.

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — added in 51578836a. Two new tests cover the nightly Promise.all timeout path:

  • surfaces a timeout when only the nightly dist-tag stalls — mocks nightly as a never-resolving promise and latest as a fast success, advances timers past FETCH_TIMEOUT_MS, and asserts Promise.all propagates the timeout AND the error message names for nightly. This catches both directions of the wiring: that the timer actually reaches inside Promise.all, and that the correct dist-tag gets tagged.
  • surfaces a timeout when both nightly dist-tags stall — full outage; asserts a typed UpdateCheckTimeoutError with either dist-tag on the message (whichever rejection Promise.all sees first is a valid symptom of the same failure).

Comment on lines +27 to +32
export class UpdateCheckTimeoutError extends Error {
constructor(timeoutMs: number) {
super(`update-notifier fetchInfo timed out after ${timeoutMs}ms`);
this.name = 'UpdateCheckTimeoutError';
}
}

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.

[Suggestion] The timeout error message ("fetchInfo timed out after 2000ms") carries no indication of which dist-tag (nightly vs latest) failed. In the nightly path where two fetches run concurrently via Promise.all, a timeout from either produces an identical error. — Concrete cost: when investigating a timeout from logs alone (e.g., corporate proxy blocking only one dist-tag), the oncall engineer cannot tell which registry endpoint was unreachable.

Suggested change
export class UpdateCheckTimeoutError extends Error {
constructor(timeoutMs: number) {
super(`update-notifier fetchInfo timed out after ${timeoutMs}ms`);
this.name = 'UpdateCheckTimeoutError';
}
}
export class UpdateCheckTimeoutError extends Error {
constructor(timeoutMs: number, distTag?: string) {
const tag = distTag ? ` for ${distTag}` : '';
super(`update-notifier fetchInfo timed out after ${timeoutMs}ms${tag}`);
this.name = 'UpdateCheckTimeoutError';
}
}

Then pass the dist-tag from fetchInfoWithTimeout through to the error constructor.

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — applied in 51578836a. UpdateCheckTimeoutError now takes an optional distTag (also exposed as a public field), fetchInfoWithTimeout threads it through, and both call sites pass the tag. Non-nightly path is latest; nightly path passes nightly and latest to the two Promise.all fetches respectively. Error messages now read update-notifier fetchInfo timed out after 2000ms for latest / ... for nightly, so a log reader can tell which endpoint stalled.

@C0d3N1nja97342

Copy link
Copy Markdown
Contributor Author

The upstream duplicate import (#6884) is now fixed via #6890 — merged main into this branch to pick it up. CI should now be able to exercise the actual diff.

… timeout paths

Address bot review on QwenLM#6887:

- `UpdateCheckTimeoutError` now takes an optional `distTag` argument that
  is threaded through by `fetchInfoWithTimeout` and appended to the
  message. The nightly path fires `nightly` and `latest` fetches
  concurrently via `Promise.all`; without a dist-tag on the error, an
  oncall reading logs cannot tell which registry endpoint stalled (e.g.
  a corporate proxy that lets `nightly` through but blocks `latest`).
  The tag also lands on the error instance as a public `distTag` field
  so callers can branch on it programmatically.

- Add two regression tests for the nightly `Promise.all` timeout path:
  a single stalled dist-tag (asserts Promise.all propagates the timeout
  and names the exact tag) and both stalled (full outage — asserts we
  still surface a typed error with a valid tag). The non-nightly test
  now also asserts the message contains `for latest`.
@C0d3N1nja97342

Copy link
Copy Markdown
Contributor Author

@qwen-code /review

@qwen-code-ci-bot qwen-code-ci-bot left a comment

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.

Reviewed. Not reviewed: chunk 1 — no agent reported covering these; nobody read them.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

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.

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification — real tests on macOS (build & run)

Verified @ 51578836a in an isolated git worktree with real vitest (not CI mocks-only). This complements the green CI: the PR's OS table marks 🍏 macOS / 🐧 Linux as ⚠️ (author ran Windows), so this fills the macOS gap and adds a real-runtime before/after the unit tests can't show with fake timers.

Verdict: verified — good to merge. Both halves of the PR do what they claim; the new tests are load-bearing; formatting/lint/types are clean.


1 · All affected suites green — 89/89

Ran the file the PR changes plus the four other suites named in its test plan:

Suite Result
updateCheck.test.ts 19 passed (15 pre-existing + 4 new)
update.test.ts 10 passed
update-command.test.ts 15 passed
startup-prefetch.test.ts 21 passed
handleAutoUpdate.test.ts 24 passed
Total 89 / 89

Minor: the PR description says "17/17 (15 + 2 new)" — the head commit 5157883 added two more nightly-path tests, so it is now 19/19 (15 + 4 new). Worth updating the description; not a blocker.

suites

2 · The new tests are load-bearing (controlled experiment)

Reverted only the timeout wiring (fetchInfoWithTimeout(...) → bare createNotifier('latest').fetchInfo()), keeping UpdateCheckTimeoutError exported so imports still resolve. The three never-resolving tests then hang for 8s and fail — reproducing the exact silent-hang from #6857 — while the healthy-path test and all 15 pre-existing tests stay green. Restoring the fix → back to 19/19. So the tests genuinely exercise the timer, they don't just pass trivially.

load-bearing

3 · Real-runtime before/after + the new debug line

The unit tests use fake timers, so I drove the real checkForUpdatesDetailed() against the real event loop and measured wall-clock:

Scenario Result
Unreachable registry (fetchInfo never resolves) cut off at 2002 mserror (UpdateCheckTimeoutError)
Slow fetch that would resolve at 5 s cut off at 2001 mserror (not awaited)
Healthy fetch (50 ms) returns at 52 msupdate (timer never fires early)
main (pre-fix), same input still hanging after 4 s, never settled

Second change confirmed too: with a bound debug session I read the on-disk log and saw the new line —
[DEBUG] [UPDATE_CHECK] fetchInfo returned {"current":"1.0.0","latest":"1.0.0"} for current=1.0.0.

realtimer + debug

4 · Hygiene

prettier --check ✓ · eslint ✓ · the two PR files are type-clean (the only tsc output is two unrelated TS6305 build-staleness notes on core/dist/.../toml-to-markdown-converter, an artifact of the un-built worktree — nothing in the diff).

Notes for the merge decision

  • Intended behavior change: a slow-but-reachable registry that takes >2 s now reports error instead of hanging. That's the point of the fix; if 2 s proves too tight in the field, FETCH_TIMEOUT_MS is a one-line tune (the author already flagged this).
  • Promise.race + finally { clearTimeout } leaves no dangling timers on either branch; the nightly Promise.all path propagates the first timeout correctly (both concurrent-stall cases covered).

Environment: macOS (darwin 24.6.0) · Node 22.23.1 · vitest 3.2.4 · isolated worktree @ 51578836a.

🇨🇳 中文说明(点击展开)

✅ 本地验证 —— macOS 上真实构建并运行测试

隔离的 git worktree 中对 @ 51578836a 用真实 vitest 做了验证(不是只跑 CI 的 mock)。这是对绿色 CI 的补充:PR 的系统表把 🍏 macOS / 🐧 Linux 标为 ⚠️(作者只在 Windows 上跑过),本次验证补上了 macOS 这一块,并给出了单元测试用 fake timer 无法展示的真实运行时 before/after

结论:验证通过,可以合并。 PR 的两部分改动都名副其实;新增测试确实是"承重"的;格式 / lint / 类型均干净。

1 · 所有相关测试集全绿 —— 89/89

跑了 PR 改动的文件,以及它测试计划里点名的另外四个测试集:

测试集 结果
updateCheck.test.ts 19 通过(15 原有 + 4 新增
update.test.ts 10 通过
update-command.test.ts 15 通过
startup-prefetch.test.ts 21 通过
handleAutoUpdate.test.ts 24 通过
合计 89 / 89

小提示: PR 描述写的是 "17/17(15 + 2 新增)" —— head commit 5157883 又加了两个 nightly 路径的测试,现在是 19/19(15 + 4 新增)。建议更新一下描述,不影响合并。

2 · 新增测试是"承重"的(对照实验)

回退超时接线(把 fetchInfoWithTimeout(...) 改回裸的 createNotifier('latest').fetchInfo()),保留 UpdateCheckTimeoutError 的导出以便 import 仍能解析。此时三个"永不 resolve"的测试会挂起 8 秒并失败——正好复现 #6857 的静默 hang——而健康路径测试和全部 15 个原有测试保持绿色。恢复修复后又回到 19/19。这证明这些测试确实在检验计时器逻辑,而不是碰巧通过。

3 · 真实运行时 before/after + 新增 debug 日志

单元测试用的是 fake timer,所以我用真实事件循环驱动了真实的 checkForUpdatesDetailed(),并测量实际墙钟时间:

场景 结果
不可达 registry(fetchInfo 永不 resolve) 2002 ms 被切断 → errorUpdateCheckTimeoutError
本会在 5 s 才 resolve 的慢请求 2001 ms 被切断 → error(不再干等)
健康请求(50 ms) 52 ms 返回 → update(计时器不会误触)
main(修复前),同样输入 4 秒后仍在挂起,从未 settle

第二处改动也已确认:绑定一个 debug session 后,我从磁盘上的日志读到了新增的那一行——
[DEBUG] [UPDATE_CHECK] fetchInfo returned {"current":"1.0.0","latest":"1.0.0"} for current=1.0.0

4 · 规范检查

prettier --check ✓ · eslint ✓ · PR 的两个文件类型干净tsc 唯一的输出是两条与本 PR 无关的 TS6305 构建陈旧告警,位于 core/dist/.../toml-to-markdown-converter,是 worktree 未构建的产物,diff 里没有任何相关内容)。

合并决策备注

  • 刻意的行为变更: 一个可达但耗时 >2 s 的 registry 现在会报 error 而不是干等。这正是本修复的目的;若实测发现 2 s 太紧,FETCH_TIMEOUT_MS 只是一行的可调项(作者已提前说明)。
  • Promise.race + finally { clearTimeout } 在两条分支上都不会留下悬挂计时器;nightly 的 Promise.all 路径能正确传播首个超时(两个并发挂起的用例都已覆盖)。

环境: macOS(darwin 24.6.0)· Node 22.23.1 · vitest 3.2.4 · 隔离 worktree @ 51578836a

Verification performed locally by the maintainer; screenshots are real terminal output rendered for readability.

@wenshao
wenshao added this pull request to the merge queue Jul 15, 2026
Merged via the queue into QwenLM:main with commit 0957e18 Jul 15, 2026
91 of 92 checks passed
RehDen pushed a commit to RehDen/qwen-code that referenced this pull request Jul 21, 2026
…raise timeout to 5s (QwenLM#7409)

* fix(cli): soften update-check failure UX and raise timeout budget (QwenLM#7049)

The startup update check now surfaces failures as a soft warning with the
concrete failure reason, instead of an alarming red error:

- Raise FETCH_TIMEOUT_MS from 2s to 5s, matching comparable CLIs, so slow
  mirrors and corporate proxies stop tripping the check.
- Emit startup check failures on 'update-failed' with severity 'warning';
  setUpdateHandler renders them as a yellow warning. Actual update install
  failures keep error severity.
- Add classifyUpdateCheckError/describeUpdateCheckFailure so messages say
  what happened - timeout, unreachable registry, or registry error, e.g.
  "Update check skipped (registry unreachable) - run /update to retry."
- /update keeps loud error styling but now includes the failure reason.
- Update en/zh/zh-TW locales; drop the now-unused generic failure key.

This is not a revert of QwenLM#6857/QwenLM#6887 - the loud-failure direction stays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): translate new update-check failure keys for remaining locales

Review follow-up: the previous commit removed the old generic failure key
from ca/de/fr/ja/pt/ru but only added the new parameterized keys to
en/zh/zh-TW, leaving those six locales falling back to English. Add the
translations, reusing each locale's existing registry/update terminology.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: ComplexSimply <rudy.arrowsong@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants