fix(cli): preserve empty MCP prompt args#5323
Conversation
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Hey @tt-a1i, thanks for the fix — the bug is real and the approach looks right.
The PR body doesn't follow our PR template though. It's missing a few required sections:
- Reviewer Test Plan — how to verify the fix, with before/after evidence
- Risk & Scope — tradeoffs, out-of-scope items, breaking changes
- Linked Issues —
Fixes #5322should live in its own section, not buried in the summary
Please update the PR body to match the template and we'll pick this back up. The code change itself looks minimal and correct, so this should be quick.
中文说明
@tt-a1i 你好,感谢修复——这个 bug 确实存在,方案也看起来正确。
但 PR 描述没有按照我们的 PR 模板 来写,缺少以下必要部分:
- Reviewer Test Plan — 如何验证修复,附前后对比证据
- Risk & Scope — 权衡、不在范围内的内容、破坏性变更
- Linked Issues —
Fixes #5322应该放在单独的章节里,而不是混在摘要中
请按模板更新 PR 描述,我们会尽快继续审查。代码改动本身很小且正确,这应该很快就能搞定。
— Qwen Code · qwen3.7-max
|
@qwen-code /triage |
|
Thanks for the PR, @tt-a1i! Template looks good ✓ — all required sections are present and well-filled (What, Why, Reviewer Test Plan, Risk & Scope, Linked Issues, 中文说明). The previous template-flagging review was a false positive; the PR body fully follows the template. On direction: this is a real bug — MCP prompt named arguments set to an empty string being silently dropped is surprising and incorrect behavior. The fix is squarely within On approach: the scope is minimal and exactly right — two truthy checks replaced with Moving on to code review. 🔍 中文说明感谢贡献,@tt-a1i! 模板完整 ✓ — 所有必要章节均已填写(做了什么、为什么、审阅者测试计划、风险与范围、关联 Issue、中文说明)。此前关于模板缺失的审查是误报,PR 描述完全符合模板。 方向:这是一个真实的 bug — MCP prompt 具名参数被设为空字符串时被静默丢弃,这是令人意外且不正确的行为。修复完全在 方案:范围极小且恰到好处 — 两处真值判断替换为 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewThe fix is correct and clean. I independently arrived at the same approach: replace the two truthy checks with presence checks so that empty-string values are retained rather than discarded. Two changes, both correct:
No correctness issues, no security concerns, no AGENTS.md violations. The diff is exactly the minimal change needed. TestingUnit tests (worktree): 25/25 passed, including both new test cases:
CI (GitHub): All 9 required checks passed (Lint, CodeQL, Test × 3 platforms). 12 skipped (integration tests, coverage, etc.). Real-scenario (tmux): Not applicable for this fix — the bug is in internal argument parsing logic, not user-visible TUI behavior. Testing would require a configured MCP server with prompt definitions, which is beyond the scope of automated triage. The unit tests cover the fix comprehensively. 中文说明代码审查修复正确且干净。我独立得出的方案相同:将两处真值判断替换为存在性判断,使空字符串值被保留而非丢弃。 两处改动均正确:
无正确性问题、无安全隐患、无 AGENTS.md 违规。diff 恰好是所需的最小改动。 测试单元测试(worktree): 25/25 通过,包括两个新测试用例:
CI(GitHub): 全部 9 项必检通过(Lint、CodeQL、Test × 3 平台)。12 项跳过(集成测试、覆盖率等)。 真实场景(tmux): 不适用于此修复 — bug 在内部参数解析逻辑中,非用户可见的 TUI 行为。测试需要配置了 prompt 定义的 MCP 服务器,超出自动分诊范围。单元测试已全面覆盖该修复。 — Qwen Code · qwen3.7-max |
|
This is a clean, well-scoped bug fix that does exactly what it says. The independent proposal I formed before reading the diff matched the PR's approach exactly — replace truthy checks with The bug is real (empty-string MCP prompt args silently dropped), the fix is minimal (2 lines of logic), and the tests are focused (2 new cases that exercise both branches of the fix). Every line in the diff is necessary; nothing to cut. The prior CHANGES_REQUESTED review flagged the template as incomplete, but the PR body actually follows the template fully — that was a false positive that shouldn't have blocked this PR. Approving. ✅ 中文说明这是一个干净、范围恰当的 bug 修复,完全如描述所述。我在阅读 diff 之前独立形成的方案与 PR 的方法完全一致 — 将真值判断替换为 bug 确实存在(空字符串 MCP prompt 参数被静默丢弃),修复最小化(2 行逻辑),测试有针对性(2 个新用例覆盖了修复的两个分支)。diff 中每一行都是必要的,没有可删减的内容。 此前的 CHANGES_REQUESTED 审查标记模板不完整,但 PR 描述实际上完全遵循模板 — 那是一个不应阻止此 PR 的误报。 批准 ✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
✅ Local verification report — PR #5323
|
| Check | Result |
|---|---|
PR test suite (McpPromptLoader.test.ts) |
✅ 27/27 pass (25 pre-existing + 2 new) |
| Genuine-regression — PR's tests on BASE source | ✅ exactly the 2 new tests fail ({} vs {trail:''}; Error: Missing required argument(s): --name, --age vs {name:'',age:''}); 25 pre-existing pass |
Adversarial edge sweep (real parseArgs, 7 cases) |
✅ AFTER 7/7 pass · BASE 6/7 fail (see below) |
tsc --noEmit cli (authoritative) |
✅ 0 new type errors (2 pre-existing stale-dist TS6305 on an unrelated toml-to-markdown-converter core file, identical base/after) |
npm run build core |
✅ exit 0 |
prettier --check + eslint (both files) + git diff --check |
✅ all exit 0 |
Adversarial edge sweep (AFTER all pass; BASE diverges)
| case | PR (after) | base |
|---|---|---|
--trail="" (optional) |
{trail:''} ✅ |
{} ❌ |
--name="" --age="" (2 required) |
{name:'',age:''} ✅ |
Error: missing ❌ |
--trail="" hello (empty named + positional) |
{trail:'',pos:'hello'} ✅ |
{pos:'hello'} (trail lost) ❌ |
--name="" --age="" Lion (2 empty + 1 positional) |
{name:'',age:'',species:'Lion'} ✅ |
Error: missing --age ❌ |
--flag="0" |
{flag:'0'} ✅ |
{flag:'0'} ✅ (truthy, never broken) |
arg named toString, --toString="" |
{toString:''} ✅ |
{} ❌ |
required arg named toString, NOT provided |
value is not the inherited fn ✅ | leaks Function.prototype.toString ❌ |
The last two cases are a robustness bonus: Object.hasOwn (unlike the old truthy/in-style access) won't pick up an inherited Object.prototype member when a prompt arg happens to be named toString/constructor/etc. On base, a required toString arg that the user omits silently "passes" with the inherited function as its value — the PR fixes that latent quirk too.
Note on pre-existing behavior (not changed, not a regression)
A single unfilled required arg with no input is filled with '' via positionalArgs.join('') (the "one unfilled arg" branch), rather than reported missing. So a lone empty required arg already happened to "work"; the user-visible bug bit when two or more required args were empty (the issue's --name="" --age="" case). The PR's fix makes all these cases consistent.
Reproduce
git worktree add --detach /tmp/wt-after $(gh pr view 5323 --json headRefOid -q .headRefOid)
git worktree add --detach /tmp/wt-before origin/main
for w in wt-after wt-before; do ln -s "$PWD/node_modules" /tmp/$w/node_modules; \
ln -s "$PWD/packages/cli/node_modules" /tmp/$w/packages/cli/node_modules; \
ln -s "$PWD/packages/core/node_modules" /tmp/$w/packages/core/node_modules; done
(cd /tmp/wt-after/packages/cli && ../../node_modules/.bin/vitest run --coverage.enabled=false src/services/McpPromptLoader.test.ts) # 27/27
# Catch the bug: copy PR's test onto BASE source -> 2 fail
cp /tmp/wt-after/packages/cli/src/services/McpPromptLoader.test.ts /tmp/wt-before/packages/cli/src/services/McpPromptLoader.test.ts
(cd /tmp/wt-before/packages/cli && ../../node_modules/.bin/vitest run --coverage.enabled=false src/services/McpPromptLoader.test.ts)🇨🇳 中文版(点击展开)
✅ 本地验证报告 — PR #5323 fix(cli): preserve empty MCP prompt args
结论:强烈建议合并。 这是一个正确、最小、范围聚焦的修复。空字符串具名参数(--trail=""、--name="")在 McpPromptLoader.parseArgs 里被真值判断丢弃;改用 Object.hasOwn 存在性判断即可修复。新测试确实能抓到这个 bug,修复在 7 个对抗性边界用例下都成立,甚至比旧判断更正确(不会泄漏原型成员),且无回归、无类型/lint/格式问题。
根因与修复
具名参数正则其实已经把 --trail="" 存成 argValues['trail'] = '';bug 出在消费它的判断:
- 拷贝循环:
if (argValues[arg.name])→''为假 → 丢弃。改为if (Object.hasOwn(argValues, arg.name))。 - 未填/必填检查:
!promptInputs[arg.name]→''为假 → 误判缺失。改为!Object.hasOwn(promptInputs, arg.name)。
验证方式
在 tmux 中、两个隔离 worktree 里运行真实 vitest:AFTER = e3f22f0e(PR head),BEFORE = f91054a3(origin/main),软链接复用仓库 node_modules。Node v22.22.2、vitest 3.2.4。可干净合并;目标文件在 merge-base 与 main 之间一致。
| 检查项 | 结果 |
|---|---|
PR 测试(McpPromptLoader.test.ts) |
✅ 27/27 通过(25 既有 + 2 新增) |
| 回归测试有效性 —— PR 的测试跑 BASE 源码 | ✅ 恰好这 2 个新测试失败({} vs {trail:''};Error: Missing required argument(s): --name, --age vs {name:'',age:''});25 既有通过 |
对抗性边界扫描(真实 parseArgs,7 个用例) |
✅ AFTER 7/7 通过 · BASE 6/7 失败(见下表) |
tsc --noEmit cli(权威) |
✅ 0 新增类型错误(2 个既有 stale-dist TS6305,来自与本 PR 无关的 toml-to-markdown-converter core 文件,base/after 一致) |
npm run build core |
✅ exit 0 |
prettier --check + eslint(两个文件)+ git diff --check |
✅ 均 exit 0 |
对抗性边界扫描(AFTER 全过;BASE 分叉)
| 用例 | PR(修复后) | base |
|---|---|---|
--trail=""(可选) |
{trail:''} ✅ |
{} ❌ |
--name="" --age=""(2 个必填) |
{name:'',age:''} ✅ |
Error: 缺失 ❌ |
--trail="" hello(空具名 + 位置参数) |
{trail:'',pos:'hello'} ✅ |
{pos:'hello'}(trail 丢失)❌ |
--name="" --age="" Lion(2 空 + 1 位置) |
{name:'',age:'',species:'Lion'} ✅ |
Error: 缺失 --age ❌ |
--flag="0" |
{flag:'0'} ✅ |
{flag:'0'} ✅(真值,从未坏) |
参数名为 toString,--toString="" |
{toString:''} ✅ |
{} ❌ |
必填参数名为 toString,未提供 |
值不是继承的函数 ✅ | 泄漏 Function.prototype.toString ❌ |
最后两个用例是一个健壮性加成:Object.hasOwn(不同于旧的真值/in 式访问)在 prompt 参数名恰好是 toString/constructor 等时,不会取到继承自 Object.prototype 的成员。在 base 上,用户省略一个必填的 toString 参数时会“静默通过”,其值是继承的函数——本 PR 也顺带修掉了这个隐藏问题。
关于既有行为的说明(未改动、非回归)
当只有一个未填的必填参数且无输入时,会通过 positionalArgs.join('') 被填成 ''(“单个未填参数”分支),而不是报告缺失。所以单个空必填参数本来就“碰巧能用”;用户可见的 bug 在两个及以上必填参数为空时才出现(即 issue 里的 --name="" --age="")。本 PR 让这些情况都变得一致。
Verified locally with real vitest under tmux on isolated worktrees of the PR head and origin/main; bug reality proven by running the PR's tests against base source; parseArgs audited with a 7-case adversarial edge sweep (incl. a prototype-member-name robustness case) against base behavior.
What this PR does
Fixes argument parsing in the MCP prompt loader so that a named argument explicitly set to an empty string is treated as provided rather than missing.
In
McpPromptLoader.parseArgs, two truthy checks are replaced with presence checks (Object.hasOwn):--trail=""(previously dropped, producing{}).--name=""are no longer reported as missing.Why it's needed
MCP prompt named arguments explicitly set to an empty string were parsed as if they were absent. An empty string is still a valid string value, but the old truthy checks in
McpPromptLoader.parseArgsdiscarded it:--trail=""produced{}for an optionaltrailargument.--name="" --age=""reported both required arguments as missing, even though both flags were supplied.This fix makes the parser track whether a named argument was present rather than whether its value is truthy. (See issue #5322.)
Reviewer Test Plan
How to verify
Reproduce the bug by passing empty-string named args to an MCP prompt: with the old code,
--trail=""yields{}and--name="" --age=""is flagged as missing required arguments; with this change, the empty values are preserved ({ trail: '' },{ name: '', age: '' }) and required args are no longer reported missing.This is covered by two new unit tests in
McpPromptLoader.test.ts("should preserve empty optional named arguments" and "should treat empty required named arguments as provided").Verification commands run by the author:
npx vitest run src/services/McpPromptLoader.test.tsfrompackages/clinpm run typecheck --workspace=packages/clinpm run build --workspace=packages/corenpm run build --workspace=packages/clinpm run lintnpx prettier --experimental-cli --check packages/cli/src/services/McpPromptLoader.ts packages/cli/src/services/McpPromptLoader.test.tsgit diff --checkEvidence (Before & After)
N/A — non-visible logic fix; covered by the unit tests above.
Tested on
✅ tested ·⚠️ not tested · N/A
Environment (optional)
Unit tests only (npm workspaces).
Risk & Scope
McpPromptLoader.parseArgs. The behavior change is limited to named arguments whose value is an empty string — they are now retained instead of dropped.Linked Issues
Fixes #5322
中文说明
这个 PR 做了什么
修复 MCP prompt 加载器的参数解析:当某个具名参数被显式设置为空字符串时,现在会被视为已提供,而不是缺失。
在
McpPromptLoader.parseArgs中,两处真值(truthy)判断被替换为存在性判断(Object.hasOwn):--trail=""这样的可选空参数(此前会被丢弃,得到{})。--name=""这样的必填具名参数不再被报告为缺失。为什么需要它
被显式设置为空字符串的 MCP prompt 具名参数此前会被当作不存在处理。空字符串仍然是合法的字符串值,但
McpPromptLoader.parseArgs中旧的真值判断把它丢弃了:trail参数,--trail=""会得到{}。--name="" --age=""会把这两个必填参数都报告为缺失,尽管两个标志都已传入。本次修复让解析器跟踪具名参数是否存在,而不是判断其值是否为真值。(见 issue #5322。)
审阅者测试计划
如何验证
复现该 bug:向 MCP prompt 传入空字符串具名参数。使用旧代码时,
--trail=""得到{},而--name="" --age=""会被标记为缺失必填参数;应用本次改动后,空值被保留({ trail: '' }、{ name: '', age: '' }),且必填参数不再被报告为缺失。这一点由
McpPromptLoader.test.ts中两个新增单元测试覆盖("should preserve empty optional named arguments" 与 "should treat empty required named arguments as provided")。作者运行的验证命令:
packages/cli下执行npx vitest run src/services/McpPromptLoader.test.tsnpm run typecheck --workspace=packages/clinpm run build --workspace=packages/corenpm run build --workspace=packages/clinpm run lintnpx prettier --experimental-cli --check packages/cli/src/services/McpPromptLoader.ts packages/cli/src/services/McpPromptLoader.test.tsgit diff --check证据(前后对比)
N/A —— 非可见的逻辑修复;已由上述单元测试覆盖。
测试平台
✅ 已测试 ·⚠️ 未测试 · N/A
环境(可选)
仅单元测试(npm workspaces)。
风险与范围
McpPromptLoader.parseArgs。行为变化仅限于值为空字符串的具名参数——它们现在会被保留而非丢弃。关联 Issue
Fixes #5322
AI Assistance Disclosure
I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.