forked from ChelseaKR/sprout
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_ics.py
More file actions
120 lines (89 loc) · 4.22 KB
/
Copy pathtest_ics.py
File metadata and controls
120 lines (89 loc) · 4.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
"""ICS export: RFC 5545 structure, RRULE, deterministic UIDs, escaping, folding.
Export is a pure function from reminders to text (EXP-10). These tests pin the
VCALENDAR/VEVENT shape, the DAILY RRULE with the reminder's own interval, and
that the reminders JSON stays untouched (no import path, no state mutation).
"""
from __future__ import annotations
from datetime import UTC, date, datetime
from pathlib import Path
from sprout.ics import reminders_to_ics
from sprout.reminders import Reminder, ReminderStore
_CLOCK = lambda: datetime(2026, 6, 1, 12, 0, 0, tzinfo=UTC) # noqa: E731
def _unfolded(text: str) -> str:
"""Undo RFC 5545 line folding so substring assertions ignore fold points."""
return text.replace("\r\n ", "")
def _reminder(**overrides: object) -> Reminder:
base = dict(
reminder_id="abc123",
plant="pothos",
kind="water",
interval_days=7,
created_at="2026-06-01",
next_due="2026-06-08",
language="en",
note="",
source=None,
)
base.update(overrides)
return Reminder.model_validate(base)
def test_wraps_in_valid_vcalendar() -> None:
text = reminders_to_ics([_reminder()], now=_CLOCK)
assert text.startswith("BEGIN:VCALENDAR\r\n")
assert text.endswith("END:VCALENDAR\r\n")
assert "VERSION:2.0\r\n" in text
assert "PRODID:-//Sprout//Reminders Export 1.0//EN\r\n" in text
assert text.count("BEGIN:VEVENT") == 1
assert text.count("END:VEVENT") == 1
def test_rrule_uses_daily_freq_and_interval() -> None:
text = reminders_to_ics([_reminder(interval_days=14)], now=_CLOCK)
assert "RRULE:FREQ=DAILY;INTERVAL=14\r\n" in text
def test_dtstart_is_next_due_all_day() -> None:
text = reminders_to_ics([_reminder(next_due="2026-07-04")], now=_CLOCK)
assert "DTSTART;VALUE=DATE:20260704\r\n" in text
def test_uid_is_deterministic_from_reminder_id() -> None:
text = reminders_to_ics([_reminder(reminder_id="xyz789")], now=_CLOCK)
assert "UID:xyz789@sprout.local\r\n" in text
# Same input -> byte-identical output (determinism, per CLAUDE.md).
again = reminders_to_ics([_reminder(reminder_id="xyz789")], now=_CLOCK)
assert text == again
def test_summary_localized_by_reminder_language() -> None:
en = reminders_to_ics([_reminder(kind="fertilize", language="en", plant="fern")], now=_CLOCK)
es = reminders_to_ics([_reminder(kind="fertilize", language="es", plant="fern")], now=_CLOCK)
assert "SUMMARY:Fertilize: fern\r\n" in en
assert "SUMMARY:Fertilizar: fern\r\n" in es
def test_note_and_source_appear_in_description_never_as_fact() -> None:
text = _unfolded(
reminders_to_ics([_reminder(note="by the window", source="ADR-0011")], now=_CLOCK)
)
assert "Note: by the window" in text
assert "Motivated by: ADR-0011" in text
assert "not a cited horticultural fact" in text
def test_special_characters_are_escaped() -> None:
text = _unfolded(reminders_to_ics([_reminder(plant="Grandma's Fern, Inc.; #1")], now=_CLOCK))
assert "Grandma's Fern\\, Inc.\\; #1" in text
def test_long_line_is_folded_at_75_octets() -> None:
long_note = "x" * 200
text = reminders_to_ics([_reminder(note=long_note)], now=_CLOCK)
for line in text.split("\r\n"):
assert len(line.encode("utf-8")) <= 75
def test_multiple_reminders_each_get_a_vevent() -> None:
text = reminders_to_ics(
[_reminder(reminder_id="a"), _reminder(reminder_id="b", plant="monstera")],
now=_CLOCK,
)
assert text.count("BEGIN:VEVENT") == 2
assert "UID:a@sprout.local\r\n" in text
assert "UID:b@sprout.local\r\n" in text
def test_empty_store_still_produces_valid_calendar() -> None:
text = reminders_to_ics([], now=_CLOCK)
assert "BEGIN:VEVENT" not in text
assert text.startswith("BEGIN:VCALENDAR\r\n")
assert text.endswith("END:VCALENDAR\r\n")
def test_export_reads_store_without_mutating_it(tmp_path: Path) -> None:
path = tmp_path / "reminders.json"
store = ReminderStore(path, clock=lambda: date(2026, 6, 1))
store.add(plant="pothos", kind="water", interval_days=7)
before = path.read_text(encoding="utf-8")
reminders_to_ics(store.all_reminders(), now=_CLOCK)
after = path.read_text(encoding="utf-8")
assert before == after