morning, routine: reject the two configs that silently do nothing (V-581)

Both Due functions key their last-fired map by routine name, so two routines sharing a name took turns suppressing each other and one of them never fired. Validate now rejects a duplicate name in either package.

parseHHMM checked the digits arithmetically, which let a stray character cancel out: window_start of 2 :00 loaded as 04:00 and passed the validation that exists to catch that typo. Each of the four positions is now checked as a digit, which makes the negative bounds unreachable and they are gone.

Folded the three copies of the unevidenced-item loop in Evaluate, Outstanding and Due into one helper.
This commit is contained in:
2026-08-06 03:12:36 +04:00
parent 85456d3833
commit 5447f08c06
5 changed files with 69 additions and 25 deletions
+11 -2
View File
@@ -38,13 +38,22 @@ type Routine struct {
}
// Validate reports the first structural problem with a routine set: a missing
// name/cron/body or an unparseable cron expression. Called at config load so a
// typo surfaces at startup, not as a silently-never-firing routine at runtime.
// name/cron/body, a duplicate name or an unparseable cron expression. Called at
// config load so a typo surfaces at startup, not as a silently-never-firing
// routine at runtime.
//
// Names must be unique because Due keys its last-fired map by name. Two
// routines sharing one would take turns being suppressed by each other's fire.
func Validate(routines []Routine) error {
seen := make(map[string]bool, len(routines))
for _, r := range routines {
if r.Name == "" {
return fmt.Errorf("routine: name is required")
}
if seen[r.Name] {
return fmt.Errorf("routine %q: duplicate name", r.Name)
}
seen[r.Name] = true
if r.Body == "" {
return fmt.Errorf("routine %q: body is required", r.Name)
}