Skip to content

fix(serve): validate readText line limits#5639

Merged
wenshao merged 2 commits into
QwenLM:mainfrom
tt-a1i:fix/readtext-limit-boundary
Jun 22, 2026
Merged

fix(serve): validate readText line limits#5639
wenshao merged 2 commits into
QwenLM:mainfrom
tt-a1i:fix/readtext-limit-boundary

Conversation

@tt-a1i

@tt-a1i tt-a1i commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Rejects invalid readText limit values at the WorkspaceFileSystem boundary, matching the existing validation for line. When limit is provided, it must now be a positive safe integer; 0, negatives, fractions, NaN, infinities, and unsafe integers return parse_error.

Why it's needed

readText already documents and enforces line as a positive integer, but limit could still reach the lower-level slicing logic unchecked when called directly from the filesystem service. That made the shared boundary less predictable for internal callers and left edge cases such as Infinity, NaN, floats, and unsafe integers to degrade into implementation-specific truncation behavior.

Reviewer Test Plan

How to verify

Confirm that WorkspaceFileSystem.readText rejects invalid opts.limit values with parse_error, while the existing opts.line behavior remains covered by the same test file.

Commands run locally:

npx vitest run src/serve/fs/workspace-file-system.test.ts --coverage.enabled=false
npx prettier --check packages/cli/src/serve/fs/workspace-file-system.ts packages/cli/src/serve/fs/workspace-file-system.test.ts
npx eslint packages/cli/src/serve/fs/workspace-file-system.ts packages/cli/src/serve/fs/workspace-file-system.test.ts
git diff --check
npm run build --workspace @qwen-code/qwen-code-core
npm run build --workspace @qwen-code/acp-bridge
npm run build --workspace @qwen-code/qwen-code
npm run typecheck --workspace @qwen-code/qwen-code

Evidence (Before & After)

Before: readText(r, { limit: bad }) accepted invalid limit values at the service boundary and passed them to the lower-level read/slice path. After: invalid limit values return an FsError with kind === 'parse_error'. This is covered by the new unit test for Infinity, -Infinity, Number.MAX_SAFE_INTEGER + 1, 0, -1, 1.5, and NaN.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

Local macOS arm64, Node.js v26.3.0.

Risk & Scope

  • Main risk or tradeoff: Low; this only tightens invalid limit inputs at the shared filesystem service boundary and mirrors existing line validation.
  • Not validated / out of scope: Windows and Linux were not tested locally; CI should cover those platforms.
  • Breaking changes / migration notes: No expected breaking change for valid callers. Direct internal callers passing invalid limit values now receive parse_error instead of undefined slicing behavior.

Linked Issues

Fixes #5623

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

中文说明

What this PR does

这个 PR 在 WorkspaceFileSystem 边界拒绝非法的 readText limit 值,和现有的 line 校验保持一致。现在只要传入 limit,它就必须是正的安全整数;0、负数、小数、NaN、正负无穷以及不安全整数都会返回 parse_error

Why it's needed

readText 已经把 line 记录并校验为正整数,但直接从文件系统服务调用时,limit 仍然可以绕过校验进入更底层的切片逻辑。这会让共享边界对内部调用方不够可预测,也会让 InfinityNaN、小数和不安全整数这类边界值退化成依赖实现细节的截断行为。

Reviewer Test Plan

How to verify

确认 WorkspaceFileSystem.readText 会对非法的 opts.limit 返回 parse_error,同时现有的 opts.line 行为仍由同一个测试文件覆盖。

本地运行过的命令:

npx vitest run src/serve/fs/workspace-file-system.test.ts --coverage.enabled=false
npx prettier --check packages/cli/src/serve/fs/workspace-file-system.ts packages/cli/src/serve/fs/workspace-file-system.test.ts
npx eslint packages/cli/src/serve/fs/workspace-file-system.ts packages/cli/src/serve/fs/workspace-file-system.test.ts
git diff --check
npm run build --workspace @qwen-code/qwen-code-core
npm run build --workspace @qwen-code/acp-bridge
npm run build --workspace @qwen-code/qwen-code
npm run typecheck --workspace @qwen-code/qwen-code

Evidence (Before & After)

修复前:readText(r, { limit: bad }) 会在服务边界接受非法的 limit 值,并继续传给更底层的读取和切片路径。修复后:非法的 limit 值会返回 kind === 'parse_error'FsError。新增单元测试覆盖了 Infinity-InfinityNumber.MAX_SAFE_INTEGER + 10-11.5NaN

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

本地环境为 macOS arm64,Node.js v26.3.0。

Risk & Scope

  • 主要风险或取舍:风险较低;这只是在共享文件系统服务边界收紧非法 limit 输入,并与现有 line 校验保持一致。
  • 未验证或不在范围内:本地没有测试 Windows 和 Linux;这些平台交给 CI 覆盖。
  • 破坏性变更或迁移说明:对合法调用方没有预期破坏性影响。直接传入非法 limit 的内部调用方现在会收到 parse_error,而不是进入未定义的切片行为。

Linked Issues

Fixes #5623

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

@tt-a1i
tt-a1i marked this pull request as ready for review June 22, 2026 13:25
@wenshao

wenshao commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao

wenshao commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Verification report — real-execution A/B

I checked out this branch and exercised the real WorkspaceFileSystem service against a real temp file (the actual code path the PR changes), ran an A/B (PR vs. the same code with the new check removed), and traced every caller of readText to confirm reachability and regression-safety.

TL;DR: verified correct, consistent with the existing line guard, and regression-free. Safe to merge.

Method

  • git worktree on the PR head; npm ci; vitest drives the real createWorkspaceFileSystemFactory over a real mkdtemp workspace (no mocks).
  • A/B: ran a probe that records readText's actual outcome for valid + the 7 invalid limit values, once on the PR source and once with the 9-line check reverted.
  • (No TUI/tmux here — this is a server-side service-boundary validation, so "real test" = driving the real service on real files.)

Results

1. A/B on a real 5-line file (L1..L5) — the new check is exactly what stops the silent, inconsistent truncation the PR describes:

limit Base (check removed) PR
2 (valid) "L1\nL2" "L1\nL2" ✅ unchanged
1 (valid) "L1" "L1" ✅ unchanged
Infinity all 5 lines parse_error
-Infinity "" (empty) parse_error
MAX_SAFE_INTEGER+1 all 5 lines parse_error
0 "" (empty) parse_error
-1 5 lines but drops the trailing newline parse_error
1.5 "L1" (float-truncated) parse_error
NaN "" (empty) parse_error

Base degrades into 4 different behaviors (all / empty / truncated / newline-dropping) depending on the bad value — the PR makes all of them a consistent parse_error, while valid limits are untouched.

2. The test is load-bearing. Reverting the 9-line check makes the PR's own new test fail (expected false to be truereadText returns content instead of throwing). With the check, the full file is 76/76 green; eslint, prettier --check, and tsc --noEmit (cli workspace) all pass.

3. Reachability — the check is correctly scoped as service-boundary defense-in-depth. All external entry points already validate limit before readText, so this introduces no external behavior change:

  • GET /file route (workspace-file-read.ts:232) — parseIntInRange(req.query['limit'], 1, MAX_FILE_LINE_LIMIT)400 parse_error.
  • ACP HTTP dispatch (acpHttp/dispatch.ts:1556) — parseOptionalSafeIntegerInRange(...) → RPC error.
  • ACP bridge adapter (bridge-file-system-adapter.ts) — drops null and non-positive limit.

The new guard protects direct/internal service callers (the scenario the PR body calls out), which is exactly what the new unit test exercises.

4. No regression for the one internal limit: Infinity caller. workspace-file-system.ts:1156 (inside edit()) passes limit: Number.POSITIVE_INFINITY to this.deps.lowFs.readTextFile(...) — the low-level read, not the guarded service readText — so the new check does not touch it.

5. Consistency. The new block mirrors the existing line guard verbatim (!Number.isSafeInteger(x) || x < 1), lives in the same try (so it gets the same fs.denied audit-on-error treatment), and like line applies no service-level upper bound (the route owns the policy cap). The existing comment above the line check already documents the exact Infinity/float/NaN/0 degradation rationale this extends to limit.

Verdict

Approve. Correct, minimal, well-tested, no regression, no external behavior change, and consistent with the established pattern. The only nit (optional): the new FsError message says limit must be a positive integer — fine as-is; the route's richer "in [1, N]" message stays at the HTTP layer where the cap applies.

中文版(点击展开)

验证报告 — 真实执行 A/B

我检出本分支,针对真实临时文件驱动了真实的 WorkspaceFileSystem 服务(也就是本 PR 实际改动的代码路径),做了 A/B(PR 版 vs. 移除新校验后的同一份代码),并追踪了 readText 的所有调用方以确认可达性与无回归。

结论:校验正确、与现有 line 校验一致、无回归。可以合并。

方法

  • 在 PR head 上 git worktree + npm ci;用 vitest 驱动真实的 createWorkspaceFileSystemFactory,在真实 mkdtemp 工作区上运行(无 mock)。
  • A/B:用一个探针记录 readText 对合法值 + 7 个非法 limit 的真实结果,分别在 PR 源码和「移除那 9 行校验」的源码上各跑一次。
  • (这里没有用 TUI/tmux——这是服务端边界校验,所以「真实测试」= 在真实文件上驱动真实服务。)

结果

1. 真实 5 行文件(L1..L5)上的 A/B —— 新校验正是阻止 PR 所述「静默且不一致的截断」的关键:

limit Base(移除校验) PR
2(合法) "L1\nL2" "L1\nL2" ✅ 不变
1(合法) "L1" "L1" ✅ 不变
Infinity 全部 5 行 parse_error
-Infinity ""(空) parse_error
MAX_SAFE_INTEGER+1 全部 5 行 parse_error
0 ""(空) parse_error
-1 5 行但丢了末尾换行 parse_error
1.5 "L1"(小数被截断) parse_error
NaN ""(空) parse_error

Base 会根据非法值退化成 4 种不同行为(全部 / 空 / 截断 / 丢换行);PR 把它们统一为 parse_error,同时合法值完全不受影响。

2. 测试是有效的(load-bearing)。 移除那 9 行校验后,PR 自己的新测试会失败(expected false to be true——readText 返回了内容而没有抛错)。保留校验时整个测试文件 76/76 通过eslintprettier --checktsc --noEmit(cli workspace)均通过。

3. 可达性——该校验作为「服务边界纵深防御」定位准确。 所有外部入口在调用 readText 之前 都已校验 limit,因此不改变任何外部行为

  • GET /file 路由(workspace-file-read.ts:232)—— parseIntInRange(..., 1, MAX_FILE_LINE_LIMIT)400 parse_error
  • ACP HTTP dispatch(acpHttp/dispatch.ts:1556)—— parseOptionalSafeIntegerInRange(...) → RPC 错误。
  • ACP bridge adapter(bridge-file-system-adapter.ts)—— 丢弃 null 和非正数 limit

新校验保护的是直接/内部服务调用方(正是 PR 描述里点名的场景),也正是新单测覆盖的路径。

4. 对唯一一个内部 limit: Infinity 调用方无回归。 workspace-file-system.ts:1156(在 edit() 内)把 limit: Number.POSITIVE_INFINITY 传给 this.deps.lowFs.readTextFile(...)——是底层读取,而非被校验的服务级 readText——所以新校验不会影响它。

5. 一致性。 新代码块与现有 line 校验逐字对齐(!Number.isSafeInteger(x) || x < 1),处于同一个 try 中(因此享有相同的 fs.denied 出错审计),并且和 line 一样在服务层不设上限(上限策略由路由层负责)。line 校验上方的现有注释已经记录了本次扩展到 limit 的那套 Infinity/小数/NaN/0 退化原因。

结论

建议合并(Approve)。 正确、最小、测试充分、无回归、无外部行为变化,且与既有模式一致。唯一可选的小点:新 FsError 文案是 limit must be a positive integer——保持现状即可;带 in [1, N] 的更详细文案留在 HTTP 层(上限在那里生效)。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓ — all required sections present and filled in.

On direction: this is a clean defensive-boundary fix. Issue #5623 identifies a real inconsistency — readText validates line but not limit at the same service boundary, and the degradation behaviors (empty string, full file, float-truncated, newline-dropping) are genuinely surprising. Tightening the contract at the shared boundary is exactly what the service layer should do. No CHANGELOG precedent needed; this is internal API hygiene, not a user-facing feature.

On approach: the scope is minimal — 9 lines of validation mirroring the existing line guard verbatim, plus a focused test covering the 7 edge values from the issue. No scope creep, no drive-by refactors. The two checks could theoretically share a helper, but at two call sites that's premature abstraction. This is the right shape.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓ — 所有必填部分均已填写。

方向:这是一个干净的服务边界防御修复。Issue #5623 指出了一个真实的不一致 — readText 在同一服务边界校验了 line 但没有校验 limit,而退化行为(空字符串、返回全文、小数截断、丢换行)确实令人意外。在共享边界收紧契约正是服务层该做的事。不需要 CHANGELOG 先例;这是内部 API 卫生,不是用户可见的功能。

方案:范围最小化 — 9 行校验代码逐字镜像现有 line 守卫,加上一个覆盖 issue 中 7 个边界值的聚焦测试。没有范围蔓延,没有顺手重构。两个校验理论上可以共享一个 helper,但只有两个调用点就抽象属于过度。这个形状是对的。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: Given the issue — readText validates line but not limit — I would add the same Number.isSafeInteger(x) || x < 1 guard for limit, placed right after the line check in the same try block, throwing FsError('parse_error', ...). Test would cover the same 7 bad values (Infinity, -Infinity, MAX_SAFE_INTEGER+1, 0, -1, 1.5, NaN).

Comparison with diff: the PR does exactly this. The guard is a verbatim mirror of the line check — same condition, same error type, same placement. No surprises.

Correctness: sound. Number.isSafeInteger correctly rejects NaN, Infinity, -Infinity, floats, and unsafe integers in one check. The < 1 catches 0 and negatives. The opts.limit !== undefined guard correctly skips validation when limit is not provided.

Reuse check: the line guard above uses the identical pattern. A shared helper (assertPositiveSafeInteger) could DRY these two blocks, but at two call sites this isn't worth the abstraction — AGENTS.md's "simplicity first" principle supports keeping them inline.

Test quality: good. The test iterates all 7 bad values from the issue and asserts both isFsError and kind === 'parse_error'. Valid limit values remain covered by the existing tests above in the same file.

No blockers found.

Test Results

Unit tests (run from worktree on PR branch):

✓ src/serve/fs/workspace-file-system.test.ts (76 tests) 212ms

 Test Files  1 passed (1)
      Tests  76 passed (76)
   Duration  5.46s

Typecheck: tsc --noEmit -p packages/cli/tsconfig.json — clean, no errors.

CI status: all green — Test (windows, macos, ubuntu Node 22.x), Lint, CodeQL, Integration Tests (No-AK Smoke), Classify PR all pass.

Real-Scenario Testing

N/A — this is a non-user-visible service-boundary validation. The change is internal API contract enforcement; there is no CLI behavior to exercise via tmux. The unit tests drive the real WorkspaceFileSystem service against real temp files, which is the appropriate test level for this change.

中文说明

代码审查

独立方案: 针对该问题 — readText 校验了 line 但没校验 limit — 我会添加相同的 Number.isSafeInteger(x) || x < 1 守卫,放在同一个 try 块中 line 校验之后,抛出 FsError('parse_error', ...)。测试覆盖相同的 7 个非法值。

与 diff 对比: PR 正是这么做的。守卫逐字镜像 line 校验 — 相同条件、相同错误类型、相同位置。没有意外。

正确性: 可靠。Number.isSafeInteger 一次检查就正确拒绝了 NaNInfinity-Infinity、小数和不安全整数。< 1 捕获 0 和负数。opts.limit !== undefined 守卫在未传 limit 时正确跳过校验。

复用检查: 上方的 line 守卫使用了完全相同的模式。一个共享 helper(assertPositiveSafeInteger)可以合并这两处,但只有两个调用点不值得抽象 — AGENTS.md 的「简洁优先」原则支持保持内联。

测试质量: 良好。测试遍历了 issue 中所有 7 个非法值,并断言了 isFsErrorkind === 'parse_error'。合法 limit 值由同文件上方的现有测试覆盖。

无阻塞问题。

测试结果

单元测试(在 worktree 的 PR 分支上运行):76/76 通过。

类型检查: tsc --noEmit — 无错误。

CI 状态: 全绿。

真实场景测试

N/A — 这是非用户可见的服务边界校验。变更是内部 API 契约执行,没有可通过 tmux 测试的 CLI 行为。单元测试已经驱动了真实的 WorkspaceFileSystem 服务对真实临时文件操作,这是该变更的适当测试层级。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Final Decision

This PR is exactly what it claims to be: a 9-line service-boundary guard that makes limit validation consistent with the already-existing line validation in the same method. The diff is minimal, the test covers all edge cases from the linked issue, and there's zero scope creep.

Going back to my independent proposal from Stage 2 — the PR matches it exactly. I can't find a simpler path. The implementation mirrors the existing pattern verbatim, lives in the right place (same try block, same error handling), and doesn't touch any callers (because all external entry points already validate limit upstream).

The only question worth asking is whether to extract a shared assertPositiveSafeInteger helper for the two near-identical blocks. But at two call sites, that's premature — and the PR author correctly chose to match the existing pattern rather than refactor it.

76/76 tests pass, typecheck clean, CI all green across Windows, macOS, and Linux. The collaborator verification report (A/B testing on real files) independently confirmed the same findings.

Approving.

中文说明

最终决定

这个 PR 正是它所声称的:一个 9 行的服务边界守卫,使 limit 校验与同方法中已有的 line 校验保持一致。diff 最小化,测试覆盖了关联 issue 中的所有边界情况,零范围蔓延。

回到我在第二阶段的独立方案 — PR 与之完全一致。我找不到更简单的路径。实现逐字镜像了现有模式,放在正确的位置(同一个 try 块,同一个错误处理),且不影响任何调用方(因为所有外部入口已经在调用前校验了 limit)。

唯一值得讨论的是是否提取一个共享的 assertPositiveSafeInteger helper。但只有两个调用点,这属于过早优化 — PR 作者正确地选择了匹配现有模式而非重构。

76/76 测试通过,类型检查干净,CI 在 Windows、macOS 和 Linux 上全绿。协作者的验证报告(真实文件 A/B 测试)也独立确认了相同的发现。

通过。

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 merged commit 150ded7 into QwenLM:main Jun 22, 2026
34 checks passed
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.

bug(serve): readText accepts invalid line limits at the filesystem boundary

3 participants