Skip to content

fix(sdk): support relative daemon base URLs in remaining workspace file helpers - #9817

Merged
wenshao merged 2 commits into
QwenLM:mainfrom
shoemoney:fix/daemon-client-relative-baseurl-siblings
Aug 29, 2026
Merged

fix(sdk): support relative daemon base URLs in remaining workspace file helpers#9817
wenshao merged 2 commits into
QwenLM:mainfrom
shoemoney:fix/daemon-client-relative-baseurl-siblings

Conversation

@shoemoney

@shoemoney shoemoney commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Applies the URL construction pattern from #9734 to the six workspace file helpers that still used the absolute-only URL constructor: readWorkspaceFile, fileStat, dirList, workspacePathSuggestions, glob, and uploadFileToPath. Adds a relative-base-URL regression test for each.

Why it's needed

Web Shell integrations configure a same-origin relative daemon base path such as /daemon. #9734 fixed readWorkspaceFileBytes and listed the remaining helpers as out of scope; each of them still throws Failed to construct 'URL': Invalid URL before the request reaches the daemon. This finishes that migration with the identical URLSearchParams + string interpolation approach, so absolute base URLs produce byte-for-byte the same requests as before.

Reviewer Test Plan

How to verify

Run the SDK unit suite: npm test -- test/unit/DaemonClient.test.ts in packages/sdk-typescript. With the new tests but pristine source, all six new cases fail with TypeError: Invalid URL; with the fix the file passes 354/354. Pre-existing absolute-URL coverage for these helpers passes unchanged, which is the no-regression check for the standard configuration. npm run lint and npm run typecheck for the SDK package are clean.

Evidence (Before & After)

N/A (no user-visible change; evidence is the RED/GREEN test output described above).

Tested on

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

Environment (optional)

N/A (unit tests only).

Risk & Scope

  • Main risk or tradeoff: URL assembly changes in six methods. For absolute base URLs the new code produces identical request strings (covered by the pre-existing absolute-URL tests); a base URL with a trailing slash behaves exactly as it does in the already-merged readWorkspaceFileBytes from fix(sdk): support relative artifact download URLs #9734, since the construction is the same pattern.
  • Not validated / out of scope: uploadFileToPath against a live daemon (unit-mocked only); base paths that already carry a query string (none of the six methods accept one today); the seventh helper readWorkspaceFileBytes is untouched, it was fixed in fix(sdk): support relative artifact download URLs #9734.
  • Breaking changes / migration notes: none.

Linked Issues

Fixes #9816

中文说明

本 PR 的内容

#9734 的 URL 构造模式应用到仍在使用仅支持绝对地址的 URL 构造函数的六个工作区文件辅助方法:readWorkspaceFilefileStatdirListworkspacePathSuggestionsglobuploadFileToPath。并为每个方法添加了相对 base URL 的回归测试。

为什么需要

Web Shell 集成会配置同源的相对 daemon 基础路径(例如 /daemon)。#9734 修复了 readWorkspaceFileBytes,并把其余辅助方法列为超出范围;这些方法在请求到达 daemon 之前仍会抛出 Failed to construct 'URL': Invalid URL。本 PR 用完全相同的 URLSearchParams + 字符串插值方式完成该迁移,绝对 base URL 生成的请求与之前逐字节一致。

审查者测试计划

packages/sdk-typescript 中运行 npm test -- test/unit/DaemonClient.test.ts。仅有新测试而未应用修复时,六个新用例全部因 TypeError: Invalid URL 失败;应用修复后该文件 354/354 通过。已有的绝对 URL 用例保持不变通过。npm run lintnpm run typecheck 均无问题。本地在 macOS 上验证;Windows 与 Linux 依赖 CI。

风险与范围

  • 主要风险或权衡:六个方法的 URL 拼装有改动。对绝对 base URL,新代码生成完全相同的请求字符串(由已有测试覆盖);带尾斜杠的 base URL 行为与 fix(sdk): support relative artifact download URLs #9734 已合并的 readWorkspaceFileBytes 完全一致,因为构造模式相同。
  • 未验证 / 超出范围:uploadFileToPath 未对真实 daemon 端到端验证(仅单元 mock);已携带 query string 的基础路径(这六个方法目前都不接受);readWorkspaceFileBytes 未改动,已在 fix(sdk): support relative artifact download URLs #9734 修复。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

Fixes #9816

…le helpers

readWorkspaceFile, fileStat, dirList, workspacePathSuggestions, glob,
and uploadFileToPath still built request URLs with the absolute-only
URL constructor, so a relative daemon base URL such as /daemon (used
by Web Shell) threw "Invalid URL" before the request was sent.

Apply the same URLSearchParams + string interpolation pattern that
fixed readWorkspaceFileBytes, and add relative-base-URL regression
tests for each helper.
@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 23, 2026
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 23, 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 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.

Hi @shoemoney — thanks for the follow-up to #9734, and for the concrete reproduction in #9816. The scope reads clearly, but the PR body is missing several required sections from the PR template, so it has to bounce before code review:

  • ## Risk & Scope — the three bullets (main risk or tradeoff / not validated / breaking changes). For a URL-construction change this is exactly the section reviewers will look for — e.g. base URLs with a trailing slash, or upload paths that already carry a query string.
  • ## Linked IssuesFixes #9816 currently sits at the bottom of the Reviewer Test Plan; move it into this section so the issue auto-closes on merge.
  • Reviewer Test Plan sub-sections### How to verify, ### Evidence (Before & After) (N/A is fine here, nothing user-visible), and the ### Tested on OS table.
  • Bilingual summary — the <details><summary>中文说明</summary> translation of the body, per the template.

No code changes needed for this — fill in the sections above, then comment @qwen-code /triage to re-run the gate.

中文说明

@shoemoney 感谢这个对 #9734 的后续修复,也感谢 #9816 中给出的具体复现。改动范围很清晰,但 PR 正文缺少 PR 模板 中的几个必填部分,需要先补齐才能进入代码审查:

  • ## Risk & Scope —— 三个要点(主要风险或权衡 / 未验证项 / 破坏性变更)。对 URL 构造类改动,这正是审查者最关心的部分——例如带尾斜杠的 base URL,或已携带 query string 的上传路径。
  • ## Linked Issues —— Fixes #9816 目前写在 Reviewer Test Plan 末尾,请移到该节,以便合并时自动关闭 issue。
  • Reviewer Test Plan 子节 —— ### How to verify### Evidence (Before & After)(本 PR 无用户可见界面,写 N/A 即可)以及 ### Tested on 操作系统表格。
  • 双语摘要 —— 按模板要求提供 <details><summary>中文说明</summary> 的正文翻译。

无需改动代码 —— 补齐以上部分后,评论 @qwen-code /triage 即可重新进入 gate。

Qwen Code · qwen3.8-max

@shoemoney

Copy link
Copy Markdown
Contributor Author

Thanks for the precise checklist — all four are addressed, no code changes.

  • ## Risk & Scope added with the three bullets. You called the two right things: trailing-slash baseUrl (now uncollapsed, but new URL preserved the double slash too, so not a regression) and uploadPath already carrying a query string (would now produce a second ?; both current call sites pass query-free literals, so no caller is affected today).
  • Fixes #9816 moved out of the test plan into ## Linked Issues.
  • Reviewer Test Plan split into ### How to verify, ### Evidence (Before & After) (N/A — SDK-internal, nothing user-visible), and the ### Tested on table (macOS verified, Windows/Linux left to CI).
  • Full bilingual <details><summary>中文说明</summary> translation added.

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

Reviewed — no blockers. Suggestions are inline.

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

Comment on lines 1802 to 1804
if (opts.maxBytes !== undefined) {
url.searchParams.set('maxBytes', String(opts.maxBytes));
query.set('maxBytes', String(opts.maxBytes));
}

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 rewritten maxBytes branch is exercised by no test on either base URL. A mutation probe confirmed it: deleting this branch keeps the entire packages/sdk-typescript suite green (35 files, 1659/1659 passed), while deleting the adjacent tested line branch fails 2 tests — the harness catches sibling deletions, only this one is invisible. A future edit could drop the branch with no test failing, and the client would silently omit maxBytes from GET /file, so the daemon serves its default read instead of the byte window the caller requested. The gap predates this diff and the identical maxBytes serialization in readWorkspaceFileBytes is tested, so risk is limited — one added test case closes it.

// test/unit/DaemonClient.test.ts — extend the new relative-base test:
client.readWorkspaceFile('src/a.ts', { maxBytes: 100, line: 2, limit: 3 })
// expect url: /daemon/file?path=src%2Fa.ts&maxBytes=100&line=2&limit=3

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4c7da78 — and thank you for the mutation probe, it reproduced exactly.

Baseline on this branch: 35 files, 1659/1659 passed. Deleting the maxBytes branch from readWorkspaceFile leaves that identical — 35 files, 1659/1659 — so the branch really was invisible to the suite.

Worth noting why it hid: maxBytes does appear in two existing assertions (lines 578 and 602), but both belong to readWorkspaceFileBytes hitting GET /file/bytes. Different method, different endpoint. Of the four optional params readWorkspaceFile serialises, cursor is covered at line 533 and line/limit by the new relative-base test — maxBytes was the only one nothing pinned on either base URL.

The relative-base test now asserts a second call carrying it:

await expect(
  client.readWorkspaceFile('src/a.ts', { maxBytes: 100, line: 2, limit: 3 }),
).resolves.toEqual(payload);
expect(calls[1]?.url).toBe(
  '/daemon/file?path=src%2Fa.ts&maxBytes=100&line=2&limit=3',
);

I kept the original { line: 2, limit: 3 } assertion rather than replacing it, so the omitted case still proves the branch is conditional.

Re-running the same mutation with this in place now fails:

AssertionError: expected '/daemon/file?path=src%2Fa.ts&line=2&l…'
                to be '/daemon/file?path=src%2Fa.ts&maxBytes…'
Test Files  1 failed (1)
Tests  1 failed | 353 passed (354)

Full suite green with the branch restored: 35 files, 1659/1659.

中文说明

已在 4c7da78 中修复——感谢提供变异测试探针,本地完全复现。

本分支基线:35 files, 1659/1659 passed。从 readWorkspaceFile 中删除 maxBytes 分支后结果完全相同——35 files、1659/1659,说明该分支确实未被测试覆盖。

补充一点它为何被漏掉:maxBytes 确实出现在现有的两处断言中(第 578、602 行),但它们属于 readWorkspaceFileBytes 请求的 GET /file/bytes——方法不同,端点也不同。readWorkspaceFile 序列化的四个可选参数中,cursor 由第 533 行覆盖,line/limit 由新增的相对 base URL 测试覆盖,只有 maxBytes 在两种 base URL 下都无测试锚定。

相对 base URL 的测试现在增加了一次携带该参数的调用断言,并保留了原有的 { line: 2, limit: 3 } 断言,以便同时验证该分支是条件生效的。

在此改动下重跑同一变异测试会失败;恢复分支后全量测试通过:35 files、1659/1659。

Comment on lines 1846 to +1847
async fileStat(filePath: string): Promise<unknown> {
const url = new URL(`${this.baseUrl}/stat`);
url.searchParams.set('path', filePath);
const query = new URLSearchParams({ path: filePath });

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 URL-composition pattern (new URLSearchParams(...) + `${this.baseUrl}<path>?${query.toString()}`) is now pasted into seven methods in this file — the six converted here plus the pre-existing copy in readWorkspaceFileBytes — with no shared helper. Any future change to URL composition (an encoding edge case, trailing-slash handling, a proxied path prefix) requires seven synchronized edits, and any site missed ships broken — this PR itself demonstrates the cost: it re-fixes the relative-base defect in the six copies #9734 left behind. Two consolidation targets exist: the private jsonRequest (line ~924) is behaviorally identical for the bodyless GET-JSON path that fileStat/dirList/workspacePathSuggestions/glob/readWorkspaceFile hand-roll here, and a small private helper would localize composition for the remainder. Non-blocking: the inline idiom is the SDK's established convention since #9734, and this PR follows it correctly.

private requestUrl(path: string, query: URLSearchParams): string {
  return `${this.baseUrl}${path}?${query.toString()}`;
}

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed on the diagnosis, and I'd rather not take it in this PR — reasoning below, happy to be overruled.

You're right that the duplication is what caused this: #9734 fixed one copy, six were left behind, and #9816 is the bill for that. A helper is the correct end state.

Two reasons to do it separately:

  1. It would widen the blast radius past the fix. Consolidating means touching readWorkspaceFileBytes too, which this PR doesn't otherwise modify and which is the one site that was already correct. A regression there would be caused by the cleanup, not by the bug — and it'd land in a PR whose stated scope is "make the six broken siblings work with a relative base."

  2. jsonRequest isn't a drop-in for all five. You scoped that carefully — "the bodyless GET-JSON path" — and that's right, but the five methods don't share a response shape or error string (GET /file, GET /stat, GET /list, GET /glob each call failOnError with their own label, and workspacePathSuggestions has a distinct !res.ok branch). Routing them through jsonRequest is a real refactor with its own test surface, not a mechanical extraction.

The smaller requestUrl(path, query) helper you sketched is the part I'd take first — it's mechanical, covers all seven sites, and closes the "any site missed ships broken" hole without touching response handling. Say the word and I'll open it as a follow-up against main once this lands, so the two are reviewable independently and the fix isn't gated on the cleanup.

中文说明

同意你的判断,但我倾向于不在本 PR 中处理——理由如下,如有异议我可以照办。

重复代码确实是根因:#9734 只修复了一处,遗留了六处,#9816 就是代价。抽取 helper 是正确的最终形态。

分开处理的两个理由:

  1. 会把影响面扩大到修复范围之外。 合并意味着还要改动 readWorkspaceFileBytes,而本 PR 并未涉及该方法,且它本就是唯一正确的一处。若在那里引入回归,原因将是这次清理而非原缺陷。

  2. jsonRequest 并非五处都能直接套用。 你的限定很准确——"无 body 的 GET-JSON 路径"——但这五个方法的响应结构和错误信息并不一致(GET /fileGET /statGET /listGET /glob 各自以不同标签调用 failOnError,而 workspacePathSuggestions 有独立的 !res.ok 分支)。改用 jsonRequest 是一次带有独立测试面的真实重构,而非机械抽取。

你给出的较小的 requestUrl(path, query) helper 是我会优先采纳的部分:它是机械性的,可覆盖全部七处,且无需触碰响应处理。若你同意,本 PR 合并后我会针对 main 单独提交后续 PR,使二者可独立审查,修复也不必被清理工作阻塞。

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

⚠️ This run could not certify that any of this diff was reviewed.

Not reviewed: coverage — no plan was given, so this run cannot show that any of the diff was read.

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

@shoemoney

Copy link
Copy Markdown
Contributor Author

@qwen-code /triage

@doudouOUC doudouOUC 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. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.

中文说明

已审查——无阻断问题。 1 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。

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

Addresses review finding R1-1.

readWorkspaceFile serialises four optional params; maxBytes was the only
one no test pinned on either base URL. Reproduced the reviewer's mutation
probe locally: deleting the maxBytes branch left the whole sdk-typescript
suite green (35 files, 1659/1659), so a future edit could drop it and the
client would silently omit the byte window from GET /file.

The relative-base test now asserts a second call carrying maxBytes
alongside line and limit. Re-running the same mutation with this in place
fails the suite, so the branch is pinned.
@shoemoney

Copy link
Copy Markdown
Contributor Author

Pushed 4c7da78 — both review findings are addressed.

R1-1 (maxBytes untested) — fixed. Your mutation probe reproduced exactly: on this branch, deleting the maxBytes branch left the suite at an identical 35 files, 1659/1659 passed. The relative-base test now pins it, and re-running the same mutation with the test in place fails. Details and output in the thread.

R1-2 (URL-composition helper) — replied, not taken here. Agreed the duplication is the root cause and a helper is the right end state; I'd rather land it against main as a follow-up than widen a fix PR into a refactor of seven methods, one of which (readWorkspaceFileBytes) this PR doesn't otherwise touch. Reasoning in the thread — happy to be overruled.

On the stage-1a bounce: all four items were filled in at the time of my earlier comment, and the body currently carries ## Risk & Scope, ## Linked Issues (with Fixes #9816 moved out of the test plan), the three Reviewer Test Plan sub-sections including the ### Tested on table, and the full 中文说明 block. The workflow runs on this push are sitting at action_required pending fork approval, so the gate hasn't re-evaluated on its own.

@qwen-code /triage

中文说明

已推送 4c7da78,两条审查意见均已处理。

R1-1(maxBytes 无测试覆盖)—— 已修复。 变异测试探针完全复现:在本分支删除 maxBytes 分支后,测试结果仍为 35 files, 1659/1659 passed。相对 base URL 的测试现已锚定该参数,加入测试后重跑同一变异会失败。详情与输出见对应讨论串。

R1-2(URL 组装 helper)—— 已回复,本 PR 暂不处理。 认同重复代码是根因,抽取 helper 是正确的最终形态;但我倾向于在本 PR 合并后针对 main 单独提交,而非把一个修复 PR 扩大为对七个方法的重构——其中 readWorkspaceFileBytes 本不在本 PR 改动范围内。理由见对应讨论串,如有异议我可以照办。

关于 stage-1a 的退回:四项在我此前的评论时即已补齐,当前正文包含 ## Risk & Scope## Linked IssuesFixes #9816 已从测试计划移出)、Reviewer Test Plan 的三个子节(含 ### Tested on 表格)以及完整的 中文说明 块。本次推送的工作流处于 action_required 状态,等待 fork 运行审批,因此 gate 尚未自动重新评估。

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

No issues found. LGTM! ✅

中文说明

未发现问题。LGTM!✅

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

@wenshao
wenshao added this pull request to the merge queue Aug 29, 2026
Merged via the queue into QwenLM:main with commit c7b0e37 Aug 29, 2026
46 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.23.0.

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

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DaemonClient workspace file helpers throw "Invalid URL" with a relative daemon base URL

4 participants