fix(cli): parse heatmapDays strictly#7218
Conversation
Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
|
Thanks for the PR! Template looks good ✓ Problem: observed behavior mismatch — the code comment says "anything invalid falls back to the default," but Direction: aligned — this is a small input-validation fix making the parser match its documented contract on a daemon query surface. No CHANGELOG reference needed for a parse-correctness fix. Size: not applicable (no core paths touched — Approach: the scope is tight — one regex gate before the existing parseInt + clamp, plus four new test cases. Minimal and focused. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:已观测到的行为不一致——代码注释说"无效值回退到默认",但 方向:对齐——这是一个小型输入验证修复,让解析器符合其在 daemon 查询接口上的文档约定。 规模:不适用(未触及核心路径)。 方案:范围紧凑——在现有 parseInt + clamp 前加一个正则校验,外加四个新测试用例。最小且聚焦。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
Code ReviewIndependent proposal: given the problem (parseInt prefix-parses partial numeric strings), I'd add a Findings: no blockers. The implementation is correct:
Tests cover the four malformed categories ( Testing
Ubuntu unit tests and the Serve A/B integration both pass on the reviewed commit. Windows/macOS test jobs were skipped (infra scheduling, not PR-related). No failures. Real-scenario testing: N/A — this is a query-parameter parser fix with no user-visible TUI surface; the route test and Serve A/B integration cover the behavior. 中文说明代码审查独立方案:给定问题(parseInt 前缀解析部分数字字符串),我会在现有 parseInt + clamp 前加 无阻塞问题。实现正确:正则拒绝负数、小数、指数、尾部垃圾和空字符串;guard 之后 parseInt 安全;clamp 逻辑不变。测试覆盖四类 malformed 输入。无范围蔓延。 测试Ubuntu 单元测试和 Serve A/B 集成测试在审查提交上通过。Windows/macOS 测试任务被跳过(基础设施调度,非 PR 相关)。无失败。 真实场景测试:N/A——这是查询参数解析修复,无用户可见 TUI 界面;路由测试和 Serve A/B 集成已覆盖行为。 — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Confidence: 5/5 — clean across every stage; minimal fix that makes the parser match its documented contract, CI green, no reservations. This is exactly the kind of PR that's easy to say yes to: one regex gate, four test cases, zero scope creep. The parseInt prefix-parsing gotcha is well-known and the fix is the obvious correct one. The before/after in the PR body demonstrates the problem concretely, and the route test pins the new behavior. If I had to maintain this in six months, I'd thank the author — the code reads clearly and the comment above the function now matches what it actually does. Approving. ✅ 中文说明置信度:5/5——每个阶段都干净;最小修复让解析器符合文档约定,CI 绿色,无保留意见。 这是一个容易说"是"的 PR:一个正则校验、四个测试用例、零范围蔓延。parseInt 前缀解析的陷阱众所周知,修复方案是显而易见的正确做法。批准。✅ — Qwen Code · qwen3.8-max-preview Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
🩺 serve daemon A/BBuilt the PR base vs this PR head ✅ No response changes against the PR base across 4 scenario(s). — Qwen Code · serve A/B |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
— qwen3.8-max-preview via Qwen Code /review
| if (typeof raw !== 'string' || !/^\d+$/.test(raw)) { | ||
| return DEFAULT_HEATMAP_DAYS; | ||
| } |
There was a problem hiding this comment.
[Suggestion] The new /^\d+$/ guard also changes behavior for negative integer strings: ?heatmapDays=-5 previously parsed to -5 and clamped to the minimum (1), but now falls back to the default (183). That is consistent with the "anything invalid falls back to the default" contract, so it is not a bug — but no test pins it down. — Concrete cost: if a future change relaxes the regex (e.g. /^-?\d+$/), negative inputs would silently revert to clamping at 1 with no failing test to catch it.
Consider adding a negative-value assertion to the existing clamps the heatmapDays query test:
const negative = await request(app).get('/usage/dashboard?heatmapDays=-5');
expect(negative.body.heatmapDays).toBe(183);— qwen3.8-max-preview via Qwen Code /review
There was a problem hiding this comment.
Done in 6066b03c: I added the explicit ?heatmapDays=-5 route-test assertion so negative strings are pinned to the documented default fallback behavior. I also updated the PR body evidence/risk wording to mention the negative-value behavior.
Co-authored-by: qwen-code-ci-bot <[email protected]>
|
Thanks for the review note on #7218 (comment). I added an explicit route-test assertion for ?heatmapDays=-5\ so the new invalid-value fallback behavior is pinned: negative strings now stay on the documented default path instead of slipping back to min-clamp behavior. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
— qwen3.8-max-preview via Qwen Code /review
🔍 Local Verification Report — PR #7218Scope: +16 lines, 2 files — parse heatmapDays strictly Test Results
tmux CLI Startup✅ CLI 正常启动(v0.20.0) Architecture Review论点: 输入解析应使用严格正则而非宽松 论据:
论证: Verdict✅ 7/7 测试通过,CLI 正常启动。建议合并。 Verified locally: unit tests + CLI startup on macOS |
Code Review — #7218
|
| Check | Result |
|---|---|
usage-stats.test.ts @ head 6066b03 |
✅ 7/7 pass |
Discriminating A/B — revert only usage-stats.ts to base, keep new test |
✅ fails 1 failed | 6 passed, AssertionError: expected 1 to be 183 |
prettier --check + eslint on both files |
✅ clean |
| Full old-vs-new parser matrix (19 inputs) | ✅ see below |
Reverting only the source (not the test) is what proves the new assertions are load-bearing rather than vacuously green.
Old vs new, full matrix
"-5" old=1 new=183 CHANGED
"1.5" old=1 new=183 CHANGED
"7junk" old=7 new=183 CHANGED
"1e2" old=1 new=183 CHANGED
" 7" / "7 " old=7 new=183 CHANGED (not in PR body)
"+5" old=5 new=183 CHANGED (not in PR body)
"30.0" old=30 new=183 CHANGED (not in PR body)
"0x10" old=1 new=183 CHANGED (not in PR body)
"9"×400 old=183 new=366 CHANGED (not in PR body — see note 2)
"abc" / "" old=183 new=183
"0" old=1 new=1 (clamp preserved ✓)
"99999" old=366 new=366 (clamp preserved ✓)
"007" old=7 new=7
["5","6"] old=183 new=183 (Express array → default, unchanged)
All valid-unsigned-integer behavior is preserved exactly as the PR body claims.
1. The convention argument is stronger than the PR body makes it (context, not a change request)
The /^\d+$/-then-parseInt shape isn't ad hoc — it is the house pattern for daemon range params, in two places already:
packages/cli/src/serve/server/request-helpers.ts:351—parseMaxQueuedQuerypackages/cli/src/serve/routes/workspace-file-read.ts:133—parseIntInRange, whose docstring states it outright:"Strict on
^\d+$so'','abc','1.5','-3'all reject — the daemon's other range parsers (parseMaxQueuedQuery) use the same regex shape, keeping query-validation behavior consistent."
Worth citing in the PR body, since it pre-empts the obvious reviewer alternative ("why not Number(raw) + Number.isInteger, which would map 1e2 → 100?"). Consistency with the two sibling parsers is a better tiebreaker than either parse semantic on its own.
2. [Low] Dropped Number.isFinite guard — one undocumented behavior delta
The old code's if (!Number.isFinite(value)) is gone. It was load-bearing in exactly one case: a ≥309-digit input overflows parseInt to Infinity.
"9".repeat(400) → old: 183 (default) new: 366 (clamped to MAX)
Not a bug — still bounded by the clamp, and 366 is arguably more consistent (99999 → 366 too, so the old code was the odd one out). And it's not a perf lever: buildHeatmap (usage-dashboard-service.ts:119) iterates records, not days, so a larger window costs nothing extra.
Two small asks:
- Add this row to the PR body's before/after table — it's the one delta not listed.
- Consider one line for exact parity with the siblings, both of which retain a numeric guard (
parseIntInRangeusesNumber.isSafeInteger,parseMaxQueuedQueryusesNumber.isFinite):This PR is currently the only one of the three without such a guard.const value = Number.parseInt(raw, 10); if (!Number.isSafeInteger(value)) return DEFAULT_HEATMAP_DAYS;
3. [Low] SDK-side: a non-integer heatmapDays now silently widens the window
DaemonClient.usageDashboard (packages/sdk-typescript/src/daemon/DaemonClient.ts:971-976) types the option as heatmapDays?: number — not integer-constrained — and serializes with String(opts.heatmapDays). So an external SDK consumer computing e.g. 30.5 used to get a 30-day window and now silently gets the 183-day default: a wider window than requested, with no error. Same for useDaemonUsageDashboard in webui.
No in-repo caller is affected — I checked every useDaemonUsageDashboard / loadUsageDashboard call site and none passes heatmapDays at all (web-shell relies on the server default), which also explains the clean serve-A/B bot result. So this is doc-tightening, not a fix:
- Either
params.set('heatmapDays', String(Math.floor(opts.heatmapDays)))in the SDK, - or say "integer" in the jsdoc at
DaemonClient.ts:970anduseDaemonUsageDashboard.ts:16-17(both currently only say "clamped server-side to 1..366", which reads as though any number is accepted).
4. [Nit] Test hygiene — 4 assertions appended to one it()
All four new cases land in the existing it('clamps the heatmapDays query'). My A/B confirms the cost: with the fix reverted, only the -5 case reports (expected 1 to be 183) — the 1.5 / 7junk / 1e2 regressions are masked behind the first failure. it.each would give per-case signal for the same line count:
it.each([
['99999', 366], ['0', 1], ['abc', 183],
['-5', 183], ['1.5', 183], ['7junk', 183], ['1e2', 183],
])('heatmapDays=%s → %i', async (raw, expected) => {
const res = await request(app).get(`/usage/dashboard?heatmapDays=${raw}`);
expect(res.status).toBe(200);
expect(res.body.heatmapDays).toBe(expected);
});Also: the block asserts only .body.heatmapDays and never .status. If the route 500s, the failure surfaces as expected undefined to be 183 rather than pointing at the actual cause — the file's other tests do assert status.
5. Scope note (follow-up, not this PR)
This normalizes one of several loose parsers on the same surface. Still outstanding, same bug class:
workspace-git-log.ts:95-96—parsePaginationuses bareparseIntfor?limit=/?skip=, so?limit=7junk→7session.ts:257,workspace-status.ts:115,workspace-channel-observed-contacts.ts:30— bareNumber()(different profile: rejects7junk, but accepts1e2," 7",1.5)
Fine to leave; worth a tracking issue if the goal is uniform query validation across the daemon.
Security / performance
No impact either way. Both old and new clamp to [1, 366], so no unbounded window was ever reachable; buildHeatmap is O(records) not O(days); the route is an open read-only GET by existing design; and the regex ^\d+$ is linear with no backtracking risk. Multi-value (?heatmapDays=5&heatmapDays=6 → array) still falls to the default, unchanged.
中文说明
结论:LGTM,可以合并。 改动正确、最小,而且和 daemon 已有的 query 校验约定一致。以下均为可选打磨,不阻塞合并。
本地验证:head 6066b03 上 usage-stats.test.ts 7/7 通过;只回滚 usage-stats.ts(保留新测试)后 1 failed / 6 passed(expected 1 to be 183),证明新断言确实是 load-bearing 而非空转;prettier + eslint 干净;另外跑了 19 组输入的新旧解析对照矩阵,合法无符号整数行为(0→1、99999→366、007→7)完全保留。
-
约定论据比 PR 描述里写的更强:
/^\d+$/+parseInt正是 daemon range 参数的既有写法 ——request-helpers.ts:351的parseMaxQueuedQuery和workspace-file-read.ts:133的parseIntInRange,后者的注释明确写了「和parseMaxQueuedQuery用同样的正则形状,保持 query 校验行为一致」。建议在 PR 正文里引用,可以直接挡掉「为什么不用Number()+isInteger(那样1e2→ 100)」这个必然会被问到的替代方案。 -
[Low] 去掉
Number.isFinite带来一个未记录的行为变化:≥309 位的数字串会让parseInt溢出成Infinity,旧代码返回默认值 183,新代码 clamp 成 366。不是 bug(仍有上界,而且和99999→366 更一致),但建议补进 before/after 表格;另外两个同类 parser 都保留了数值 guard(Number.isSafeInteger/Number.isFinite),可以加一行if (!Number.isSafeInteger(value)) return DEFAULT_HEATMAP_DAYS;做到完全对齐。 -
[Low] SDK 侧:非整数会静默把窗口变大:
DaemonClient.usageDashboard的类型是heatmapDays?: number(未约束整数),用String()序列化。外部 SDK 调用方若传30.5,以前得到 30 天,现在静默得到默认的 183 天 —— 比请求的更宽且不报错。仓库内没有任何调用点传heatmapDays(web-shell 用服务端默认值),所以这也解释了 serve A/B bot 无差异的结果。建议 SDK 里Math.floor,或在 jsdoc 里写明「整数」。 -
[Nit] 测试组织:4 个新断言都塞进同一个
it()。A/B 显示回滚修复后只有-5这条报出来,1.5/7junk/1e2被首个失败掩盖;改成it.each行数相同但每条单独出信号。另外该块只断言.body.heatmapDays没断言.status,一旦 500 会报成expected undefined to be 183,掩盖真实原因。 -
范围备注(后续,不属于本 PR):同类 loose parser 仍在 ——
workspace-git-log.ts:95-96(?limit=7junk→ 7)、session.ts:257、workspace-status.ts:115、workspace-channel-observed-contacts.ts:30。可留作 follow-up issue。
安全 / 性能:无影响。新旧都 clamp 到 [1, 366];buildHeatmap 是 O(records) 而非 O(days);^\d+$ 线性无回溯风险;多值参数(数组)仍走默认值,行为不变。
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
What this PR does
Strictly parses the
heatmapDaysquery parameter for the usage dashboard before applying the existing[1, 366]clamp.Unsigned decimal integer strings keep the current behavior, including
0clamping to1and oversized values clamping to366. Malformed values such as-5,1.5,7junk, and1e2now fall back to the default183days instead of being accepted throughNumber.parseInt()prefix parsing or signed-number parsing.Why it's needed
GET /usage/dashboard?heatmapDays=is a public daemon/Web Shell query surface, and the local parser says invalid values should fall back to the default. Before this change,parseHeatmapDays()usedNumber.parseInt(raw, 10), so malformed strings could still be treated as numbers:-5became-5and then clamped to1,1.5became1,7junkbecame7, and1e2became1.That made malformed query values produce a different heatmap window than the caller actually requested. This PR keeps the existing clamp semantics for valid unsigned integer inputs while making malformed values follow the documented default fallback path.
Reviewer Test Plan
How to verify
Review
parseHeatmapDays()and the route test. A reviewer can confirm that valid unsigned integer query values still clamp as before, while negative values and malformed partial numeric strings now return the default heatmap window.Run:
npm test --workspace=packages/cli -- src/serve/routes/usage-stats.test.ts npx prettier --check packages/cli/src/serve/routes/usage-stats.ts packages/cli/src/serve/routes/usage-stats.test.ts npx eslint packages/cli/src/serve/routes/usage-stats.ts packages/cli/src/serve/routes/usage-stats.test.tsEvidence (Before & After)
Before, the parser accepted signed or partial numeric strings before clamping:
After, the focused route test pins negative and malformed partial numeric values to the default:
Tested on
node_modules(@rollup/rollup-linux-x64-gnu)Environment (optional)
Windows local validation used Node.js v24.15.0 from the existing workspace environment.
Risk & Scope
Number.parseInt()behavior for malformed values such as-5or7junkwill now receive the documented default instead.npm run typecheck --workspace=packages/clicurrently fails in this checkout on unrelated upstream/local build-state errors involving ACP bridge declarations, channel memory exports, and Ink selection types; this PR only ran the focused route test plus file-level format/lint checks.heatmapDaysvalues.Linked Issues
N/A
中文说明
这个 PR 做了什么
这个 PR 收紧了 usage dashboard 的
heatmapDaysquery 参数解析:只有完整的无符号十进制整数字符串才会进入原有的[1, 366]clamp 逻辑。合法整数仍保持原行为,例如
0clamp 到1、超大值 clamp 到366。但-5、1.5、7junk、1e2这类格式错误的值不再通过Number.parseInt()被当作数字接受,而是回退到默认的183天。为什么需要
GET /usage/dashboard?heatmapDays=是 daemon/Web Shell usage dashboard 的公开查询参数。当前注释说明 invalid values 应回退到默认值,但旧实现使用Number.parseInt(raw, 10),导致 malformed 输入被悄悄转成数字:-5会先变成-5再 clamp 到1,1.5变成1,7junk变成7,1e2变成1。这会让调用方传入的错误参数产生一个非预期的 heatmap window。这个 PR 保留合法无符号整数的 clamp 行为,只把 malformed value 拉回到已有的默认 fallback 语义。
Reviewer Test Plan
可以检查
parseHeatmapDays()和现有 route test:合法无符号整数仍按原逻辑 clamp,负数和格式错误的 partial numeric string 会回到默认183。已在 Windows 本地运行:
npm test --workspace=packages/cli -- src/serve/routes/usage-stats.test.ts npx prettier --check packages/cli/src/serve/routes/usage-stats.ts packages/cli/src/serve/routes/usage-stats.test.ts npx eslint packages/cli/src/serve/routes/usage-stats.ts packages/cli/src/serve/routes/usage-stats.test.tsWSL/Linux 侧尝试运行同一 focused test,但当前共享的 Windows
node_modules缺少 Rollup Linux optional dependency,因此测试在启动 Vitest 前失败;这不是本 PR 代码路径的失败。风险与范围
风险较低。唯一行为变化是 malformed 值不再被
Number.parseInt()接受,而是按注释回到默认值;合法无符号整数输入不受影响。