from __future__ import annotations import pytest import kneiff.cli.picker as cli_picker import kneiff.cli.style as cli_style from kneiff.training.lora.run_state import ( TRAINING_STATUS_COMPLETE, TRAINING_STATUS_FAILED, TRAINING_STATUS_INCOMPLETE, TRAINING_STATUS_NOT_STARTED, TRAINING_STATUS_RUNNING, ) @pytest.mark.parametrize( ("key", "action"), [ ("\x1b[A", "up"), ("\x00H", "up"), ("k", "up"), ("\x1b[B", "down"), ("\x00P", "down"), ("j", "down"), (" ", "toggle"), ("a", "all"), ("\r", "select"), ("\n", "select"), ("\x1b", "cancel"), ("\x03", "cancel"), ("q", "cancel"), ("x", "ignore"), ], ) def test_picker_action_from_key(key: str, action: str) -> None: """Normalize supported terminal keypresses.""" assert cli_picker.picker_action_from_key(key) == action def test_move_picker_index_wraps_and_clamps() -> None: """Move through picker rows without allowing an invalid active index.""" assert cli_picker.move_picker_index(0, "up", total=3) == 2 assert cli_picker.move_picker_index(2, "down", total=3) == 0 assert cli_picker.move_picker_index(99, "down", total=3) == 0 assert cli_picker.move_picker_index(-5, "ignore", total=3) == 0 with pytest.raises(ValueError, match="at least one selectable item"): cli_picker.move_picker_index(0, "down", total=0) def test_toggle_picker_indices_toggles_one_and_all() -> None: """Track checked rows for multi-select menus.""" selected = cli_picker.toggle_picker_indices( frozenset(), "toggle", active_index=1, total=3, ) assert selected == frozenset({1}) selected = cli_picker.toggle_picker_indices( selected, "toggle", active_index=1, total=3, ) assert selected == frozenset() selected = cli_picker.toggle_picker_indices( frozenset({1}), "all", active_index=1, total=3, ) assert selected == frozenset({0, 1, 2}) selected = cli_picker.toggle_picker_indices( selected, "all", active_index=1, total=3, ) assert selected == frozenset() with pytest.raises(ValueError, match="at least one selectable item"): cli_picker.toggle_picker_indices(frozenset(), "toggle", active_index=0, total=0) def test_picker_section_header_and_footer_use_shared_styles() -> None: """Render shared menu chrome with the common picker vocabulary.""" header = cli_style.picker_section_header("-- Resume Runs --") move_hint = cli_style.picker_move_hint_text() single_footer = cli_style.picker_single_footer_text() footer = cli_style.picker_footer_text() assert header.plain == " -- Resume Runs --" assert str(header.style) == "bold white" assert move_hint.plain == "Use Up/Down or j/k to move." assert str(move_hint.style) == "dim" assert single_footer.plain == " [enter] select [q]uit" assert any("red" in str(span.style) for span in single_footer.spans) assert footer.plain == " [enter] select [space] toggle [a] all [q]uit" assert any("red" in str(span.style) for span in footer.spans) def test_picker_option_footer_and_feedback_use_shared_styles() -> None: """Render arbitrary key choices and acknowledgements with shared chrome.""" footer = cli_style.picker_option_footer_text( (("s", "tart"), ("j", "son"), ("d", "iff"), ("q", "uit")) ) feedback = cli_style.picker_feedback_text("Selected action: ", "start") assert footer.plain == " [s]tart [j]son [d]iff [q]uit" assert any("cyan" in str(span.style) for span in footer.spans) assert any("red" in str(span.style) for span in footer.spans) assert feedback.plain == "Selected action: start" assert str(feedback.style) == "dim" assert any("cyan" in str(span.style) for span in feedback.spans) @pytest.mark.parametrize( ("status", "style"), [ (TRAINING_STATUS_NOT_STARTED, "cyan"), (TRAINING_STATUS_INCOMPLETE, "yellow"), (TRAINING_STATUS_FAILED, "red"), (TRAINING_STATUS_COMPLETE, "green"), (TRAINING_STATUS_RUNNING, "magenta"), ("custom", "magenta"), ], ) def test_training_status_style_uses_semantic_lifecycle_colors( status: str, style: str, ) -> None: """Keep training status colors consistent across CLI pickers.""" assert cli_style.training_status_style(status) == style assert cli_style.training_status_style(status, selected=True) == f"bold {style}"