Skip to content

test(cli): pin the model selection across multi-provider template updates - #8879

Merged
wenshao merged 5 commits into
QwenLM:mainfrom
ComplexSimply:test/batch-template-update-selection-8863
Aug 12, 2026
Merged

test(cli): pin the model selection across multi-provider template updates#8879
wenshao merged 5 commits into
QwenLM:mainfrom
ComplexSimply:test/batch-template-update-selection-8863

Conversation

@ComplexSimply

Copy link
Copy Markdown
Collaborator

What this PR does

Adds one regression test pinning the multi-provider batch path of the #8863 fix: two providers with stale templates, a current model owned by neither, a single Update all confirmation — and no model.name / model.baseUrl writes from either pass of the update loop.

Why it's needed

The worst case reported in #8863 was the batch path: onConfirm loops executeUpdate over every pending provider, and before #8868 each iteration rewrote model.name in turn — the last provider in ALL_PROVIDERS order won, regardless of the user's intent (observed as token-plan + grok both stale ending on grok-4.5). The #8868 gate protects this, but its regression tests cover a single updating provider; if the batch behavior regressed, nothing failed. Follow-up promised in #8873 (a parallel fix for #8863 superseded by #8868).

Reviewer Test Plan

How to verify

  • vitest run src/ui/hooks/useProviderUpdates.test.ts in packages/cli — 21/21.
  • Mutation check: reverting the fix(cli): keep the selected model when refreshing another provider's template #8868 gate to its pre-fix form (if (previousModelStillAvailable) without !updatesActiveProvider ||) fails this test alongside the two single-provider does not move the user off a model … tests — the batch scenario is the added dimension.

Evidence (Before & After)

N/A (test-only).

Tested on

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

Environment (optional)

N/A (unit tests only).

Risk & Scope

  • Main risk or tradeoff: none — no production code changes.
  • Not validated / out of scope: nothing beyond the useProviderUpdates suite.
  • Breaking changes / migration notes: none.

Linked Issues

Refs #8863, #8868, #8873.

中文说明

本 PR 做了什么

#8863 修复的多 provider 批量路径补一个回归测试:两个 provider 模板同时过期、当前模型不属于其中任何一个、一次 Update all 确认——更新循环的两轮都不得写 model.name / model.baseUrl

为什么需要

#8863 报告的最坏情形正是批量路径:onConfirm 对每个待更新 provider 循环执行 executeUpdate#8868 之前每轮都会改写一次 model.nameALL_PROVIDERS 注册表顺序最后的胜出(实测 token-plan + grok 同时过期最终落在 grok-4.5)。#8868 的门控保护了它,但其回归测试只覆盖单 provider 更新;批量行为若回归不会有测试失败。这是在 #8873(与 #8868 平行的 #8863 修复,已被取代关闭)中承诺的后续。

验证方式

风险与范围

  • 主要风险:无——不含生产代码改动。
  • 未验证/范围外:useProviderUpdates 套件之外无。
  • 破坏性变更:无。

关联 Issue

参考 #8863#8868#8873

…ates

The worst case reported in QwenLM#8863 was the batch path: with several
providers updating in one confirmation, each executeUpdate in the loop
rewrote model.name in turn and the last provider in registry order won.
The QwenLM#8868 gate protects it, but no test failed if that regressed — its
regression tests cover a single updating provider. Pin the batch
scenario: two stale providers, a current model owned by neither, one
'Update all' confirmation, and no model.name/model.baseUrl writes from
either pass. Reverting the gate to the pre-QwenLM#8868 form now fails this
test alongside the two single-provider ones.
@ComplexSimply

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 10, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR — re-running triage on the new head, which adapted the tests to #8889.

Template looks good ✓

Problem: real and verified. The #8863 worst case was the multi-provider batch path — onConfirm loops executeUpdate over every pending provider, and each pass used to rewrite model.name in turn. I confirmed the coverage gap still exists on current main: the existing batch test (updates both provider metadata keys from a batched prompt) only pins the metadata-version writes, and every model-selection invariant test (leaves the model selection alone when the previous model is gone, does not move the user off a model …) is single-provider. Nothing exercises "a batched update must not touch the selection."

Direction: aligned — a regression pin for an already-merged fix chain (#8868#8889), test-only.

Size: not applicable — 0 production lines. The whole diff is 137 added lines in one *.test.ts file, which the core-module size gate excludes entirely.

Approach: scope is right. Two tests covering the two batch axes — an all-inactive batch, and a mixed batch containing the active provider — each pinning the batch shape (entries.length === 2 asserted before confirming) and each carrying a positive completion guard so neither can pass vacuously. The second test was correctly inverted after #8889 to pin the new invariant's batch-loop side (no entry may write model.name, including the active provider's). Nothing to cut.

Risk: no elevated risk signals — the only changed file is a test, and high-risk-path detection excludes test files.

Moving on to code review. 🔍

中文说明

感谢贡献——新 head(已适配 #8889)重新执行 triage。

模板完整 ✓

问题:真实存在且已核实。#8863 的最坏情形是多 provider 批量路径——onConfirm 对每个待更新 provider 循环执行 executeUpdate,修复前每轮都会改写一次 model.name。我已在当前 main 上确认覆盖缺口仍在:现有批量测试(updates both provider metadata keys from a batched prompt)只钉住元数据版本写入,而所有模型选择不变量测试(leaves the model selection alone when the previous model is gonedoes not move the user off a model …)都是单 provider 场景。目前没有任何测试覆盖"批量更新不得触碰所选模型"。

方向:对齐——为已合并的修复链(#8868#8889)补回归钉桩,纯测试改动。

规模:不适用——0 行生产代码。整个 diff 是一个 *.test.ts 文件中新增的 137 行,核心模块规模门控完全排除测试文件。

方案:范围合理。两个测试覆盖批量的两个轴——全非活跃批量、以及包含活跃 provider 的混合批量——各自显式钉住批量形态(确认前断言 entries.length === 2),且各自带正向完成守卫,不会空转通过。第二个测试在 #8889 之后已正确反转为钉住新不变量的批量侧(任何条目都不得写 model.name,包括活跃 provider 的条目)。没有可砍的部分。

风险:无升级风险信号——唯一改动文件是测试,高风险路径检测排除测试文件。

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Code review

My independent proposal for "pin the batch path after #8889", before reading the diff: two tests in the existing suite — one batch where no provider is active (assert nothing writes model.name / model.baseUrl, with positive guards proving both entries actually ran), and one mixed batch where the first entry IS the active provider, since that is exactly where the unconditional #8889 delete bites differently from a per-provider gate. Pin the batch shape explicitly before confirming.

The PR does exactly this. What I verified statically (worktree on main at b77c2af; the PR head's useProviderUpdates.ts is untouched — the diff is 1 file, +137/−0, test-only):

No critical blockers, no convention violations.

Testing evidence

Unattended run — PR code is never executed here; the evidence below is the PR's own CI read via the API plus the static trace above. The PR's CI (Qwen Code CI) is green on the reviewed commit: Test (ubuntu-latest, Node 22.x), the unit suite carrying this file, passed. macOS/Windows test legs and the CLI integration job show skipped (path-filtering for a single test file under packages/cli), not failed; nothing red on this commit. Real-scenario (tmux) coverage: N/A — test-only change with nothing user-visible.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
route ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Mutation check: not executed here — PR code is never run in this environment. The kill argument above is a deterministic static trace of install.ts; independently, the maintainer's local verification report (wenshao, above in this thread — maintainer evidence, not the author's claim) ran an empirical mutation matrix on this exact head and found the one mutant main's suite is blind to is caught precisely by these two added tests. A sandboxed verify run triggered with this triage pass will post its own report when it completes.

中文说明

代码审查

读 diff 之前,我对"#8889 之后钉住批量路径"的独立方案是:在现有套件中加两个测试——一个所有 provider 均非活跃的批量(断言没有任何写入触碰 model.name / model.baseUrl,并用正向守卫证明两个条目确实执行),以及一个第一个条目恰为活跃 provider 的混合批量——因为无条件 #8889 删除与逐 provider 门控的行为差异正好体现在这里。确认前显式钉住批量形态。

PR 正是这样做的。静态核实(worktree 位于 mainb77c2af;PR head 的 useProviderUpdates.ts 未被改动——diff 为 1 个文件,+137/−0,纯测试):

无关键阻塞项,无规范违规。

测试证据

无人值守运行——本环境从不执行 PR 代码;以下证据来自 API 读取的 PR 自身 CI 结果及上述静态追溯。PR 自身 CI(Qwen Code CI)在被审查提交上全绿:承载该文件的单测 Test (ubuntu-latest, Node 22.x) 通过。macOS/Windows 测试腿与 CLI 集成任务显示 skippedpackages/cli 单个测试文件的路径过滤)而非失败;该提交无任何红色。真实场景(tmux)覆盖:不适用——纯测试改动、无用户可见变化。

变异检查:未在此执行——本环境从不运行 PR 代码。上面的杀死论证是对 install.ts 的确定性静态追溯;另有维护者的本地验证报告(wenshao,见本线程上方——维护者证据而非作者声明)在该 head 上运行了实证变异矩阵,发现 main 套件完全看不见的那个变异体恰由这两个新增测试捕获。本次 triage 触发的沙箱 verify 运行完成后会自行发布报告。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — clean across every stage on the new head: a verified coverage gap, minimal idiomatic tests, green CI, and the pin substantiated in both directions.

Stepping back: this is the follow-through the #8863#8868 chain promised, now correctly adapted to #8889's unconditional drop. The two tests sit exactly at the seam where coverage was missing — the batch loop — and each kills a mutant the other doesn't: removing the protection entirely (every pass rewrites model.name, last provider in registry order wins — the original incident) versus reintroducing active-provider migration inside the loop (pre-#8889 semantics). Both pin the batch shape before confirming and carry positive completion guards, so neither can pass vacuously. 137 lines, no new scaffolding, no drive-by edits — in six months this reads as free insurance. The maintainer's independent local verification (31/31 on macOS plus a mutation matrix showing the batch-only mutant is invisible to main's suite but caught here) closes what the static trace left open.

One standing nit, non-blocking and already flagged by wenshao: the PR body still says "one regression test" / "21/21" and its mutation instructions describe the pre-#8889 gate — worth refreshing before merge, but hygiene doesn't gate approval.

CI is fully green on the reviewed commit, so approving now, pinned to that commit.

中文说明

回顾全局:这是 #8863#8868 链条承诺的后续,现已正确适配 #8889 的无条件丢弃。两个测试恰好落在覆盖缺失的接缝——批量循环——上,且各自杀死对方杀不掉的变异体:完全移除保护(每轮都改写 model.name、注册表顺序最后的 provider 胜出——原始事故)与在循环内重新引入活跃 provider 迁移(#8889 之前的语义)。两者都在确认前钉住批量形态,且都带正向完成守卫,不会空转通过。137 行、无新脚手架、无顺手改动——六个月后回看是零成本的保险。维护者的独立本地验证(macOS 31/31,变异矩阵显示批量专属变异体对 main 套件完全不可见、却被这两个测试捕获)补上了静态追溯未覆盖的部分。

一个长期存在的小问题,不阻塞且 wenshao 已指出:PR 正文仍写"一个回归测试"/"21/21",其变异操作说明描述的也是 #8889 之前的门控——合并前值得刷新,但规范问题不影响批准。

被审查提交上 CI 全绿,现在批准,钉在该提交。

Qwen Code · qwen3.8-max

Reviewed at f36e9e044f325a09eebfb262bd47a2d973934a03 · 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 — CI landed green after the 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 — no blockers. Suggestions are inline. Test Plan (not a blocker): src/ui/hooks/useProviderUpdates.test.tsno such file or directory.

中文说明

已审查——无阻断问题。 建议见行内评论。 Test Plan(非阻断):src/ui/hooks/useProviderUpdates.test.tsno such file or directory

— qwen3.8-max via Qwen Code /review (v0.21.8)

Comment on lines +613 to +615
await waitFor(() => {
expect(mockSettings.setValue).toHaveBeenCalled();
});

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 batch-confirm regression test asserts only negatives after onConfirm('update') — it never verifies that both providers' updates actually executed. This waitFor is satisfied by the first provider's metadata writes alone, and every assertion after it is negative, so a batch loop that drops the second provider still passes. — Failure scenario: a future change that breaks the onConfirm('update') loop after the first provider (early break/return, an off-by-one, or a second executeUpdate that throws — executeUpdate catches every error and the loop ignores its return value) leaves the token-plan template stale; findAllPendingUpdates then re-surfaces the update prompt on every startup while all 21 tests, including this dedicated batch regression test, stay green. Mutation-verified: truncating the loop to pendingList.slice(0, 1) leaves 21/21 green; the assertions below kill that mutant and stay green without it.

Suggested change
await waitFor(() => {
expect(mockSettings.setValue).toHaveBeenCalled();
});
await waitFor(() => {
expect(mockSettings.setValue).toHaveBeenCalledWith(
expect.anything(),
`${PROVIDER_METADATA_NS}.${METADATA_KEY}.version`,
chinaVersion,
);
expect(mockSettings.setValue).toHaveBeenCalledWith(
expect.anything(),
`${PROVIDER_METADATA_NS}.${TOKEN_METADATA_KEY}.version`,
tokenVersion,
);
});
中文说明

[建议] 批量确认回归测试在 onConfirm('update') 之后只断言了否定条件——从未验证两个 provider 的更新是否都真正执行。这个 waitFor 仅凭第一个 provider 的元数据写入即可通过,而其后的断言又全是否定形式,因此即便批量循环丢掉了第二个 provider,测试依然会通过。— 失败场景:未来某个改动在第一个 provider 之后中断 onConfirm('update') 循环(提前 break/return、差一错误,或第二个 executeUpdate 抛异常——executeUpdate 会捕获所有错误,且循环忽略其返回值),token-plan 模板将保持过期,findAllPendingUpdates 会在每次启动时重新弹出更新提示,而包括这个专门的批量回归测试在内的全部 21 个测试仍然全绿。已通过变异验证:把循环截断为 pendingList.slice(0, 1) 后 21/21 仍全绿;下方建议的断言能杀死该变异,且自身保持绿色。

— qwen3.8-max via Qwen Code /review (v0.21.8)

Comment on lines +609 to +611
expect(result.current.providerUpdateRequest!.entries.length).toBe(2);

await result.current.providerUpdateRequest!.onConfirm('update');

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 mixed-batch scenario has zero coverage: no test confirms 'update' on a batch mixing the ACTIVE provider (whose update removes the current model) with an inactive one. This new test covers two entries that are both inactive, and the gate's keep-modelSelection branch (updatesActiveProvider && !previousModelStillAvailable) is exercised only by the single-provider 'switches model when previous model is no longer available' test. — Failure scenario: a refactor that derives per-provider state once per batch — hoisting updatesActiveProvider/previousModel out of executeUpdate into the onConfirm loop, or applying the first entry's gate decision to all entries — lets an inactive co-batched provider keep its modelSelection and rewrite model.name after the active provider's intended migration, reproducing the exact #8863 issue symptom while all 21 tests stay green. Mutation-verified: with the gate hoisted, the suite stays 21/21 green and a mixed-batch probe test fails (the inactive entry writes model.name a second time). Suggested fix: add one mixed-batch test — both metadata keys stale; modelProviders = chinaTemplate + tokenTemplate; active credentials = Coding Plan (the default mock config); getModel → a model id absent from both templates; confirm 'update'; then assert model.name was written exactly once (the fallback), syncAfterAuthRefresh was called exactly once, and no second model.name/model.baseUrl write followed from the inactive entry.

中文说明

[建议] 混合批量场景完全没有覆盖:没有任何测试对一个「包含活跃 provider(其更新会移除当前模型)与非活跃 provider」的批量确认 'update' 进行验证。本新测试覆盖的两个条目均不活跃,而门控的保留 modelSelection 分支(updatesActiveProvider && !previousModelStillAvailable)此前仅由单 provider 的 'switches model when previous model is no longer available' 测试覆盖。— 失败场景:未来某个重构把逐 provider 的状态改为按批量推导一次——例如把 updatesActiveProvider/previousModelexecuteUpdate 提升到 onConfirm 循环中,或把第一个条目的门控决定套用到所有条目——就会让同批次中的非活跃 provider 保留其 modelSelection,在活跃 provider 完成预期迁移之后再次改写 model.name,从而重现 #8863 的确切症状,而全部 21 个测试仍然全绿。已通过变异验证:门控被提升后套件仍为 21/21 全绿,而混合批量探针测试会失败(非活跃条目第二次写入了 model.name)。建议修复:新增一个混合批量测试——两个元数据键均过期;modelProviders = chinaTemplate + tokenTemplate;活跃凭据 = Coding Plan(默认 mock 配置);getModel → 一个在两个模板中都不存在的模型 id;确认 'update';然后断言 model.name 恰好被写入一次(回退模型)、syncAfterAuthRefresh 恰好被调用一次,且非活跃条目没有引发第二次 model.name/model.baseUrl 写入。

— qwen3.8-max via Qwen Code /review (v0.21.8)

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

⚠️ AutoFix round 4 ended without publishing a reportview run.

中文说明

⚠️ AutoFix 第 4 轮结束但未发布报告 —— 查看运行

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (idle-timeout (no output for 1200000ms — the sandbox likely hung at startup)) (attempt 1/100) — it will retry on the next scan.

⚠️ This change was NOT pushed — any commit referenced below was made only in the runner workspace and has been discarded. What the agent reported:
Qwen failed during address-review: idle-timeout (no output for 1200000ms — the sandbox likely hung at startup).

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31399630023


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

qwen-code-dev-bot and others added 2 commits August 10, 2026 15:24
…pdate-selection-8863

# Conflicts:
#	packages/cli/src/ui/hooks/useProviderUpdates.test.ts
…gate

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round summary — PR #8879

Feedback points and decisions

  • [Suggestion] R1-1 (batch-confirm test asserts only negatives)Implemented. Replaced the weak waitFor(() => expect(mockSettings.setValue).toHaveBeenCalled()) in 'leaves the model selection alone across a multi-provider batch update' with positive assertions that both providers' providerMetadata.<key>.version writes happened (chinaVersion and tokenVersion). The waitFor is now only satisfied when both entries of the batch actually executed, so the reported loop-truncation mutant (pendingList.slice(0, 1)) fails the test instead of passing it.
  • [Suggestion] R1-2 (no mixed-batch coverage)Implemented. Added 'migrates the model only for the active provider in a mixed batch': both metadata keys stale, modelProviders = Coding Plan + Token Plan templates, active credentials = Coding Plan (the default mock config), current model absent from both templates, confirm 'update'. It waits for two reloadModelProvidersConfig calls (both entries complete, order-independent), then asserts model.name was written exactly once (the active provider's fallback model), model.baseUrl exactly once, and syncAfterAuthRefresh exactly once with the fallback model. If the per-provider gate were hoisted into the batch loop, the inactive entry would write model.name a second time and this test would fail — killing the described mutant.
  • Review note (not a blocker): Test Plan path src/ui/hooks/useProviderUpdates.test.ts not foundNo code change needed. The file lives at packages/cli/src/ui/hooks/useProviderUpdates.test.ts; the path in the note is missing the packages/cli/ prefix. All 28 tests in that file pass.
  • Budget warning (previous round ran out of time) — acknowledged. Both remaining suggestions were small, test-only, and inside this PR's exact scope (the PR only extends this test file), so the full batch was addressed this round rather than deferred; nothing was declined.

Conflict resolution (--conflict true)

Merged origin/main (merge commit e7bd8181a9). One content conflict in packages/cli/src/ui/hooks/useProviderUpdates.test.ts: this branch inserted the new batch regression test immediately before the old "later" test, while origin/main (#8829) renamed that test to 'persists a cooldown (not a full update) when user chooses "later"', rewrote its body, and added six "later"-cooldown tests at the same location. Resolution keeps both sides: the PR's batch test stays, followed by main's renamed cooldown test and its new tests. No changes were made to useProviderUpdates.ts (main's version taken as-is).

Changes

  • packages/cli/src/ui/hooks/useProviderUpdates.test.ts — strengthened the batch waitFor (R1-1), added the mixed-batch test (R1-2). Test-only; no production code changed. Commit c6da0275e2.

Verification

  • git merge origin/main — one conflict resolved, committed as e7bd8181a9
  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx vitest run src/ui/hooks/useProviderUpdates.test.ts (in packages/cli) — 28/28 passed
  • npm run test --workspace packages/cli --if-present -- --changed origin/main --passWithNoTests — 1 file, 28/28 passed
  • npx prettier --check packages/cli/src/ui/hooks/useProviderUpdates.test.ts — clean

Not run: integration tests (change is unit-test-only and exercised directly by Vitest, not through the bundled CLI), npm run generate:settings-schema (no settings source changed).

中文说明

Autofix 审查轮次总结 — PR #8879

反馈点及处理决定

  • [建议] R1-1(批量确认测试只断言了否定条件)已实现。'leaves the model selection alone across a multi-provider batch update' 中薄弱的 waitFor(() => expect(mockSettings.setValue).toHaveBeenCalled()) 替换为正向断言:两个 provider 的 providerMetadata.<key>.version 写入均已发生(chinaVersiontokenVersion)。该 waitFor 现在只有在批量的两个条目都真正执行后才会通过,因此报告中提到的循环截断变异(pendingList.slice(0, 1))会使测试失败,而不再被放过。
  • [建议] R1-2(混合批量场景无覆盖)已实现。 新增 'migrates the model only for the active provider in a mixed batch':两个元数据键均过期,modelProviders = Coding Plan + Token Plan 模板,活跃凭据 = Coding Plan(默认 mock 配置),当前模型在两个模板中均不存在,确认 'update'。测试等待 reloadModelProvidersConfig 被调用两次(两个条目均完成,与顺序无关),然后断言 model.name 恰好写入一次(活跃 provider 的回退模型)、model.baseUrl 恰好一次、syncAfterAuthRefresh 恰好一次且参数为该回退模型。如果逐 provider 的门控被提升到批量循环中,非活跃条目会第二次写入 model.name,该测试将失败——杀死所述变异。
  • 审查备注(非阻断):Test Plan 路径 src/ui/hooks/useProviderUpdates.test.ts 未找到无需代码改动。 该文件实际位于 packages/cli/src/ui/hooks/useProviderUpdates.test.ts;备注中的路径缺少 packages/cli/ 前缀。该文件全部 28 个测试均通过。
  • 预算警告(上一轮超时) — 已知悉。剩余的两条建议都很小、纯测试改动,且完全落在本 PR 的范围内(本 PR 只扩展该测试文件),因此本轮一次性处理了整批反馈,没有搁置项。

冲突解决(--conflict true

已合并 origin/main(合并提交 e7bd8181a9)。packages/cli/src/ui/hooks/useProviderUpdates.test.ts 出现一处内容冲突:本分支在旧的 "later" 测试之前插入了新的批量回归测试,而 origin/main#8829)将该测试重命名为 'persists a cooldown (not a full update) when user chooses "later"'、重写了其主体,并在同一位置新增了六个 "later" 冷却测试。解决方式为两边都保留:PR 的批量测试保留在前,随后是 main 重命名后的冷却测试及其新增测试。useProviderUpdates.ts 未做任何改动(原样采用 main 的版本)。

变更内容

  • packages/cli/src/ui/hooks/useProviderUpdates.test.ts — 强化批量 waitFor(R1-1),新增混合批量测试(R1-2)。纯测试改动;未改动任何生产代码。提交 c6da0275e2

验证

  • git merge origin/main — 解决一处冲突,提交为 e7bd8181a9
  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npx vitest run src/ui/hooks/useProviderUpdates.test.ts(位于 packages/cli)— 28/28 通过
  • npm run test --workspace packages/cli --if-present -- --changed origin/main --passWithNoTests — 1 个文件,28/28 通过
  • npx prettier --check packages/cli/src/ui/hooks/useProviderUpdates.test.ts — 干净

未运行:集成测试(本次改动为纯单元测试,由 Vitest 直接覆盖,不经过打包后的 CLI)、npm run generate:settings-schema(未改动任何 settings 源)。

Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@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: reverse audit — reached the 5-round cap without two consecutive dry rounds (rounds 3, 4 and 5 each reported new findings).

Not explored to full depth (tool budget reached): You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget..

Test Plan (not a blocker): src/ui/hooks/useProviderUpdates.test.tsno such file or directory.

中文说明

已审查。 建议见行内评论。

未审查:reverse audit — reached the 5-round cap without two consecutive dry rounds (rounds 3, 4 and 5 each reported new findings)。

未探索到全部深度(达到工具调用预算):You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.

Test Plan(非阻断):src/ui/hooks/useProviderUpdates.test.tsno such file or directory

— qwen3.8-max via Qwen Code /review (v0.21.9)

expect(mockModelsConfig.syncAfterAuthRefresh).not.toHaveBeenCalled();
});

it('migrates the model only for the active provider in a mixed batch', async () => {

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 mixed-batch test only exercises the one registry order where the active provider is first: ALL_PROVIDERS iterates coding-plan before token-plan and the default mock credentials point at coding-plan, so the migration winner is also the batch's first entry — the test cannot distinguish gating by credentials from gating by batch position. — Failure scenario: a refactor that grants migration rights by batch position (treating the first pending entry as the migrator whenever any entry matches credentials) passes both new tests unchanged yet reintroduces the exact #8863 symptom for a user whose active provider is later in registry order. Probe-verified: such a mutant passes this whole file, and the variant below fails it (observed model.name write of qwen3.5-plus instead of the token template's model). Suggested fix: add one variant of this test where getContentGeneratorConfig returns the Token Plan credentials (active provider second in ALL_PROVIDERS), asserting the single model.name write is tokenTemplate[0]!.id.

中文说明

[建议] 混合批量测试只演练了活跃 provider 恰好排在注册表第一位的这一种顺序:ALL_PROVIDERS 中 coding-plan 在 token-plan 之前,而默认 mock 凭据指向 coding-plan,因此迁移的胜出者同时也是批量的第一个条目——测试无法区分「按凭据门控」与「按批量位置门控」。— 失败场景:未来某个重构按批量位置授予迁移权(只要任一条目匹配凭据就把第一个待更新条目当作迁移者),两个新测试仍会全绿,但对活跃 provider 在注册表中靠后的用户会重现 #8863 的确切症状。已通过探针验证:该变异通过整个测试文件,而下面建议的变体会使其失败(观察到写入的 model.nameqwen3.5-plus 而非 token 模板的模型)。建议修复:为本测试增加一个变体,getContentGeneratorConfig 返回 Token Plan 凭据(活跃 provider 在 ALL_PROVIDERS 中排第二),断言唯一的 model.name 写入是 tokenTemplate[0]!.id

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +658 to +660
mockSettings.merged['modelProviders'] = {
[AuthType.USE_OPENAI]: [...chinaTemplate, ...tokenTemplate],
};

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] Neither batch test can detect a regression where a later entry's modelProviders patch is computed from the pre-batch store and silently reverts the earlier entry's merged model list: mockSettings.setValue is a bare vi.fn() that never writes back into mockSettings.merged — real LoadedSettings recomputes merged on every write — so entry 2's patch is always composed against the pre-batch list here, and no assertion inspects any list write or reload payload. — Failure scenario: probe-verified — a snapshot-before-loop regression passes all 28 tests: entry 2's final modelProviders.USE_OPENAI write reverts entry 1's added model while both version writes bump, after which findAllPendingUpdates never re-prompts and the user is silently stuck on the stale list. Suggested fix: make the mock apply modelProviders.* writes into mockSettings.merged (mirroring production write-through), then assert the final modelProviders.USE_OPENAI write contains ids from both chinaTemplate and tokenTemplate.

中文说明

[建议] 两个批量测试都无法发现这样的回归:后一个条目的 modelProviders 补丁基于批量开始前的存储计算,从而悄悄回滚前一个条目合并后的模型列表。mockSettings.setValue 是一个裸 vi.fn(),从不写回 mockSettings.merged——而真实的 LoadedSettings 每次写入都会重算 merged——因此这里条目 2 的补丁始终基于批量开始前的列表合成,且没有任何断言检查列表写入或 reload 载荷。— 失败场景:已通过探针验证——「循环前快照一次」的回归通过全部 28 个测试:条目 2 最终的 modelProviders.USE_OPENAI 写入回滚了条目 1 新增的模型,而两个 version 写入照常推进,之后 findAllPendingUpdates 永远不会再次提示,用户被静默卡在过期列表上。建议修复:让 mock 把 modelProviders.* 写入应用到 mockSettings.merged(模拟生产环境的写穿透),然后断言最终的 modelProviders.USE_OPENAI 写入同时包含 chinaTemplatetokenTemplate 的 id。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +661 to +664
// Default mock credentials point at Coding Plan, so only the first entry
// is the active provider. The current model exists in neither template,
// so the active entry must migrate the selection to its fallback model.
mockConfig.getModel.mockReturnValue('removed-model');

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] Because every test's current model exists in neither template, a batch-hoisted availability mutant survives both new tests — the per-provider availability computation is not actually pinned. — Failure scenario: probe-verified — feeding the gate a pendingList.some(...) union check keeps both tests green. The discriminating input is a mixed batch whose current model is offered only by the inactive entry (a token-plan-only model id with Coding Plan active — reachable, since the model picker enumerates across authTypes): correct per-provider code migrates once to chinaTemplate[0]!.id; the union mutant computes "some batch entry offers it", deletes modelSelection and writes nothing — leaving the selection on a model the active provider doesn't offer, the #8863 symptom class. Suggested fix: add one mixed-batch case with such a model id, asserting the same single migration.

中文说明

[建议] 由于每个测试的当前模型都不存在于任何模板中,「把可用性检查提升到批量层级」的变异能在两个新测试下存活——逐 provider 的可用性计算实际上并未被钉住。— 失败场景:已通过探针验证——给门控喂一个 pendingList.some(...) 并集检查,两个测试仍然全绿。区分性输入是:混合批量中当前模型只由非活跃条目提供(Coding Plan 活跃时选了一个仅 token-plan 提供的模型 id——这是可达状态,因为模型选择器会跨 authType 枚举)。正确的逐 provider 代码会迁移一次到 chinaTemplate[0]!.id;并集变异算出「某个批量条目提供了它」,删除 modelSelection 且什么都不写——选择被留在一个活跃 provider 并不提供的模型上,属于 #8863 的症状类别。建议修复:新增一个使用该模型 id 的混合批量用例,断言同样的单次迁移。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +682 to +685
// Both entries ran to completion, regardless of provider order.
await waitFor(() => {
expect(mockConfig.reloadModelProvidersConfig).toHaveBeenCalledTimes(2);
});

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] Three distinct properties this completion gate should pin are unpinned — reported as three separate comments on this block; this one is about version persistence: this test measures entry completion only by reload count and never asserts that either entry persisted its new providerMetadata.<key>.version on the migration path. No test anywhere pins version persistence when a migration happened (the three existing .version assertions are all on non-migration paths). — Failure scenario: probe-verified end to end — a one-line regression skipping the providerState step when modelSelection was applied passes all 28 tests here plus core install.test.ts and useAuth.test.ts; the user then gets the identical update prompt on every launch because metadata.version stays at the old hash. Suggested fix: after this wait, assert both version writes as the first batch test does (chinaVersion / tokenVersion) — adding them flips the probe.

中文说明

[建议] 该完成门控应当钉住的三个不同属性都未被钉住——已作为三条评论分别报告在这个代码块上;本条关于版本持久化:本测试只用 reload 次数衡量条目完成,从未断言任一条目在迁移路径上持久化了新的 providerMetadata.<key>.version。整个仓库没有任何测试钉住「发生迁移时」的版本持久化(现有三处 .version 断言都在非迁移路径上)。— 失败场景:已端到端探针验证——当 modelSelection 被应用时跳过 providerState 步骤的一行回归,能通过这里全部 28 个测试以及 core 的 install.test.tsuseAuth.test.ts;由于 metadata.version 停在旧哈希上,用户每次启动都会看到同样的更新提示。建议修复:在此等待之后,像第一个批量测试那样断言两个 version 写入(chinaVersion / tokenVersion)——加上后探针翻转。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +682 to +685
// Both entries ran to completion, regardless of provider order.
await waitFor(() => {
expect(mockConfig.reloadModelProvidersConfig).toHaveBeenCalledTimes(2);
});

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] (Second of the three unpinned properties on this gate — sequential execution.) Neither new batch test pins the loop's sequential await executeUpdate semantics — the completion gates count events that also accumulate under concurrent execution. — Failure scenario: probe-verified — replacing the loop with Promise.all (or dropping the await) keeps all 28 tests green. In production both providers share USE_OPENAI, and each applyProviderInstallPlan snapshots getModelProviders() and calls backup()/restore() on failure, so concurrent entries can roll each other's persisted writes back and reload stale maps — one provider's update silently lost after its success message was already posted. Suggested fix: record event order through the mocks (reload / refreshAuth-resolved) and assert the interleaved order ['reload', 'refreshAuth-resolved', 'reload'] — it fails against the Promise.all mutant and passes on current code.

中文说明

[建议](该门控上三个未钉住属性中的第二个——顺序执行。)两个新批量测试都没有钉住循环的逐个 await executeUpdate 语义——完成门控统计的事件在并发执行下同样会累积。— 失败场景:已通过探针验证——把循环换成 Promise.all(或丢掉 await)后 28 个测试仍全绿。生产中两个 provider 共享 USE_OPENAI,每个 applyProviderInstallPlan 都会快照 getModelProviders() 并在失败时调用 backup()/restore(),因此并发条目可能互相回滚已持久化的写入、并用过期快照 reload——某个 provider 的更新在成功消息已经发出后被静默丢失。建议修复:通过 mock 记录事件顺序(reload / refreshAuth-resolved),断言交错的顺序 ['reload', 'refreshAuth-resolved', 'reload']——它对 Promise.all 变异失败、对当前代码通过。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +682 to +685
// Both entries ran to completion, regardless of provider order.
await waitFor(() => {
expect(mockConfig.reloadModelProvidersConfig).toHaveBeenCalledTimes(2);
});

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] (Third of the three unpinned properties on this gate — failure continuation.) No test in this suite ever makes an entry fail (zero mockRejectedValue in the file), so the batch loop's continue-on-entry-failure semantics — executeUpdate's per-entry try/catch feeding the loop — are unpinned. — Failure scenario: probe-verified — a mutant making the catch rethrow keeps all 28 tests green. In production, if the first (active) entry's refreshAuth rejects (endpoint briefly unreachable), without the catch the rejection propagates out of the loop: entry 2 never runs, its version is never persisted, no error item is emitted, and the user is re-prompted on every launch with no explanation. Suggested fix: add a batch variant with mockConfig.refreshAuth.mockRejectedValueOnce(new Error('endpoint down')), asserting the error addItem for entry 1 and that the token-plan version write still happens — it fails under the mutant and passes on current code.

中文说明

[建议](该门控上三个未钉住属性中的第三个——失败后继续。)本套件没有任何测试让某个条目失败(整个文件没有一处 mockRejectedValue),因此批量循环「单条目失败后继续」的语义——executeUpdate 的逐条目 try/catch 喂养循环——未被钉住。— 失败场景:已通过探针验证——让 catch 重新抛出的变异能使 28 个测试全绿。生产中,如果第一个(活跃)条目的 refreshAuth 被拒绝(端点短暂不可达),没有该 catch 时拒绝会传出循环:条目 2 永远不会执行,其 version 不会被持久化,不会发出任何错误条目,用户每次启动都被重新提示且没有任何解释。建议修复:新增一个批量变体,mockConfig.refreshAuth.mockRejectedValueOnce(new Error('endpoint down')),断言条目 1 的错误 addItem 且 token-plan 的 version 写入仍然发生——该测试在变异下失败、对当前代码通过。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +693 to +696
const modelBaseUrlWrites = mockSettings.setValue.mock.calls.filter(
(call: unknown[]) => call[1] === 'model.baseUrl',
);
expect(modelBaseUrlWrites).toHaveLength(1);

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] This test pins the value of the model.name write but only the count of the model.baseUrl write. — Failure scenario: a regression that migrates with the correct model id but the wrong baseUrl (the inactive entry's base URL leaking into the active entry's modelSelection, or a stale non-empty disambiguator surviving) keeps toHaveLength(1) green and ships the wrong model.baseUrl to user settings. The expected value is knowable: syncAfterAuthRefresh is asserted to receive undefined, and applyProviderInstallPlan then writes the '' tombstone — probe-verified that the assertion below passes on current code.

Suggested change
const modelBaseUrlWrites = mockSettings.setValue.mock.calls.filter(
(call: unknown[]) => call[1] === 'model.baseUrl',
);
expect(modelBaseUrlWrites).toHaveLength(1);
const modelBaseUrlWrites = mockSettings.setValue.mock.calls.filter(
(call: unknown[]) => call[1] === 'model.baseUrl',
);
expect(modelBaseUrlWrites).toEqual([
[expect.anything(), 'model.baseUrl', ''],
]);
中文说明

[建议] 本测试钉住了 model.name 写入的,却只钉住了 model.baseUrl 写入的次数。— 失败场景:迁移时模型 id 正确但 baseUrl 错误的回归(非活跃条目的 baseUrl 泄漏进活跃条目的 modelSelection,或非空的过期消歧值残留)仍能让 toHaveLength(1) 通过,把错误的 model.baseUrl 写进用户设置。期望值可以确定:syncAfterAuthRefresh 已被断言收到 undefined,而 applyProviderInstallPlan 随后写入 '' 墓碑值——已探针验证下方断言在当前代码上通过。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +697 to +702
expect(mockModelsConfig.syncAfterAuthRefresh).toHaveBeenCalledTimes(1);
expect(mockModelsConfig.syncAfterAuthRefresh).toHaveBeenCalledWith(
AuthType.USE_OPENAI,
chinaTemplate[0]!.id,
undefined,
);

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] This test measures the inactive entry's completion only by reload count and never asserts it persisted its modelProviders.<authType> list write. — Failure scenario: probe-verified — a mutant deleting installPlan.modelProviders for inactive entries passes all 28 tests (including the pre-existing inactive single-entry tests). In production findAllPendingUpdates then skips that provider forever (metadata.version === currentVersion), so the user silently never receives that provider's added/removed models and the update prompt never returns for it. Suggested fix: filter setValue calls on modelProviders.${AuthType.USE_OPENAI} and expect 2 writes (or assert the final reload payload contains tokenTemplate models) — the length-2 assertion fails against the mutant.

中文说明

[建议] 本测试只用 reload 次数衡量非活跃条目的完成,从未断言它持久化了自己的 modelProviders.<authType> 列表写入。— 失败场景:已通过探针验证——对非活跃条目删除 installPlan.modelProviders 的变异能通过全部 28 个测试(包括既有的非活跃单条目测试)。生产中 findAllPendingUpdates 从此永远跳过该 provider(metadata.version === currentVersion),用户静默地永远收不到该 provider 新增/移除的模型,更新提示也不会再为它出现。建议修复:过滤 modelProviders.${AuthType.USE_OPENAI} 上的 setValue 调用并期望 2 次写入(或断言最终 reload 载荷包含 tokenTemplate 的模型)——长度为 2 的断言对该变异失败。

— qwen3.8-max via Qwen Code /review (v0.21.9)

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (idle-timeout (no output for 1200000ms — the sandbox likely hung at startup)) (attempt 3/100) — it will retry on the next scan.

What I found before stopping:
Qwen failed during address-review: idle-timeout (no output for 1200000ms — the sandbox likely hung at startup).

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31416010267


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix stopped: this counting window now contains 3 time-budget exhaustions (pushed rounds in between included; this round itself may have failed differently). That is 3 full agent runs that pushed nothing. 3 of those were silent-sandbox (idle) timeouts that no budget increase can cure — investigate the sandbox image and runner docker daemon for those. A human should investigate the sandbox image and runner docker daemon, then comment @qwen-code /retry to re-arm. Until then future scans will skip this PR.

What I found before stopping:
Qwen failed during address-review: idle-timeout (no output for 1200000ms — the sandbox likely hung at startup).

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31419817978


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

⏸️ Takeover paused: this PR reached its round cap (100/100). Comment @qwen-code /takeover to re-arm a fresh window and continue management, or @qwen-code /takeover stop to release.

中文说明

⏸️ 托管已暂停:本 PR 达到轮次上限(100/100)。评论 @qwen-code /takeover 可重新武装、开启新窗口继续托管;或评论 @qwen-code /takeover stop 释放。

@ComplexSimply

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

(Re-arming after the round cap — the substantive work is already done: c6da0275 resolves the base conflict and addresses both inline suggestions, verified locally at 28/28 on that head. The exhausted rounds were silent-sandbox idle timeouts, not failed fixes. 重新挂载:实质工作已完成——c6da0275 已解决 base 冲突并落实两条 inline 建议,本地该 head 上 28/28 验证通过;耗尽的轮次是沙箱空转超时而非修复失败。)

ComplexSimply added 2 commits August 11, 2026 22:37
QwenLM#8889 made the template update path drop the plan's model selection
unconditionally, so the mixed-batch test that expected the active
provider to migrate now pins removed behavior. Invert it: a batch
mixing the active provider (current model gone from its plan) with an
inactive one must leave model.name and model.baseUrl untouched while
both updates still run to completion — the batch-loop side of the
invariant whose single-provider side QwenLM#8889 pins itself.
@ComplexSimply

Copy link
Copy Markdown
Collaborator Author

Synced with main and adapted to #8889 (cd245e9b + f36e9e04).

#8889 made the update path drop the plan's modelSelection unconditionally — which removed the active-provider migration that the mixed-batch test here pinned. That test would have failed on the merged tree (verified locally: 1 failed / 30 passed before the fix). It is now inverted to pin the new invariant's batch-loop side: a batch mixing the active provider (current model gone from its plan) with an inactive one leaves model.name / model.baseUrl untouched while both updates still run to completion. 31/31 locally after the sync.

(Worth noting: #8889's unconditional drop converges with the approach originally proposed in #8873 — nice to see the semantics land either way.)

中文

已同步 main 并适配 #8889cd245e9b + f36e9e04)。

#8889 将更新路径改为无条件丢弃 plan 的 modelSelection——这移除了本 PR 中 mixed-batch 测试所钉住的「活动 provider 迁移」行为,该测试在合并树上会失败(本地已验证:修复前 1 失败 / 30 通过)。现已反转为钉住新不变量的批量侧:活动 provider(其 plan 中当前模型已消失)与非活动 provider 混合批量更新时,model.name / model.baseUrl 均不被触碰,且两个更新都完整执行。同步后本地 31/31。

(顺带一提:#8889 的无条件丢弃与 #8873 最初提出的方案殊途同归。)

@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 explored to full depth (tool budget reached): This PR adds two regression tests to packages/cli/src/ui/...: full log of the first 6-failure run was not captured; its attribution to environment ENOSPC rests on the captured ENOSPC run plus ~30 consecutive green runs aft….

Test Plan (not a blocker): src/ui/hooks/useProviderUpdates.test.tsno such file or directory.

中文说明

已审查。 建议见行内评论。

未探索到全部深度(达到工具调用预算):This PR adds two regression tests to packages/cli/src/ui/...:full log of the first 6-failure run was not captured; its attribution to environment ENOSPC rests on the captured ENOSPC run plus ~30 consecutive green runs aft…

Test Plan(非阻断):src/ui/hooks/useProviderUpdates.test.tsno such file or directory

— qwen3.8-max via Qwen Code /review (v0.21.10)

Comment on lines +683 to +688
const foreignModel = {
id: 'my-own-model',
baseUrl: 'https://my-own-gateway.example.com/v1',
envKey: 'MY_OWN_KEY',
name: '[Mine] my-own-model',
};

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] R3-1: foreignModel fixture duplicated from the pre-existing it.each test — this block is byte-identical to the fixture in 'does not move the user off a model $name' (~lines 615-620): same four fields and the same getModel/getContentGeneratorConfig wiring. The file already keeps shared template fixtures (chinaTemplate, tokenTemplate) at module scope, so the reuse point exists. — Failure scenario: both copies encode "a user model owned by neither plan provider"; a future change to that fixture (a newly required ModelInfo field, an id/envKey rename) must be applied in two places 65 lines apart, and updating only one makes the two tests silently pin different "foreign model" scenarios while both copies still look canonical.

Suggested fix — hoist the fixture to module scope alongside chinaTemplate/tokenTemplate and reference it from both tests (mockConfig.getModel.mockReturnValue(foreignModel.id)):

const foreignModel = {
  id: 'my-own-model',
  baseUrl: 'https://my-own-gateway.example.com/v1',
  envKey: 'MY_OWN_KEY',
  name: '[Mine] my-own-model',
};
中文说明

[建议] R3-1:foreignModel 夹具与既有 it.each 测试 'does not move the user off a model $name'(约 615-620 行)中的夹具逐字相同:四个字段与 getModel/getContentGeneratorConfig 接线完全一致。本文件已在模块作用域维护共享模板夹具(chinaTemplatetokenTemplate),复用点已存在。— 失败场景:两处副本编码同一概念——「不属于任何计划 provider 的用户自有模型」;未来对该夹具的任何修改(新增必需的 ModelInfo 字段、id/envKey 改名)都必须在相隔 65 行的两处同步应用,只改一处会让两个测试静默钉住不同的「外部模型」场景,而两处副本看起来都像是权威定义。

建议修复——将夹具提升到模块作用域,与 chinaTemplate/tokenTemplate 并列,两个测试改为引用它(如 mockConfig.getModel.mockReturnValue(foreignModel.id))。

— qwen3.8-max via Qwen Code /review (v0.21.10)

Comment on lines +707 to +709
mockSettings.merged['modelProviders'] = {
[AuthType.USE_OPENAI]: [foreignModel, ...chinaTemplate, ...tokenTemplate],
};

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] R3-4: This test seeds a user-owned provider entry (foreignModel) into the same protocol list as the two batch-updated providers, but neither batch test ever asserts the merged list's content — the entry's survival across two sequential applyModelProvidersPatch merges rests solely on the preservedModels filter and is pinned by nothing. (The pre-existing 'persists the template version and preserves custom models' covers only one entry and only provider-owned ids carried via customIds — a different mechanism.) — Failure scenario: probe-verified — a merge regression, or a widened ownsModel predicate, silently drops the user's custom endpoint from modelProviders during a two-provider batch update (their self-hosted model vanishes from the picker) while both tests stay green.

Suggested fix — assert both reload payloads keep the foreign entry; match baseUrl as well, since an id-only matcher would miss the widened-ownsModel variant, which re-adds the id rebuilt under the provider's baseUrl:

const [firstReload, secondReload] =
  mockConfig.reloadModelProvidersConfig.mock.calls;
for (const reloaded of [firstReload?.[0], secondReload?.[0]]) {
  expect(reloaded?.[AuthType.USE_OPENAI]).toEqual(
    expect.arrayContaining([
      expect.objectContaining({
        id: 'my-own-model',
        baseUrl: 'https://my-own-gateway.example.com/v1',
      }),
    ]),
  );
}
中文说明

[建议] R3-4:本测试把用户自有的 provider 条目(foreignModel)与两个批量更新的 provider 放进同一协议列表,但两个批量测试都从未断言合并后列表的内容——该条目在连续两次 applyModelProvidersPatch 合并后仍然存活,完全依赖 preservedModels 过滤器,却没有任何断言钉住它。(既有测试 'persists the template version and preserves custom models' 只覆盖单条目、且只覆盖经由 customIds 携带的 provider 自有 id——机制不同。)— 失败场景:已通过探针验证——合并逻辑回归、或 ownsModel 判定被放宽,都会在双 provider 批量更新期间静默丢弃用户的自定义端点(其自托管模型从选择器中消失),而两个测试仍然全绿。

建议修复——断言两次 reload 载荷都保留了该外部条目;同时匹配 baseUrl,因为仅匹配 id 会漏掉「放宽 ownsModel」的变体——该变体会把同一 id 以 provider 的 baseUrl 重建后重新加回列表。

— qwen3.8-max via Qwen Code /review (v0.21.10)

Comment on lines +727 to +732
await waitFor(() => {
expect(mockSettings.setValue).toHaveBeenCalledWith(
expect.anything(),
`${PROVIDER_METADATA_NS}.${METADATA_KEY}.version`,
chinaVersion,
);

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] R3-3: This completion gate is recorded before every failure-capable step and survives rollback — a per-entry failure swallowed by executeUpdate's catch leaves the test green. The version writes asserted here happen at the providerState step — before persist, reloadModelProviders and refreshAuth — and the catch-path rollback cannot un-record mock calls; neither batch test asserts the success addItem the single-provider sibling 'leaves the model selection alone when the previous model is gone' uses to pin completion. — Failure scenario: probe-verified — making any post-providerState step throw for one entry rolls that entry back and shows the user a "Failed to update provider configuration" item, yet this gate is already satisfied and all negative assertions hold vacuously — the batch test passes with one provider not updated at all. The second test's reloadModelProvidersConfig x2 gate has the same hole (a rollback reload also satisfies the count), and asserting version writes does not close it — version writes are equally rollback-invisible in this harness.

Suggested fix — pin completion via the per-provider success items, the one observable that distinguishes success from swallowed failure (in both batch tests; optionally plus no type: 'error' item):

expect(mockAddItem).toHaveBeenCalledWith(
  expect.objectContaining({
    text: 'Coding Plan configuration updated successfully.',
  }),
  expect.any(Number),
);
expect(mockAddItem).toHaveBeenCalledWith(
  expect.objectContaining({
    text: 'Token Plan configuration updated successfully.',
  }),
  expect.any(Number),
);
中文说明

[建议] R3-3:该完成门控记录在所有可能失败的步骤之前、且能在回滚后幸存——executeUpdate 的 catch 吞掉的单条目失败仍会让测试全绿。这里断言的 version 写入发生在 providerState 步骤——早于 persistreloadModelProvidersrefreshAuth——而 catch 路径的回滚无法取消已记录的 mock 调用;两个批量测试都没有断言单 provider 同类测试 'leaves the model selection alone when the previous model is gone' 用来钉住「完成」的成功 addItem。— 失败场景:已通过探针验证——让任一 providerState 之后的步骤对某个条目抛错,会使该条目回滚并向用户展示 "Failed to update provider configuration" 条目,但此门控此时已经满足、所有负向断言也空转通过——批量测试在一个 provider 完全未更新的情况下仍然通过。第二个测试的 reloadModelProvidersConfig x2 门控有同样的漏洞(回滚 reload 也会计入次数),且断言 version 写入并不能堵住它——在本测试环境中 version 写入同样对回滚不可见。

建议修复——用逐 provider 的成功条目钉住「完成」,这是本环境中唯一能区分成功与被吞失败的信号(两个批量测试都加;可选再加「无 type: 'error' 条目」断言)。

— qwen3.8-max via Qwen Code /review (v0.21.10)

Comment on lines +778 to +779
mockConfig.getModel.mockReturnValue('removed-model');
mockConfig.refreshAuth.mockResolvedValue(undefined);

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] R3-2: This test's stated purpose is to pin the ACTIVE-provider side of a mixed batch, but no assertion verifies the active-provider path was actually exercised — the premise rests entirely on the shared beforeEach credentials (verified to hold at this commit). refreshAuth is stubbed here but never asserted, and since #8889 the only observable active-vs-inactive difference in a template update is the refreshAuth call. The sibling 'executes update when user confirms with "update"' pins the same side condition with toHaveBeenCalledWith(AuthType.USE_OPENAI). — Failure scenario: probe-verified — a future edit to the shared beforeEach mock credentials, the coding-plan preset's baseUrl list, or providerMatchesCredentials makes the first entry silently stop being the active provider; every assertion still passes (no template update touches model.name regardless of active/inactive status), so the file stays green while this test no longer exercises the path it is titled for.

Suggested fix — after onConfirm('update') and the existing completion gate:

expect(mockConfig.refreshAuth).toHaveBeenCalledTimes(1);

(optionally expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_OPENAI);).

中文说明

[建议] R3-2:本测试声称要钉住混合批量中活跃 provider 的一侧,但没有任何断言验证活跃 provider 路径真的被执行——该前提完全依赖共享的 beforeEach 凭据(已验证在当前提交下成立)。这里对 refreshAuth 只做了 stub、从未断言其被调用;而 #8889 之后,模板更新中活跃与非活跃 provider 唯一可观测的差异就是 refreshAuth 调用。同类测试 'executes update when user confirms with "update"'toHaveBeenCalledWith(AuthType.USE_OPENAI) 钉住了同样的前提条件。— 失败场景:已通过探针验证——未来对共享 beforeEach mock 凭据、coding-plan 预设 baseUrl 列表或 providerMatchesCredentials 的修改,可能让第一个条目静默不再是活跃 provider;而所有断言仍会通过(模板更新无论活跃与否都不会碰 model.name),文件全绿,这个以「混合批量中的活跃 provider」为名的测试却不再演练该路径。

建议修复——在 onConfirm('update') 与既有完成门控之后加:expect(mockConfig.refreshAuth).toHaveBeenCalledTimes(1);(可选再加 toHaveBeenCalledWith(AuthType.USE_OPENAI))。

— qwen3.8-max via Qwen Code /review (v0.21.10)

@ComplexSimply

Copy link
Copy Markdown
Collaborator Author

@yiliang114 Gentle ping — this batch-path regression pin for your #8868 (and its post-#8889 adaptation) is green and only waiting on the required approvals. No rush. / 顺手 ping:#8868 批量路径的回归测试(已适配 #8889)已全绿,只差必需的 approve,方便时请过目,不急。

@ComplexSimply
ComplexSimply requested a review from chiga0 August 12, 2026 05:50
@wenshao

wenshao commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Local verification report (maintainer)

I built a real environment for this PR and ran it end to end on macOS, plus a mutation matrix to measure what the two added tests actually buy us.

Verdict: verified — safe to merge. One mutant that today's main suite is completely blind to is caught by the added tests, so they are not duplicates of the existing single-provider assertions. Two accuracy nits on the PR body below (please refresh it before merge).

Environment

item value
host macOS 26.6, Apple Silicon
Node / Vitest v24.18.1 / vitest 3.2.4
PR head f36e9e044f
merge-base 542ef73fd3
origin/main at test time 8858d4340b
setup dedicated git worktree + clean npm ci

1. Merge shape

packages/cli/src/ui/hooks/useProviderUpdates.ts at PR head is byte-identical to current origin/main, and the only delta between the PR and origin/main in this area is the 137 added test lines. So merging is a pure test-only addition with zero production risk — no rebase drift to worry about.

2. Suite on macOS — 31/31

CI ran the ubuntu leg only here (Test (macos-latest, …) and Test (windows-latest, …) are SKIPPED), and the PR's Tested-on table marks macOS ⚠️. Running it locally closes that gap:

suite

  • vitest run src/ui/hooks/useProviderUpdates.test.ts31 passed (31)
  • 5 consecutive runs: green every time (no flake)
  • 3 shuffled-order seeds (--sequence.shuffle.tests --sequence.seed=7|14|21): 31/31 each — no order coupling with the existing tests
  • whole directory vitest run src/ui/hooks69 files / 1473 tests passed — no cross-file mock leakage
  • tsc --noEmit -p packages/cli: clean · eslint: clean · prettier --check: clean

So macOS can be promoted from ⚠️ to ✅.

3. Mutation matrix — does the PR earn its 137 lines?

I applied five production mutants to useProviderUpdates.ts and ran the same mutant against two test files: main's (29 tests) and this PR's (31 tests).

matrix

mutant main test file PR 8879 test file
none (pristine) 29 pass / 0 fail 31 pass / 0 fail
M1 pre-#8868 gate — if (previousModelStillAvailable) delete … 3 fail (caught) 5 fail (caught)
M2 #8868 gate, i.e. revert #8889if (!updatesActiveProvider || previousModelStillAvailable) delete … 1 fail (caught) 2 fail (caught)
M3 batch loop stops after the 1st provider (break) 1 fail (caught) 3 fail (caught)
M4 guard applied once per batch instead of per provider 0 fail — BLIND 2 fail — CAUGHT

What this says:

  • M4 is the payoff. A refactor that hoists the "never change the selected model" guard out of the per-provider path — the single most plausible way this can regress once someone touches executeUpdate/onConfirm — slips past main's entire suite untouched, and is caught by exactly the two tests this PR adds. The failure is the literal bug(providers): built-in provider update silently overwrites model.name and model.baseUrl when the current model belongs to another provider (#5819 regression) #8863 symptom: the second provider in the batch (Token Plan) writes model.name = "qwen3.7-plus" over the user's selection.

    m4

  • The two added tests are not redundant with each other. M2 is caught by only one of them (the mixed-batch/active-provider case), M1 by both. They cover distinct axes: all-inactive batch vs. active-provider-in-batch.

  • M1/M2/M3 are already covered by the existing single-provider tests (leaves the model selection alone when the previous model is gone, does not move the user off a model …, updates both provider metadata keys from a batched prompt). For those, the new tests add a second signal rather than a unique one — which is fine and expected; M4 is where the new dimension lives.

4. Nits on the PR body (not blocking, but please refresh)

  1. Stale counts. The body says "one regression test" and "21/21". The branch now carries two tests and the file runs 31/31. (Later commits c6da0275e2 / f36e9e044f added the second test.)
  2. Stale mutation recipe. The body's reviewer test plan says to revert "the fix(cli): keep the selected model when refreshing another provider's template #8868 gate (if (previousModelStillAvailable) without !updatesActiveProvider ||)". fix(core,vscode): keep provider update versions in sync #8889 has since replaced that gate with an unconditional delete installPlan.modelSelection;, so that recipe no longer describes the code. Reverting to the fix(cli): keep the selected model when refreshing another provider's template #8868 gate is my M2, and it is caught by only one of the two new tests plus one pre-existing test — a reader following the body verbatim will draw the wrong conclusion about which test is doing the work. The current guard is line 279-280 of useProviderUpdates.ts.

Reproduce

git worktree add head <PR-head-sha> && cd head && npm ci
cd packages/cli && npx vitest run src/ui/hooks/useProviderUpdates.test.ts --reporter=verbose

# M4 mutant: in executeUpdate, replace the unconditional
#   delete installPlan.modelSelection;
# with a guard that only fires on the first provider of a batch
# (reset a counter before the `for (const p of pendingList)` loop in onConfirm),
# then re-run against main's test file vs this PR's.
中文版本

本地验证报告(维护者)

我在本地搭了真实环境跑完这个 PR,并额外做了变异测试矩阵,用来量化新增的两个用例到底值不值这 137 行。

结论:验证通过,可以合并。 有一类变异当前 main 的整套用例完全测不出来,而本 PR 新增的两个用例能抓住,说明它们不是既有单 provider 断言的重复。PR 正文有两处已经过时,建议合并前更新(见下)。

环境

主机 macOS 26.6,Apple Silicon
Node / Vitest v24.18.1 / vitest 3.2.4
PR head f36e9e044f
merge-base 542ef73fd3
测试时的 origin/main 8858d4340b
方式 独立 git worktree + 干净 npm ci

1. 合并形态

PR head 上的 packages/cli/src/ui/hooks/useProviderUpdates.ts 与当前 origin/main 逐字节相同,这块区域里 PR 相对 origin/main 的唯一差异就是新增的 137 行测试。所以合并是纯测试增量,生产代码零风险,也不存在 rebase 漂移。

2. macOS 上跑套件 —— 31/31

CI 这里只实跑了 ubuntu 腿Test (macos-latest, …)Test (windows-latest, …) 都是 SKIPPED),PR 的 Tested-on 表里 macOS 标的是 ⚠️。本地跑正好补上这个缺口:

  • vitest run src/ui/hooks/useProviderUpdates.test.ts31 passed (31)
  • 连跑 5 次全绿(无 flake)
  • 3 个乱序种子(--sequence.shuffle.tests --sequence.seed=7|14|21):各 31/31 —— 与既有用例无顺序耦合
  • 整个目录 vitest run src/ui/hooks69 个文件 / 1473 个用例全过 —— 无跨文件 mock 泄漏
  • tsc --noEmit -p packages/cli 干净 · eslint 干净 · prettier --check 干净

macOS 可以从 ⚠️ 改成 ✅。

3. 变异矩阵 —— 这 137 行值不值

我对 useProviderUpdates.ts 施加了 5 种生产代码变异,同一个变异体分别用 main 的测试文件(29 个用例)和本 PR 的测试文件(31 个用例)各跑一遍。

变异 main 测试文件 PR 8879 测试文件
无(原始) 29 通过 / 0 失败 31 通过 / 0 失败
M1 #8868 之前的门控 —— if (previousModelStillAvailable) delete … 3 失败(抓到) 5 失败(抓到)
M2 #8868 的门控,即回退 #8889 —— if (!updatesActiveProvider || previousModelStillAvailable) delete … 1 失败(抓到) 2 失败(抓到)
M3 批量循环在第一个 provider 后 break 1 失败(抓到) 3 失败(抓到)
M4 门控按每批只生效一次,而不是每个 provider 各生效 0 失败 —— 完全测不出 2 失败 —— 抓到

解读:

  • M4 是关键收益。 把"模板更新绝不改变已选模型"这个保护从 per-provider 路径上提出去——这正是以后有人动 executeUpdate/onConfirm 时最可能出现的回归方式——main 的整套用例毫无反应,而抓住它的正是本 PR 新增的那两个用例。失败现象就是 bug(providers): built-in provider update silently overwrites model.name and model.baseUrl when the current model belongs to another provider (#5819 regression) #8863 的原始症状:批量里第二个 provider(Token Plan)把用户选的模型改写成 model.name = "qwen3.7-plus"
  • 新增的两个用例彼此也不重复。 M2 只被其中一个抓到(混合批量 / 活跃 provider 那个),M1 两个都抓到。它们覆盖的是两条不同的轴:全部非活跃 provider 的批量,和批量中包含活跃 provider。
  • M1/M2/M3 已被既有用例覆盖leaves the model selection alone when the previous model is gonedoes not move the user off a model …updates both provider metadata keys from a batched prompt)。对这几种变异,新用例提供的是第二重信号而非唯一信号——这很正常,新增的维度体现在 M4 上。

4. PR 正文的两处小问题(不阻塞,但建议更新)

  1. 数字过时。 正文写的是"一个回归测试""21/21",但分支上现在是两个测试,文件实跑 31/31(第二个测试来自后续的 c6da0275e2 / f36e9e044f)。
  2. 变异方法过时。 正文的 Reviewer Test Plan 让人回退"fix(cli): keep the selected model when refreshing another provider's template #8868 的门控(去掉 !updatesActiveProvider ||)",但 fix(core,vscode): keep provider update versions in sync #8889 之后该门控已被替换成无条件的 delete installPlan.modelSelection;,这个说法与当前代码对不上。回退到 fix(cli): keep the selected model when refreshing another provider's template #8868 门控就是我这里的 M2,它只被两个新用例中的一个 + 一个既有用例抓到——照正文原样操作的读者会对"是哪个测试在起作用"得出错误结论。当前的保护在 useProviderUpdates.ts 第 279-280 行。

@wenshao
wenshao enabled auto-merge August 12, 2026 18:51
@wenshao

wenshao commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ inconclusive — completed without a usable structured verdict - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 28 passed · 0 failed · 28 total

中文 — 判定:⚠️ 无法判定 · 已完成但无可用的结构化判定

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:28 通过 · 0 失败 · 28 总计

Verification report

PR #8879 verification — test(cli): pin the model selection across multi-provider template updates

Verdict: merge-ready — 28/28 scripted assertions passed (0 unexpected failures), verified head f36e9e044f325a09eebfb262bd47a2d973934a03 (merge-ref base 187637449b). Test-only PR (+137 lines, one test file); both new tests are load-bearing and correctly attributed; no mutant regressed killed→survived; targeted gates clean.

中文摘要
  • 结论merge-ready。28/28 脚本化断言通过,0 个意外失败。
  • A/B 结论(见下文 "Central claim" 表):两个新批量测试是 load-bearing 的——恢复 fix(core,vscode): keep provider update versions in sync #8889 删除的 delete installPlan.modelSelection(M1)或截断批量循环(M2)时,旧测试文件杀 4/1 个测试,新测试文件额外恰好杀掉这两个新测试(6/3),且失败断言正是测试名所承诺的 model.name 缺失断言与 reloadModelProvidersConfig 两次调用。无 mutant 从 killed 退化为 survived。
  • Findings(均不阻塞):(1) 核心层 奇怪的bug,升级以后默认会使用更高单价的model自动修改setting.json中的参数,并自行调用浪费tokens的策略 #5819 keep-gate(planOffersCurrentModel)在全仓库无任何测试钉住——既有缺口,非本 PR 引入,已给出可钉住它的 fixture(探针测试无变异绿、M4 下红);(2) 描述中的变异配方引用的 previousModelStillAvailable / !updatesActiveProvider || 代码形态在 fix(core,vscode): keep provider update versions in sync #8889 之后已不存在(等价变异 M1 确实杀掉新测试,实质主张成立,配方文本过期);"21/21" 计数亦过期(实际 base 29 / head 31,+2 差值正确)。
  • 未覆盖:逐 commit 归因(depth-2 浅克隆,仅 PR head 可达,已验证聚合 diff);cli 全工作区测试与全仓 lint(委托 PR CI,已跑定向套件 + typecheck + eslint + prettier 且均做了 liveness 证明);TUI/E2E(无用户可见面变更)。

Central claim + A/B

Central claim: the two added tests pin the multi-provider batch dimension of the template-update invariant ("a template update never changes the selected model") — the worst case of #8863 — and are load-bearing: mutating the production guard fails them.

The production guard at this head is two layers: the hook's unconditional delete installPlan.modelSelection (#8889) in useProviderUpdates.ts, and the core planOffersCurrentModel keep-gate (#5819) in providers/install.ts. The mutation matrix below drives single-point mutants of these (unmodified) production files against the base test file (arm old, 29 tests) and the PR test file (arm new, 31 tests), via the repo's own vitest config (core resolves to source through the cli vitest alias, so no rebuild is needed and both layers are live).

mutant old arm (base tests) new arm (PR tests) status
M0 control (none) 0/29 red 0/31 red green both arms
M1 remove delete installPlan.modelSelection 4/29 red 6/31 red killed/killed — both new tests newly red
M2 break; after first executeUpdate in the batch loop 1/29 red 3/31 red killed/killed — both new tests newly red
M3 unconditional refreshAuth pass-through 3/29 red 3/31 red killed/killed (same pre-existing killers)
M4 disable core planOffersCurrentModel gate 0/29 red 0/31 red survived/survived → Finding 1

Witnesses: evidence/01-suite-head-31-pass.png (head suite 31/31 verbose), evidence/02-mutation-matrix.png (teed live matrix output), evidence/03-m1-new-arm-red.png (M1 red run; the failure detail shows the pre-#8889 wire behavior — 7th spy call writes model.name = qwen3.7-plus, the plan default, then a model.baseUrl tombstone — failing at the mixed-batch test's model.name assertion, [6/6]).

Attribution (scripted, from vitest JSON): under M1 both new tests fail on expected "spy" to not be called with arguments: [Anything, 'model.name', Anything] — exactly the assertion their names promise. Under M2, test 1 fails on the missing second provider's version write and test 2 on expected "spy" to be called 2 times, but got 1 times — a seam-level proof that the unmutated batch loop runs both iterations and the new tests pin both (reachability/vacuity check: the negative model.name assertions cannot pass vacuously; each test also asserts entries.length === 2 and a positive completion before the negatives).

Fixture-name check: fixture-check.mjs (11/11, folded into the assertion count) verified the tests' naming claims against the real registry in the built core package: removed-model/my-own-model are in neither provider's built-in list; coding-plan precedes token-plan in ALL_PROVIDERS; the default mock credentials make coding-plan the active provider and token-plan inactive (so "mixed batch" is literally true); the metadata keys match resolveMetadataKey.

No mutant regressed killed→survived (M1/M2/M3 killed on both arms; M4 survived on both).

Corrections

These correct the PR description, not the code — no change requested:

  1. The Reviewer Test Plan's mutation recipe is stale. It says reverting the fix(cli): keep the selected model when refreshing another provider's template #8868 gate to if (previousModelStillAvailable) without !updatesActiveProvider || fails the new test. Neither identifier exists anywhere in the tree at this head: fix(core,vscode): keep provider update versions in sync #8889 replaced that gate with the unconditional delete installPlan.modelSelection in the hook (the core planOffersCurrentModel gate is the 奇怪的bug,升级以后默认会使用更高单价的model自动修改setting.json中的参数,并自行调用浪费tokens的策略 #5819 install-path gate, a different layer). The substantive claim holds — the intent-equivalent revert (M1) fails both new tests alongside the single-provider ones (6 red) — but a reviewer following the recipe verbatim will find no code to revert.
  2. "21/21" is stale. The suite is 31 tests at head and 29 at base (the it.each cases expand); the +2 delta the PR adds is correct.

Findings (non-blocking)

F1 — Suggestion (pre-existing completeness gap, not introduced by this PR): the core keep-gate is pinned by no test anywhere

M4 (disabling if (planOffersCurrentModel) in packages/core/src/providers/install.ts) survives every suite that touches the install path: the useProviderUpdates suite on both arms (0/29, 0/31), the full core providers directory (146/146 green), and the cli auth+updates suites (56/56 green) — all measured with the mutant applied. The gate's keep-branch ("if the plan still offers the current model, keep it", #5819) is live code (reachable via /auth re-runs and ACP reconnects) but has no failing test when disabled; every existing model.name assertion in the repo is on the write side.

This is not a defect in this PR: the PR's subject — the template-update path — is protected by the hook's delete (M1, pinned by this PR's tests and the pre-existing single-provider ones), and M4 cannot leak into template updates because the hook strips modelSelection before applyProviderInstallPlan. It is a completeness report on the neighboring layer, labeled as such, not a merge condition.

Reproduce (survivor):

# in packages/core/src/providers/install.ts: s/if (planOffersCurrentModel) {/if (false) {/
cd packages/core && npx vitest run src/providers --coverage.enabled=false   # 146/146 green

The fixture that would pin it (measured: green unmutated, red under M4 — adjudicate.mjs section 3b): an adapter whose getValue('model.name') returns 'model-a', a plan offering model-a with modelSelection: { modelId: 'model-b' }, asserting setValue is never called with 'model.name'. If a follow-up PR adds that test, it should ship with it.

F2 — Note: the new batch tests do not pin the refreshAuth gate in batch mode

Under M3 (unconditional refreshAuth pass-through), both arms fail identically on the three pre-existing single-provider tests (does not change auth when updating an inactive provider ×2, does not refresh auth before auth initialization completes); the two new batch tests carry no refreshAuth assertion, so they add nothing on that axis. The behavior remains pinned by the pre-existing tests — completeness note only.

Not covered

  • Per-commit attribution: the checkout is depth-2/shallow; git rev-list HEAD^1..HEAD^2 yields only f36e9e044f while the metadata lists 5 commits, so per-commit verification was out of reach. The aggregate HEAD^1..HEAD diff (one test file, +137) was verified and matches the stated intent.
  • Full cli workspace suite and repo-wide lint: delegated to the PR's own CI. Targeted gates run here with liveness proofs: tsc --noEmit in packages/cli (exit 0, empty), eslint on the changed file (exit 0; planted unused-var probe caught, exit 1), prettier --check on the changed file (exit 0; misformatted probe caught, exit 1).
  • TUI/E2E behavior: the change is unit-test-only; no user-observable surface.
  • The description's mutation recipe as literally written: the code shape no longer exists (see Corrections); M1 is the intent-equivalent.
  • vscode-ide-companion's applyProviderInstallPlanToFile: checked for a duplicated predicate — it has a different concern (runtime-snapshot-ID filter), no second keep-gate; not mutation-tested.

Methodology

Environment: CI verify container (node:22-bookworm), merge-ref checkout at depth 2 (HEAD merge 9d87c27999, base 187637449b, head f36e9e044f), npm ci + build pre-run; no GitHub access. Harnesses (all in this artifact dir, rerunnable): mutation-matrix.mjs (10 vitest cells = 5 mutants × 2 test-file arms, JSON reporter, mutants applied/restored with post-run git status verified clean), fixture-check.mjs (registry oracle over the built core dist), adjudicate.mjs (28 scripted assertions incl. M4 lethality pairs, the keep-branch probe, and liveness-proven gates; writes assertions.json). Raw per-cell logs and JSON in logs/. Core code under test ran from source via the cli vitest alias, so mutants in both packages took effect without rebuilds.

Evidence images

01-suite-head-31-pass

02-mutation-matrix

03-m1-new-arm-red

04-m4-core-survivor

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@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 added this pull request to the merge queue Aug 12, 2026
Merged via the queue into QwenLM:main with commit 055b021 Aug 12, 2026
108 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.11.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants