#include "nocal/domain/date.hpp" #include "nocal/tui/Screen.hpp" #include "nocal/tui/TuiApp.hpp" #include #include #include #include #include #include #include #include #include #include #include #include namespace { int failures = 0; void check(const bool condition, const std::string_view message) { if (!condition) { ++failures; std::cerr << "FAIL: " << message << '\n'; } } nocal::Event make_event(std::string uid, std::string title, const nocal::Date start_day, const unsigned start_hour, const unsigned start_minute, const nocal::Date end_day, const unsigned end_hour, const unsigned end_minute, const bool all_day = false) { nocal::Event event; event.uid = std::move(uid); event.title = std::move(title); event.start = nocal::make_local_time(start_day, start_hour, start_minute); event.end = nocal::make_local_time(end_day, end_hour, end_minute); event.all_day = all_day; return event; } std::string strip_ansi(const std::string& input) { std::string output; for (std::size_t index = 0; index < input.size();) { if (input[index] == '\x1b' && index + 1 < input.size() && input[index + 1] == '[') { index += 2; while (index < input.size() && input[index] != 'm') ++index; if (index < input.size()) ++index; } else { output += input[index++]; } } return output; } nocal::tui::ScreenState screen_for(const nocal::Date day, const int width = 100, const int height = 32) { nocal::tui::ScreenState state; state.visible_month = day.year() / day.month(); state.selected_day = day; state.today = day; state.width = width; state.height = height; state.ansi = false; return state; } bool focus_is(const nocal::tui::TuiApp& app, const std::string_view uid) { return app.state().focused_event_id && *app.state().focused_event_id == uid; } bool focus_starts_with(const nocal::tui::TuiApp& app, const std::string_view prefix) { return app.state().focused_event_id && app.state().focused_event_id->starts_with(prefix); } bool same_event(const nocal::Event& left, const nocal::Event& right) { return left.uid == right.uid && left.title == right.title && left.start == right.start && left.end == right.end && left.all_day == right.all_day && left.location == right.location && left.description == right.description && left.calendar_id == right.calendar_id && left.color == right.color && left.time_basis == right.time_basis && left.time_zone == right.time_zone && left.recurrence == right.recurrence && left.recurrence_additions == right.recurrence_additions && left.recurrence_exceptions == right.recurrence_exceptions; } void replace_editor_title(nocal::tui::TuiApp& app, const std::string_view title) { const auto* editor = app.editor(); check(editor != nullptr, "title replacement requires an active editor"); if (editor == nullptr) return; const auto length = editor->field_value(nocal::tui::EditorField::title).size(); for (std::size_t index = 0; index < length; ++index) app.handle_input("\x7f"); app.handle_input(title); } void test_event_focus_cycles_in_display_order() { using namespace std::chrono; const auto day = nocal::today(); const auto tomorrow = nocal::from_sys_days(nocal::to_sys_days(day) + days{1}); std::vector events; events.push_back(make_event("late", "Late review", day, 14, 30, day, 15, 0)); events.push_back(make_event("all-day", "Release day", day, 0, 0, tomorrow, 0, 0, true)); events.push_back(make_event("early", "Morning review", day, 9, 15, day, 10, 0)); nocal::tui::TuiApp app{events, -1, -1}; app.dispatch(nocal::tui::Action::next_event); check(focus_is(app, "all-day"), "Tab initially focuses the all-day event shown first"); app.dispatch(nocal::tui::Action::next_event); check(focus_is(app, "early"), "Tab advances to the earliest timed event"); app.dispatch(nocal::tui::Action::next_event); check(focus_is(app, "late"), "Tab advances through timed events chronologically"); app.dispatch(nocal::tui::Action::next_event); check(focus_is(app, "all-day"), "Tab wraps from the final event to the first"); app.dispatch(nocal::tui::Action::previous_event); check(focus_is(app, "late"), "Shift-Tab moves backwards and wraps"); std::vector reverse_events = events; nocal::tui::TuiApp reverse_app{reverse_events, -1, -1}; reverse_app.dispatch(nocal::tui::Action::previous_event); check(focus_is(reverse_app, "late"), "Shift-Tab with no focus starts at the final event of the day"); } void test_open_close_and_day_movement() { using namespace std::chrono; const auto day = nocal::today(); std::vector events{ make_event("reader", "Reader event", day, 11, 0, day, 12, 0), }; nocal::tui::TuiApp app{events, -1, -1}; app.dispatch(nocal::tui::Action::select); check(focus_is(app, "reader"), "Enter focuses the first event when none is focused"); check(app.state().show_event_details, "Enter opens the focused event reader"); app.dispatch(nocal::tui::Action::back); check(!app.state().show_event_details, "Escape closes the event reader"); check(focus_is(app, "reader"), "closing the reader keeps the event focused"); const auto selected = nocal::to_sys_days(app.state().selected_day); app.dispatch(nocal::tui::Action::right); check(nocal::to_sys_days(app.state().selected_day) == selected + days{1}, "day navigation still works after closing the reader"); check(!app.state().focused_event_id, "moving to another day clears event focus"); check(!app.state().show_event_details, "moving to another day leaves the reader closed"); } void test_empty_day_is_safe() { std::vector events; nocal::tui::TuiApp app{events, -1, -1}; app.dispatch(nocal::tui::Action::next_event); app.dispatch(nocal::tui::Action::previous_event); app.dispatch(nocal::tui::Action::select); app.dispatch(nocal::tui::Action::back); check(!app.state().focused_event_id, "event navigation on an empty day keeps focus empty"); check(!app.state().show_event_details, "Enter on an empty day does not open the reader"); } void test_reader_navigation_and_synthetic_ids() { const auto day = nocal::today(); std::vector events; events.push_back(make_event("duplicate", "First duplicate", day, 8, 0, day, 9, 0)); events.push_back(make_event("duplicate", "Second duplicate", day, 10, 0, day, 11, 0)); events.push_back(make_event("", "No UID", day, 12, 0, day, 13, 0)); nocal::tui::TuiApp app{events, -1, -1}; app.dispatch(nocal::tui::Action::select); check(focus_is(app, "@nocal:0"), "duplicate UIDs receive a focusable source identity"); check(app.state().show_event_details, "synthetic identities resolve in the reader"); app.dispatch(nocal::tui::Action::down); check(focus_is(app, "@nocal:1"), "reader Down advances to the next appointment"); app.dispatch(nocal::tui::Action::up); check(focus_is(app, "@nocal:0"), "reader Up returns to the previous appointment"); app.dispatch(nocal::tui::Action::next_event); app.dispatch(nocal::tui::Action::next_event); check(focus_is(app, "@nocal:2"), "an empty UID remains focusable and readable"); const auto frame = nocal::tui::render_month(events, app.state()); check(frame.find("No UID") != std::string::npos, "renderer shares the controller's synthetic identity scheme"); } void test_key_decoding() { using nocal::tui::Action; using nocal::tui::decode_key; check(decode_key("\t") == Action::next_event, "Tab decodes as next event"); check(decode_key("\x1b[Z") == Action::previous_event, "terminal Shift-Tab decodes as previous event"); check(decode_key("\r") == Action::select, "carriage-return Enter decodes as select"); check(decode_key("\n") == Action::select, "newline Enter decodes as select"); check(decode_key("\x1b") == Action::back, "a lone Escape decodes as back"); check(decode_key("\x1b[A") == Action::up, "arrow decoding remains intact"); check(decode_key("a") == Action::add_event, "a decodes as add appointment"); check(decode_key("e") == Action::edit_event, "e decodes as edit appointment"); check(decode_key("d") == Action::delete_event, "d decodes as delete appointment"); check(decode_key("u") == Action::undo, "u decodes as undo"); check(decode_key("\x12") == Action::redo, "Ctrl-R decodes as redo"); check(decode_key("g") == Action::agenda, "g decodes as agenda"); } void test_crud_preconditions_and_add() { std::vector events; int save_calls = 0; std::vector saved; nocal::tui::TuiApp app{ events, [&](const std::span values) { ++save_calls; saved.assign(values.begin(), values.end()); }, -1, -1, }; app.dispatch(nocal::tui::Action::edit_event); check(!app.editor_active(), "edit without a focused appointment does not open an editor"); check(app.state().notification_is_error, "edit without focus explains its appointment precondition"); app.dispatch(nocal::tui::Action::delete_event); check(!app.delete_confirmation_active(), "delete without a focused appointment does not open confirmation"); check(app.state().notification_is_error, "delete without focus explains its appointment precondition"); const auto selected_day = app.state().selected_day; app.dispatch(nocal::tui::Action::add_event); check(app.editor_active(), "add opens the event editor on an empty day"); check(app.editor() != nullptr && app.editor()->field_value(nocal::tui::EditorField::start_date) == nocal::format_date(selected_day), "add initializes the editor from the selected date"); check(app.editor() != nullptr && app.editor()->render(60, 18, false).find("NEW APPOINTMENT") != std::string::npos, "the active add editor can be rendered by the controller"); app.handle_input("Created locally"); app.handle_input("\x13"); check(!app.editor_active(), "a valid Ctrl-S submit closes the add editor"); check(events.size() == 1 && events.front().title == "Created locally", "add commits the validated editor result to the local model"); check(save_calls == 1 && saved.size() == 1 && saved.front().title == "Created locally", "add calls the persistence boundary with the mutated model"); check(!events.front().uid.empty() && focus_is(app, events.front().uid), "a successful add focuses the new appointment"); check(app.state().notification == "Appointment added." && !app.state().notification_is_error, "a successful add reports a non-error status"); } void test_edit_preserves_uid_and_rolls_back_on_save_failure() { const auto day = nocal::today(); auto original = make_event("stable-uid", "Original title", day, 9, 0, day, 10, 0); original.location = "Desk"; original.description = "Original notes"; original.calendar_id = "personal"; original.color = "blue"; std::vector events{original}; int save_calls = 0; nocal::tui::TuiApp app{ events, [&](std::span) { ++save_calls; throw std::runtime_error{"disk full"}; }, -1, -1, }; app.dispatch(nocal::tui::Action::next_event); app.dispatch(nocal::tui::Action::edit_event); check(app.editor_active() && app.editor() != nullptr && app.editor()->editing(), "edit opens a populated editing form for the focused appointment"); replace_editor_title(app, "Changed title"); app.handle_input("\x13"); check(save_calls == 1, "edit attempts persistence exactly once"); check(events.size() == 1 && same_event(events.front(), original), "a throwing saver restores the exact event vector after edit"); check(focus_is(app, "stable-uid"), "edit rollback restores the original focus identity"); check(!app.editor_active(), "save failure returns to the month so its error is visible"); check(app.state().notification_is_error && app.state().notification.find("disk full") != std::string::npos, "save failure is shown to the user with the persistence error"); int successful_saves = 0; nocal::tui::TuiApp successful_app{ events, [&](std::span) { ++successful_saves; }, -1, -1, }; successful_app.dispatch(nocal::tui::Action::next_event); successful_app.dispatch(nocal::tui::Action::edit_event); replace_editor_title(successful_app, "Changed title"); successful_app.handle_input("\x13"); check(successful_saves == 1 && events.front().title == "Changed title", "a successful edit commits and persists its changed fields"); check(events.front().uid == "stable-uid", "editing preserves the appointment UID used by storage and focus"); } void test_delete_cancel_confirm_and_rollback() { const auto day = nocal::today(); const auto target = make_event("delete-me", "Delete target", day, 9, 0, day, 10, 0); const auto keep = make_event("keep-me", "Keep target", day, 11, 0, day, 12, 0); std::vector events{target, keep}; int save_calls = 0; nocal::tui::TuiApp app{ events, [&](std::span) { ++save_calls; }, -1, -1, }; app.dispatch(nocal::tui::Action::next_event); app.dispatch(nocal::tui::Action::delete_event); check(app.delete_confirmation_active(), "delete requires an explicit confirmation step"); const auto confirmation = strip_ansi(nocal::tui::render_month(events, app.state())); check(confirmation.find("Delete target") != std::string::npos && confirmation.find("y confirm") != std::string::npos && confirmation.find("n cancel") != std::string::npos && confirmation.find("Esc cancel") != std::string::npos, "delete confirmation names the appointment and shows its keys"); app.handle_input("n"); check(!app.delete_confirmation_active() && events.size() == 2 && save_calls == 0, "n cancels deletion without mutating or saving"); check(focus_is(app, "delete-me"), "cancelled deletion retains appointment focus"); app.dispatch(nocal::tui::Action::delete_event); app.handle_input("y"); check(events.size() == 1 && events.front().uid == "keep-me" && save_calls == 1, "y deletes the focused appointment and persists once"); check(!app.state().focused_event_id && app.state().notification == "Appointment deleted.", "successful deletion clears stale focus and reports success"); std::vector rollback_events{target}; nocal::tui::TuiApp rollback_app{ rollback_events, [](std::span) { throw std::runtime_error{"read only"}; }, -1, -1, }; rollback_app.dispatch(nocal::tui::Action::next_event); rollback_app.dispatch(nocal::tui::Action::delete_event); rollback_app.handle_input("y"); check(rollback_events.size() == 1 && same_event(rollback_events.front(), target), "a throwing saver restores the exact vector after delete"); check(focus_is(rollback_app, "delete-me"), "delete rollback restores the focused appointment identity"); check(rollback_app.state().notification_is_error, "delete rollback displays a persistence error"); } void test_crud_targets_synthetic_identities() { const auto day = nocal::today(); std::vector events{ make_event("duplicate", "First duplicate", day, 8, 0, day, 9, 0), make_event("duplicate", "Second duplicate", day, 10, 0, day, 11, 0), make_event("", "Empty UID", day, 12, 0, day, 13, 0), }; nocal::tui::TuiApp app{events, -1, -1}; app.dispatch(nocal::tui::Action::next_event); check(focus_is(app, "@nocal:0"), "the first duplicate starts with its synthetic identity"); app.dispatch(nocal::tui::Action::edit_event); replace_editor_title(app, "Edited first duplicate"); app.handle_input("\x13"); check(events[0].title == "Edited first duplicate" && events[1].title == "Second duplicate" && events[0].uid == "duplicate", "edit resolves a synthetic identity to exactly the intended source event"); app.dispatch(nocal::tui::Action::next_event); app.dispatch(nocal::tui::Action::next_event); check(focus_is(app, "@nocal:2"), "empty UID remains targetable after editing a duplicate"); app.dispatch(nocal::tui::Action::delete_event); app.handle_input("y"); check(events.size() == 2 && events[0].title == "Edited first duplicate" && events[1].title == "Second duplicate", "delete resolves an empty-UID synthetic identity to the intended source event"); } void test_add_edit_delete_undo_redo() { std::vector events; int save_calls = 0; nocal::tui::TuiApp app{ events, [&](std::span) { ++save_calls; }, -1, -1, }; const auto original_day = app.state().selected_day; app.dispatch(nocal::tui::Action::add_event); app.handle_input("History event"); app.handle_input("\x13"); check(events.size() == 1, "history test creates an appointment"); const auto added = events.front(); check(focus_is(app, added.uid), "successful add establishes the post-mutation focus"); app.dispatch(nocal::tui::Action::undo); check(events.empty() && !app.state().focused_event_id && app.state().selected_day == original_day, "undo add restores the exact pre-add model and selection state"); check(app.state().notification == "Undid add appointment.", "undo add names the reversed operation"); app.dispatch(nocal::tui::Action::redo); check(events.size() == 1 && same_event(events.front(), added) && focus_is(app, added.uid), "redo add restores the exact event metadata and post-add focus"); app.dispatch(nocal::tui::Action::select); check(app.state().show_event_details, "reader is open before starting the edit"); app.dispatch(nocal::tui::Action::edit_event); replace_editor_title(app, "Edited through history"); app.handle_input("\x13"); const auto edited = events.front(); check(edited.title == "Edited through history", "history test commits an edit"); app.dispatch(nocal::tui::Action::undo); check(events.size() == 1 && same_event(events.front(), added) && focus_is(app, added.uid) && app.state().show_event_details, "undo edit restores metadata, focus, and the pre-editor reader state"); app.dispatch(nocal::tui::Action::redo); check(events.size() == 1 && same_event(events.front(), edited) && focus_is(app, edited.uid) && !app.state().show_event_details, "redo edit restores the exact edited model and post-editor view state"); app.dispatch(nocal::tui::Action::delete_event); app.handle_input("y"); check(events.empty(), "history test commits a deletion"); app.dispatch(nocal::tui::Action::undo); check(events.size() == 1 && same_event(events.front(), edited) && focus_is(app, edited.uid), "undo delete restores the exact deleted event and its focus"); app.dispatch(nocal::tui::Action::redo); check(events.empty() && !app.state().focused_event_id, "redo delete restores the empty model and cleared focus"); check(save_calls == 9, "three mutations and their undo/redo operations each persist exactly once"); } void test_history_persistence_failure_is_transactional() { const auto day = nocal::today(); const auto original = make_event("history-failure", "Original", day, 9, 0, day, 10, 0); std::vector events{original}; bool fail = false; int save_calls = 0; nocal::tui::TuiApp app{ events, [&](std::span) { ++save_calls; if (fail) throw std::runtime_error{"history disk failure"}; }, -1, -1, }; app.dispatch(nocal::tui::Action::next_event); app.dispatch(nocal::tui::Action::edit_event); replace_editor_title(app, "Changed"); app.handle_input("\x13"); const auto changed = events.front(); fail = true; app.dispatch(nocal::tui::Action::undo); check(events.size() == 1 && same_event(events.front(), changed) && focus_is(app, "history-failure"), "failed undo persistence leaves the current model and focus untouched"); check(app.state().notification_is_error && app.state().notification.find("history disk failure") != std::string::npos, "failed undo reports the persistence error"); fail = false; app.dispatch(nocal::tui::Action::undo); check(events.size() == 1 && same_event(events.front(), original), "undo remains available after a failed persistence attempt"); fail = true; app.dispatch(nocal::tui::Action::redo); check(events.size() == 1 && same_event(events.front(), original) && focus_is(app, "history-failure"), "failed redo persistence leaves the restored model and focus untouched"); fail = false; app.dispatch(nocal::tui::Action::redo); check(events.size() == 1 && same_event(events.front(), changed), "redo remains available after a failed persistence attempt"); check(save_calls == 5, "successful mutation plus four history attempts call the saver"); } void test_new_mutation_invalidates_redo() { const auto day = nocal::today(); std::vector events{ make_event("redo-invalidation", "Original", day, 9, 0, day, 10, 0), }; nocal::tui::TuiApp app{events, -1, -1}; app.dispatch(nocal::tui::Action::next_event); app.dispatch(nocal::tui::Action::edit_event); replace_editor_title(app, "First branch"); app.handle_input("\x13"); app.dispatch(nocal::tui::Action::undo); app.dispatch(nocal::tui::Action::edit_event); replace_editor_title(app, "Second branch"); app.handle_input("\x13"); app.dispatch(nocal::tui::Action::redo); check(events.front().title == "Second branch", "a new successful mutation after undo invalidates the redo branch"); check(app.state().notification_is_error && app.state().notification == "Nothing to redo.", "an invalidated redo branch gives contextual feedback"); } void test_history_is_bounded_to_one_hundred_mutations() { const auto day = nocal::today(); std::vector events{ make_event("bounded-history", "Version 0", day, 9, 0, day, 10, 0), }; nocal::tui::TuiApp app{events, -1, -1}; app.dispatch(nocal::tui::Action::next_event); for (int version = 1; version <= 101; ++version) { app.dispatch(nocal::tui::Action::edit_event); replace_editor_title(app, "Version " + std::to_string(version)); app.handle_input("\x13"); } for (int count = 0; count < 100; ++count) app.dispatch(nocal::tui::Action::undo); check(events.front().title == "Version 1", "the bounded history retains the most recent one hundred mutations"); app.dispatch(nocal::tui::Action::undo); check(events.front().title == "Version 1" && app.state().notification == "Nothing to undo.", "the oldest mutation is discarded once the one-hundred entry bound is reached"); } void test_history_is_unavailable_in_modal_states() { const auto day = nocal::today(); std::vector events{ make_event("modal-history", "Modal event", day, 9, 0, day, 10, 0), }; nocal::tui::TuiApp app{events, -1, -1}; app.dispatch(nocal::tui::Action::add_event); app.dispatch(nocal::tui::Action::undo); check(app.editor_active() && app.state().notification_is_error, "undo is unavailable while the editor owns input"); app.dispatch(nocal::tui::Action::back); app.dispatch(nocal::tui::Action::next_event); auto footer_state = screen_for(day, 180, 32); footer_state.focused_event_id = "modal-history"; const auto month_frame = nocal::tui::render_month(events, footer_state); check(month_frame.find("u undo") != std::string::npos && month_frame.find("Enter read") != std::string::npos && month_frame.find("Tab next") != std::string::npos, "the month footer shows focused hints"); app.dispatch(nocal::tui::Action::delete_event); app.handle_input("u"); check(app.delete_confirmation_active() && app.state().notification_is_error && app.state().notification.find("deletion") != std::string::npos, "history shortcuts are unavailable during delete confirmation"); const auto confirmation = nocal::tui::render_month(events, app.state()); check(confirmation.find("Finish or cancel deletion") != std::string::npos, "delete confirmation renders its contextual history error"); app.handle_input("n"); } void test_focused_line_rendering() { using namespace std::chrono; const auto day = year{2026} / July / std::chrono::day{17}; auto state = screen_for(day, 112, 38); state.focused_event_id = "focused"; const std::vector items{ {.id = "plain", .title = "Plain item", .day = day, .time_of_day = hours{8}, .all_day = false, .end_time_of_day = hours{8} + minutes{30}, .start_day = day, .end_day = day, .location = {}, .description = {}, .detail_title = "Plain item", .detail_all_day = false, .detail_start_time_of_day = hours{8}, .segment = nocal::tui::ItemSegment::single, .recurring = false, .recurrence_summary = {}, .source_time_zone = {}}, {.id = "focused", .title = "Focused item", .day = day, .time_of_day = hours{9} + minutes{15}, .all_day = false, .end_time_of_day = hours{10}, .start_day = day, .end_day = day, .location = {}, .description = {}, .detail_title = "Focused item", .detail_all_day = false, .detail_start_time_of_day = hours{9} + minutes{15}, .segment = nocal::tui::ItemSegment::single, .recurring = false, .recurrence_summary = {}, .source_time_zone = {}}, }; const auto frame = nocal::tui::render_month(items, state); check(frame.find("› 09:15 Focu") != std::string::npos, "the focused appointment line has a visible cursor when ANSI is disabled"); check(frame.find("› 08:00 Plain") == std::string::npos, "the focus cursor is only rendered on the focused appointment"); check(frame.find("\x1b[") == std::string::npos, "focused line rendering respects disabled ANSI styling"); } void test_focus_brings_hidden_event_into_view() { using namespace std::chrono; const auto day = year{2026} / July / std::chrono::day{17}; auto state = screen_for(day, 100, 30); state.focused_event_id = "last"; std::vector items; for (unsigned index = 0; index < 6; ++index) { nocal::tui::CalendarItem item; item.id = index == 5 ? "last" : "event-" + std::to_string(index); item.title = index == 5 ? "Hidden focus target" : "Earlier event"; item.day = day; item.time_of_day = hours{8 + index}; items.push_back(std::move(item)); } const auto frame = nocal::tui::render_month(items, state); check(frame.find("› 13:00") != std::string::npos, "focused overflow event is windowed into the visible cell"); check(frame.find("hidden") != std::string::npos, "dense cells retain an overflow indicator while focus is windowed"); } void test_timed_event_details() { using namespace std::chrono; const auto day = year{2026} / July / std::chrono::day{17}; auto event = make_event("detail", "Architecture review", day, 9, 15, day, 10, 45); event.location = "Studio 4B"; event.description = "Review keyboard navigation and approve the terminal layout."; const std::vector events{event}; auto state = screen_for(day, 80, 24); state.focused_event_id = "detail"; state.show_event_details = true; const auto frame = nocal::tui::render_month(events, state); check(frame.find("APPOINTMENT") != std::string::npos, "the reader has a clear heading"); check(frame.find("Architecture review") != std::string::npos, "the reader renders the full appointment title"); check(frame.find("Friday, 17 July 2026") != std::string::npos, "the reader renders the full friendly date"); check(frame.find("09:15 – 10:45") != std::string::npos, "the reader renders the complete local time range"); check(frame.find("Studio 4B") != std::string::npos, "the reader renders the event location"); check(frame.find("Review keyboard navigation and approve the terminal layout.") != std::string::npos, "the reader renders the event description"); check(frame.find("Esc back") != std::string::npos, "the reader footer explains how to close it"); // Verify the new chip grammar: hints left, ordinal right check(frame.find("Tab") != std::string::npos, "reader footer contains Tab hint"); check(frame.find("del") != std::string::npos, "reader footer contains del hint"); check(frame.find("1/") != std::string::npos, "reader footer contains ordinal"); } void test_all_day_and_multiday_details() { using namespace std::chrono; const auto first = year{2026} / July / std::chrono::day{16}; const auto middle = year{2026} / July / std::chrono::day{17}; const auto last = year{2026} / July / std::chrono::day{18}; const auto after = year{2026} / July / std::chrono::day{19}; const std::vector all_day{ make_event("festival", "Terminal festival", first, 0, 0, after, 0, 0, true), }; auto all_day_state = screen_for(middle, 80, 24); all_day_state.focused_event_id = "festival"; all_day_state.show_event_details = true; const auto all_day_frame = nocal::tui::render_month(all_day, all_day_state); check(all_day_frame.find("Time All day") != std::string::npos, "all-day events are described as all day"); check(all_day_frame.find("Ends Saturday, 18 July 2026") != std::string::npos, "exclusive all-day ends are presented as the last occupied day"); const std::vector timed{ make_event("overnight", "Long migration", first, 20, 0, last, 11, 30), }; auto timed_state = screen_for(middle, 80, 24); timed_state.focused_event_id = "overnight"; timed_state.show_event_details = true; const auto timed_frame = nocal::tui::render_month(timed, timed_state); check(timed_frame.find("Thursday, 16 July 2026") != std::string::npos, "a multi-day reader retains the event's actual start date"); check(timed_frame.find("Ends Saturday, 18 July 2026") != std::string::npos, "a timed multi-day reader renders its end date"); check(timed_frame.find("20:00 – 11:30") != std::string::npos, "a timed multi-day continuation retains its real times"); check(timed_frame.find("Time All day") == std::string::npos, "a timed multi-day continuation is not mislabeled all day"); } void test_narrow_detail_wrapping_without_ansi() { using namespace std::chrono; const auto day = year{2026} / July / std::chrono::day{17}; auto event = make_event("wrap", "Wrap check", day, 13, 0, day, 14, 0); event.description = "Alpha beta gamma delta epsilon zeta eta theta."; const std::vector events{event}; auto state = screen_for(day, 32, 20); state.focused_event_id = "wrap"; state.show_event_details = true; const auto frame = nocal::tui::render_month(events, state); check(frame.find("Alpha beta gamma delta epsilon zeta eta theta.") == std::string::npos, "narrow readers wrap descriptions instead of overflowing a line"); check(frame.find("Alpha beta gamma delta") != std::string::npos, "the first wrapped description line is retained"); check(frame.find("epsilon zeta eta theta.") != std::string::npos, "the remaining wrapped description text is retained"); check(frame.find("\x1b[") == std::string::npos, "narrow detail rendering emits no ANSI when styling is disabled"); } void test_multiday_segment_labels_and_series_messaging() { using namespace std::chrono; const auto first = year{2026} / July / std::chrono::day{16}; const auto middle = year{2026} / July / std::chrono::day{17}; const auto last = year{2026} / July / std::chrono::day{18}; std::vector items{ {.id = "series@occ:1", .title = "Migration", .day = first, .time_of_day = std::nullopt, .all_day = true, .end_time_of_day = std::nullopt, .start_day = first, .end_day = last, .location = {}, .description = {}, .detail_title = "Migration", .detail_all_day = true, .detail_start_time_of_day = std::nullopt, .segment = nocal::tui::ItemSegment::start, .recurring = true, .recurrence_summary = "Every week", .source_time_zone = "Europe/London"}, {.id = "series@occ:1", .title = "Migration", .day = middle, .time_of_day = std::nullopt, .all_day = true, .end_time_of_day = std::nullopt, .start_day = first, .end_day = last, .location = {}, .description = {}, .detail_title = "Migration", .detail_all_day = true, .detail_start_time_of_day = std::nullopt, .segment = nocal::tui::ItemSegment::continuation, .recurring = true, .recurrence_summary = "Every week", .source_time_zone = "Europe/London"}, {.id = "series@occ:1", .title = "Migration", .day = last, .time_of_day = std::nullopt, .all_day = true, .end_time_of_day = std::nullopt, .start_day = first, .end_day = last, .location = {}, .description = {}, .detail_title = "Migration", .detail_all_day = true, .detail_start_time_of_day = std::nullopt, .segment = nocal::tui::ItemSegment::end, .recurring = true, .recurrence_summary = "Every week", .source_time_zone = "Europe/London"}, }; auto state = screen_for(middle, 112, 38); state.focused_event_id = "series@occ:1"; const auto month = nocal::tui::render_month(items, state); check(month.find("│ Migration") != std::string::npos, "a middle multi-day segment has a visible continuation marker without ANSI"); state.show_event_details = true; const auto reader = nocal::tui::render_month(items, state); check(reader.find("RECURRING APPOINTMENT") != std::string::npos && reader.find("Every week") != std::string::npos && reader.find("Europe/London") != std::string::npos && reader.find("every occurrence") != std::string::npos, "the reader identifies recurrence, source timezone, and whole-series edits"); state.show_event_details = false; state.confirm_delete = true; const auto confirmation = nocal::tui::render_month(items, state); check(confirmation.find("entire recurring series") != std::string::npos, "recurring delete confirmation explicitly targets the whole series"); } void test_recurring_occurrence_navigation_and_whole_series_crud() { using namespace std::chrono; const auto selected = nocal::today(); const auto previous = nocal::from_sys_days(nocal::to_sys_days(selected) - days{1}); auto series = make_event("duplicate", "Daily series", previous, 9, 0, previous, 10, 0); series.recurrence = nocal::RecurrenceRule{ .frequency = nocal::RecurrenceFrequency::daily, .interval = 1, .count = 4, .until = std::nullopt, .by_weekday = {}, .by_month_day = {}, .by_month = {}}; auto duplicate = make_event("duplicate", "Duplicate UID", selected, 11, 0, selected, 12, 0); std::vector events{series, duplicate}; nocal::tui::TuiApp app{events, -1, -1}; app.dispatch(nocal::tui::Action::next_event); check(focus_starts_with(app, "@nocal:0@occ:"), "a recurring duplicate-UID occurrence has a stable source-and-start identity"); const auto series_focus = app.state().focused_event_id; app.dispatch(nocal::tui::Action::next_event); check(focus_is(app, "@nocal:1"), "Tab navigates from a recurring instance to a duplicate-UID one-off event"); app.dispatch(nocal::tui::Action::previous_event); check(app.state().focused_event_id == series_focus, "occurrence navigation returns to the exact recurring instance"); app.dispatch(nocal::tui::Action::edit_event); replace_editor_title(app, "Edited entire series"); app.handle_input("\x13"); check(events.front().title == "Edited entire series" && events.front().recurrence.has_value() && app.state().notification == "Recurring series updated." && focus_starts_with(app, "@nocal:0@occ:"), "editing an occurrence updates its source series and retains coherent focus"); app.dispatch(nocal::tui::Action::undo); check(events.front().title == "Daily series" && events.front().recurrence == series.recurrence, "undo restores the complete recurring series metadata"); app.dispatch(nocal::tui::Action::redo); check(events.front().title == "Edited entire series" && events.front().recurrence.has_value(), "redo reapplies the whole-series edit"); app.dispatch(nocal::tui::Action::delete_event); app.handle_input("y"); check(events.size() == 1 && events.front().title == "Duplicate UID" && app.state().notification == "Recurring series deleted.", "deleting a recurring occurrence removes only its entire source series"); app.dispatch(nocal::tui::Action::undo); check(events.size() == 2 && events.front().recurrence.has_value(), "undo restores a deleted recurring series"); } void test_rdate_only_occurrence_identity_and_whole_series_crud() { using namespace std::chrono; const auto selected = nocal::today(); const auto previous = nocal::from_sys_days(nocal::to_sys_days(selected) - days{1}); auto series = make_event("rdate-only", "Extra date", previous, 9, 0, previous, 10, 0); series.recurrence_additions = { nocal::make_local_time(selected, 9, 0), }; std::vector events{series}; nocal::tui::TuiApp app{events, -1, -1}; app.dispatch(nocal::tui::Action::next_event); check(focus_starts_with(app, "rdate-only@occ:"), "an RDATE-only occurrence has a source-and-start focus identity"); app.dispatch(nocal::tui::Action::edit_event); replace_editor_title(app, "Edited extra-date series"); app.handle_input("\x13"); check(events.front().title == "Edited extra-date series" && events.front().recurrence_additions == series.recurrence_additions && app.state().notification == "Recurring series updated.", "editing an RDATE-only occurrence updates and preserves the whole series"); app.dispatch(nocal::tui::Action::delete_event); app.handle_input("y"); check(events.empty() && app.state().notification == "Recurring series deleted.", "deleting an RDATE-only occurrence removes its entire source series"); } void test_recurrence_grid_expansion_and_source_zone_display() { using namespace std::chrono; const auto first = year{2026} / July / std::chrono::day{17}; auto recurring = make_event("grid-series", "Grid repeat", first, 9, 0, first, 10, 0); recurring.recurrence = nocal::RecurrenceRule{ .frequency = nocal::RecurrenceFrequency::daily, .interval = 1, .count = 3, .until = std::nullopt, .by_weekday = {}, .by_month_day = {}, .by_month = {}}; std::vector events{recurring}; auto state = screen_for(first, 140, 38); const auto frame = nocal::tui::render_month(events, state); std::size_t repeats = 0; for (std::size_t at = 0; (at = frame.find("Grid repeat", at)) != std::string::npos; at += std::string_view{"Grid repeat"}.size()) { ++repeats; } check(repeats == 3, "the month adapter expands recurring instances only into visible days"); auto compact_state = screen_for(first, 32, 12); compact_state.ansi = false; const auto compact = nocal::tui::render_month(events, compact_state); check(compact.find("\x1b[") == std::string::npos && std::count(compact.begin(), compact.end(), '\n') == 11, "recurrence rendering preserves compact NO_COLOR frame geometry"); const char* prior_tz_value = std::getenv("TZ"); const std::optional prior_tz = prior_tz_value ? std::optional{std::string{prior_tz_value}} : std::nullopt; (void)::setenv("TZ", "UTC0", 1); ::tzset(); const auto* zone = std::chrono::locate_zone("America/New_York"); nocal::Event zoned; zoned.uid = "zoned-reader"; zoned.title = "Zoned meeting"; zoned.time_basis = nocal::TimeBasis::zoned; zoned.time_zone = "America/New_York"; zoned.start = zone->to_sys(local_days{first} + hours{9}, choose::earliest); zoned.end = zone->to_sys(local_days{first} + hours{10}, choose::earliest); std::vector zoned_events{zoned}; auto zoned_state = screen_for(first, 100, 28); const auto occurrences = nocal::occurrences_on_day(zoned_events, first); check(!occurrences.empty(), "the zoned test event occurs on the user's displayed day"); if (!occurrences.empty()) { zoned_state.focused_event_id = nocal::tui::occurrence_focus_id(zoned_events, occurrences.front()); zoned_state.show_event_details = true; const auto reader = nocal::tui::render_month(zoned_events, zoned_state); check(reader.find("America/New_York") != std::string::npos && reader.find("13:00 – 14:00") != std::string::npos && reader.find("09:00 – 10:00") == std::string::npos, "the reader names the source TZID while displaying instants in the user zone"); } if (prior_tz) (void)::setenv("TZ", prior_tz->c_str(), 1); else (void)::unsetenv("TZ"); ::tzset(); } void test_search_prompt_results_and_occurrence_jump() { using namespace std::chrono; const auto first_day = nocal::today(); const auto second_day = nocal::from_sys_days(nocal::to_sys_days(first_day) + days{1}); const auto third_day = nocal::from_sys_days(nocal::to_sys_days(first_day) + days{2}); const auto fourth_day = nocal::from_sys_days(nocal::to_sys_days(first_day) + days{3}); auto recurring = make_event("search-series", "Quarter Sync", first_day, 9, 0, first_day, 10, 0); recurring.description = "Planning notes"; recurring.recurrence = nocal::RecurrenceRule{ .frequency = nocal::RecurrenceFrequency::daily, .interval = 1, .count = 3, .until = std::nullopt, .by_weekday = {}, .by_month_day = {}, .by_month = {}, }; auto other = make_event("other", "Unrelated", second_day, 12, 0, second_day, 13, 0); other.location = "Quarter room"; auto holiday = make_event("holiday", "Quarter holiday", third_day, 0, 0, fourth_day, 0, 0, true); std::vector events{recurring, other, holiday}; int saves = 0; nocal::tui::TuiApp app{ events, [&](std::span) { ++saves; }, -1, -1}; app.handle_input("/"); check(app.search_active() && app.state().search_prompt, "slash opens the search prompt"); app.handle_input("QUARTER SYNX"); app.handle_input("\x7f"); app.handle_input("C"); check(app.state().search_query == "QUARTER SYNC", "search query accepts text and backspace editing"); app.handle_input("\r"); check(app.state().show_search_results && app.state().search_results.size() == 3, "search expands every matching recurrence in the bounded window"); check(app.state().search_results.front().day == first_day && app.state().search_results.back().day == third_day, "search results retain chronological occurrence dates"); app.dispatch(nocal::tui::Action::down); app.dispatch(nocal::tui::Action::down); app.dispatch(nocal::tui::Action::select); check(!app.search_active() && app.state().selected_day == third_day && app.state().visible_month == third_day.year() / third_day.month() && focus_starts_with(app, "search-series@occ:"), "choosing a recurring result jumps to and focuses that exact instance"); check(saves == 0, "search and result navigation never cross the persistence boundary"); const auto selected_after_jump = app.state().selected_day; app.handle_input("/"); app.handle_input("\x1b"); check(!app.search_active() && app.state().selected_day == selected_after_jump, "cancelling search preserves the calendar selection"); app.handle_input("/"); app.handle_input("quarter"); app.handle_input("\r"); check(app.state().search_results.size() == 5 && app.state().search_results[0].day == first_day && app.state().search_results[1].day == second_day && app.state().search_results[2].day == second_day && app.state().search_results[3].title == "Quarter holiday" && app.state().search_results[4].title == "Quarter Sync", "search results sort by date with all-day events first within each day"); app.dispatch(nocal::tui::Action::back); app.handle_input("/"); app.handle_input("missing"); app.handle_input("\r"); check(app.state().show_search_results && app.state().search_results.empty(), "a query with no matches produces a safe empty results view"); app.dispatch(nocal::tui::Action::back); check(!app.search_active(), "Esc/back closes empty search results"); } void test_calendar_visibility_filters_without_partial_saves() { const auto day = nocal::today(); auto default_event = make_event("default", "D", day, 8, 0, day, 9, 0); auto personal_event = make_event("personal", "P", day, 9, 0, day, 10, 0); personal_event.calendar_id = "personal"; auto work_event = make_event("work", "W", day, 10, 0, day, 11, 0); work_event.calendar_id = "work"; std::vector events{default_event, personal_event, work_event}; int save_calls = 0; std::vector saved; nocal::tui::TuiApp app{ events, [&](const std::span values) { ++save_calls; saved.assign(values.begin(), values.end()); }, -1, -1, }; check(app.calendars().size() == 3 && app.calendars()[0].id.empty() && app.calendars()[0].name == "Default" && app.calendars()[1].id == "personal" && app.calendars()[2].id == "work", "controller derives a stable default-first calendar list from event metadata"); app.dispatch(nocal::tui::Action::calendars); check(app.calendar_picker_active(), "c opens the calendar picker"); app.dispatch(nocal::tui::Action::down); app.dispatch(nocal::tui::Action::down); app.dispatch(nocal::tui::Action::select); check(!app.calendars()[2].visible && save_calls == 0, "toggling calendar visibility never crosses the persistence boundary"); app.dispatch(nocal::tui::Action::back); auto render_state = app.state(); render_state.width = 120; render_state.height = 36; auto frame = nocal::tui::render_month(events, app.calendars(), render_state); check(frame.find("08:00 D") != std::string::npos && frame.find("09:00 P") != std::string::npos && frame.find("10:00 W") == std::string::npos, "month rendering excludes only events from hidden calendars"); app.handle_input("/"); app.handle_input("work"); app.handle_input("\r"); check(app.state().search_results.empty(), "search shares the month view's calendar visibility contract"); app.dispatch(nocal::tui::Action::back); app.dispatch(nocal::tui::Action::next_event); check(focus_is(app, "default"), "focus starts on the first visible calendar event"); app.dispatch(nocal::tui::Action::next_event); check(focus_is(app, "personal"), "focus advances to another visible calendar"); app.dispatch(nocal::tui::Action::next_event); check(focus_is(app, "default"), "focus traversal skips hidden-calendar events"); app.dispatch(nocal::tui::Action::delete_event); app.handle_input("y"); check(save_calls == 1 && saved.size() == 2 && std::any_of(saved.begin(), saved.end(), [](const nocal::Event& event) { return event.uid == "work" && event.calendar_id == "work"; }), "mutations save the full model, including events in hidden calendars"); app.dispatch(nocal::tui::Action::calendars); app.dispatch(nocal::tui::Action::select); check(app.calendars()[2].visible && save_calls == 1, "calendar picker retains its selection and can reveal a calendar without saving"); app.dispatch(nocal::tui::Action::back); app.dispatch(nocal::tui::Action::next_event); app.dispatch(nocal::tui::Action::next_event); check(focus_is(app, "work"), "revealed calendar events become focusable again"); app.dispatch(nocal::tui::Action::calendars); app.dispatch(nocal::tui::Action::select); check(!app.state().focused_event_id, "hiding the focused event's calendar clears stale appointment focus"); check(save_calls == 1, "all visibility-only interactions remain unsaved session state"); } void test_agenda_window_order_visibility_and_bounds() { using namespace std::chrono; const auto first_day = nocal::today(); const auto second_day = nocal::from_sys_days(nocal::to_sys_days(first_day) + days{1}); const auto after_window = nocal::from_sys_days(nocal::to_sys_days(first_day) + days{42}); auto series = make_event("agenda-series", "Daily standup", first_day, 9, 0, first_day, 9, 30); series.recurrence = nocal::RecurrenceRule{ .frequency = nocal::RecurrenceFrequency::daily, .interval = 1, .count = 100, .until = std::nullopt, .by_weekday = {}, .by_month_day = {}, .by_month = {}, }; auto hidden = make_event("hidden-agenda", "Hidden work", first_day, 7, 0, first_day, 8, 0); hidden.calendar_id = "work"; std::vector events{ make_event("timed", "Morning review", first_day, 8, 0, first_day, 8, 30), make_event("all-day", "Release day", first_day, 0, 0, second_day, 0, 0, true), series, hidden, make_event("outside", "Outside window", after_window, 6, 0, after_window, 7, 0), }; int save_calls = 0; nocal::tui::TuiApp app{ events, [&](std::span) { ++save_calls; }, -1, -1}; app.dispatch(nocal::tui::Action::calendars); app.dispatch(nocal::tui::Action::down); app.dispatch(nocal::tui::Action::select); app.dispatch(nocal::tui::Action::back); app.handle_input("g"); check(app.agenda_active() && app.state().agenda_start_day == first_day, "g opens a 42-day agenda anchored at the selected day"); check(app.state().agenda_items.size() == 44, "agenda recurrence expansion is bounded to its 42-day window"); if (app.state().agenda_items.size() == 44) { check(app.state().agenda_items[0].focus_id == "all-day" && app.state().agenda_items[1].focus_id == "timed" && app.state().agenda_items[2].focus_id.starts_with("agenda-series@occ:"), "agenda retains domain occurrence order with all-day items first"); check(app.state().agenda_items.back().day == nocal::from_sys_days(nocal::to_sys_days(first_day) + days{41}), "agenda includes the final day inside the half-open 42-day window"); } check(std::none_of(app.state().agenda_items.begin(), app.state().agenda_items.end(), [&](const nocal::tui::AgendaItem& item) { return item.focus_id == "hidden-agenda" || item.focus_id == "outside" || nocal::to_sys_days(item.day) >= nocal::to_sys_days(after_window); }), "agenda excludes hidden calendars and occurrences outside its window"); check(save_calls == 0, "opening and filtering agenda never saves events"); } void test_agenda_navigation_shift_choice_and_close() { using namespace std::chrono; const auto first_day = nocal::today(); auto series = make_event("choose-series", "Choose exact occurrence", first_day, 9, 0, first_day, 10, 0); series.recurrence = nocal::RecurrenceRule{ .frequency = nocal::RecurrenceFrequency::daily, .interval = 1, .count = 100, .until = std::nullopt, .by_weekday = {}, .by_month_day = {}, .by_month = {}, }; std::vector events{series}; int save_calls = 0; nocal::tui::TuiApp app{ events, [&](std::span) { ++save_calls; }, -1, -1}; const auto month_day = app.state().selected_day; const auto month = app.state().visible_month; app.dispatch(nocal::tui::Action::agenda); app.handle_input("k"); check(app.state().agenda_index == 0, "agenda navigation clamps at the first item instead of wrapping"); app.handle_input("j"); check(app.state().agenda_index == 1, "j advances agenda selection"); for (std::size_t index = 0; index < app.state().agenda_items.size() + 2; ++index) { app.dispatch(nocal::tui::Action::down); } check(app.state().agenda_index + 1 == app.state().agenda_items.size(), "agenda navigation clamps at the final item instead of wrapping"); app.handle_input("n"); check(app.state().agenda_start_day == nocal::from_sys_days(nocal::to_sys_days(first_day) + days{42}) && app.state().agenda_index == 0 && !app.state().agenda_items.empty() && app.state().agenda_items.front().day == app.state().agenda_start_day, "n shifts exactly 42 days and refreshes agenda from its new start"); app.handle_input("\x1b[5~"); check(app.state().agenda_start_day == first_day && app.state().agenda_index == 0, "Page Up shifts agenda exactly 42 days back and resets selection safely"); app.dispatch(nocal::tui::Action::down); const auto chosen = app.state().agenda_items[app.state().agenda_index]; app.dispatch(nocal::tui::Action::select); check(!app.agenda_active() && app.state().selected_day == chosen.day && app.state().visible_month == chosen.day.year() / chosen.day.month() && app.state().focused_event_id == std::optional{chosen.focus_id}, "Enter returns to month and focuses the exact recurring occurrence"); const auto chosen_day = app.state().selected_day; const auto chosen_month = app.state().visible_month; app.dispatch(nocal::tui::Action::agenda); app.dispatch(nocal::tui::Action::next_month); app.dispatch(nocal::tui::Action::agenda); check(!app.agenda_active() && app.state().selected_day == chosen_day && app.state().visible_month == chosen_month, "g closes agenda without changing the prior month selection"); app.dispatch(nocal::tui::Action::agenda); app.dispatch(nocal::tui::Action::previous_month); app.dispatch(nocal::tui::Action::back); check(!app.agenda_active() && app.state().selected_day == chosen_day && app.state().visible_month == chosen_month, "Escape closes agenda without applying its shifted window to month selection"); check(month_day == first_day && month == first_day.year() / first_day.month(), "agenda navigation fixture starts on the controller's month selection"); check(save_calls == 0, "all agenda navigation and selection remains read-only"); std::vector empty_events; nocal::tui::TuiApp empty{empty_events, -1, -1}; empty.dispatch(nocal::tui::Action::agenda); empty.dispatch(nocal::tui::Action::up); empty.dispatch(nocal::tui::Action::down); empty.dispatch(nocal::tui::Action::select); check(empty.agenda_active() && empty.state().agenda_items.empty() && empty.state().agenda_index == 0, "empty agenda navigation and Enter are safe no-ops"); } void test_agenda_search_and_calendar_overlays() { const auto day = nocal::today(); auto personal = make_event("agenda-personal", "Personal", day, 8, 0, day, 9, 0); personal.calendar_id = "personal"; std::vector events{ make_event("agenda-default", "Default", day, 7, 0, day, 8, 0), personal}; int save_calls = 0; nocal::tui::TuiApp app{ events, [&](std::span) { ++save_calls; }, -1, -1}; app.dispatch(nocal::tui::Action::agenda); app.handle_input("/"); check(app.agenda_active() && app.search_active(), "search prompt can overlay an active agenda"); app.handle_input("\x1b"); check(app.agenda_active() && !app.search_active(), "Escape closes search and returns to the underlying agenda"); app.handle_input("c"); check(app.agenda_active() && app.calendar_picker_active(), "calendar picker can overlay an active agenda"); app.dispatch(nocal::tui::Action::down); app.dispatch(nocal::tui::Action::select); check(!app.calendars()[1].visible && app.state().agenda_items.size() == 1 && app.state().agenda_items.front().focus_id == "agenda-default", "hiding a calendar immediately refreshes the agenda beneath the picker"); app.dispatch(nocal::tui::Action::back); check(app.agenda_active() && !app.calendar_picker_active(), "closing the picker returns to the agenda"); check(save_calls == 0, "agenda overlays and visibility changes never save events"); } } // namespace int main() { test_event_focus_cycles_in_display_order(); test_open_close_and_day_movement(); test_empty_day_is_safe(); test_reader_navigation_and_synthetic_ids(); test_key_decoding(); test_crud_preconditions_and_add(); test_edit_preserves_uid_and_rolls_back_on_save_failure(); test_delete_cancel_confirm_and_rollback(); test_crud_targets_synthetic_identities(); test_add_edit_delete_undo_redo(); test_history_persistence_failure_is_transactional(); test_new_mutation_invalidates_redo(); test_history_is_bounded_to_one_hundred_mutations(); test_history_is_unavailable_in_modal_states(); test_focused_line_rendering(); test_focus_brings_hidden_event_into_view(); test_timed_event_details(); test_all_day_and_multiday_details(); test_narrow_detail_wrapping_without_ansi(); test_multiday_segment_labels_and_series_messaging(); test_recurring_occurrence_navigation_and_whole_series_crud(); test_rdate_only_occurrence_identity_and_whole_series_crud(); test_recurrence_grid_expansion_and_source_zone_display(); test_search_prompt_results_and_occurrence_jump(); test_calendar_visibility_filters_without_partial_saves(); test_agenda_window_order_visibility_and_bounds(); test_agenda_navigation_shift_choice_and_close(); test_agenda_search_and_calendar_overlays(); if (failures != 0) { std::cerr << failures << " TUI focus test(s) failed\n"; return EXIT_FAILURE; } std::cout << "TUI focus tests passed\n"; return EXIT_SUCCESS; }