SAFETY-123: the day plan's hour loop steps in UTC and always ends
checks / unit (pull_request) Successful in 48m40s
infra-pin / fresh (pull_request) Successful in 2s

dayplan._hour_slices() added an hour to the local wall clock and read it
back. Without tzdata (tariff._EUTimezone) 02:00 on the spring change reads
as 00:00 UTC again, so the loop never advanced and the list grew until the
process died. Each step now adds the time left in the local hour to the UTC
instant. Ordinary days give the old slices; the spring day gives 23 slices
and the autumn day 25. No option, no text change.

test_safety123.py: every quarter of 27-29 March and 30 Oct-1 Nov 2027 (and
the US change days) for the slices, dayplan.plan() and
Controller._mode_roles(), each call under a hard time limit.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
This commit is contained in:
glenn schrooyen
2026-10-27 08:58:54 +01:00
co-authored by Claude
parent a1d07030f8
commit d2820e8b9b
3 changed files with 564 additions and 4 deletions
+6 -4
View File
@@ -157,13 +157,15 @@ class Plan:
def _hour_slices(t0, t1):
"""Local-hour slices of [t0, t1): (start, end, local hour)."""
"""Local-hour slices of [t0, t1): (start, end, local hour).
Steps in UTC, so `t` always advances (SAFETY-123, DECISIONS)."""
out, t = [], t0
while t < t1:
lt = _local(t)
nxt = (lt.replace(minute=0, second=0, microsecond=0)
+ timedelta(hours=1)).astimezone(timezone.utc)
e = min(nxt, t1)
left = timedelta(minutes=60 - lt.minute) - timedelta(
seconds=lt.second, microseconds=lt.microsecond)
e = min(t.astimezone(timezone.utc) + left, t1)
out.append((t, e, lt.hour))
t = e
return out