feat(hands,memory): the recall carries the next week of both calendars daily
This commit is contained in:
+91
-1
@@ -4,6 +4,7 @@ from pathlib import Path
|
||||
from beaver_gateway.conversations.texts import UserSaid
|
||||
from beaver_gateway.conversations.envelope import RecallContext
|
||||
|
||||
from beaver_agent.hands.calendars import Calendars, Feed
|
||||
from beaver_agent.memory.recall import (
|
||||
LABEL,
|
||||
Recall,
|
||||
@@ -22,7 +23,7 @@ def _card(path: Path, aliases: list[str] | None = None) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _vault(tmp_path: Path) -> Recall:
|
||||
def _vault(tmp_path: Path, calendar: Calendars | None = None) -> Recall:
|
||||
people = tmp_path / "👤 люди"
|
||||
_card(people / "личное" / "Зина.md", ["Зина"])
|
||||
_card(people / "личное" / "Прохор.md", ["Прохор Тестов"])
|
||||
@@ -68,6 +69,7 @@ def _vault(tmp_path: Path) -> Recall:
|
||||
diary_dir=diary,
|
||||
boards_dir=boards,
|
||||
tz="Europe/Warsaw",
|
||||
calendar=calendar,
|
||||
)
|
||||
|
||||
|
||||
@@ -186,3 +188,91 @@ def test_notes_head_picks_sections_for_the_seed(tmp_path: Path) -> None:
|
||||
assert notes_head(recall.notes_dir / "нет.md", ("сейчас",)) is None
|
||||
short = notes_head(path, ("сейчас", "паттерны"), cap=2)
|
||||
assert short is not None and short.endswith("… ещё 3 строк в файле")
|
||||
|
||||
|
||||
def _ics(*events: str) -> str:
|
||||
body = "".join(f"BEGIN:VEVENT\n{e}\nEND:VEVENT\n" for e in events)
|
||||
return f"BEGIN:VCALENDAR\nVERSION:2.0\n{body}END:VCALENDAR\n"
|
||||
|
||||
|
||||
def _calendar(text: str | None) -> Calendars:
|
||||
"""Фид, который отдаёт готовый ical; None - фид, который падает."""
|
||||
|
||||
class Fake(Calendars):
|
||||
def fetch(self, url: str) -> str:
|
||||
if text is None:
|
||||
raise OSError("фид не ответил")
|
||||
return text
|
||||
|
||||
return Fake((Feed("личный", "https://cal.example/x.ics"),), tz="Europe/Warsaw")
|
||||
|
||||
|
||||
BUSY = _ics(
|
||||
"SUMMARY:Созвон\nDTSTART:20260902T160000Z",
|
||||
"SUMMARY:Analiza matematyczna II - Egzamin\nDTSTART;VALUE=DATE-TIME:20260905T110000\n"
|
||||
"DESCRIPTION:Sala: 3\\nA23 - CW\\n\nLOCATION:ul. Piotrowo 2\\, Poznań",
|
||||
"SUMMARY:Врач\nDTSTART:20260903T090000Z",
|
||||
"SUMMARY:за окном\nDTSTART:20260925T080000Z",
|
||||
"SUMMARY:завтрак\nDTSTART;TZID=Europe/Warsaw:20260902T080000\nRRULE:FREQ=DAILY",
|
||||
"SUMMARY:ужин\nDTSTART;TZID=Europe/Warsaw:20260902T200000\nRRULE:FREQ=DAILY",
|
||||
)
|
||||
|
||||
|
||||
def test_calendar_section_sorts_the_week_and_says_what_did_not_fit(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
recall = _vault(tmp_path, _calendar(BUSY))
|
||||
now = datetime(2026, 9, 1, 12, 0, tzinfo=UTC)
|
||||
block = recall.block(RecallContext(text="привет", kind="master", now=now))
|
||||
assert block is not None
|
||||
lines = [ln for ln in block.splitlines() if "календар" in ln or ln.startswith(" ")]
|
||||
assert lines[0] == ("🗓 календарь (7 дней), событий 16, данные на 2026-09-01 14:00:")
|
||||
assert lines[1:4] == [
|
||||
" - 09-02 08:00 завтрак",
|
||||
" - 09-02 18:00 Созвон",
|
||||
" - 09-02 20:00 ужин",
|
||||
]
|
||||
assert (
|
||||
" - 09-05 11:00 Analiza matematyczna II - Egzamin · Sala: 3, ul. Piotrowo 2, Poznań"
|
||||
in lines
|
||||
)
|
||||
assert len([ln for ln in lines if ln.startswith(" - ")]) == 15
|
||||
assert lines[-1] == " … и ещё 1 в окне"
|
||||
# секция дневная, как и сроки досок: второй конверт за те же сутки её не несёт
|
||||
again = recall.block(RecallContext(text="и ещё", kind="master", now=now))
|
||||
assert again is None or "календарь" not in again
|
||||
|
||||
|
||||
def test_calendar_section_is_printed_even_when_the_week_is_empty(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
recall = _vault(
|
||||
tmp_path, _calendar(_ics("SUMMARY:потом\nDTSTART:20261101T080000Z"))
|
||||
)
|
||||
now = datetime(2026, 9, 1, 12, 0, tzinfo=UTC)
|
||||
block = recall.block(RecallContext(text="просто привет", kind="master", now=now))
|
||||
assert block is not None
|
||||
assert (
|
||||
"🗓 календарь (7 дней): событий нет, данные на 2026-09-01 14:00"
|
||||
in block.splitlines()
|
||||
)
|
||||
|
||||
|
||||
def test_calendar_failure_shows_up_in_the_envelope_instead_of_disappearing(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
recall = _vault(tmp_path, _calendar(None))
|
||||
now = datetime(2026, 9, 1, 12, 0, tzinfo=UTC)
|
||||
block = recall.block(RecallContext(text="просто привет", kind="master", now=now))
|
||||
assert block is not None
|
||||
assert "🗓 календарь: ОШИБКА личный: OSError: фид не ответил" in block.splitlines()
|
||||
# ветке справка календарь не носит, как и сроки досок
|
||||
branch = recall.block(RecallContext(text="просто привет", kind="branch", now=now))
|
||||
assert branch is None
|
||||
|
||||
|
||||
def test_without_configured_feeds_there_is_no_calendar_section(tmp_path: Path) -> None:
|
||||
recall = _vault(tmp_path)
|
||||
now = datetime(2026, 9, 1, 12, 0, tzinfo=UTC)
|
||||
block = recall.block(RecallContext(text="просто привет", kind="master", now=now))
|
||||
assert block is not None and "календарь" not in block
|
||||
|
||||
Reference in New Issue
Block a user