Skip to content

fix(extension): accept uppercase URL schemes in Claude plugin sources - #5461

Merged
wenshao merged 3 commits into
QwenLM:mainfrom
he-yufeng:fix/claude-converter-uppercase-url
Jun 20, 2026
Merged

fix(extension): accept uppercase URL schemes in Claude plugin sources#5461
wenshao merged 3 commits into
QwenLM:mainfrom
he-yufeng:fix/claude-converter-uppercase-url

Conversation

@he-yufeng

Copy link
Copy Markdown
Contributor

What

Make the string plugin-source URL-scheme check in resolvePluginSource (Claude plugin conversion) case-insensitive.

Why

A string plugin source in marketplace.json was compared against http:// / https:// case-sensitively, so an uppercase scheme such as HTTPS://github.com/owner/repo fell through to local-path handling and failed with Plugin source not found at .../HTTPS:/github.com/owner/repo.

This is the same bug class already fixed for MCP transport detection (#5426), extension install sources (#5429), and weixin CDN uploads (#5439).

Reviewer Test Plan

  • npx vitest run packages/core/src/extension/claude-converter.test.ts
  • The new test convertClaudePluginPackage — string URL source › treats an uppercase HTTPS:// source as a URL download, not a local path fails before this change (throws Plugin source not found) and passes after it.

Risk

Low. One-line normalization — lowercase the source before the scheme check, matching the earlier fixes. Valid lowercase URLs and non-URL local paths are unaffected, and the rest of the claude-converter suite still passes.

he-yufeng and others added 2 commits June 20, 2026 17:08
resolvePluginSource compared a string plugin source against 'http://' and
'https://' case-sensitively, so a marketplace.json source such as
'HTTPS://github.com/owner/repo' fell through to local-path handling and
failed with "Plugin source not found". Lowercase the source before the
scheme check, matching QwenLM#5426 / QwenLM#5429 / QwenLM#5439.
@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

The Lint job and all three Test jobs are red for the same reason — a TypeScript compile error, not an actual test failure. They all die inside npm ci, because build (which runs tsc --build) is wired into the prepare lifecycle hook, so CI never even reaches the lint rules or the test runner.

src/extension/claude-converter.test.ts(1393,7): error TS2345: Argument of type
'(_meta: ExtensionInstallMetadata, dir: string) => Promise<void>' is not assignable to parameter of type
'NormalizedProcedure<(installMetadata: ExtensionInstallMetadata, destination: string) => Promise<GitHubDownloadResult>>'.
  Type 'Promise<void>' is not assignable to type 'Promise<GitHubDownloadResult>'.
    Type 'void' is not assignable to type 'GitHubDownloadResult'.

Root cause: the new test mocks downloadFromGitHubRelease, but the mock implementation returns nothing (Promise<void>), while the real signature returns Promise<GitHubDownloadResult> (packages/core/src/extension/github.ts:266):

export async function downloadFromGitHubRelease(...): Promise<GitHubDownloadResult>
// GitHubDownloadResult = { tagName: string; type: 'git' | 'github-release' }

vi.fn().mockImplementation() is typed to match the mocked function's signature, so a void-returning impl is rejected by tsc.

Fix — return a GitHubDownloadResult at the end of the mock:

vi.mocked(downloadFromGitHubRelease).mockImplementation(
  async (_meta, dir) => {
    fs.mkdirSync(path.join(dir as string, '.claude-plugin'), { recursive: true });
    fs.writeFileSync(
      path.join(dir as string, '.claude-plugin', 'plugin.json'),
      JSON.stringify({ name: 'p', version: '1.0.0' }),
      'utf-8',
    );
    return { tagName: 'v1.0.0', type: 'github-release' }; // <-- add this
  },
);

The production change itself (the case-insensitive scheme check in resolvePluginSource) is correct — only the test mock needs this one line.

中文版

Lint 和三个平台的 Test job 全红是同一个原因——一个 TypeScript 编译错误,并不是测试真的跑挂了。它们全部在 npm ci 阶段就崩了,因为 build(会执行 tsc --build)挂在 prepare 生命周期钩子上,所以 CI 根本没走到 lint 规则和测试执行那一步。

src/extension/claude-converter.test.ts(1393,7): error TS2345 ...
  Type 'Promise<void>' is not assignable to type 'Promise<GitHubDownloadResult>'.

根因: 新增测试 mock 了 downloadFromGitHubRelease,但 mock 实现没有任何返回值(Promise<void>),而真实函数签名返回的是 Promise<GitHubDownloadResult>(packages/core/src/extension/github.ts:266):

export async function downloadFromGitHubRelease(...): Promise<GitHubDownloadResult>
// GitHubDownloadResult = { tagName: string; type: 'git' | 'github-release' }

mockImplementation 在类型上要求实现的签名与被 mock 的函数一致,所以返回 void 的实现会被 tsc 拒绝。

修法——在 mock 末尾返回一个 GitHubDownloadResult

return { tagName: 'v1.0.0', type: 'github-release' }; // <-- 补这一行

生产代码本身(resolvePluginSource 里大小写不敏感的 scheme 判断)是对的——只有测试 mock 需要补这一行。

The mockImplementation returned void, which tsc --build rejected (TS2345)
even though vitest passed. Return a GitHubDownloadResult so the build is clean.
@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR @he-yufeng!

Template: Most sections present (What, Why, Reviewer Test Plan, Risk), but a few template subsections are missing — "How to verify", "Evidence (Before & After)", "Tested on" table, "Linked Issues", and the Chinese translation. Not blocking on this since the PR content is clear enough, but worth filling in next time for faster review turnaround.

Direction: Solid bugfix. The case-sensitive URL scheme check in resolvePluginSource is the same bug class as the earlier fixes for MCP transport (#5426), extension install (#5429), and weixin CDN (#5439). Straightforward alignment — no direction concerns.

Approach: Minimal and correct. Create lowerSource for the scheme comparison, pass the original source to downloadFromGitHubRelease/cloneFromGit so the actual URL is preserved. One normalization, one test, no scope creep. This is exactly what a focused bugfix should look like.

Moving on to code review. 🔍

中文说明

感谢 @he-yufeng 的贡献!

模板: 大部分章节齐全(What、Why、Reviewer Test Plan、Risk),但缺少部分模板子章节——"How to verify"、"Evidence (Before & After)"、"Tested on" 表格、"Linked Issues" 和中文翻译。不因此阻塞,但建议下次补全以加快审查速度。

方向: 合理的 bugfix。resolvePluginSource 中的大小写敏感 URL scheme 检查与之前的 MCP transport (#5426)、extension install (#5429)、weixin CDN (#5439) 属于同类问题。方向无争议。

方案: 最小改动且正确。创建 lowerSource 用于 scheme 比较,原始 source 传给 downloadFromGitHubRelease/cloneFromGit 以保留实际 URL。一处归一化、一个测试、无多余改动。这就是一个聚焦 bugfix 该有的样子。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: The fix for case-sensitive URL scheme matching in resolvePluginSource is straightforward — lowercase the source string before the startsWith('http://') / startsWith('https://') check, while preserving the original source value for the actual download/clone calls. That's exactly what this PR does.

Diff assessment: The change is correct and minimal. lowerSource is scoped only to the scheme comparison; the original source is correctly passed through to downloadFromGitHubRelease and cloneFromGit, so actual URL handling is unaffected. No blockers found.

Test Results

Before (reverted fix — main behavior)

The new test fails, confirming the bug exists without the fix:

 ❯ src/extension/claude-converter.test.ts (41 tests | 1 failed) 120ms
   × convertClaudePluginPackage — string URL source > treats an uppercase HTTPS:// source as a URL download, not a local path 14ms
     → Plugin source not found at /tmp/claude-url-oM0l6k/HTTPS:/github.com/owner/repo

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯

 FAIL  src/extension/claude-converter.test.ts > convertClaudePluginPackage — string URL source > treats an uppercase HTTPS:// source as a URL download, not a local path
Error: Plugin source not found at /tmp/claude-url-oM0l6k/HTTPS:/github.com/owner/repo
 ❯ resolvePluginSource src/extension/claude-converter.ts:1041:13
    1039|
    1040|     if (!fs.existsSync(sourcePath)) {
    1041|       throw new Error(
       |             ^
    1042|         `Plugin source not found at ${sanitizeForError(sourcePath)}`,
    1043|       );

 Test Files  1 failed (1)
      Tests  1 failed | 40 passed (41)
   Duration  7.16s

After (with PR fix applied)

All 41 tests pass, including the new uppercase URL scheme test:

 ✓ src/extension/claude-converter.test.ts (41 tests) 110ms

 Test Files  1 passed (1)
      Tests  41 passed (41)
   Duration  7.44s
中文说明

代码审查

独立方案: 修复 resolvePluginSource 中大小写敏感的 URL scheme 匹配问题——在 startsWith('http://') / startsWith('https://') 检查前将 source 转为小写,同时保留原始 source 值用于实际的下载/克隆调用。PR 的实现与此完全一致。

Diff 评估: 改动正确且最小化。lowerSource 仅用于 scheme 比较;原始 source 正确传递给 downloadFromGitHubReleasecloneFromGit,实际 URL 处理不受影响。无阻塞问题。

测试结果

修复前: 新增测试失败,确认 bug 存在——Plugin source not found at .../HTTPS:/github.com/owner/repo(1 failed | 40 passed)

修复后: 全部 41 个测试通过,包括新增的大写 URL scheme 测试。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Verdict: Approve ✅

This is a clean, minimal bugfix that does exactly what it says. The case-sensitive URL scheme check was a real bug — an uppercase HTTPS:// in marketplace.json would silently fall through to local-path handling and fail. The fix is a single normalization (source.toLowerCase()) scoped correctly so the original URL is preserved for downstream use.

The test is well-crafted: it reproduces the exact failure mode (uppercase scheme → Plugin source not found), and the before/after confirms it — 1 test fails without the fix, all 41 pass with it.

This follows the same pattern already established for MCP transport, extension install sources, and weixin CDN uploads. Consistent defensive coding.

中文说明

结论:批准 ✅

这是一个干净、最小化的 bugfix。大小写敏感的 URL scheme 检查是一个真实的 bug——marketplace.json 中的大写 HTTPS:// 会默默走到本地路径处理并失败。修复方案是单次归一化(source.toLowerCase()),作用域正确,原始 URL 保留给下游使用。

测试设计良好:精确复现了失败场景(大写 scheme → Plugin source not found),前后对比证实了修复效果——无修复时 1 个测试失败,修复后全部 41 个通过。

与 MCP transport、extension install、weixin CDN 的同类修复保持一致。

Qwen Code · qwen3.7-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.

LGTM, looks ready to ship. ✅

@wenshao wenshao 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.

No issues found. The fix is correct and minimal: normalizing the source string to lowercase before the URL scheme check matches the established pattern from #5426, #5429, and #5439. Test coverage is adequate for the change.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification — real tests (recommend merge)

TL;DR: The fix is correct, minimal, and consistent with the sibling case-insensitivity fixes (#5426 / #5429 / #5439). The new test genuinely reproduces the bug, and there are no regressions. I built and ran the real claude-converter suite in a local tmux session on the PR head (8fc886ea4, which already includes the merge from main), then added an A/B revert-proof, a 10-case branch/edge harness, and an independent typecheck pass.

Environment: Node v22.22.2 · npm 10.9.7 · vitest 3.2.4 · real runs in a dedicated tmux session.

1) PR's own suite — green

npx vitest run packages/core/src/extension/claude-converter.test.ts
→ Test Files 1 passed (1) · Tests 41 passed (41)

Includes the new string URL source › treats an uppercase HTTPS:// source as a URL download, not a local path.

2) A/B revert-proof — the test really catches the bug

Reverting only the one-line fix (restoring the case-sensitive check) while keeping the new test:

Source state New uppercase-URL test Suite
Base (case-sensitive) Error: Plugin source not found at /tmp/claude-url-XXXX/HTTPS:/github.com/owner/repo — thrown at resolvePluginSource …claude-converter.ts:1037 1 failed / 40 passed
PR (case-insensitive) ✅ routed to the download branch 41 passed

The base-state error string matches the PR description verbatim (…/HTTPS:/github.com/owner/repo — the :// collapses to :/ once path.join treats it as a local path). The other 40 tests pass in both states, so nothing outside the targeted branch changes.

3) Branch/edge harness — 10 extra cases on the real code path

Drove the real resolvePluginSource (through convertClaudePluginPackage, mocking only the two network functions, exactly like the PR's own test):

  • 7 scheme variantshttp://, https://, HTTP://, HTTPS://, Https://, HtTpS://, hTTPs:// — all routed to the download branch, and the original case-preserved source string is what reaches the downloader (installMetadata.source === original). Lowercasing is detection-only and never mangles the URL that gets downloaded.
  • Download-throws → cloneFromGit fallback is still reached for an uppercase URL (both downloader entry points work).
  • Local paths that merely contain "http"/"https" (https-helpers, HTTP-TOOLS) stay on the local branch with no download — the fix does not widen URL matching.

Discrimination (same harness, base vs PR):

Cases Base PR
http://, https:// (lowercase URL) ✅ — no regression
https-helpers, HTTP-TOOLS (local path) ✅ — not misclassified
5 × uppercase/mixed scheme + clone-fallback

→ Base: 6 failed / 4 passed; PR: 10 passed. (Run together with the PR's own file: 51 passed.)

4) Typecheck — validates the 3rd commit (vitest does not typecheck)

tsc --noEmit on core is clean for both changed files. Reverting the mock's return value to void reproduces the exact TS2345 at claude-converter.test.ts:1393

Argument of type '…=> Promise<void>' is not assignable to parameter of type
'NormalizedProcedure<…=> Promise<GitHubDownloadResult>>'.
  Type 'void' is not assignable to type 'GitHubDownloadResult'.

— that commit 8fc886ea4 fixes. So that commit is both necessary and sufficient; with the PR's actual return ({ tagName, type }) the build is clean.

An unrelated TS2339: Property 'mergeModelsByIdentity' does not exist on type 'ProviderConfig' appears in providers/__tests__/presets/ — that is a local artifact of my symlinked node_modules (that test imports ProviderConfig from the published @qwen-code/qwen-code-core dist, the #5404 area), not touched by this PR and not present in CI.

Nit (non-blocking, in favor of the PR)

The fix correctly uses toLowerCase() (Unicode-default, locale-independent) rather than toLocaleLowerCase(), so scheme detection stays correct even under the Turkish I → ı locale. 👍

Verdict: ✅ LGTM — safe to merge.

🇨🇳 中文版本(点击展开)

✅ 本地真实测试验证(建议合并)

结论: 修复正确、最小化,且与同类大小写不敏感修复(#5426 / #5429 / #5439)保持一致。新增测试能真实复现该 bug,且无任何回归。我在本地 tmux 会话中基于 PR HEAD(8fc886ea4,已包含对 main 的合并)构建并运行了真实的 claude-converter 测试套件,并补充了 A/B 反证、10 个分支/边界用例的 harness,以及一次独立的类型检查。

环境: Node v22.22.2 · npm 10.9.7 · vitest 3.2.4 · 在专用 tmux 会话中真实运行。

1)PR 自带测试套件 — 通过

npx vitest run packages/core/src/extension/claude-converter.test.ts
→ Test Files 1 passed (1) · Tests 41 passed (41)

其中包含新增用例 treats an uppercase HTTPS:// source as a URL download, not a local path

2)A/B 反证 — 证明该测试确实能抓住 bug

回退这一行修复(恢复大小写敏感判断)、保留新增测试:

源码状态 新增大写 URL 用例 套件
Base(大小写敏感) Error: Plugin source not found at /tmp/claude-url-XXXX/HTTPS:/github.com/owner/repo,抛出于 resolvePluginSource …claude-converter.ts:1037 1 失败 / 40 通过
PR(大小写不敏感) ✅ 进入下载分支 41 通过

Base 状态下的报错字符串与 PR 描述完全一致(…/HTTPS:/github.com/owner/repo —— 一旦被当作本地路径,path.join 会把 :// 折叠成 :/)。其余 40 个用例在两种状态下都通过,说明改动范围之外没有任何行为变化。

3)分支/边界 harness — 在真实代码路径上补充 10 个用例

直接驱动真实resolvePluginSource(经由 convertClaudePluginPackage,仅 mock 两个网络函数,与 PR 自带测试做法完全一致):

  • 7 种 scheme 变体 —— http://https://HTTP://HTTPS://Https://HtTpS://hTTPs:// —— 全部进入下载分支;并且传给下载函数的是保留原始大小写的 source 字符串(installMetadata.source === 原值)。小写化只用于"判断",不会篡改真正用于下载的 URL。
  • 下载抛错 → cloneFromGit 回退:大写 URL 仍能到达回退路径(两个下载入口都正常)。
  • 仅包含 "http"/"https" 子串的本地路径https-helpersHTTP-TOOLS)仍停留在本地分支、触发下载 —— 修复没有扩大 URL 匹配范围。

区分度(同一 harness,base vs PR):

用例 Base PR
http://https://(小写 URL) ✅ —— 无回归
https-helpersHTTP-TOOLS(本地路径) ✅ —— 未被误判
5 个大写/混合大小写 scheme + clone 回退

→ Base:6 失败 / 4 通过;PR:10 通过。(与 PR 自带文件一起运行:51 通过。)

4)类型检查 — 验证第 3 个提交(vitest 不做类型检查)

core 执行 tsc --noEmit,两个被改文件均无错误。把 mock 的返回值改回 void 会精确复现 claude-converter.test.ts:1393 处的 TS2345

Argument of type '…=> Promise<void>' is not assignable to parameter of type
'NormalizedProcedure<…=> Promise<GitHubDownloadResult>>'.
  Type 'void' is not assignable to type 'GitHubDownloadResult'.

—— 这正是提交 8fc886ea4 所修复的问题。因此该提交既必要又充分;用 PR 的真实返回值({ tagName, type })后构建干净。

providers/__tests__/presets/ 中出现的 TS2339: Property 'mergeModelsByIdentity' does not exist on type 'ProviderConfig' 与本 PR 无关 —— 它是我本地软链接 node_modules 的产物(该测试从已发布的 @qwen-code/qwen-code-core dist 引入 ProviderConfig,属于 #5404 范围),本 PR 未触及该文件,CI 中也不会出现。

小建议(不阻塞,且对 PR 有利)

修复正确地使用了 toLowerCase()(Unicode 默认、与区域设置无关),而非 toLocaleLowerCase(),因此即便在土耳其语 I → ı 区域设置下,scheme 判断依然正确。👍

结论:✅ LGTM —— 可以安全合并。

Verification artifacts: dedicated tmux session + worktree on PR head; full logs of all four layers retained locally.

@wenshao
wenshao merged commit 234777c into QwenLM:main Jun 20, 2026
24 checks passed
@wenshao

wenshao commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

✅ Verification report — local real-world testing (routing fix correct; one important gap to consider)

Verified locally on Linux (Node 22.22.2) with unit tests, a before/after regression check, a routing/downstream/sibling harness against the built bundle, and live tmux runs of the real CLI (qwen extensions install). The routing fix is correct and a clear net improvement, and all checks pass. Live testing also surfaced one important functional gap: the fix is necessary but not sufficient for uppercase URLs that resolve via git clone — details below.

What the bug was

resolvePluginSource checked the plugin source scheme case-sensitively (source.startsWith('http://'|'https://')), so HTTPS://github.com/owner/repo fell through to local-path handling and failed with Plugin source not found at .../HTTPS:/.... The PR lowercases the source before the scheme check. ✔️ correct and matches the earlier #5426/#5429/#5439 fixes.

Verification performed

1. claude-converter.test.ts — 41/41 pass, including the new uppercase-HTTPS test.

2. Before/after (test is load-bearing). Reverting only the source to the old case-sensitive check makes the new test fail with the exact bug:

Error: Plugin source not found at /tmp/claude-url-XXXX/HTTPS:/github.com/owner/repo

Restoring the fix → passes.

3. Harness against the shipped dist — routing 10/10, downstream 4/4.

  • Routing: HTTPS:// / HTTP:// / HtTpS:// → URL; ./rel, /abs, bare, empty, HTTPS_NOT_A_URL/x → local. No regression for lowercase.
  • Downstream: the real parseGitHubRepoForReleases('HTTPS://github.com/owner/repo'){owner, repo} for all casings — so the GitHub release path handles uppercase end-to-end.

4. Live tmux E2E — real qwen extensions install <localMarketplace>:p (plugin source is the variable; all else identical):

# plugin source observed result
A HTTPS://github.com/… (uppercase) routed to URL download → git: 'remote-HTTPS' is not a git command / remote helper 'HTTPS' aborted
B https://github.com/… (lowercase) routed to URL download → reached network (could not read Username…)
C ./does-not-exist (local) Plugin source not found … ✔️ correct, no over-correction

A no longer produces C's local-path error → the routing fix works. But A and B then diverge (see below).

5. Static + CI. eslint clean · prettier --check clean · git diff --check clean · tsc (core) 0 errors · GitHub CI all green (Lint, Test ubuntu/macos/windows, CodeQL).


⚠️ Important — the fix is necessary but not sufficient (uppercase still fails on the git clone path)

resolvePluginSource passes the original (uppercase) source downstream. Two downstream paths behave differently:

  • GitHub release (downloadFromGitHubRelease): parses to {owner, repo} and rebuilds fresh https://api.github.com/...uppercase works. ✔️
  • git clone fallback (cloneFromGit, used when the repo has no release): runs git clone HTTPS://... with the raw scheme. git is case-sensitive about the scheme and treats HTTPS:// as a remote helper:
$ git ls-remote HTTPS://github.com/QwenLM/qwen-code   # real, existing repo
git: 'remote-HTTPS' is not a git command.
fatal: remote helper 'HTTPS' aborted session
$ git ls-remote https://github.com/QwenLM/qwen-code
dc1465019f03b7a75f08876275bde158faab0887  HEAD          # works

cloneFromGit only normalizes the scheme incidentally when GITHUB_TOKEN is set (the token-injection branch calls new URL(...).toString()); with no token it sends the raw uppercase URL to git. So a Claude plugin whose source is an uppercase URL to a repo without GitHub releases still fails to install — now with a more cryptic remote helper 'HTTPS' aborted instead of Plugin source not found. Net improvement over before, but the PR's stated goal ("accept uppercase URL schemes in Claude plugin sources") isn't fully met for the clone path.

Suggestion: normalize just the scheme to lowercase when building installMetadata.source (or inside cloneFromGit), e.g. source.replace(/^(https?):\/\//i, (_, s) => s.toLowerCase() + '://'). The PR's own test masks this because it mocks downloadFromGitHubRelease to always succeed, so the clone fallback with an uppercase URL is never exercised — worth a test that drives the clone path.

Completeness — sibling checks with the same bug class (not touched by this PR)

  • sourceRegistry.ts:131 parseExtensionSourceType — confirmed live: parseExtensionSourceType('HTTPS://github.com/o/r') returns 'local' (should be 'github').
  • marketplace.ts:286 loadMarketplaceConfigFromSourcetrimmed (line 264) isn't lowercased, so an uppercase marketplace URL isn't recognized.
  • (Note isGitUrl at marketplace.ts:116 already lowercases — so the file is internally inconsistent.) A shared isUrlScheme() helper would prevent the next instance.

Verdict

The routing fix is correct, minimal, and a net improvement — mergeable on its own. But to actually deliver the stated goal for all uppercase URLs, I recommend also normalizing the scheme before git clone (the gap is reproducible live), and consider the sibling spots + a clone-path test. Your call whether to extend this PR or follow up.

🇨🇳 中文版(点击展开)

✅ 验证报告 —— 本地真实测试(路由修复正确;有一个值得注意的缺口)

Linux(Node 22.22.2) 上用单测、前后对照、针对构建产物的路由/下游/兄弟函数电池,以及 真实 CLI 的 tmux 实机 qwen extensions install 完成验证。路由修复 正确且是明显改进,各项检查通过。但实机测试发现一个 重要的功能缺口:对于走 git clone 的大写 URL,本修复 必要但不充分

漏洞本身

resolvePluginSource 对插件 source 的 scheme 做了大小写敏感判断,导致 HTTPS://github.com/owner/repo 落到本地路径分支并报 Plugin source not found at .../HTTPS:/...。PR 在判断前先 toLowerCase()。✔️ 正确,且与 #5426/#5429/#5439 一致。

已完成的验证

  1. claude-converter.test.ts 41/41 通过,含新增大写 HTTPS 用例。

  2. 前后对照:仅把源码改回大小写敏感判断 → 新用例报出原 bug:Plugin source not found at .../HTTPS:/github.com/owner/repo;恢复修复 → 通过。

  3. 针对 dist 的电池:路由 10/10(大写/混合大小写→URL;./rel/abs、空、HTTPS_NOT_A_URL/x→本地;小写不回归);下游 4/4(真实 parseGitHubRepoForReleases('HTTPS://...'){owner,repo},即 GitHub release 路径端到端支持大写)。

  4. 实机 tmux(真实 qwen extensions install <本地市场>:p,仅插件 source 不同)

    • A 大写 HTTPS://… → 路由到下载 → git: 'remote-HTTPS' is not a git command / remote helper 'HTTPS' aborted
    • B 小写 https://… → 路由到下载 → 进到网络阶段(could not read Username…
    • C 本地 ./does-not-existPlugin source not found … ✔️ 正确(无过度纠正)

    A 不再产生 C 那种本地路径错误 → 路由修复生效;但 A 与 B 随后出现分歧(见下)。

  5. 静态 + CIeslintprettier --checkgit diff --check 干净;tsc(core)0 错误;CI 全绿。

⚠️ 重要 —— 修复必要但不充分(大写在 git clone 路径仍失败)

resolvePluginSource原始(大写)source 传给下游,两条下游路径表现不同:

  • GitHub releasedownloadFromGitHubRelease):解析成 {owner, repo} 并重建全新小写 https://api.github.com/...大写可用 ✔️
  • git clone 回退cloneFromGit,仓库无 release 时):直接 git clone HTTPS://...。git 对 scheme 大小写敏感,会把 HTTPS:// 当作 remote helper:
$ git ls-remote HTTPS://github.com/QwenLM/qwen-code   # 真实存在的仓库
git: 'remote-HTTPS' is not a git command.
fatal: remote helper 'HTTPS' aborted session
$ git ls-remote https://github.com/QwenLM/qwen-code
dc1465019f03b7a75f08876275bde158faab0887  HEAD          # 正常

cloneFromGit 只有在 设置了 GITHUB_TOKEN 时才会顺带规范化 scheme(token 注入分支调用 new URL(...).toString());无 token 时把原始大写 URL 交给 git。因此 source 为大写 URL、且仓库 无 GitHub release 的 Claude 插件仍然装不上 —— 只是错误从 Plugin source not found 变成更隐晦的 remote helper 'HTTPS' aborted。相比改动前是改进,但 PR 目标(“接受大写 URL scheme 的 Claude 插件源”)对 clone 路径未完全达成。

建议:在构建 installMetadata.source 时(或在 cloneFromGit 内)仅规范化 scheme,例如 source.replace(/^(https?):\/\//i, (_, s) => s.toLowerCase() + '://')。PR 自带测试把 downloadFromGitHubRelease mock 成必然成功,因此从未走到带大写 URL 的 clone 回退 —— 建议补一条驱动 clone 路径的测试。

完整性 —— 同类大小写问题的兄弟点(本 PR 未触及)

  • sourceRegistry.ts:131 parseExtensionSourceType —— 实测确认parseExtensionSourceType('HTTPS://github.com/o/r') 返回 'local'(应为 'github')。
  • marketplace.ts:286 loadMarketplaceConfigFromSource —— trimmed(264 行)未小写化,大写市场 URL 不被识别。
  • (注意 marketplace.ts:116isGitUrl 已经小写化 —— 同一文件内不一致。)抽一个共享的 isUrlScheme() 可避免再次出现。

结论

路由修复正确、最小、是净改进 —— 可单独合并。但要对 所有 大写 URL 真正达成目标,建议同时在 git clone 前规范化 scheme(缺口已实机复现),并考虑兄弟点 + 补一条 clone 路径测试。是否在本 PR 内扩展或另开后续,由你决定。

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