fix(budget): correct multi-day rollover in standardized reset-time handlers

_handle_hour_reset, _handle_minute_reset and _handle_second_reset reduced
the overflowing next_hour with % 24 but always advanced the date by a
single timedelta(days=1). Any duration spanning >= 48h (e.g. 48h, 72h,
3000m, 180000s) therefore reset up to days too early -- 48h from 10:00
landed the reset only 14h out, and 72h advanced a single day instead of
three.

Capture days_ahead = next_hour // 24 before the modulo so the full
whole-day overflow is applied. Fixes #17993.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N6RtoHuxrDqTUo9Mw9h4Cv
This commit is contained in:
Loi Nguyen 2026-07-19 19:56:01 +07:00
parent bd44c9e305
commit c602c9a94a
2 changed files with 31 additions and 3 deletions

View file

@ -236,8 +236,9 @@ def _handle_hour_reset(current_time: datetime, base_midnight: datetime, value: i
# Handle overnight case
if next_hour >= 24:
days_ahead = next_hour // 24
next_hour = next_hour % 24
next_day = base_midnight + timedelta(days=1)
next_day = base_midnight + timedelta(days=days_ahead)
return next_day.replace(hour=next_hour)
return current_time.replace(hour=next_hour, minute=0, second=0, microsecond=0)
@ -270,8 +271,9 @@ def _handle_minute_reset(current_time: datetime, base_midnight: datetime, value:
# Handle overnight case
if next_hour >= 24:
days_ahead = next_hour // 24
next_hour = next_hour % 24
next_day = base_midnight + timedelta(days=1)
next_day = base_midnight + timedelta(days=days_ahead)
return next_day.replace(hour=next_hour, minute=next_minute, second=0, microsecond=0)
return current_time.replace(hour=next_hour, minute=next_minute, second=0, microsecond=0)
@ -309,8 +311,9 @@ def _handle_second_reset(current_time: datetime, base_midnight: datetime, value:
# Handle overnight case
if next_hour >= 24:
days_ahead = next_hour // 24
next_hour = next_hour % 24
next_day = base_midnight + timedelta(days=1)
next_day = base_midnight + timedelta(days=days_ahead)
return next_day.replace(hour=next_hour, minute=next_minute, second=next_second, microsecond=0)
return current_time.replace(hour=next_hour, minute=next_minute, second=next_second, microsecond=0)

View file

@ -71,6 +71,31 @@ class TestStandardizedResetTime(unittest.TestCase):
second_result = get_next_standardized_reset_time("15s", base_time, "UTC")
self.assertEqual(second_result, second_expected)
def test_multi_day_rollover_resets(self):
"""Durations spanning more than 24h must advance the full number of days."""
# Base time: 2023-05-15 10:00 UTC
base_time = datetime(2023, 5, 15, 10, 0, 0, tzinfo=timezone.utc)
# 48h aligns to midnight + 48h; first boundary after 10:00 is the 17th.
self.assertEqual(
get_next_standardized_reset_time("48h", base_time, "UTC"),
datetime(2023, 5, 17, 0, 0, 0, tzinfo=timezone.utc),
)
# 72h -> the 18th (previously advanced only a single day).
self.assertEqual(
get_next_standardized_reset_time("72h", base_time, "UTC"),
datetime(2023, 5, 18, 0, 0, 0, tzinfo=timezone.utc),
)
# 3000m == 50h and 180000s == 50h -> 10:00 + 50h == the 17th at 12:00.
self.assertEqual(
get_next_standardized_reset_time("3000m", base_time, "UTC"),
datetime(2023, 5, 17, 12, 0, 0, tzinfo=timezone.utc),
)
self.assertEqual(
get_next_standardized_reset_time("180000s", base_time, "UTC"),
datetime(2023, 5, 17, 12, 0, 0, tzinfo=timezone.utc),
)
def test_timezone_handling(self):
"""Test timezone handling with different regions"""
# Base time: 2023-05-15 22:30:00 UTC (late in UTC day)