Files
nanobot-runtime/skills/remind/tests/test_random_times.py
2026-06-24 08:11:12 +02:00

163 lines
5.3 KiB
Python

from datetime import date, datetime
import pytest
from random_times import MIN_GAP_MIN, compute_fire_times
# Window 09:00-21:00 = minutes 540..1260 -> 720 min wide.
WINDOW = "09:00-21:00"
WINDOW_START = datetime(2026, 3, 21, 9, 0)
WINDOW_END = datetime(2026, 3, 21, 21, 0)
def cfg(**overrides) -> dict:
base = {"times_per_day": 5, "window": WINDOW}
base.update(overrides)
return base
def test_deterministic():
day = date(2026, 3, 21)
assert compute_fire_times(day, "drink water", cfg()) == compute_fire_times(day, "drink water", cfg())
def test_differs_per_text():
day = date(2026, 3, 21)
assert compute_fire_times(day, "drink water", cfg()) != compute_fire_times(day, "stretch", cfg())
@pytest.mark.parametrize("count", [1, 2, 5, 10])
def test_count_matches_times_per_day(count):
times = compute_fire_times(date(2026, 3, 21), "x", cfg(times_per_day=count))
assert len(times) == count
def test_min_gap_respected():
times = compute_fire_times(date(2026, 3, 21), "x", cfg(times_per_day=8))
for earlier, later in zip(times, times[1:]):
assert (later - earlier).total_seconds() >= MIN_GAP_MIN * 60
def test_within_window():
for fire in compute_fire_times(date(2026, 3, 21), "x", cfg()):
assert WINDOW_START <= fire <= WINDOW_END
@pytest.mark.parametrize(
"day,expected",
[
(date(2026, 3, 23), True), # Monday
(date(2026, 3, 27), True), # Friday
(date(2026, 3, 28), False), # Saturday
(date(2026, 3, 29), False), # Sunday
],
)
def test_days_weekday_filter(day, expected):
times = compute_fire_times(day, "x", cfg(days="1-5"))
assert bool(times) == expected
@pytest.mark.parametrize(
"spec,day,expected",
[
("*", date(2026, 3, 28), True), # Saturday allowed by wildcard
("0", date(2026, 3, 29), True), # Sunday as 0
("7", date(2026, 3, 29), True), # Sunday as 7
("1,3,5", date(2026, 3, 25), True), # Wednesday
("1,3,5", date(2026, 3, 24), False), # Tuesday
],
)
def test_days_parser(spec, day, expected):
times = compute_fire_times(day, "x", cfg(days=spec))
assert bool(times) == expected
def test_from_until_filter():
bounded = cfg(**{"from": "2026-06-01", "until": "2026-06-30"})
assert compute_fire_times(date(2026, 5, 31), "x", bounded) == []
assert compute_fire_times(date(2026, 7, 1), "x", bounded) == []
assert len(compute_fire_times(date(2026, 6, 15), "x", bounded)) == 5
def test_infeasible_count_raises():
# 50 times * 15-min gap = 735 min required > 720 min window.
with pytest.raises(ValueError):
compute_fire_times(date(2026, 3, 21), "x", cfg(times_per_day=50))
def test_bad_window_raises():
with pytest.raises(ValueError):
compute_fire_times(date(2026, 3, 21), "x", cfg(window="21:00-09:00"))
def test_config_validated_before_date_filter():
# Out-of-range date still surfaces a structural error rather than returning [].
with pytest.raises(ValueError):
compute_fire_times(date(2000, 1, 1), "x", cfg(times_per_day=50, until="1999-01-01"))
# --- Weekly period -----------------------------------------------------------
# Week of Mon 2026-03-23 .. Sun 2026-03-29.
WEEK = [date(2026, 3, d) for d in range(23, 30)]
def weekly_cfg(**overrides) -> dict:
base = {"times_per_day": 2, "window": WINDOW, "period": "week"}
base.update(overrides)
return base
def _week_fires(text: str, cfg_dict: dict) -> list[datetime]:
return [fire for day in WEEK for fire in compute_fire_times(day, text, cfg_dict)]
def test_weekly_count_across_week():
assert len(_week_fires("x", weekly_cfg(times_per_day=2))) == 2
def test_weekly_distinct_days():
fires = _week_fires("x", weekly_cfg(times_per_day=3))
assert len({f.date() for f in fires}) == 3
def test_weekly_deterministic_across_days():
# Every day of the week must agree on the same plan, so summing per-day calls
# over the week yields a stable set regardless of call order.
assert _week_fires("walk", weekly_cfg()) == _week_fires("walk", weekly_cfg())
def test_weekly_within_window():
for fire in _week_fires("x", weekly_cfg(times_per_day=4)):
assert WINDOW_START.time() <= fire.time() <= WINDOW_END.time()
def test_weekly_days_filter_limits_eligible():
fires = _week_fires("x", weekly_cfg(times_per_day=2, days="1-5"))
assert all(f.weekday() < 5 for f in fires)
def test_weekly_count_clamped_to_eligible_days():
# Capacity (7 days) admits 3, but until clips this week to Mon+Tue -> 2 fires, no error.
bounded = weekly_cfg(times_per_day=3, until="2026-03-24")
fires = _week_fires("x", bounded)
assert len(fires) == 2
assert {f.date() for f in fires} == {date(2026, 3, 23), date(2026, 3, 24)}
def test_weekly_from_until_clips_to_partial_week():
bounded = weekly_cfg(times_per_day=2, **{"from": "2026-03-25", "until": "2026-03-27"})
fires = _week_fires("x", bounded)
assert all(date(2026, 3, 25) <= f.date() <= date(2026, 3, 27) for f in fires)
assert len(fires) == 2
def test_weekly_count_exceeds_capacity_raises():
with pytest.raises(ValueError):
compute_fire_times(WEEK[0], "x", weekly_cfg(times_per_day=8))
def test_weekly_count_exceeds_filtered_capacity_raises():
with pytest.raises(ValueError):
compute_fire_times(WEEK[0], "x", weekly_cfg(times_per_day=3, days="1,2"))