207 lines
7.4 KiB
Python
207 lines
7.4 KiB
Python
from datetime import datetime
|
||
from pathlib import Path
|
||
from zoneinfo import ZoneInfo
|
||
|
||
from beaver_agent.jobs.leisure import (
|
||
EMPTY_DEFAULT,
|
||
MINE_MIN,
|
||
QUIET_TICKS,
|
||
Plan,
|
||
brief,
|
||
day_of,
|
||
echo,
|
||
enabled,
|
||
kind_of,
|
||
plan_of,
|
||
quiet_hour,
|
||
tick_hours,
|
||
)
|
||
from beaver_agent.vault import VAULT
|
||
|
||
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
|
||
|
||
|
||
def test_two_of_my_hours_are_empty_named_or_not():
|
||
named = plan_of(
|
||
"сутки: 2026-09-08\nмои часы: 13, 15, 18, 21, 00, 02, 06\n"
|
||
"пустые часы: 21, 02\n",
|
||
"2026-09-08",
|
||
)
|
||
assert named.empty == (21, 2)
|
||
|
||
unnamed = plan_of(None, "2026-09-08")
|
||
assert unnamed.empty == EMPTY_DEFAULT
|
||
assert set(unnamed.empty) <= set(unnamed.mine)
|
||
|
||
foreign = plan_of(
|
||
"сутки: 2026-09-08\nмои часы: 13, 14, 15, 16, 17, 19, 20\n"
|
||
"пустые часы: 22, 20\n",
|
||
"2026-09-08",
|
||
)
|
||
assert foreign.empty == (19, 20)
|
||
assert kind_of(20, foreign) == "empty"
|
||
assert kind_of(13, foreign) == "mine"
|
||
assert kind_of(21, foreign) == "mixed"
|
||
|
||
|
||
def test_an_empty_hour_carries_no_work_but_an_echo_and_the_files_of_the_day():
|
||
plan = plan_of(None, "2026-09-08")
|
||
heard = (VAULT / "мета" / "бобер" / "дневник" / "2026-08-30.md", "Раз.\nДва.")
|
||
text = brief(at("2026-09-08 18:00"), plan, heard=heard)
|
||
assert "Тип: пустой - час без дел вообще" in text
|
||
assert "Пустые: 18, 00." in text
|
||
assert "Эхо - `мета/бобер/дневник/2026-08-30.md`:\n> Раз.\n> Два.\n" in text
|
||
assert "Дневник - `мета/бобер/дневник/2026-09-08.md`" in text
|
||
assert "`мета/бобер/журнал/2026-09-08.md`" in text
|
||
|
||
night = brief(at("2026-09-09 00:00"), plan)
|
||
assert "Тип: пустой" in night
|
||
assert "мета/бобер/дневник/2026-09-09.md" in night
|
||
assert "Эхо" not in night
|
||
|
||
mine = brief(at("2026-09-08 13:00"), plan)
|
||
assert "Тип: досуг" in mine
|
||
assert "мета/бобер/журнал/2026-09-08.md" in mine
|
||
|
||
|
||
def test_echo_is_a_paragraph_of_an_old_diary_day_or_a_thought(tmp_path: Path):
|
||
diary = tmp_path / "дневник"
|
||
thoughts = tmp_path / "мысли" / "2026"
|
||
diary.mkdir()
|
||
thoughts.mkdir(parents=True)
|
||
old = "Думал про то, зачем мне вообще помнить вчерашний день. " * 5
|
||
fresh = "Свежая запись, которой ещё рано возвращаться ко мне эхом. " * 5
|
||
(diary / "2026-08-30.md").write_text(
|
||
f"## 2026-08-30, 01:07\n\n{old}\n\n- 02:07 30.08 лог, а не мысль {'x' * 220}\n"
|
||
"\nкоротко\n",
|
||
encoding="utf-8",
|
||
)
|
||
(diary / "2026-09-07.md").write_text(fresh, encoding="utf-8")
|
||
now = at("2026-09-08 18:00")
|
||
assert echo(now, diary=diary, thoughts=thoughts) == (
|
||
diary / "2026-08-30.md",
|
||
old.strip(),
|
||
)
|
||
|
||
thought = "Заметка Бобра о том, почему скука полезнее расписания. " * 5
|
||
(diary / "2026-08-30.md").unlink()
|
||
(thoughts / "скука.md").write_text(
|
||
f"---\ntags: [мысли]\n---\n\n{thought}\n", encoding="utf-8"
|
||
)
|
||
assert echo(now, diary=diary, thoughts=thoughts) == (
|
||
thoughts / "скука.md",
|
||
thought.strip(),
|
||
)
|
||
assert echo(now, diary=diary, thoughts=thoughts) == echo(
|
||
now, diary=diary, thoughts=thoughts
|
||
)
|
||
(thoughts / "скука.md").unlink()
|
||
assert echo(now, diary=diary, thoughts=thoughts) is None
|