/** * @file test_tui_workflows.c * @brief Scripted regressions through the production TUI workflow functions. */ #include #include /* The workflows remain file-private in production. Including their translation * unit lets this test drive the exact code paths with the terminal port mocked, * without adding a public test API to Trainlog. */ #include "../src/tui.c" struct TrainlogTerminal { int events[128]; size_t event_count; size_t event_index; char output[32768]; size_t output_used; int rows; int columns; bool coordinate_overflow; bool text_overflow; size_t surface_draw_count; }; struct TrainlogPanel { int unused; }; struct TrainlogSurface { TrainlogTerminal *terminal; }; #define CHECK(condition) do { if (!(condition)) { \ (void)fprintf(stderr, "CHECK failed at %s:%d: %s\n", \ __FILE__, __LINE__, #condition); return false; } } while (0) TrainlogTerminal *trainlog_terminal_create(void) { return NULL; } void trainlog_terminal_destroy(TrainlogTerminal *terminal) { (void)terminal; } int trainlog_terminal_rows(const TrainlogTerminal *terminal) { return terminal->rows; } int trainlog_terminal_columns(const TrainlogTerminal *terminal) { return terminal->columns; } void trainlog_terminal_erase(TrainlogTerminal *terminal) { (void)terminal; } void trainlog_terminal_render(TrainlogTerminal *terminal) { (void)terminal; } void trainlog_terminal_style_on(TrainlogTerminal *terminal, TrainlogTextStyle style) { (void)terminal; (void)style; } void trainlog_terminal_style_off(TrainlogTerminal *terminal, TrainlogTextStyle style) { (void)terminal; (void)style; } void trainlog_terminal_printf(TrainlogTerminal *terminal, int row, int column, const char *format, ...) { va_list arguments; int written; if (row < 0 || row >= terminal->rows || column < 0 || column >= terminal->columns) terminal->coordinate_overflow = true; if (terminal->output_used >= sizeof(terminal->output)) return; va_start(arguments, format); written = vsnprintf(terminal->output + terminal->output_used, sizeof(terminal->output) - terminal->output_used, format, arguments); va_end(arguments); if (written > 0 && (size_t)written < sizeof(terminal->output) - terminal->output_used) { const char *text = terminal->output + terminal->output_used; const char *at = text; int cells = 0; while (*at != '\0') { utf8proc_int32_t codepoint; utf8proc_ssize_t bytes = utf8proc_iterate((const utf8proc_uint8_t *)at, -1, &codepoint); int codepoint_cells; if (bytes <= 0) { bytes = 1; codepoint = (unsigned char)*at; } codepoint_cells = utf8proc_charwidth(codepoint); cells += codepoint_cells < 0 ? 1 : codepoint_cells; at += bytes; } if (column + cells >= terminal->columns) terminal->text_overflow = true; terminal->output_used += (size_t)written; terminal->output[terminal->output_used++] = '\n'; terminal->output[terminal->output_used] = '\0'; } } void trainlog_terminal_putn(TrainlogTerminal *terminal, const char *text, size_t length) { (void)terminal; (void)text; (void)length; } void trainlog_terminal_move(TrainlogTerminal *terminal, int row, int column) { (void)terminal; (void)row; (void)column; } void trainlog_terminal_cursor_yx(const TrainlogTerminal *terminal, int *row, int *column) { (void)terminal; if (row) *row = 0; if (column) *column = 40; } void trainlog_terminal_clear_to_end(TrainlogTerminal *terminal) { (void)terminal; } void trainlog_terminal_cursor_visible(TrainlogTerminal *terminal, bool visible) { (void)terminal; (void)visible; } void trainlog_terminal_draw(TrainlogTerminal *terminal, int row, int column, uint32_t codepoint) { (void)terminal; (void)row; (void)column; (void)codepoint; } void trainlog_terminal_box(TrainlogTerminal *terminal, int top, int left, int bottom, int right) { (void)terminal; (void)top; (void)left; (void)bottom; (void)right; } int trainlog_terminal_get_key(TrainlogTerminal *terminal) { int key = terminal->event_index < terminal->event_count ? terminal->events[terminal->event_index++] : TRAINLOG_KEY_NONE; if (key == TRAINLOG_KEY_RESIZE) { terminal->rows = 20; terminal->columns = 72; } return key; } bool trainlog_terminal_read_unicode(TrainlogTerminal *terminal, int *codepoint, char utf8[5]) { int value = trainlog_terminal_get_key(terminal); if (value == TRAINLOG_KEY_NONE) return false; *codepoint = value; utf8[0] = value >= 0 && value < 128 ? (char)value : '\0'; utf8[1] = '\0'; return true; } bool trainlog_terminal_push_key(TrainlogTerminal *terminal, int key) { (void)terminal; (void)key; return false; } bool trainlog_terminal_refresh_geometry(TrainlogTerminal *terminal) { (void)terminal; return true; } TrainlogPanel *tui_panel_create(TrainlogTerminal *terminal, int height, int width, int top, int left) { (void)terminal; (void)height; (void)width; (void)top; (void)left; return NULL; } void tui_panel_destroy(TrainlogPanel *panel) { (void)panel; } void tui_panel_box(TrainlogPanel *panel) { (void)panel; } void tui_panel_style_on(TrainlogPanel *panel, TrainlogTextStyle style) { (void)panel; (void)style; } void tui_panel_style_off(TrainlogPanel *panel, TrainlogTextStyle style) { (void)panel; (void)style; } void tui_panel_print(TrainlogPanel *panel, int row, int column, const char *format, ...) { (void)panel; (void)row; (void)column; (void)format; } void tui_panel_commit(TrainlogPanel *panel) { (void)panel; } TrainlogSurface *trainlog_surface_create(TrainlogTerminal *terminal, const char *name, int top, int left, int height, int width) { static TrainlogSurface surface; (void)name; (void)top; (void)left; (void)height; (void)width; surface.terminal = terminal; return &surface; } bool trainlog_surface_set_rect(TrainlogSurface *surface, int top, int left, int height, int width) { (void)surface; (void)top; (void)left; (void)height; (void)width; return true; } void trainlog_surface_destroy(TrainlogSurface *surface) { (void)surface; } void trainlog_surface_erase(TrainlogSurface *surface) { (void)surface; } void trainlog_surface_set_role(TrainlogSurface *surface, TrainlogColorRole foreground, unsigned background_rgb, TrainlogTextStyle style) { (void)surface; (void)foreground; (void)background_rgb; (void)style; } void trainlog_surface_printf(TrainlogSurface *surface, int row, int column, const char *format, ...) { va_list arguments; if (surface == NULL || surface->terminal == NULL) return; va_start(arguments, format); if (surface->terminal->output_used < sizeof(surface->terminal->output)) { int written = vsnprintf(surface->terminal->output + surface->terminal->output_used, sizeof(surface->terminal->output) - surface->terminal->output_used, format, arguments); if (written > 0 && (size_t)written < sizeof(surface->terminal->output) - surface->terminal->output_used) surface->terminal->output_used += (size_t)written; } va_end(arguments); (void)row; (void)column; } void trainlog_surface_draw(TrainlogSurface *surface, int row, int column, uint32_t codepoint) { if (surface != NULL && surface->terminal != NULL) ++surface->terminal->surface_draw_count; (void)row; (void)column; (void)codepoint; } void trainlog_surface_move_top(TrainlogSurface *surface) { (void)surface; } static bool test_knowledge_scrolls_long_lists_at_minimum_terminal(void) { TrainlogAppContext app; TrainlogTerminal terminal; TrainlogSurface surface; size_t page; (void)memset(&app, 0, sizeof(app)); (void)snprintf(app.exercise_detail.exercise_id, sizeof(app.exercise_detail.exercise_id), "%s", "ex_a72fa713-4b0e-431d-95e2-42d95beb77b1"); (void)snprintf(app.exercise_detail.name, sizeof(app.exercise_detail.name), "%s", "Lat pull"); (void)memset(&terminal, 0, sizeof(terminal)); terminal.rows = 20; terminal.columns = 72; surface.terminal = &terminal; app.terminal = &terminal; app.content = &surface; app.layout.usable = true; app.layout.content.height = 16; app.layout.content.width = 72; app.focus = TRAINLOG_FOCUS_CONTENT; app.navigation.current.route = TRAINLOG_ROUTE_EXERCISE_KNOWLEDGE; for (page = 0U; page < 5U; ++page) { app_shell_render_content(&app); app_shell_dispatch(&app, TRAINLOG_KEY_PAGE_DOWN); } CHECK(!terminal.coordinate_overflow); CHECK(!terminal.text_overflow); CHECK(strstr(terminal.output, "Supra-épineux") != NULL); CHECK(strstr(terminal.output, "Sources :") != NULL); return true; } static bool seed_generator_leg_press(TrainlogDatabase *database) { return trainlog_database_insert_exercise_profiled(database, "ex_b432623f-bfe9-4daf-a653-60ec7fdffbde", "Leg press", "leg press", TRAINLOG_TRACKING_REPS, TRAINLOG_RECORDING_SETS, 0U) == TRAINLOG_STATUS_OK; } static bool test_shell_session_navigation_preserves_run_draft(void) { TrainlogDatabase *database = NULL; TrainlogAppContext app; size_t sessions = 99U; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); (void)memset(&app, 0, sizeof(app)); app.database = database; trainlog_navigation_init(&app.navigation); app.navigation.current.route = TRAINLOG_ROUTE_SESSION_CURRENT; app.session.has_draft = true; app.session.dirty = true; app.session.draft_count = 1U; (void)snprintf(app.session.drafts[0].input.exercise_id, sizeof(app.session.drafts[0].input.exercise_id), "%s", "ex_11111111-1111-4111-8111-111111111111"); app_shell_open_route(&app, TRAINLOG_ROUTE_EXERCISES); CHECK(app.navigation.current.route == TRAINLOG_ROUTE_EXERCISES); CHECK(app.session.has_draft && app.session.draft_count == 1U); CHECK(strcmp(app.session.drafts[0].input.exercise_id, "ex_11111111-1111-4111-8111-111111111111") == 0); CHECK(trainlog_database_session_count(database, &sessions) == TRAINLOG_STATUS_OK); CHECK(sessions == 0U); trainlog_database_close(database); return true; } static bool test_shell_generator_keep_and_accept_has_no_actuals(void) { TrainlogDatabase *database = NULL; TrainlogAppContext app; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); CHECK(seed_generator_leg_press(database)); (void)memset(&app, 0, sizeof(app)); app.database = database; trainlog_navigation_init(&app.navigation); app.navigation.current.route = TRAINLOG_ROUTE_SESSION_GENERATOR; app.session.phase = TRAINLOG_SESSION_GENERATOR_PREVIEW; app.session.generated_preview = true; app.session.generated.exercise_count = 1U; (void)snprintf(app.session.generated.exercises[0].exercise_id, sizeof(app.session.generated.exercises[0].exercise_id), "%s", "ex_b432623f-bfe9-4daf-a653-60ec7fdffbde"); (void)snprintf(app.session.generated.exercises[0].equipment_id, sizeof(app.session.generated.exercises[0].equipment_id), "%s", "leg_press"); app.session.generated.exercises[0].target_sets = 3; app.session.generated.exercises[0].target_repetitions = 10; app.session.generated.exercises[0].rest_seconds = 120; app_shell_open_route(&app, TRAINLOG_ROUTE_EXERCISES); CHECK(app.navigation.current.route == TRAINLOG_ROUTE_SESSION_GENERATOR); CHECK(app.session.phase == TRAINLOG_SESSION_CONFIRM_LEAVE); CHECK(app_shell_dispatch_session(&app, TRAINLOG_KEY_ENTER)); CHECK(app.navigation.current.route == TRAINLOG_ROUTE_EXERCISES); CHECK(app.session.generated_preview); app.navigation.current.route = TRAINLOG_ROUTE_SESSION_GENERATOR; app.session.phase = TRAINLOG_SESSION_GENERATOR_PREVIEW; CHECK(session_controller_accept_generator(&app)); CHECK(app.session.has_draft && app.session.draft_count == 1U); CHECK(app.session.drafts[0].input.target_sets == 3); CHECK(app.session.drafts[0].input.set_count == 0U); trainlog_database_close(database); return true; } static bool test_shell_session_forms_keep_plan_and_actuals_separate(void) { TrainlogDatabase *database = NULL; TrainlogAppContext app; TrainlogSessionDraftExercise *draft; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); (void)memset(&app, 0, sizeof(app)); app.database = database; trainlog_navigation_init(&app.navigation); app.navigation.current.route = TRAINLOG_ROUTE_SESSION_CURRENT; app.session.has_draft = true; app.session.draft_count = 1U; app.session.phase = TRAINLOG_SESSION_DRAFT; draft = &app.session.drafts[0]; draft->tracking_mode = TRAINLOG_TRACKING_REPS; draft->input.recording_mode = TRAINLOG_RECORDING_SETS; draft->input.target_sets = 3; draft->input.target_reps = 10; draft_bind_input(draft); CHECK(app_shell_dispatch_session(&app, 'e')); CHECK(app_shell_dispatch_session(&app, 'a')); CHECK(app_shell_dispatch_session(&app, '8')); CHECK(app_shell_dispatch_session(&app, TRAINLOG_KEY_ENTER)); CHECK(draft->input.set_count == 1U && draft->sets[0].reps == 8); CHECK(draft->input.target_sets == 3 && draft->input.target_reps == 10); trainlog_database_close(database); return true; } static bool test_shell_continuous_supplemental_fields_have_no_fake_set(void) { TrainlogDatabase *database = NULL; TrainlogAppContext app; TrainlogSessionDraftExercise *draft; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); (void)memset(&app, 0, sizeof(app)); app.database = database; app.navigation.current.route = TRAINLOG_ROUTE_SESSION_CURRENT; app.session.has_draft = true; app.session.draft_count = 1U; app.session.phase = TRAINLOG_SESSION_ACTUALS; draft = &app.session.drafts[0]; draft->tracking_mode = TRAINLOG_TRACKING_DURATION; draft->input.recording_mode = TRAINLOG_RECORDING_CONTINUOUS; draft->input.data_fields = TRAINLOG_EXERCISE_DATA_SPEED_KMH | TRAINLOG_EXERCISE_DATA_DISTANCE_KM; draft_bind_input(draft); CHECK(app_shell_dispatch_session(&app, 'e')); CHECK(app_shell_dispatch_session(&app, '9')); CHECK(app_shell_dispatch_session(&app, '0')); CHECK(app_shell_dispatch_session(&app, TRAINLOG_KEY_ENTER)); CHECK(app_shell_dispatch_session(&app, 's')); CHECK(app_shell_dispatch_session(&app, '1')); CHECK(app_shell_dispatch_session(&app, '2')); CHECK(app_shell_dispatch_session(&app, TRAINLOG_KEY_ENTER)); CHECK(app_shell_dispatch_session(&app, 'k')); CHECK(app_shell_dispatch_session(&app, '2')); CHECK(app_shell_dispatch_session(&app, '.')); CHECK(app_shell_dispatch_session(&app, '5')); CHECK(app_shell_dispatch_session(&app, TRAINLOG_KEY_ENTER)); CHECK(draft->input.continuous_duration_seconds == 90); CHECK(draft->input.continuous_has_speed && draft->input.continuous_speed_kmh == 12.0); CHECK(draft->input.continuous_has_distance && draft->input.continuous_distance_km == 2.5); CHECK(draft->input.set_count == 0U && !draft->input.has_max_weight); trainlog_database_close(database); return true; } static bool test_shell_generator_dose_edit_drops_stale_load_provenance(void) { TrainlogDatabase *database = NULL; TrainlogAppContext app; TrainlogGeneratedExercise *generated; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); (void)memset(&app, 0, sizeof(app)); app.database = database; app.navigation.current.route = TRAINLOG_ROUTE_SESSION_GENERATOR; app.session.generation_zone = trainlog_body_zone_catalog_lookup("thighs"); app.session.generation_goal = &trainlog_session_generation_policy_v1.goals[0]; app.session.generation_duration_minutes = 30; CHECK(seed_generator_leg_press(database)); CHECK(session_controller_generate(&app)); CHECK(app.session.generated.exercise_count == 1U); generated = &app.session.generated.exercises[0]; generated->target_sets = 3; generated->target_repetitions = 10; generated->has_target_weight = true; generated->target_weight_kg = 80.0; (void)snprintf(generated->load_source_session_id, sizeof(generated->load_source_session_id), "se_source"); CHECK(app_shell_dispatch_session(&app, 'e')); CHECK(app_shell_dispatch_session(&app, TRAINLOG_KEY_BACKSPACE)); CHECK(app_shell_dispatch_session(&app, '4')); CHECK(app_shell_dispatch_session(&app, TRAINLOG_KEY_ENTER)); CHECK(app_shell_dispatch_session(&app, TRAINLOG_KEY_BACKSPACE)); CHECK(app_shell_dispatch_session(&app, TRAINLOG_KEY_BACKSPACE)); CHECK(app_shell_dispatch_session(&app, '1')); CHECK(app_shell_dispatch_session(&app, '2')); CHECK(app_shell_dispatch_session(&app, TRAINLOG_KEY_ENTER)); CHECK(generated->target_sets == 4 && generated->target_repetitions == 12); CHECK(!generated->has_target_weight && generated->load_source_session_id[0] == '\0'); trainlog_database_close(database); return true; } static bool test_shell_inline_equipment_creation_resumes_occurrence(void) { TrainlogDatabase *database = NULL; TrainlogAppContext app; const char *name = "Cable maison"; const char *type = "cable"; size_t index; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); (void)memset(&app, 0, sizeof(app)); app.database = database; app.navigation.current.route = TRAINLOG_ROUTE_SESSION_CURRENT; app.session.has_draft = true; app.session.draft_count = 1U; app.session.phase = TRAINLOG_SESSION_EQUIPMENT_PICKER; CHECK(app_shell_dispatch_session(&app, 'n')); for (index = 0U; name[index] != '\0'; ++index) CHECK(app_shell_dispatch_session(&app, (unsigned char)name[index])); CHECK(app_shell_dispatch_session(&app, TRAINLOG_KEY_ENTER)); CHECK(app_shell_dispatch_session(&app, TRAINLOG_KEY_ENTER)); for (index = 0U; type[index] != '\0'; ++index) CHECK(app_shell_dispatch_session(&app, (unsigned char)type[index])); CHECK(app_shell_dispatch_session(&app, TRAINLOG_KEY_ENTER)); CHECK(app_shell_dispatch_session(&app, TRAINLOG_KEY_ENTER)); CHECK(app.session.phase == TRAINLOG_SESSION_EQUIPMENT_PICKER); CHECK(app.session.equipment_selected > 0U); CHECK(app_shell_dispatch_session(&app, TRAINLOG_KEY_ENTER)); CHECK(app.session.phase == TRAINLOG_SESSION_DRAFT); CHECK(app.session.drafts[0].input.equipment_id[0] != '\0'); CHECK(app.session.drafts[0].input.load_mode == TRAINLOG_LOAD_EXTERNAL); trainlog_database_close(database); return true; } static bool test_shell_equipment_catalog_create_preserves_invalid_form(void) { TrainlogDatabase *database = NULL; TrainlogAppContext app; TrainlogCustomEquipment items[2]; size_t count = 0U; const char *name = "Poulie personnelle"; const char *type = "cable"; size_t index; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); (void)memset(&app, 0, sizeof(app)); app.database = database; app.navigation.current.route = TRAINLOG_ROUTE_EQUIPMENT; app.layout.content.height = 20; trainlog_list_init(&app.list); equipment_controller_start(&app); CHECK(app_shell_dispatch_equipment_controller(&app, TRAINLOG_KEY_ENTER)); CHECK(app.equipment_controller.phase == TRAINLOG_EQUIPMENT_NAME); CHECK(app.equipment_controller.form.active); CHECK(strstr(app.equipment_controller.message, "obligatoire") != NULL); for (index = 0U; name[index] != '\0'; ++index) CHECK(app_shell_dispatch_equipment_controller(&app, (unsigned char)name[index])); app_shell_open_route(&app, TRAINLOG_ROUTE_HOME); CHECK(app.navigation.current.route == TRAINLOG_ROUTE_EQUIPMENT); CHECK(app.equipment_controller.phase == TRAINLOG_EQUIPMENT_CONFIRM_DISCARD); CHECK(strcmp(app.equipment_controller.form.text, name) == 0); CHECK(app_shell_dispatch_equipment_controller(&app, '0')); CHECK(app.equipment_controller.phase == TRAINLOG_EQUIPMENT_NAME); CHECK(app_shell_dispatch_equipment_controller(&app, TRAINLOG_KEY_ENTER)); CHECK(app_shell_dispatch_equipment_controller(&app, TRAINLOG_KEY_ENTER)); for (index = 0U; type[index] != '\0'; ++index) CHECK(app_shell_dispatch_equipment_controller(&app, (unsigned char)type[index])); CHECK(app_shell_dispatch_equipment_controller(&app, TRAINLOG_KEY_ENTER)); CHECK(app.equipment_controller.phase == TRAINLOG_EQUIPMENT_LOAD); CHECK(app_shell_dispatch_equipment_controller(&app, '3')); CHECK(app_shell_dispatch_equipment_controller(&app, TRAINLOG_KEY_ENTER)); CHECK(app.equipment_controller.phase == TRAINLOG_EQUIPMENT_MESSAGE); CHECK(trainlog_database_list_custom_equipment(database, items, 2U, &count) == TRAINLOG_STATUS_OK); CHECK(count == 1U); CHECK(strcmp(items[0].display_name, name) == 0); CHECK(strcmp(items[0].equipment_type, type) == 0); CHECK(strcmp(items[0].load_semantics, "assistance") == 0); trainlog_database_close(database); return true; } static bool test_shell_inline_exercise_creation_and_stable_edit(void) { TrainlogDatabase *database = NULL; TrainlogAppContext app; TrainlogExercise created; TrainlogExercise listed[4]; size_t count = 0U; const char *name = "Course inclinée"; size_t index; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); (void)memset(&app, 0, sizeof(app)); app.database = database; app.navigation.current.route = TRAINLOG_ROUTE_SESSION_CURRENT; app.session.phase = TRAINLOG_SESSION_EXERCISE_PICKER; exercise_controller_start_create(&app, true); for (index = 0U; name[index] != '\0'; ++index) CHECK(app_shell_dispatch_exercise_controller(&app, (unsigned char)name[index])); CHECK(app_shell_dispatch_exercise_controller(&app, TRAINLOG_KEY_ENTER)); CHECK(app_shell_dispatch_exercise_controller(&app, '2')); CHECK(app_shell_dispatch_exercise_controller(&app, TRAINLOG_KEY_ENTER)); CHECK(app_shell_dispatch_exercise_controller(&app, '2')); CHECK(app_shell_dispatch_exercise_controller(&app, TRAINLOG_KEY_ENTER)); CHECK(app_shell_dispatch_exercise_controller(&app, '1')); CHECK(app_shell_dispatch_exercise_controller(&app, TRAINLOG_KEY_ENTER)); CHECK(app_shell_dispatch_exercise_controller(&app, '0')); CHECK(app_shell_dispatch_exercise_controller(&app, TRAINLOG_KEY_ENTER)); CHECK(app.exercise_controller.phase == TRAINLOG_EXERCISE_ZONES); CHECK(app_shell_dispatch_exercise_controller(&app, TRAINLOG_KEY_ENTER)); CHECK(app.exercise_controller.phase == TRAINLOG_EXERCISE_IDLE); CHECK(app.session.phase == TRAINLOG_SESSION_EXERCISE_PICKER); CHECK(app.session.picker_count == 1U); created = app.session.picker[app.session.picker_selected]; CHECK(created.recording_mode == TRAINLOG_RECORDING_CONTINUOUS); CHECK(created.tracking_mode == TRAINLOG_TRACKING_DURATION); CHECK((created.data_fields & TRAINLOG_EXERCISE_DATA_SPEED_KMH) != 0U); app.exercise_detail = created; exercise_controller_start_edit(&app); trainlog_form_init(&app.exercise_controller.form, "Course renommée"); app.exercise_controller.dirty = true; CHECK(app_shell_dispatch_exercise_controller(&app, TRAINLOG_KEY_ENTER)); CHECK(app.exercise_controller.phase == TRAINLOG_EXERCISE_ZONES); CHECK(app_shell_dispatch_exercise_controller(&app, TRAINLOG_KEY_ENTER)); CHECK(strcmp(app.exercise_detail.exercise_id, created.exercise_id) == 0); CHECK(strcmp(app.exercise_detail.name, "Course renommée") == 0); CHECK(trainlog_database_list_exercises(database, listed, 4U, &count) == TRAINLOG_STATUS_OK); CHECK(count == 1U && strcmp(listed[0].exercise_id, created.exercise_id) == 0); trainlog_database_close(database); return true; } static bool test_shell_navigation_focus_consumes_input(void) { TrainlogDatabase *database = NULL; TrainlogAppContext app; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); (void)memset(&app, 0, sizeof(app)); app.database = database; app.layout.usable = true; app.layout.sidebar_visible = true; app.layout.content.height = 20; app.focus = TRAINLOG_FOCUS_CONTENT; trainlog_navigation_init(&app.navigation); app_shell_dispatch(&app, TRAINLOG_KEY_F6); CHECK(app.focus == TRAINLOG_FOCUS_NAVIGATION); app_shell_dispatch(&app, TRAINLOG_KEY_ESCAPE); CHECK(app.focus == TRAINLOG_FOCUS_CONTENT); CHECK(app.navigation.current.route == TRAINLOG_ROUTE_HOME); app_shell_dispatch(&app, TRAINLOG_KEY_SHIFT_TAB); CHECK(app.focus == TRAINLOG_FOCUS_SEARCH); app_shell_dispatch(&app, TRAINLOG_KEY_SHIFT_TAB); CHECK(app.focus == TRAINLOG_FOCUS_NAVIGATION); app.navigation_selected = 3U; app_shell_dispatch(&app, TRAINLOG_KEY_ENTER); CHECK(app.navigation.current.route == TRAINLOG_ROUTE_EQUIPMENT); CHECK(app.focus == TRAINLOG_FOCUS_CONTENT); trainlog_database_close(database); return true; } static bool test_shell_actions_overlay_executes_registered_search_action(void) { TrainlogDatabase *database = NULL; TrainlogAppContext app; const TrainlogAction *search_action; TrainlogCustomEquipment equipment; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); (void)memset(&equipment, 0, sizeof(equipment)); generate_custom_equipment_uuid(equipment.equipment_id); (void)snprintf(equipment.display_name, sizeof(equipment.display_name), "overlay search needle"); (void)snprintf(equipment.equipment_type, sizeof(equipment.equipment_type), "machine"); (void)snprintf(equipment.load_semantics, sizeof(equipment.load_semantics), "external"); CHECK(trainlog_database_create_custom_equipment(database, &equipment) == TRAINLOG_STATUS_OK); (void)memset(&app, 0, sizeof(app)); app.database = database; app.layout.usable = true; app.layout.content.height = 20; app.focus = TRAINLOG_FOCUS_CONTENT; trainlog_navigation_init(&app.navigation); app.navigation.current.route = TRAINLOG_ROUTE_EQUIPMENT; trainlog_search_init(&app.search); app_shell_refresh_list(&app); app_shell_actions(&app); search_action = trainlog_actions_find_key(&app.actions, '/'); CHECK(search_action != NULL); CHECK(strcmp(search_action->identifier, "list.search") == 0); CHECK(search_action->intent == TRAINLOG_INTENT_OPEN_SEARCH); app_shell_dispatch(&app, TRAINLOG_KEY_F7); CHECK(trainlog_overlays_top(&app.overlays)->type == TRAINLOG_OVERLAY_ACTIONS); app_shell_dispatch(&app, TRAINLOG_KEY_DOWN); app_shell_dispatch(&app, TRAINLOG_KEY_ENTER); CHECK(app.focus == TRAINLOG_FOCUS_SEARCH); CHECK(app.search.open && app.search.focused); app_shell_dispatch(&app, 'n'); app_shell_dispatch(&app, 'e'); app_shell_dispatch(&app, 'e'); app_shell_dispatch(&app, 'd'); app_shell_dispatch(&app, 'l'); app_shell_dispatch(&app, 'e'); CHECK(strcmp(app.search.text, "needle") == 0); CHECK(app.loaded_count == 1U); CHECK(strcmp(app.equipment[0].display_name, "overlay search needle") == 0); app_shell_dispatch(&app, TRAINLOG_KEY_ESCAPE); CHECK(app.search.focused && app.search.bytes == 0U); CHECK(app.loaded_count > 1U); app_shell_dispatch(&app, TRAINLOG_KEY_ESCAPE); CHECK(!app.search.focused && app.focus == TRAINLOG_FOCUS_CONTENT); trainlog_database_close(database); return true; } static bool test_shell_compact_navigation_focus_contract(void) { TrainlogDatabase *database = NULL; TrainlogAppContext app; TrainlogTerminal terminal = {0}; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); (void)memset(&app, 0, sizeof(app)); terminal.columns = 120; terminal.rows = 35; app.database = database; app.terminal = &terminal; app.focus = TRAINLOG_FOCUS_CONTENT; trainlog_navigation_init(&app.navigation); CHECK(app_shell_layout(&app)); app_shell_dispatch(&app, TRAINLOG_KEY_F6); CHECK(app.focus == TRAINLOG_FOCUS_NAVIGATION); app.navigation_selected = 3U; terminal.columns = 100; terminal.rows = 25; CHECK(app_shell_layout(&app)); CHECK(!app.layout.sidebar_visible && app.focus == TRAINLOG_FOCUS_NAVIGATION); app_shell_dispatch(&app, TRAINLOG_KEY_ENTER); CHECK(trainlog_overlays_top(&app.overlays)->type == TRAINLOG_OVERLAY_NAVIGATION); app_shell_dispatch(&app, TRAINLOG_KEY_ESCAPE); CHECK(app.focus == TRAINLOG_FOCUS_NAVIGATION && app.navigation_selected == 3U); app_shell_dispatch(&app, TRAINLOG_KEY_SHIFT_TAB); CHECK(app.focus == TRAINLOG_FOCUS_ACTIONS); app_shell_dispatch(&app, TRAINLOG_KEY_SHIFT_TAB); CHECK(app.focus == TRAINLOG_FOCUS_CONTENT); app_shell_dispatch(&app, TRAINLOG_KEY_SHIFT_TAB); CHECK(app.focus == TRAINLOG_FOCUS_SEARCH && app.search.focused); app_shell_dispatch(&app, TRAINLOG_KEY_SHIFT_TAB); CHECK(app.focus == TRAINLOG_FOCUS_NAVIGATION && !app.search.focused); app_shell_dispatch(&app, TRAINLOG_KEY_TAB); CHECK(app.focus == TRAINLOG_FOCUS_SEARCH && app.search.focused); app.navigation.current.route = TRAINLOG_ROUTE_EQUIPMENT; app.search.open = true; app_shell_dispatch(&app, TRAINLOG_KEY_F6); CHECK(trainlog_overlays_top(&app.overlays)->type == TRAINLOG_OVERLAY_NAVIGATION); app_shell_dispatch(&app, TRAINLOG_KEY_ESCAPE); CHECK(app.focus == TRAINLOG_FOCUS_SEARCH && app.search.focused); app.focus = TRAINLOG_FOCUS_ACTIONS; app.search.focused = false; app_shell_dispatch(&app, TRAINLOG_KEY_ENTER); CHECK(trainlog_overlays_top(&app.overlays)->type == TRAINLOG_OVERLAY_ACTIONS); trainlog_database_close(database); return true; } static bool test_shell_detail_back_restores_stable_list_selection(void) { TrainlogDatabase *database = NULL; TrainlogAppContext app; char selected_id[TRAINLOG_SHELL_STABLE_ID_CAPACITY]; size_t selected_index; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); (void)memset(&app, 0, sizeof(app)); app.database = database; app.layout.content.height = 16; trainlog_navigation_init(&app.navigation); app.navigation.current.route = TRAINLOG_ROUTE_EQUIPMENT; trainlog_list_init(&app.list); trainlog_search_init(&app.search); app_shell_refresh_list(&app); CHECK(app.loaded_count > 1U); trainlog_list_move(&app.list, app.stable_ids, 1); (void)snprintf(selected_id, sizeof(selected_id), "%s", app.list.selected_id); selected_index = app.list.selected_index; app_shell_primary(&app); CHECK(app.navigation.current.route == TRAINLOG_ROUTE_EQUIPMENT_DETAIL); CHECK(app_shell_back(&app)); CHECK(app.navigation.current.route == TRAINLOG_ROUTE_EQUIPMENT); CHECK(strcmp(app.list.selected_id, selected_id) == 0); CHECK(app.list.selected_index == selected_index); trainlog_database_close(database); return true; } static int sync_probe_calls; static int sync_run_calls; static TrainlogSyncDirection sync_last_direction; static TrainlogAppContext *sync_running_app; static char sync_journal_root[PATH_MAX + 1U]; static TrainlogStatus mock_sync_probe(TrainlogSyncDeviceInfo *output) { ++sync_probe_calls; (void)memset(output, 0, sizeof(*output)); output->connected = true; output->storage_ready = true; (void)snprintf(output->device.vendor, sizeof(output->device.vendor), "Test"); (void)snprintf(output->device.model, sizeof(output->device.model), "Phone"); return TRAINLOG_STATUS_OK; } static TrainlogStatus mock_sync_run(TrainlogSyncTrigger trigger, bool require_request, TrainlogSyncDirection direction, TrainlogSyncReport *output) { char directory[PATH_MAX + 1U]; char history_path[PATH_MAX + 1U]; FILE *history; int run_number; CHECK(trigger == TRAINLOG_SYNC_TRIGGER_TUI); CHECK(!require_request); ++sync_run_calls; run_number = sync_run_calls; sync_last_direction = direction; CHECK(sync_running_app != NULL && sync_running_app->sync_controller.running); CHECK(strstr(sync_running_app->terminal->output, "Synchronisation en cours") != NULL); /* A re-entrant launch attempt is consumed while the sole run is active. */ CHECK(app_shell_dispatch_sync(sync_running_app, 's')); CHECK(app_shell_dispatch_sync(sync_running_app, TRAINLOG_KEY_ENTER)); CHECK(sync_run_calls == run_number); (void)memset(output, 0, sizeof(*output)); output->success = run_number != 2; output->direction = direction; (void)snprintf(output->sync_id, sizeof(output->sync_id), "sy_00000000-0000-4000-8000-%012d", run_number); (void)snprintf(output->summary, sizeof(output->summary), "%s", output->success ? "sync test réussie" : "sync test échouée"); if (!output->success) (void)snprintf(output->error, sizeof(output->error), "%s", "Appareil déconnecté pendant la synchronisation."); CHECK(snprintf(directory, sizeof(directory), "%s/trainlog", sync_journal_root) > 0); CHECK(mkdir(directory, 0700) == 0 || errno == EEXIST); CHECK(snprintf(history_path, sizeof(history_path), "%s/sync_history.log", directory) > 0); history = fopen(history_path, "ab"); CHECK(history != NULL); CHECK(fprintf(history, "%s\t11/09/2026 12:%02d\t%d\tb\t%s\n", output->sync_id, run_number, output->success ? 1 : 0, output->summary) > 0); CHECK(fclose(history) == 0); return output->success ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_SYSTEM_ERROR; } static bool test_shell_body_controller_preserves_identity_and_failed_edit(void) { TrainlogDatabase *database = NULL; TrainlogAppContext app; TrainlogBodyObservationRecord records[4]; size_t count = 0U; size_t field; char id[TRAINLOG_ID_MAX + 1U]; char observed_at[TRAINLOG_TIMESTAMP_MAX + 1U]; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); (void)memset(&app, 0, sizeof(app)); app.database = database; app.layout.usable = true; app.layout.content.height = 16; trainlog_navigation_init(&app.navigation); app.navigation.current.route = TRAINLOG_ROUTE_BODY; body_controller_start_add(&app); CHECK(app.body_controller.phase == TRAINLOG_BODY_FORM); CHECK(app_shell_dispatch_body_controller(&app, '8')); CHECK(app_shell_dispatch_body_controller(&app, '0')); for (field = 0U; field < 14U; ++field) CHECK(app_shell_dispatch_body_controller(&app, TRAINLOG_KEY_ENTER)); CHECK(app.body_controller.phase == TRAINLOG_BODY_MESSAGE); CHECK(trainlog_database_list_body_observations(database, records, 4U, &count) == TRAINLOG_STATUS_OK); CHECK(count == 1U && records[0].has_body_weight && records[0].body_weight_kg == 80.0); (void)snprintf(id, sizeof(id), "%s", records[0].observation_id); (void)snprintf(observed_at, sizeof(observed_at), "%s", records[0].observed_at); (void)memset(&app.body_controller, 0, sizeof(app.body_controller)); CHECK(body_controller_start_edit(&app, id)); trainlog_form_init(&app.body_controller.form, "-"); CHECK(app_shell_dispatch_body_controller(&app, TRAINLOG_KEY_ENTER)); for (field = 1U; field < 14U; ++field) CHECK(app_shell_dispatch_body_controller(&app, TRAINLOG_KEY_ENTER)); CHECK(app.body_controller.phase == TRAINLOG_BODY_FORM); CHECK(strstr(app.body_controller.message, "au moins une mesure") != NULL); CHECK(trainlog_database_get_body_observation(database, id, &records[0]) == TRAINLOG_STATUS_OK); CHECK(records[0].has_body_weight && records[0].body_weight_kg == 80.0); trainlog_form_init(&app.body_controller.form, "81"); app.body_controller.field = 0U; CHECK(app_shell_dispatch_body_controller(&app, TRAINLOG_KEY_ENTER)); for (field = 1U; field < 14U; ++field) CHECK(app_shell_dispatch_body_controller(&app, TRAINLOG_KEY_ENTER)); CHECK(app.body_controller.phase == TRAINLOG_BODY_MESSAGE); CHECK(trainlog_database_get_body_observation(database, id, &records[0]) == TRAINLOG_STATUS_OK); CHECK(strcmp(records[0].observation_id, id) == 0); CHECK(strcmp(records[0].observed_at, observed_at) == 0); CHECK(records[0].body_weight_kg == 81.0); (void)memset(&app.body_controller, 0, sizeof(app.body_controller)); app.navigation.current.route = TRAINLOG_ROUTE_BODY; app_shell_open_route(&app, TRAINLOG_ROUTE_BODY_METRIC); CHECK(trainlog_database_list_body_observations(database, records, 4U, &count) == TRAINLOG_STATUS_OK); CHECK(count == 1U && strcmp(records[0].observation_id, id) == 0 && records[0].body_weight_kg == 81.0); trainlog_database_close(database); return true; } static bool test_shell_sync_f7_runs_bidirectional_and_refreshes_history(void) { TrainlogDatabase *database = NULL; TrainlogAppContext app; TrainlogTerminal terminal = {0}; char temporary[] = "/tmp/trainlog-tui-sync-XXXXXX"; const char *old_data_home = getenv("XDG_DATA_HOME"); char *saved_data_home = old_data_home != NULL ? strdup(old_data_home) : NULL; size_t index; size_t sync_action_index = SIZE_MAX; size_t sync_action_count = 0U; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); CHECK(mkdtemp(temporary) != NULL); CHECK(setenv("XDG_DATA_HOME", temporary, 1) == 0); (void)snprintf(sync_journal_root, sizeof(sync_journal_root), "%s", temporary); (void)memset(&app, 0, sizeof(app)); app.database = database; terminal.columns = 120; terminal.rows = 35; app.terminal = &terminal; trainlog_navigation_init(&app.navigation); trainlog_overlays_init(&app.overlays); trainlog_sync_screen_state_init(&app.sync_controller.action); app.sync_controller.probe = mock_sync_probe; app.sync_controller.run = mock_sync_run; sync_probe_calls = 0; sync_run_calls = 0; sync_running_app = &app; app_shell_open_route(&app, TRAINLOG_ROUTE_SYNC); CHECK(app_shell_layout(&app)); CHECK(sync_probe_calls == 0 && sync_run_calls == 0); app_shell_actions(&app); for (index = 0U; index < app.actions.count; ++index) { const TrainlogAction *action = &app.actions.items[index]; CHECK(strstr(action->label, "Android→PC") == NULL); CHECK(strstr(action->label, "PC→Android") == NULL); if (strcmp(action->identifier, "sync.now") == 0) { ++sync_action_count; sync_action_index = index; CHECK(strcmp(action->label, "s Synchroniser maintenant · PC↔Android") == 0); } } CHECK(sync_action_count == 1U && sync_action_index != SIZE_MAX); /* Actual user path: Synchronisation screen -> F7 -> registered action. */ app_shell_dispatch(&app, TRAINLOG_KEY_F7); CHECK(trainlog_overlays_top(&app.overlays) != NULL); app.overlays.items[app.overlays.count - 1U].selected = sync_action_index; app_shell_dispatch(&app, TRAINLOG_KEY_ENTER); CHECK(app.sync_controller.action.confirming && sync_run_calls == 0); CHECK(app_shell_dispatch_sync(&app, TRAINLOG_KEY_ESCAPE)); CHECK(!app.sync_controller.action.confirming && sync_run_calls == 0); app_shell_dispatch(&app, TRAINLOG_KEY_F7); app.overlays.items[app.overlays.count - 1U].selected = sync_action_index; app_shell_dispatch(&app, TRAINLOG_KEY_ENTER); CHECK(sync_run_calls == 0); CHECK(app_shell_dispatch_sync(&app, TRAINLOG_KEY_ENTER)); CHECK(sync_run_calls == 1); CHECK(sync_last_direction == TRAINLOG_SYNC_BIDIRECTIONAL); CHECK(sync_probe_calls == 1); CHECK(!app.sync_controller.running && app.sync_controller.has_report); CHECK(app.sync_controller.report.success); CHECK(app.sync_controller.history_count == 1U); terminal.output_used = 0U; terminal.output[0] = '\0'; app_shell_render_sync(&app); CHECK(strstr(terminal.output, "sync test réussie") != NULL); /* Repeating the action creates the next legitimate journal event. */ app_shell_dispatch(&app, 's'); CHECK(app.sync_controller.action.confirming); app_shell_dispatch(&app, TRAINLOG_KEY_ENTER); CHECK(sync_run_calls == 2); CHECK(sync_last_direction == TRAINLOG_SYNC_BIDIRECTIONAL); CHECK(!app.sync_controller.report.success); CHECK(app.sync_controller.history_count == 2U); terminal.output_used = 0U; terminal.output[0] = '\0'; app_shell_render_sync(&app); CHECK(strstr(terminal.output, "Appareil déconnecté pendant la synchronisation.") != NULL); CHECK(app_shell_dispatch_sync(&app, 'r')); CHECK(sync_probe_calls == 3 && sync_run_calls == 2); if (saved_data_home != NULL) { CHECK(setenv("XDG_DATA_HOME", saved_data_home, 1) == 0); free(saved_data_home); } else CHECK(unsetenv("XDG_DATA_HOME") == 0); { char history_path[PATH_MAX + 1U]; char directory[PATH_MAX + 1U]; CHECK(snprintf(history_path, sizeof(history_path), "%s/trainlog/sync_history.log", temporary) > 0); CHECK(snprintf(directory, sizeof(directory), "%s/trainlog", temporary) > 0); CHECK(unlink(history_path) == 0); CHECK(rmdir(directory) == 0); CHECK(rmdir(temporary) == 0); } sync_running_app = NULL; trainlog_database_close(database); return true; } static bool test_shell_sync_history_neutralizes_legacy_directions(void) { TrainlogAppContext app; TrainlogTerminal terminal = {0}; TrainlogSurface surface = {0}; char temporary[] = "/tmp/trainlog-tui-sync-legacy-XXXXXX"; char directory[PATH_MAX + 1U]; char history_path[PATH_MAX + 1U]; char runs_directory[PATH_MAX + 1U]; char detail_path[PATH_MAX + 1U]; const char *old_data_home = getenv("XDG_DATA_HOME"); char *saved_data_home = old_data_home != NULL ? strdup(old_data_home) : NULL; FILE *history; CHECK(mkdtemp(temporary) != NULL); CHECK(snprintf(directory, sizeof(directory), "%s/trainlog", temporary) > 0); CHECK(mkdir(directory, 0700) == 0); CHECK(snprintf(history_path, sizeof(history_path), "%s/sync_history.log", directory) > 0); CHECK(snprintf(runs_directory, sizeof(runs_directory), "%s/sync_runs", directory) > 0); CHECK(mkdir(runs_directory, 0700) == 0); history = fopen(history_path, "wb"); CHECK(history != NULL); CHECK(fprintf(history, "sy_aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa\t09/09/2026 10:11\t1\ta\tAndroid→PC import ancien\n" "sy_pppppppp-pppp-4ppp-8ppp-pppppppppppp\t10/09/2026 11:12\t0\tp\tPC→Android publication ancienne\n") > 0); CHECK(fclose(history) == 0); CHECK(snprintf(detail_path, sizeof(detail_path), "%s/%s.txt", runs_directory, "sy_aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa") > 0); history = fopen(detail_path, "wb"); CHECK(history != NULL); CHECK(fputs("Direction: Android→PC\nAndroid→PC import details\n", history) >= 0); CHECK(fclose(history) == 0); CHECK(snprintf(detail_path, sizeof(detail_path), "%s/%s.txt", runs_directory, "sy_pppppppp-pppp-4ppp-8ppp-pppppppppppp") > 0); history = fopen(detail_path, "wb"); CHECK(history != NULL); CHECK(fputs("Direction: PC→Android\nPC→Android publication details\n", history) >= 0); CHECK(fclose(history) == 0); CHECK(setenv("XDG_DATA_HOME", temporary, 1) == 0); (void)memset(&app, 0, sizeof(app)); terminal.columns = 120; terminal.rows = 35; surface.terminal = &terminal; app.terminal = &terminal; app.content = &surface; app.layout.content.width = 120; app.layout.content.height = 30; trainlog_navigation_init(&app.navigation); app.navigation.current.route = TRAINLOG_ROUTE_SYNC; trainlog_sync_screen_state_init(&app.sync_controller.action); sync_history_load(app.sync_controller.history, SYNC_HISTORY_CAPACITY, &app.sync_controller.history_count); CHECK(app.sync_controller.history_count == 2U); app_shell_render_sync(&app); CHECK(strstr(terminal.output, "09/09/2026 10:11") != NULL); CHECK(strstr(terminal.output, "10/09/2026 11:12") != NULL); CHECK(strstr(terminal.output, "entrée historique") != NULL); CHECK(strstr(terminal.output, "PC↔Android") != NULL); CHECK(strstr(terminal.output, "Android→PC") == NULL); CHECK(strstr(terminal.output, "PC→Android") == NULL); /* Actual keyboard path: select and open each retained legacy artifact. */ for (app.sync_controller.selected = 0U; app.sync_controller.selected < app.sync_controller.history_count; ++app.sync_controller.selected) { terminal.output_used = 0U; terminal.output[0] = '\0'; CHECK(app_shell_dispatch_sync(&app, TRAINLOG_KEY_ENTER)); CHECK(app.sync_controller.showing_detail); app_shell_render_sync(&app); CHECK(strstr(terminal.output, "Synchronisation historique PC↔Android") != NULL); CHECK(strstr(terminal.output, "Horodatage") != NULL); CHECK(strstr(terminal.output, "État") != NULL); CHECK(strstr(terminal.output, "Android→PC") == NULL); CHECK(strstr(terminal.output, "PC→Android") == NULL); app.sync_controller.showing_detail = false; } if (saved_data_home != NULL) { CHECK(setenv("XDG_DATA_HOME", saved_data_home, 1) == 0); free(saved_data_home); } else CHECK(unsetenv("XDG_DATA_HOME") == 0); CHECK(unlink(history_path) == 0); CHECK(snprintf(detail_path, sizeof(detail_path), "%s/%s.txt", runs_directory, "sy_aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa") > 0); CHECK(unlink(detail_path) == 0); CHECK(snprintf(detail_path, sizeof(detail_path), "%s/%s.txt", runs_directory, "sy_pppppppp-pppp-4ppp-8ppp-pppppppppppp") > 0); CHECK(unlink(detail_path) == 0); CHECK(rmdir(runs_directory) == 0); CHECK(rmdir(directory) == 0); CHECK(rmdir(temporary) == 0); return true; } static bool test_equipment_collector_finds_match_after_first_pages(void) { TrainlogDatabase *database = NULL; TrainlogCustomEquipment equipment; TrainlogResolvedEquipment result[2]; bool truncated = false; bool failed = false; size_t index; size_t count; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); for (index = 0U; index < 140U; ++index) { (void)memset(&equipment, 0, sizeof(equipment)); generate_custom_equipment_uuid(equipment.equipment_id); (void)snprintf(equipment.display_name, sizeof(equipment.display_name), index == 139U ? "zz needle-late" : "custom %03zu", index); (void)snprintf(equipment.equipment_type, sizeof(equipment.equipment_type), "machine"); (void)snprintf(equipment.load_semantics, sizeof(equipment.load_semantics), "external"); CHECK(trainlog_database_create_custom_equipment(database, &equipment) == TRAINLOG_STATUS_OK); } count = equipment_collect(database, "needle-late", result, 2U, &truncated, &failed); CHECK(!failed && !truncated && count == 1U); CHECK(strcmp(result[0].display_name, "zz needle-late") == 0); trainlog_database_close(database); return true; } static bool test_body_graphs_draw_inside_minimum_content_surface(void) { TrainlogAppContext app; TrainlogTerminal terminal; TrainlogSurface surface; TrainlogDashboardMonth previous; TrainlogDashboardMonth current; long current_key; (void)memset(&app, 0, sizeof(app)); (void)memset(&terminal, 0, sizeof(terminal)); terminal.rows = 20; terminal.columns = 72; surface.terminal = &terminal; app.terminal = &terminal; app.content = &surface; app.layout.content.height = 16; app.layout.content.width = 72; app.body_global_initialized = true; app.body_global_enabled[0] = true; app.body_global_series[0].symbol = GLOBAL_BODY_SYMBOLS[0]; app.body_global_series[0].role = GLOBAL_BODY_ROLES[0]; app.body_global_series[0].baseline = 80.0; app.body_global_series[0].count = 2U; CHECK(dashboard_current_month_key(¤t_key)); dashboard_month_from_key(current_key - 1L, &previous); dashboard_month_from_key(current_key, ¤t); (void)snprintf(app.body_global_series[0].points[0].observed_at, sizeof(app.body_global_series[0].points[0].observed_at), "%04d-%02d-01T08:00:00Z", previous.year, previous.month); app.body_global_series[0].points[0].value = 80.0; (void)snprintf(app.body_global_series[0].points[1].observed_at, sizeof(app.body_global_series[0].points[1].observed_at), "%04d-%02d-01T08:00:00Z", current.year, current.month); app.body_global_series[0].points[1].value = 81.0; (void)snprintf(app.body_global_dates[0], sizeof(app.body_global_dates[0]), "%s", app.body_global_series[0].points[0].observed_at); (void)snprintf(app.body_global_dates[1], sizeof(app.body_global_dates[1]), "%s", app.body_global_series[0].points[1].observed_at); app.body_global_date_count = 2U; app_shell_render_body_global(&app); CHECK(terminal.surface_draw_count > 0U); terminal.surface_draw_count = 0U; app_shell_render_body_trends(&app); CHECK(terminal.surface_draw_count > 0U); return true; } static bool test_exercise_merge_action_search_confirm_selects_target(void) { TrainlogDatabase *database = NULL; TrainlogAppContext app; char resolved[TRAINLOG_ID_MAX + 1U]; size_t action_index; CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK); CHECK(trainlog_database_insert_exercise(database, "ex_11111111-1111-4111-8111-111111111111", "Source curl", "source curl", TRAINLOG_TRACKING_REPS) == TRAINLOG_STATUS_OK); CHECK(trainlog_database_insert_exercise(database, "ex_22222222-2222-4222-8222-222222222222", "Target curl", "target curl", TRAINLOG_TRACKING_REPS) == TRAINLOG_STATUS_OK); (void)memset(&app, 0, sizeof(app)); app.database = database; trainlog_navigation_init(&app.navigation); trainlog_overlays_init(&app.overlays); app.navigation.current.route = TRAINLOG_ROUTE_EXERCISE_DETAIL; (void)snprintf(app.exercise_detail.exercise_id, sizeof(app.exercise_detail.exercise_id), "%s", "ex_11111111-1111-4111-8111-111111111111"); (void)snprintf(app.exercise_detail.name, sizeof(app.exercise_detail.name), "%s", "Source curl"); app.exercise_detail.tracking_mode = TRAINLOG_TRACKING_REPS; app.exercise_detail.recording_mode = TRAINLOG_RECORDING_SETS; app_shell_actions(&app); for (action_index = 0U; action_index < app.actions.count; ++action_index) if (strcmp(app.actions.items[action_index].identifier, "exercise.merge") == 0) break; CHECK(action_index < app.actions.count); (void)trainlog_overlays_push(&app.overlays, TRAINLOG_OVERLAY_ACTIONS, TRAINLOG_FOCUS_CONTENT, app.exercise_detail.exercise_id); app.overlays.items[0].selected = action_index; app_shell_dispatch_overlay(&app, TRAINLOG_KEY_ENTER); CHECK(trainlog_overlays_top(&app.overlays)->type == TRAINLOG_OVERLAY_EXERCISE_MERGE); CHECK(app.exercise_controller.phase == TRAINLOG_EXERCISE_MERGE_PICKER); app_shell_dispatch_overlay(&app, 't'); app_shell_dispatch_overlay(&app, 'a'); app_shell_dispatch_overlay(&app, 'r'); CHECK(exercise_merge_match_count(&app.exercise_controller) == 1U); app_shell_dispatch_overlay(&app, TRAINLOG_KEY_ENTER); CHECK(app.exercise_controller.phase == TRAINLOG_EXERCISE_MERGE_CONFIRM); app_shell_dispatch_overlay(&app, TRAINLOG_KEY_DOWN); app_shell_dispatch_overlay(&app, TRAINLOG_KEY_ENTER); CHECK(app.exercise_controller.phase == TRAINLOG_EXERCISE_MERGE_RESULT); CHECK(strcmp(app.exercise_detail.exercise_id, "ex_22222222-2222-4222-8222-222222222222") == 0); CHECK(strcmp(app.navigation.current.stable_id, "ex_22222222-2222-4222-8222-222222222222") == 0); CHECK(trainlog_database_resolve_exercise_id(database, "ex_11111111-1111-4111-8111-111111111111", resolved, sizeof(resolved)) == TRAINLOG_STATUS_OK); CHECK(strcmp(resolved, "ex_22222222-2222-4222-8222-222222222222") == 0); app_shell_dispatch_overlay(&app, TRAINLOG_KEY_ENTER); CHECK(app.overlays.count == 0U); CHECK(app.exercise_controller.phase == TRAINLOG_EXERCISE_IDLE); trainlog_database_close(database); return true; } int main(void) { if (!test_knowledge_scrolls_long_lists_at_minimum_terminal() || !test_shell_session_navigation_preserves_run_draft() || !test_shell_generator_keep_and_accept_has_no_actuals() || !test_shell_session_forms_keep_plan_and_actuals_separate() || !test_shell_continuous_supplemental_fields_have_no_fake_set() || !test_shell_generator_dose_edit_drops_stale_load_provenance() || !test_shell_inline_equipment_creation_resumes_occurrence() || !test_shell_equipment_catalog_create_preserves_invalid_form() || !test_shell_inline_exercise_creation_and_stable_edit() || !test_shell_navigation_focus_consumes_input() || !test_shell_actions_overlay_executes_registered_search_action() || !test_shell_compact_navigation_focus_contract() || !test_shell_detail_back_restores_stable_list_selection() || !test_shell_body_controller_preserves_identity_and_failed_edit() || !test_shell_sync_f7_runs_bidirectional_and_refreshes_history() || !test_shell_sync_history_neutralizes_legacy_directions() || !test_equipment_collector_finds_match_after_first_pages() || !test_exercise_merge_action_search_confirm_selects_target() || !test_body_graphs_draw_inside_minimum_content_surface()) return 1; (void)printf("PASS tui_workflows\n"); return 0; }