File size: 4,613 Bytes
2857cf3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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}"