fix(core): require integer ReadFile pagination params#6381
Conversation
|
Thanks for the PR! Template looks good ✓ Problem: This is input-validation hardening without a live reproduction — no linked issue or before/after showing a real tool call that emitted fractional Direction: Clearly aligned — tightening tool-boundary validation for a core tool is housekeeping, not feature creep. The schema change from Approach: Scope is tight — schema type change, runtime guard, updated error messages, and tests covering fractional offset, fractional limit, and valid zero-offset. Nothing extra. This is the minimal change. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:这是输入校验加固,没有提供实际复现——没有关联的 issue 或 before/after 展示真实工具调用产生小数 offset/limit。但校验缺口可以在代码中直接观测到:当前的 方向:明显对齐——收紧核心工具的输入校验是日常维护,不是功能膨胀。schema 从 方案:范围紧凑——schema 类型变更、运行时守卫、更新错误消息、覆盖小数 offset/小数 limit/合法零 offset 的测试。没有多余内容。这是最小改动。 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewMy independent proposal before reading the diff: change the JSON Schema type from The PR matches this exactly — same files, same approach, same test coverage. No simpler path was missed. Correctness: Clean. The Tests: 65 → 70 tests, all passing. New coverage: fractional offset (1.5), fractional limit (0.5, 1.5), zero offset (valid), zero/negative limit (invalid). Existing offset-negative and limit-zero tests updated with new messages. Good coverage of the boundary conditions. Reuse: No new abstractions or helpers — the validation is inline where it belongs. No issues. No blockers found. Real-Scenario TestingInteger pagination with offset=4, limit=11 on a 50-line file — verifying the PR doesn't regress normal behavior. Before (installed build)After (this PR via dev build)Both produce identical pagination results (lines 5–15, 11 lines from offset 4). Normal integer pagination is unaffected by the schema change. Unit Tests中文说明代码审查我在看 diff 之前的独立方案:将 schema type 从 PR 完全一致——同样的文件、同样的方案、同样的测试覆盖。没有遗漏更简路径。 正确性: 没有问题。 测试: 65 → 70 测试,全部通过。新覆盖:小数 offset (1.5)、小数 limit (0.5, 1.5)、零 offset(合法)、零/负 limit(非法)。已有的 offset 负数和 limit 零测试更新了新消息。边界条件覆盖良好。 复用: 没有新增抽象或辅助函数——校验就在该在的地方内联。没有问题。 未发现阻塞项。 真实场景测试在 50 行文件上用 offset=4、limit=11 测试整数分页——验证 PR 不会回退正常行为。 修复前(已安装版本)和修复后(本 PR dev build) 均正确返回第 5–15 行(11 行),整数分页行为不受影响。 — Qwen Code · qwen3.7-max |
|
This is a clean, minimal input-validation fix. The schema change from The PR matches what I would have written independently: same files, same approach, same test coverage. No scope creep, no unnecessary abstractions, no drive-by refactors. The diff is exactly what the goal needs and nothing more. Real-scenario testing confirms integer pagination works identically before and after. Unit tests go from 65 → 70, all passing, with good boundary coverage (fractional offset/limit, zero offset, negative limit). Approving. ✅ 中文说明这是一个干净、最小化的输入校验修复。schema 从 PR 和我独立构思的方案完全一致:同样的文件、同样的方案、同样的测试覆盖。没有范围膨胀、没有不必要的抽象、没有顺手重构。diff 恰好是目标所需,不多不少。 真实场景测试确认修复前后整数分页行为一致。单元测试从 65 → 70,全部通过,边界覆盖良好(小数 offset/limit、零 offset、负 limit)。 批准。✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. Clean, focused validation tightening — schema and runtime layers are consistent, tests cover the meaningful boundaries. LGTM! ✅
Downgraded from Approve to Comment: CI still running.
— qwen3.7-max via Qwen Code /review
✅ Maintainer verification — real end-to-end runI verified this PR locally by driving the real compiled Environment: Linux (Debian 13, kernel 6.12), Node Methodology
Compiled-artifact E2E — real
|
| Input | PR build | Base (pre-PR) build |
|---|---|---|
offset: 1.5 |
❌ params/offset must be integer (schema) |
from line 2.5 |
limit: 0.5 |
❌ params/limit must be integer (schema) |
first 0.5 lines |
limit: 1.5 |
❌ params/limit must be integer (schema) |
first 1.5 lines |
offset: 1.5, limit: 2 |
❌ params/offset must be integer (schema) |
lines 2.5-3.5 |
offset: -1 |
❌ Offset must be a non-negative integer (runtime) |
❌ …non-negative number |
limit: 0 / -1 |
❌ Limit must be a positive integer (runtime) |
❌ …positive number |
offset: 0, limit: 10 |
✅ accepted → lines 1-10 |
✅ accepted → lines 1-10 |
The base column reproduces the exact bug described in the PR: fractional pagination is accepted and renders nonsensical ranges like lines 2.5-3.5. And because JS Array.slice() truncates fractional indexes (['l1','l2','l3'].slice(1.5) → ['l2','l3']), the displayed range (from line 2.5) and the actual content read (from line 2) diverge — the ambiguous semantics the PR set out to remove. Valid integer inputs (and integer-valued numbers, which JSON/JS treat as integers) are unaffected; offset: 0 still works.
A/B toggle proofs (vitest, read-file.test.ts, 70 tests)
| State | Result | Proves |
|---|---|---|
| PR as-is | ✅ 70 passed | baseline (matches the PR's Linux "70 passed") |
Revert schema integer→number only (keep runtime guard) |
❌ 3 fractional tests fail — they expect params/offset must be integer but get Offset must be a non-negative integer |
the fractional rejection is driven by the schema change; those tests are pinned to the AJV message |
Remove runtime !Number.isInteger() only (keep schema) |
✅ still 70 passed | the runtime integer-guard is shadowed by the schema and has zero test coverage |
Why this happens (control flow)
BaseDeclarativeTool.validateToolParams() runs AJV schema validation first and returns on the first error, then calls validateToolParamValues(). So:
- Fractional inputs are rejected by the schema (
type: 'integer') →params/… must be integer. The four-pass coercion inSchemaValidatoronly fixes numeric strings, so a fractional number stays rejected. - The new runtime
!Number.isInteger()clauses only ever run for values that already passed the schema (i.e. real integers), so they never fire for fractional inputs on thebuild()path.
Non-blocking observations (for the merge decision)
- The one-line schema change (
number→integer) is the real fix. The runtime!Number.isInteger()additions are harmless defense-in-depth (they'd only matter if the schema-then-runtime order ever changed), but Experiment 2 shows they are fully untested — deleting them keeps all 70 green. Consider either adding a directvalidateToolParamValuesunit test to lock the behavior in, dropping the clause, or keeping it with a// defense-in-depthcomment. - Two message styles for closely related failures: fractional →
params/offset must be integer(JSON-pointer/AJV style); negative/zero →Offset must be a non-negative integer(friendly style). This split pre-exists for negatives and isn't a regression — just noting the model/user sees two shapes.
Gate results
read-file.test.ts: 70 passed ✅typecheck --workspace=packages/core: passed (tsc --noEmit, rc=0) ✅git diff --check: clean ✅ (the only warning is a pre-existing CRLF note on an unrelatedNOTICES.txt)
Recommendation: 👍 merge. The fix does what it claims, valid callers are unaffected, and the reproduction/regression are both confirmed. Optionally address observation #1 (untested runtime guard) either now or in a follow-up.
🀄 中文版(维护者本地真实验证报告)
✅ 维护者本地真实验证(端到端)
我在本地对本 PR 做了端到端真实验证:不仅跑了单测,还直接驱动编译后的真实 ReadFileTool,并通过 A/B 开关实验精确定位“到底是哪一处改动在生效”。结论:改动正确、可以合并。 下面有一条非阻塞的观察,关于运行时那段 Number.isInteger 校验是冗余且没有测试覆盖的。
环境: Linux(Debian 13,内核 6.12)、Node v22.22.2,在独立 git worktree 中对 PR 头 fd339c558(父提交 5c8af1a1f)执行了全新的 npm ci。已确认 PR 真实改动范围就是 diff 里的 2 个文件(50+/12−),GitHub 的 files-changed 数量准确。
验证方法
- 编译产物端到端:导入构建后的
packages/core/dist/src/tools/read-file.js,对真实临时文件调用tool.build(params),抓取真实抛出的错误信息,或成功时抓取真实的getDescription()输出。分别在 PR 构建和 base(main) 构建(把该文件还原到改动前、重新构建 core)上各跑一遍。 - A/B 开关:分别单独回退修复的两半,重新运行 PR 自带的
read-file.test.ts(70 个用例),以此证明因果关系。 - 常规校验:
read-file.test.ts、typecheck --workspace=packages/core、git diff --check。
编译产物端到端 —— 真实 ReadFileTool.build()
| 输入 | PR 构建 | base(改动前)构建 |
|---|---|---|
offset: 1.5 |
❌ params/offset must be integer(schema 层) |
from line 2.5 |
limit: 0.5 |
❌ params/limit must be integer(schema 层) |
first 0.5 lines |
limit: 1.5 |
❌ params/limit must be integer(schema 层) |
first 1.5 lines |
offset: 1.5, limit: 2 |
❌ params/offset must be integer(schema 层) |
lines 2.5-3.5 |
offset: -1 |
❌ Offset must be a non-negative integer(运行时) |
❌ …non-negative number |
limit: 0 / -1 |
❌ Limit must be a positive integer(运行时) |
❌ …positive number |
offset: 0, limit: 10 |
✅ 通过 → lines 1-10 |
✅ 通过 → lines 1-10 |
base 那一列完整复现了 PR 描述的 bug:小数分页会被接受,并渲染出 lines 2.5-3.5 这类无意义的行号范围。而且由于 JS 的 Array.slice() 会对小数下标做截断(['l1','l2','l3'].slice(1.5) → ['l2','l3']),展示的范围(from line 2.5)与实际读取的内容(从第 2 行开始)并不一致——这正是本 PR 想消除的含混语义。合法整数输入(以及 JSON/JS 视为整数的取值)不受影响,offset: 0 仍然可用。
A/B 开关证明(vitest,read-file.test.ts,70 用例)
| 状态 | 结果 | 说明 |
|---|---|---|
| PR 原样 | ✅ 70 通过 | 基线(与 PR 的 Linux “70 passed” 一致) |
只回退 schema integer→number(保留运行时校验) |
❌ 3 个小数用例失败——它们断言 params/offset must be integer,实际却拿到 Offset must be a non-negative integer |
小数拒绝是由 schema 改动驱动的;这些用例绑定在 AJV 的报错文案上 |
只删掉运行时 !Number.isInteger()(保留 schema) |
✅ 仍然 70 通过 | 运行时那段整数校验被 schema 完全遮蔽,且零测试覆盖 |
原因(控制流)
BaseDeclarativeTool.validateToolParams() 先跑 AJV schema 校验,遇到第一个错误就返回,之后才调用 validateToolParamValues()。因此:
- 小数输入被 schema(
type: 'integer')拦截 →params/… must be integer。SchemaValidator里的四遍强转只修“数字字符串”,所以一个小数数字依旧会被拒。 - 新增的运行时
!Number.isInteger()只会对已经通过 schema(即已经是整数)的值执行,所以在build()路径上永远不会因为小数而触发。
非阻塞观察(供合并决策参考)
- 真正的修复是那一行 schema 改动(
number→integer)。 运行时新增的!Number.isInteger()属于无害的“纵深防御”(只有当 schema→运行时的先后顺序被改变时才有意义),但实验 2 表明它完全没有测试覆盖——删掉后 70 个用例仍全绿。建议二选一/三选一:为validateToolParamValues补一个直接的单测把行为固化下来、或删掉这段、或保留并加// defense-in-depth注释。 - 两种报错风格并存:小数 →
params/offset must be integer(JSON-pointer/AJV 风格);负数/零 →Offset must be a non-negative integer(友好风格)。这个分裂在负数场景本就存在,不是本 PR 引入的回归,仅作提示。
校验结果
read-file.test.ts:70 通过 ✅typecheck --workspace=packages/core:通过(tsc --noEmit,rc=0)✅git diff --check:干净 ✅(唯一的告警是无关文件NOTICES.txt的既有 CRLF 提示)
建议:👍 合并。 修复达成了目标,合法调用方不受影响,bug 复现与回归防护均已确认。观察点 #1(未覆盖的运行时校验)可现在处理,也可留作后续。
What this PR does
This PR tightens
ReadFileToolpagination parameters sooffsetandlimitare treated as integer line controls instead of arbitrary numbers.It changes the tool schema for both fields from
numbertointeger, adds runtime integer validation inReadFileTool.validateToolParamValues(), and adds regression tests for fractional pagination inputs.Why it's needed
ReadFileTooldescribesoffsetas a 0-based line number andlimitas the number of lines to read. Both values are discrete line controls, so they should be integers.Before this change, the schema exposed both fields as
number, and runtime validation only rejected negativeoffsetvalues and non-positivelimitvalues. Fractional inputs such as:{ "file_path": "/tmp/example.txt", "offset": 1.5, "limit": 1.5 }could pass the ReadFile-specific validation path. That leaves downstream file-reading code and display text with ambiguous pagination semantics: JavaScript array slicing coerces fractional indexes, while user-facing descriptions can still compute fractional ranges such as
lines 2.5-3.5.This PR rejects those malformed pagination requests at the tool boundary instead of letting fractional line numbers reach the file-read pipeline.
Reviewer Test Plan
How to verify
Reviewers can confirm that:
offsetandlimitare now declared asintegerin the ReadFile tool schema.offsetvalues such as1.5are rejected.limitvalues such as0.5and1.5are rejected.offset: 0and positive integer limits.Evidence (Before & After)
Before this change, the tool schema advertised:
and ReadFile-specific validation only checked:
After this change, both schema and runtime validation require integer semantics:
and:
The new tests cover the malformed fractional inputs directly.
Tested on
Environment (optional)
Windows local validation with Node.js from
D:\ZXY\Dev\nodejs.Commands run on Windows and WSL/Linux:
Windows results:
WSL/Linux results with temporary native Node.js v22.13.1:
Risk & Scope
Linked Issues
N/A
中文说明
这个 PR 做了什么
这个 PR 收紧了
ReadFileTool的分页参数校验,让offset和limit按“整数行号 / 整数行数”处理,而不是任意数字。具体改动包括:将工具 schema 中的
offset/limit从number改为integer,在ReadFileTool.validateToolParamValues()中增加运行时整数校验,并补充小数分页参数的回归测试。为什么需要它
ReadFileTool中offset表示 0-based 起始行号,limit表示读取多少行。行号和行数都是离散概念,因此语义上应当是整数。在本次修复之前,schema 将这两个字段声明为
number,运行时只拒绝负数offset和非正数limit。因此类似下面的小数输入可能绕过 ReadFile 自身的校验:{ "file_path": "/tmp/example.txt", "offset": 1.5, "limit": 1.5 }这会让下游文件读取逻辑和展示文案出现含混语义:JavaScript array slicing 会对小数索引做隐式整数化处理,而用户可见描述仍可能计算出
lines 2.5-3.5这类没有实际意义的行号范围。这个 PR 在工具边界直接拒绝小数分页参数,避免小数行号进入文件读取管线。
Reviewer 测试计划
Reviewers 可以确认:
offset和limit已声明为integer。offset: 1.5会被拒绝。limit: 0.5和limit: 1.5会被拒绝。offset: 0和正整数limit。证据
修复前 schema 使用:
并且 ReadFile 自身校验只检查:
修复后 schema 和运行时校验都明确使用整数语义:
并且增加:
新增测试直接覆盖了这些小数输入。
风险与范围
主要风险是输入契约被收紧:如果某些调用方之前意外传入小数分页参数,现在会得到校验错误。但这符合
offset/limit作为行号和行数的实际语义。本 PR 不改变 PDF pages、notebook 读取、文件内容读取、缓存逻辑或正常整数分页行为。