feat: support safe recurrence additions

Parse, persist, expand, and round-trip compatible RDATE values while preserving whole-series TUI behavior and conservative rewrite guards.

Verified: normal, warning-as-error, and ASan/UBSan builds pass all 9 suites; storage failure paths preserve calendar and backup bytes; live PTY restores terminal state.
This commit is contained in:
2026-07-18 10:14:53 +01:00
parent 989332ef9a
commit cfca6ab6d0
16 changed files with 498 additions and 69 deletions

View File

@@ -246,6 +246,8 @@ void test_zoned_edit_preserves_series_and_rejects_dst_gap()
};
event.recurrence_exceptions.push_back(
zoned_time(event.time_zone, nocal::Date{2026y / March / 9d}, 2, 30));
event.recurrence_additions.push_back(
zoned_time(event.time_zone, nocal::Date{2026y / March / 12d}, 2, 30));
nocal::tui::EventEditor editor{event};
check(editor.field_value(nocal::tui::EditorField::start_date) == "2026-03-07" &&
@@ -262,8 +264,9 @@ void test_zoned_edit_preserves_series_and_rejects_dst_gap()
check(editor.result().time_basis == nocal::TimeBasis::zoned &&
editor.result().time_zone == "America/New_York" &&
editor.result().recurrence == event.recurrence &&
editor.result().recurrence_additions == event.recurrence_additions &&
editor.result().recurrence_exceptions == event.recurrence_exceptions,
"editing preserves time basis, TZID, recurrence, and exceptions");
"editing preserves time basis, TZID, recurrence additions, and exceptions");
nocal::tui::EventEditor series_kind_editor{event};
focus(series_kind_editor, nocal::tui::EditorField::all_day);
@@ -273,8 +276,21 @@ void test_zoned_edit_preserves_series_and_rejects_dst_gap()
series_kind_editor.error().find("recurring series") != std::string_view::npos,
"a recurring series rejects timed/all-day conversion to preserve its exceptions");
auto rdate_only = event;
rdate_only.recurrence.reset();
nocal::tui::EventEditor rdate_editor{rdate_only};
check(rdate_editor.render(80, 18, false).find("EDIT ENTIRE RECURRING SERIES")
!= std::string::npos,
"an RDATE-only event is presented as a recurring series");
focus(rdate_editor, nocal::tui::EditorField::all_day);
(void)rdate_editor.handle_key(" ");
check(rdate_editor.handle_key("\x13") == nocal::tui::EditorOutcome::active &&
rdate_editor.error().find("recurring series") != std::string_view::npos,
"an RDATE-only series rejects timed/all-day conversion");
auto one_off = event;
one_off.recurrence.reset();
one_off.recurrence_additions.clear();
one_off.recurrence_exceptions.clear();
nocal::tui::EventEditor all_day_editor{one_off};
focus(all_day_editor, nocal::tui::EditorField::all_day);