fix(core): reject invalid cron task entries#5309
Conversation
37ca6c4 to
da603b3
Compare
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Hi @tt-a1i — thanks for the fix!
The PR body doesn't follow the PR template. The template requires these sections:
## What this PR does— describe the change in prose## Why it's needed— motivation / user-facing benefit## Reviewer Test Plan— with### How to verify,### Evidence (Before & After),### Tested onsub-sections## Risk & Scope— risks, out-of-scope items, breaking changes## Linked Issues—Fixes #N/Closes #N<details>中文说明</details>— full Chinese translation
Your PR currently uses ## Summary, ## Test plan, ## Demo, and ## AI Assistance Disclosure instead. Please restructure the body to match the template so reviewers can evaluate it consistently.
中文说明
你好 @tt-a1i — 感谢你的修复!
PR 正文没有按照 PR 模板 填写。模板要求以下部分:
## What this PR does— 用文字描述变更## Why it's needed— 动机 / 用户可见的收益## Reviewer Test Plan— 包含### How to verify、### Evidence (Before & After)、### Tested on子部分## Risk & Scope— 风险、不在范围内的内容、破坏性变更## Linked Issues—Fixes #N/Closes #N<details>中文说明</details>— 完整中文翻译
当前 PR 使用的是 ## Summary、## Test plan、## Demo 和 ## AI Assistance Disclosure。请按照模板重新组织内容,以便审查者能够一致地评估。
— Qwen Code · qwen3.7-max
|
@qwen-code /triage |
|
Thanks @tt-a1i for the fix! Template looks good now ✓ — all required sections present. On direction: this solves a real data-loss bug. On approach: the scope is tight — one function changed from Moving on to code review. 🔍 中文说明感谢 @tt-a1i 的修复! 模板现已完整 ✓ — 所有必需章节均已填写。 方向:这解决了一个真实的数据丢失 bug。 方案:范围很紧凑 — 一个函数从 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal: Same approach — replace the silent Comparison with the PR: The implementation matches exactly. The Findings: No correctness bugs, security issues, or AGENTS.md violations. The diff is focused — every line changed serves the stated goal. The type narrowing works because every entry must pass the Test Results22 tests pass (up from 21 on main — one new test added, one modified): New test — Modified test — Evidence (Before & After): N/A — non-visible storage-validation logic, covered by unit tests above. 中文说明代码审查独立方案: 相同的方法 — 将静默的 与 PR 的对比: 实现完全匹配。 发现: 没有正确性 bug、安全问题或违反 AGENTS.md 的情况。diff 很聚焦 — 每一行改动都服务于声明的目标。类型收窄有效,因为每个条目必须通过 测试结果22 个测试全部通过(main 上是 21 个 — 新增一个,修改一个): 新测试 — 修改的测试 — 证据(前后对比):N/A — 非用户可见的存储校验逻辑,由上述单元测试覆盖。 — Qwen Code · qwen3.7-max |
|
Stepping back: this is a clean, well-scoped fix for a real data-loss bug. The motivation is solid — silently dropping entries and then persisting the truncated list is the kind of corruption that's painful to debug when it bites. The fix aligns perfectly with the existing fail-closed philosophy already used for malformed JSON and non-array files. The implementation matches what I'd write independently. The diff is +23/-22 — almost a wash — and every line earns its place. The The new All 22 tests pass. No correctness concerns, no convention violations, no scope creep. This is the kind of PR that's easy to maintain — focused, well-tested, and does exactly what it says. Approving. ✅ 中文说明回顾:这是一个干净、范围合理的修复,解决了一个真实的数据丢失 bug。动机很充分 — 静默丢弃条目然后持久化截断后的列表,是那种一旦出问题就很难调试的损坏类型。修复与现有的非法 JSON 和非数组文件的快速失败理念完全一致。 实现与我独立设想的方案一致。diff 是 +23/-22 — 几乎持平 — 每一行都物有所值。 新的 22 个测试全部通过。没有正确性问题、没有违反规范、没有范围蔓延。这是易于维护的 PR — 聚焦、测试充分、言行一致。 批准 ✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
✅ Local build & verification — LGTM, ready to mergeBuilt the real artifacts from this PR's HEAD and verified the fail-closed behavior end-to-end on a real filesystem, including how the existing callers handle the new throw. Correct, well-scoped, and well-guarded by tests. Recommending merge. What this PR does
Environment
Real-filesystem behavior (actual source, isolated via
|
| Scenario | Observed |
|---|---|
[valid, invalid] → readCronTasks() |
throws Invalid task entry at index 1 in …/scheduled_tasks.json — fix or delete the entry; refusing to drop it from the schedule. ✅ |
[valid, invalid] → updateCronTasks(append) |
throws, and on-disk file is byte-for-byte unchanged ✅ (refuse to clobber) |
[valid, valid] → readCronTasks() |
returns both, unchanged ✅ |
[] (empty) → readCronTasks() |
returns [], no false throw ✅ |
[null, valid] → readCronTasks() |
throws at index 0 (null/non-object rejected) ✅ |
Verification matrix
| Check | Command / method | Result |
|---|---|---|
| Unit tests | vitest run …/cronTasksFile.test.ts |
✅ 22 passed (incl. the 2 new) |
| Real-fs harness | exercise real readCronTasks/updateCronTasks |
✅ table above |
| Mutation A/B | revert throw → parsed.filter(isValidTask), re-run |
✅ exactly the 2 new tests fail (throws for invalid task entries, refuses to clobber …); other 20 pass → genuine guards |
| Blast radius | inspect callers of readCronTasks |
✅ both already fail-closed — see below |
| Debug-logger removal | grep source + shipped artifact | ✅ no dangling reference; old Dropped … invalid task entr… string gone from the bundle |
| Typecheck | tsc --noEmit (packages/core) |
✅ clean |
| Lint | eslint (2 changed files) |
✅ clean |
| Format | prettier --check (2 files) |
✅ clean |
| Whitespace | git diff --check |
✅ clean |
| Real build | npm run build + npm run bundle |
✅ both exit 0 |
| Fix in shipped artifacts | grep core dist + CLI chunk | ✅ marker present in packages/core/dist/...cronTasksFile.js and dist/chunks/chunk-*.js |
| Built binary boots | node dist/cli.js --version |
✅ 0.18.3 |
Callers handle the new throw gracefully (the key safety check)
readCronTasks already threw for malformed-JSON / non-array; this PR extends the same contract to invalid entries. Both production callers were already designed for it:
cron-list.tswraps the read intry/catchand surfaces a clean error result to the user (Error listing cron jobs: …) — no crash.cronScheduler.tsloadFileTasks()catches and keeps its current in-memory view, retrying on a later reload (explicitly avoids wiping loaded durable jobs).updateCronTasks()reads before writing, so a throw means the write never happens — the file is preserved for recovery.
Behavioral tradeoff (deliberate, non-blocking)
With this change, a single invalid entry now blocks the whole schedule from loading (and surfaces an error in cron list) until the user fixes/deletes it, rather than silently loading the valid subset. That is the intended fail-closed tradeoff — identical to how malformed-JSON / non-array files already behave — and is preferable to silently and permanently losing a recoverable entry on the next write. The error names the exact index to fix.
Verdict: LGTM ✅ — a correct, consistent data-safety fix; callers degrade gracefully and the two new tests genuinely guard the behavior.
🇨🇳 中文版(点击展开)
✅ 本地构建验证 — LGTM,可以合并
我从该 PR 的 HEAD 在本地构建了真实产物,并在真实文件系统上端到端验证了「快速失败」行为,包括现有调用方如何处理这个新抛出的错误。改动正确、范围收敛、且有测试守护。建议合并。
这个 PR 做了什么
readCronTasks() 现在在「合法数组里出现第一个非法任务条目」时抛错(Invalid task entry at index <n> in <file> …),而不再静默 filter() 掉它。这避免了随后的 updateCronTasks() 把过滤后的列表写回、从而永久删除一个可恢复的、用户手工编辑的条目 —— 让非法条目的处理与现有的「非法 JSON / 非数组」行为保持一致(快速失败、拒绝写穿损坏数据)。同时移除了已不再使用的 debug logger。修复 #5308。
环境
- macOS(Darwin 25.5.0)、Node v22.22.2、npm 10.9.7
- worktree 检出在 PR HEAD
da603b3d08;base 为origin/main(落后 31 个提交,但MERGEABLE;2 个改动文件相对origin/main干净)。
真实文件系统行为(真实源码,通过 Storage.setRuntimeBaseDir 隔离,不碰真实 ~/.qwen)
| 场景 | 观察结果 |
|---|---|
[合法, 非法] → readCronTasks() |
抛错 Invalid task entry at index 1 in …/scheduled_tasks.json — fix or delete the entry; refusing to drop it from the schedule. ✅ |
[合法, 非法] → updateCronTasks(追加) |
抛错,且磁盘文件逐字节不变 ✅(拒绝覆盖) |
[合法, 合法] → readCronTasks() |
原样返回两条 ✅ |
[](空数组) → readCronTasks() |
返回 [],不会误抛 ✅ |
[null, 合法] → readCronTasks() |
在 index 0 抛错(null/非对象被拒) ✅ |
验证矩阵
| 检查项 | 命令 / 方法 | 结果 |
|---|---|---|
| 单元测试 | vitest run …/cronTasksFile.test.ts |
✅ 22 通过(含 2 个新增) |
| 真实文件系统 harness | 跑真实的 readCronTasks/updateCronTasks |
✅ 见上表 |
| 变异 A/B | 把抛错改回 parsed.filter(isValidTask) 后重跑 |
✅ 恰好那 2 个新测试失败(throws for invalid task entries、refuses to clobber …),其余 20 个通过 → 是有效守卫 |
| 影响范围 | 检查 readCronTasks 的调用方 |
✅ 两个调用方本来就快速失败 —— 见下 |
| debug logger 移除 | grep 源码 + 发布产物 | ✅ 无悬挂引用;旧的 Dropped … invalid task entr… 字符串已从 bundle 中消失 |
| 类型检查 | tsc --noEmit(packages/core) |
✅ 干净 |
| Lint | eslint(2 个改动文件) |
✅ 干净 |
| 格式 | prettier --check(2 个文件) |
✅ 干净 |
| 空白字符 | git diff --check |
✅ 干净 |
| 真实构建 | npm run build + npm run bundle |
✅ 均 exit 0 |
| 修复进入发布产物 | grep core dist + CLI chunk | ✅ 标记出现在 packages/core/dist/...cronTasksFile.js 和 dist/chunks/chunk-*.js |
| 构建出的二进制可启动 | node dist/cli.js --version |
✅ 0.18.3 |
调用方能优雅处理新抛出的错误(关键安全检查)
readCronTasks 本来就会对「非法 JSON / 非数组」抛错,这个 PR 只是把同样的契约扩展到非法条目。两个生产调用方本来就为此设计:
cron-list.ts用try/catch包住读取,并向用户返回一个干净的错误结果(Error listing cron jobs: …)—— 不会崩溃。cronScheduler.ts的loadFileTasks()捕获后保留当前内存视图,等下一次 reload 重试(明确避免清空已加载的持久任务)。updateCronTasks()先读后写,所以抛错就意味着写入根本不会发生 —— 文件被保留下来以便恢复。
行为取舍(有意为之,不阻塞)
此改动后,单个非法条目现在会阻止整个计划表加载(并在 cron list 里暴露错误),直到用户修复/删除它,而不再静默加载合法子集。这是有意的「快速失败」取舍 —— 与「非法 JSON / 非数组」文件现有行为完全一致 —— 比起在下一次写入时静默且永久丢失一个可恢复条目要更好。错误信息会指明具体要修复的 index。
结论:LGTM ✅ —— 一个正确、一致的数据安全修复;调用方能优雅降级,两个新测试能真正守住该行为。
What this PR does
Makes durable cron task reads fail closed when
scheduled_tasks.jsoncontains an invalid task entry inside an otherwise-valid array. PreviouslyreadCronTasks()ranparsed.filter(isValidTask), silently dropping malformed entries and only emitting a debug warning; a laterupdateCronTasks()would then persist the filtered list and permanently delete the dropped entry. The function now iterates over every parsed entry and throws an error (Invalid task entry at index <n> in <file> — fix or delete the entry; refusing to drop it from the schedule.) on the first invalid one, returning the array unchanged when all entries are valid. This aligns invalid-entry handling with the existing behavior for malformed JSON and non-array JSON: surface the corruption and refuse to write through it. The now-unused debug logger is removed.Why it's needed
readCronTasks()filtering invalid entries out ofscheduled_tasks.jsonmeant a subsequentupdateCronTasks()call could write the filtered list back and permanently delete a malformed (but recoverable, user-hand-edited) entry. Malformed JSON and non-array files already fail closed to avoid clobbering recoverable user data; arrays with invalid task entries should behave the same so users do not silently lose data on the next write.Reviewer Test Plan
How to verify
Repro: seed
scheduled_tasks.jsonwith an array containing one invalid task entry alongside valid ones, then callreadCronTasks()/updateCronTasks().updateCronTasks()persists the truncated list, losing the entry.readCronTasks()rejects with/Invalid task entry/, andupdateCronTasks()refuses to write — the on-disk file is left byte-for-byte unchanged.Verification commands (run from
packages/coreunless noted):npx vitest run src/services/cronTasksFile.test.tsnpm run typecheck --workspace=packages/corenpm run build --workspace=packages/corenpm run lint -- --fix packages/core/src/services/cronTasksFile.ts packages/core/src/services/cronTasksFile.test.tsnpx prettier --check packages/core/src/services/cronTasksFile.ts packages/core/src/services/cronTasksFile.test.tsgit diff --checkEvidence (Before & After)
N/A — non-visible storage-validation logic fix; covered by the unit tests above (a read that now throws on invalid entries, plus an update that refuses to clobber the file and asserts the on-disk bytes are unchanged).
Tested on
✅ tested ·⚠️ not tested · N/A
Environment (optional)
Unit tests only (npm workspaces); no special environment required.
Risk & Scope
packages/core/src/services/cronTasksFile.ts. Behavior change is limited to reads ofscheduled_tasks.jsonthat contain invalid task entries — these now throw instead of being silently filtered. Files with malformed JSON or non-array JSON already threw, and fully-valid arrays are unaffected.Linked Issues
Fixes #5308
中文说明
这个 PR 做了什么
让持久化的 cron 任务读取在
scheduled_tasks.json的合法数组中包含非法任务条目时「快速失败、拒绝继续」。此前readCronTasks()执行parsed.filter(isValidTask),会静默丢弃非法条目,仅打印一条 debug 警告;随后的updateCronTasks()会把过滤后的列表写回磁盘,从而永久删除被丢弃的条目。现在该函数会遍历每一个解析出的条目,遇到第一个非法条目就抛出错误(Invalid task entry at index <n> in <file> — fix or delete the entry; refusing to drop it from the schedule.),当所有条目都合法时则原样返回数组。这样就让「非法条目」的处理与现有的「非法 JSON」「非数组 JSON」处理保持一致:暴露损坏、拒绝写穿。同时移除了已不再使用的 debug logger。为什么需要它
readCronTasks()过滤掉scheduled_tasks.json中的非法条目,意味着随后的一次updateCronTasks()调用会把过滤后的列表写回,从而永久删除一个非法(但可恢复、由用户手工编辑的)条目。非法 JSON 和非数组文件已经会快速失败以避免覆盖可恢复的用户数据;包含非法任务条目的数组也应当采取相同行为,使用户不会在下一次写入时悄无声息地丢失数据。评审者测试计划
如何验证
复现:在
scheduled_tasks.json中写入一个数组,包含一个非法任务条目以及若干合法条目,然后调用readCronTasks()/updateCronTasks()。updateCronTasks()会持久化被截断的列表,导致该条目丢失。readCronTasks()以/Invalid task entry/拒绝,updateCronTasks()拒绝写入——磁盘上的文件逐字节保持不变。验证命令(除特别说明外,均在
packages/core目录下执行):npx vitest run src/services/cronTasksFile.test.tsnpm run typecheck --workspace=packages/corenpm run build --workspace=packages/corenpm run lint -- --fix packages/core/src/services/cronTasksFile.ts packages/core/src/services/cronTasksFile.test.tsnpx prettier --check packages/core/src/services/cronTasksFile.ts packages/core/src/services/cronTasksFile.test.tsgit diff --check证据(前后对比)
N/A —— 非用户可见的存储校验逻辑修复;已由上述单元测试覆盖(一个在遇到非法条目时会抛错的读取测试,外加一个拒绝覆盖文件并断言磁盘字节保持不变的更新测试)。
测试平台
✅ 已测试 ·⚠️ 未测试 · N/A
环境(可选)
仅单元测试(npm workspaces);无需特殊环境。
风险与范围
packages/core/src/services/cronTasksFile.ts。行为变化仅限于读取包含非法任务条目的scheduled_tasks.json——这些情况现在会抛错,而非被静默过滤。非法 JSON 或非数组 JSON 的文件本来就会抛错,完全合法的数组不受影响。关联 Issue
Fixes #5308
AI Assistance Disclosure
I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.