feat(jobs,vault): hourly leisure tick, the day plan lives in the vault

This commit is contained in:
hh
2026-09-07 23:00:14 +00:00
parent 18403e1df9
commit 0e7df793e5
4 changed files with 413 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
from datetime import datetime
from zoneinfo import ZoneInfo
from beaver_agent.jobs.leisure import (
MINE_MIN,
QUIET_TICKS,
Plan,
brief,
day_of,
enabled,
plan_of,
quiet_hour,
tick_hours,
)
ZONE = ZoneInfo("Europe/Warsaw")
def at(text: str) -> datetime:
return datetime.fromisoformat(text).replace(tzinfo=ZONE)
def test_switch_defaults_to_on_and_only_explicit_off_stops_it():
assert enabled(None)
assert enabled("# досуг\n\nничего про выключатель")
assert enabled("досуг: вкл")
assert not enabled("# досуг\n\nдосуг: выкл\nсутки: 2026-09-08")
assert not enabled("досуг: off")
def test_day_starts_at_noon():
assert day_of(at("2026-09-08 12:00")) == "2026-09-08"
assert day_of(at("2026-09-08 23:30")) == "2026-09-08"
assert day_of(at("2026-09-09 03:00")) == "2026-09-08"
assert day_of(at("2026-09-09 11:59")) == "2026-09-08"
assert day_of(at("2026-09-09 12:00")) == "2026-09-09"
def test_active_window_ticks_hourly_and_quiet_window_only_by_plan():
hours = tick_hours(QUIET_TICKS)
assert hours[0] == 12
assert len(hours) == 20
assert all(h in hours for h in (12, 18, 23, 0, 3))
assert [h for h in hours if quiet_hour(h)] == [4, 6, 8, 10]
assert 5 not in hours and 11 not in hours
assert tick_hours((5, 7))[-2:] == (5, 7)
def test_plan_is_read_from_the_file_for_todays_day_only():
text = (
"# досуг\n\n"
"досуг: вкл\n"
"сутки: 2026-09-08\n"
"тихие часы: 05, 07, 09, 11\n"
"мои часы: 13, 16, 19, 22, 01, 05, 09\n"
)
plan = plan_of(text, "2026-09-08")
assert plan.written
assert plan.quiet == (5, 7, 9, 11)
assert plan.mine == (13, 16, 19, 22, 1, 5, 9)
stale = plan_of(text, "2026-09-09")
assert not stale.written
assert stale.quiet == QUIET_TICKS
def test_missing_plan_falls_back_to_defaults():
plan = plan_of(None, "2026-09-08")
assert not plan.written
assert plan.quiet == QUIET_TICKS
assert len(plan.mine) == MINE_MIN
assert set(plan.mine) <= set(tick_hours(plan.quiet))
def test_short_plan_is_topped_up_to_the_guaranteed_minimum():
text = "сутки: 2026-09-08\nмои часы: 14, 20\n"
plan = plan_of(text, "2026-09-08")
assert plan.written
assert len(plan.mine) == MINE_MIN
assert 14 in plan.mine and 20 in plan.mine
assert plan.mine == tuple(sorted(plan.mine, key=tick_hours(plan.quiet).index))
def test_unreachable_hours_are_dropped_from_the_plan():
text = "сутки: 2026-09-08\nтихие часы: 04, 06, 08, 10\nмои часы: 05, 11, 14\n"
plan = plan_of(text, "2026-09-08")
assert 5 not in plan.mine and 11 not in plan.mine
assert 14 in plan.mine and len(plan.mine) == MINE_MIN
def test_brief_names_the_type_and_counts_spent_slots():
plan = Plan(
day="2026-09-08",
quiet=QUIET_TICKS,
mine=(13, 15, 18, 21, 0, 2, 6),
written=True,
)
text = brief(at("2026-09-08 18:00"), plan)
assert "Тип: досуг" in text
assert "израсходовано 3 из 7" in text
assert "впереди 21, 00, 02, 06" in text
assert "Сутки заканчиваются через 18 ч" in text
assert "Задания нет" in text
assert "Разложи" not in text
mixed = brief(at("2026-09-08 19:00"), plan)
assert "Тип: смешанный" in mixed
assert "израсходовано 3 из 7" in mixed
def test_first_tick_of_the_day_is_the_planning_one():
text = brief(at("2026-09-08 12:00"), plan_of(None, "2026-09-08"))
assert "Первый тик суток" in text
assert "расписания на эти сутки нет" in text.lower()
assert str(MINE_MIN) in text
def test_last_night_tick_says_nothing_is_left_ahead():
plan = Plan(day="2026-09-08", quiet=QUIET_TICKS, mine=(13, 6), written=True)
text = brief(at("2026-09-09 06:00"), plan)
assert "впереди больше нет" in text
assert "израсходовано 2 из 2" in text
assert "Сутки заканчиваются через 6 ч" in text