mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 17:57:54 -10:00
* cron: dep-free 5-field cron expression parser * cron: next-run evaluator with Vixie DOM/DOW semantics * cron: preset recipes + loop.md prompt resolution (cap + symlink reject) * cron: file-backed job store (metadata.json + runs.jsonl) * cli: zero cron add/list/rm/pause/resume over the cron store * cli: zero cron run foreground scheduler (--once/--catch-up, exec per fire) * cron: fix adversarial-review findings (DST loop, traversal, runaway re-fire, +) - Next: fix infinite loop on DST spring-forward gaps (advance hours by absolute addition + a forward-progress guard); widen the search window to 9 years so a Feb-29 schedule across a century non-leap year (2096->2104) isn't reported as impossible. - store: reject path-traversal job ids (Get/Update/Remove/AppendRun) so 'cron rm ../..' can't delete outside the store; List now surfaces corrupt jobs as a warning instead of silently dropping them. - runner: reject impossible schedules at add even with --run-now; auto-pause a job whose schedule can no longer advance (was re-firing every tick); only skip-reschedule STRICTLY-overdue jobs on startup (keep exactly-due ones); capture stderr into RunRecord.Error on non-zero exit; pass the prompt via the inline --prompt= form so dash-leading prompts aren't misparsed. - Regression tests for each (DST, leap gap, traversal, corrupt-list, pause, reconcile, dash-prompt). * cron: address #155 review (store-error surfacing, resume guard, Runs traversal, +) - fireJob/reconcile/cronRun now surface AppendRun/Update failures to stderr instead of discarding them (a failed write no longer silently re-fires/loses history); fireJob takes stderr. - cron add rejects extra positional arguments instead of silently ignoring them. - cronResume rejects an unparseable/impossible schedule instead of reactivating a job with a stale/zero NextRunAt. - store.AppendRun returns the Close() error (buffered-write failures surface on close); store.Runs now validates the id (path-traversal guard, matching the other store methods). - tests: Runs traversal rejection, cron add extra-args, resume-impossible. * cron: check store errors in cron CLI tests CodeRabbit (critical): the cron CLI tests ignored errors from store.List/Add/Get, so a store failure would surface as a misleading assertion on a zero-value job/slice instead of a clear failure. Check every store op's error across the test file (not only the two flagged sites). * cron: DefaultRoot falls back to os.UserHomeDir when HOME is unset Vasanth (P1): cron's DefaultRoot mirrored neither sessions.DefaultRoot nor reality — with no XDG_DATA_HOME and no HOME it produced a RELATIVE ".local/share/zero/cron" under the caller's cwd, so on Windows/restricted shells cron jobs scattered per working directory and could write repo-local data. Now falls back to os.UserHomeDir() like sessions.DefaultRoot. Adds TestDefaultRootEmptyHomeFallsBackToUserHome (asserts the path stays absolute).
58 lines
2.2 KiB
Go
58 lines
2.2 KiB
Go
package cron
|
|
|
|
import "testing"
|
|
|
|
func bits(ns ...int) uint64 {
|
|
var m uint64
|
|
for _, n := range ns {
|
|
m |= 1 << uint(n)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func TestParseValidFields(t *testing.T) {
|
|
cases := []struct {
|
|
expr string
|
|
minute, hour, dom, mon, dow uint64
|
|
domStar, dowStar bool
|
|
}{
|
|
{"* * * * *", full(0, 59), full(0, 23), full(1, 31), full(1, 12), full(0, 6), true, true},
|
|
{"0 9 * * *", bits(0), bits(9), full(1, 31), full(1, 12), full(0, 6), true, true},
|
|
{"*/15 * * * *", bits(0, 15, 30, 45), full(0, 23), full(1, 31), full(1, 12), full(0, 6), true, true},
|
|
{"0 0,12 * * *", bits(0), bits(0, 12), full(1, 31), full(1, 12), full(0, 6), true, true},
|
|
{"0 9-17 * * 1-5", bits(0), bits(9, 10, 11, 12, 13, 14, 15, 16, 17), full(1, 31), full(1, 12), bits(1, 2, 3, 4, 5), true, false},
|
|
{"0 0 1 */3 *", bits(0), bits(0), bits(1), bits(1, 4, 7, 10), full(0, 6), false, true},
|
|
{"0 0 * * SUN", bits(0), bits(0), full(1, 31), full(1, 12), bits(0), true, false},
|
|
{"0 0 * JAN,DEC *", bits(0), bits(0), full(1, 31), bits(1, 12), full(0, 6), true, true},
|
|
{"0 0 * * 7", bits(0), bits(0), full(1, 31), full(1, 12), bits(0), true, false}, // 7 => Sunday
|
|
}
|
|
for _, c := range cases {
|
|
s, err := Parse(c.expr)
|
|
if err != nil {
|
|
t.Fatalf("Parse(%q) error: %v", c.expr, err)
|
|
}
|
|
if s.minute != c.minute || s.hour != c.hour || s.dom != c.dom || s.month != c.mon || s.dow != c.dow {
|
|
t.Fatalf("Parse(%q) sets minute=%b hour=%b dom=%b mon=%b dow=%b", c.expr, s.minute, s.hour, s.dom, s.month, s.dow)
|
|
}
|
|
if s.domStar != c.domStar || s.dowStar != c.dowStar {
|
|
t.Fatalf("Parse(%q) star flags dom=%v dow=%v want %v/%v", c.expr, s.domStar, s.dowStar, c.domStar, c.dowStar)
|
|
}
|
|
if s.String() != c.expr {
|
|
t.Fatalf("String()=%q want %q", s.String(), c.expr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseErrors(t *testing.T) {
|
|
bad := []string{
|
|
"", "* * * *", "* * * * * *", // wrong field count
|
|
"60 * * * *", "* 24 * * *", "* * 0 * *", "* * 32 * *", "* * * 0 *", "* * * 13 *", "* * * * 8", // out of range
|
|
"*/0 * * * *", "5-2 * * * *", "a * * * *", "* * * FOO *", "1- * * * *", "1,,2 * * * *",
|
|
}
|
|
for _, expr := range bad {
|
|
if _, err := Parse(expr); err == nil {
|
|
t.Fatalf("Parse(%q) expected error", expr)
|
|
}
|
|
}
|
|
}
|