fix(cron): keep multi-time cron expressions in cron mode in the job editor (#8469) - #8644
Conversation
UX Review (Fable 5) — ✅ PASSUX-level review of UX-Verdict: PASS The editor now shows the true schedule (raw cron) instead of a weekly widget that silently misrepresented multi-time jobs and destroyed run times on save. [UX-REVIEWED] bd9ee54 |
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Tightening the weekly-mode detector to what weekly mode can actually represent fixes the root cause — silent schedule rewrites — with zero new surface. Suggestions
[DESIGN-REVIEWED] bd9ee54 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All checks complete. The fix is cause-level for #8469 (classifier only admits what Weekly mode can round-trip), exports pre-exist, screenshots follow the documented First-Principles-Verdict: CONCERNS Cause-level and complete for cron expressions, but the same lossy-reparse-then-save cause has one counted unfixed sibling: interval rounding in the same function. What this change shipsIntent: stop the job editor from silently collapsing a multi-time cron schedule to one daily run on save (#8469) — a FIX.
Watch
No duplication finding: the [FIRST-PRINCIPLES-REVIEWED] bd9ee54 |
|
Disposition — First Principles CONCERNS (accepted, deferred with a tracked home): The Watch finding is correct: interval parsing in the same function is a lossy-reparse-then-save sibling ( |
NicholasRBowers
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (4 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: fix with a clear root cause (#8469: parseInt silently truncated multi-time cron fields like '9,12,15' to 9 in the job editor's weekly-mode classifier, dropping run times on save) -- JobForm.tsx classifier tightening + new test file + 2 screenshots.
chenmingwei23
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (4 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: fix keeps multi-time cron expressions in cron mode in the job editor (2 code/test files + 2 screenshots), clear root cause #8469 (parseInt truncated a comma/step field and dropped run times on save).
dwu96
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (4 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: single-predicate fix in JobForm parseJobDefaults so a cron expression with a minute/hour list, range or step (or a dow field expandDow cannot fully represent) stays in cron mode and round-trips verbatim instead of parseInt silently truncating it and dropping run times on save; one source function plus a new regression test file.
iamwhatever
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (4 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: JobForm's weekly-mode detection accepted list/range/step minute and hour fields, so parseInt truncated '9,12,15' to 9 and a save silently dropped the other run times (#8469); the fix requires plain minute/hour fields and a fully-representable day-of-week set before classifying a job as weekly, otherwise falling through to cron mode where the raw expression round-trips verbatim.
…exactly (#8769) `parseJobDefaults` chose the interval unit by MAGNITUDE -- the largest unit that is <= `secs` -- and then rounded. For a 90-minute job that meant `secs = 5400 >= 3600`, so unit 'hours' and `Math.round(1.5) = 2`. `buildBody` re-serialises `intVal * 3600`, so opening that job and saving an unrelated field silently rewrote its schedule to 2 hours. The same shape one unit up: a 36-hour job (129600) chose 'days' and round(1.5) = 2, persisting as 2 days. Choose the largest unit that divides `secs` EVENLY instead. 90 minutes is exactly representable in the units the form already offers; the magnitude choice discarded that representation before the rounding ever ran. Where nothing divides evenly (e.g. 90 seconds) the pre-existing nearest-magnitude choice is kept deliberately. The form offers no sub-minute unit, so that schedule cannot be represented here at all -- widening the unit set is a separate question from this rounding defect, and quietly changing it under cover of the fix would be a second, undeclared change. This is the interval-side sibling of #8469, named in #8644's review as the one unfixed site of that root cause remaining in this function. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Summary
The cron job detail editor classified any 5-field cron expression with a day-of-week set and dom/month
*as a single-time Weekly schedule, without checking that the minute and hour fields are plain integers. A multi-time expression like0 9,12,15 * * 1-5opened in Weekly mode showing only 09:00 (parseInt('9,12,15')truncates to 9), and saving from that editor — even to change an unrelated field — re-serialised the schedule from the single time field, silently collapsing the job to one daily run.This is the "preserve the underlying expression" option from the issue: the weekly detector in
parseJobDefaultsnow requires/^\d{1,2}$/, ≤59 / ≤23 — same plain-field grammar ascronClockincronUtils, so the list view and editor classify the same job the same way), andexpandDow, with numeric tokens in cron's 0–7 range (expandDow('1,3-5/2')silently drops the stepped segment;parseDowTokenwraps8to Monday via% 7— both were residual silent-rewrite paths).Anything else falls through to Cron expression mode, where
buildBodyround-trips the raw expression verbatim (body.cron = expr). No new UI; genuine weekly expressions (30 9 * * 1,3,0 13 * * MON-FRI) parse exactly as before.Closes #8469
Before / after
A job scheduled
0 9,12,15 * * 1-5(9:00, 12:00, 15:00 on weekdays) opened in the editor:Tests
New
website/src/test/JobForm.multiTime.test.tsvia the exportedparseJobDefaults/buildBody:0 9,12,15 * * 1-5,*/15 9 * * 1-5,0 9-11 * * 1→cronmode, raw expression preserved0 9 * * 1,3-5/2(mixed representable + stepped dow segment),0 9 * * 8(out-of-range dow),0 007 * * 1(zero-padded hour),60 9 * * 1/0 25 * * 1(out-of-range minute/hour) →cronmode30 9 * * 1,3and0 13 * * MON-FRIstill parse asweekly(regression guards)buildBodyon parsed multi-time defaults emitsbody.cronidentical to the original expressionVerification
npx tsc -band eslint clean on the changed files; vitest runs in CIcronClock, dow 0–7 range check) — both appliedPattern harvest
Rule candidate: a form that opens a stored value in a simplified editing mode must first verify the mode can round-trip the value losslessly (parse -> re-serialise -> compare, or a strict representability grammar); any value the mode cannot faithfully represent must open in the raw/advanced mode instead. The defect class —
parseInt/lossy parse selecting the simplified mode, then save re-serialising from the truncated view — also existed here for minute, hour, and day-of-week independently, so the check belongs on every field the simplified mode collapses.