Skip to content

Commit dfbda20

Browse files
tobixenclaude
andcommitted
fix: VTODO with only COMPLETED must not crash on time-range check
When a VTODO has COMPLETED but no DTSTART, DUE, or CREATED, the VTODO branch set comp_end from COMPLETED but left comp_start=None. The existing 'if comp_end and not comp_start' guard runs before COMPLETED is checked, so it did not fire. Add a second guard after the CREATED/COMPLETED block to mirror comp_start = comp_end in that case. Test parametrized over with/without RRULE, as an RRULE-only VTODO without DTSTART (valid per RFC 5545 §3.6.2) hit a separate KeyError in xandikos when attempting server-side RRULE expansion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ce4ebe9 commit dfbda20

2 files changed

Lines changed: 30 additions & 1 deletion

File tree

src/icalendar_searcher/filters.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ def _check_range(
107107
comp_start = _normalize_dt(component["CREATED"].dt)
108108
if "COMPLETED" in component:
109109
comp_end = _normalize_dt(component["COMPLETED"].dt)
110+
## COMPLETED may have set comp_end while comp_start is still None
111+
if comp_end and not comp_start:
112+
comp_start = comp_end
110113

111114
## * A task may have a DUE before the DTSTART. The
112115
## complicated OR-logic in the table may be eliminated
@@ -159,7 +162,9 @@ def _check_completed_filter(
159162
pre-resolved value from check_component() to avoid mutating self.
160163
:return: True if the component should be included, False if it should be filtered out
161164
"""
162-
include_completed = _include_completed if _include_completed is not None else self.include_completed
165+
include_completed = (
166+
_include_completed if _include_completed is not None else self.include_completed
167+
)
163168
if include_completed:
164169
return True
165170

tests/test_check_component.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,27 @@ def test_yule_tree2() -> None:
179179
searcher.add_property_filter(*prop_filter)
180180
assert not searcher._check_property_filters(cal.subcomponents[0])
181181
assert not searcher.check_component(cal)
182+
183+
184+
@pytest.mark.parametrize("rrule", [None, "RRULE:FREQ=DAILY\n"])
185+
def test_vtodo_no_dtstart_range_check(rrule: str | None) -> None:
186+
"""
187+
A VTODO with no DTSTART/DUE/CREATED must not crash on a time-range search,
188+
whether or not it has an RRULE.
189+
"""
190+
rrule_line = rrule or ""
191+
data = f"""BEGIN:VCALENDAR
192+
VERSION:2.0
193+
BEGIN:VTODO
194+
COMPLETED:20231001T105204Z
195+
DTSTAMP:20231001T124832Z
196+
{rrule_line}STATUS:COMPLETED
197+
SUMMARY:g\xe5 tur
198+
UID:d432d0a2-6058-11ee-97c8-982cbcdd642c
199+
END:VTODO
200+
END:VCALENDAR"""
201+
cal = Calendar.from_ical(data)
202+
end = datetime(2026, 5, 28, 0, 0, 0)
203+
searcher = Searcher(todo=True, end=end, include_completed=True)
204+
result = searcher.check_component(cal)
205+
assert bool(result)

0 commit comments

Comments
 (0)