"""Tests for UplinkEncoder — AGC INLINK command formatting. Verifies that DSKY command sequences (VERB, NOUN, DATA, PROCEED) are correctly encoded as (channel, value) pairs for delivery to AGC channel 045. No GNU Radio required. """ import pytest from apollo.constants import AGC_CH_INLINK from apollo.protocol import form_io_packet, parse_io_packet from apollo.uplink_encoder import ( KEYCODE_DIGITS, KEYCODE_ENTER, KEYCODE_MINUS, KEYCODE_NOUN, KEYCODE_PLUS, KEYCODE_VERB, UplinkEncoder, ) @pytest.fixture def encoder(): return UplinkEncoder() class TestKeycodeEncoding: """Basic keycode → (channel, value) encoding.""" def test_channel_is_inlink(self, encoder): """All encoded pairs use the INLINK channel by default.""" ch, _ = encoder.encode_keycode(KEYCODE_VERB) assert ch == AGC_CH_INLINK def test_custom_channel(self): """Custom channel overrides the default.""" enc = UplinkEncoder(channel=99) ch, _ = enc.encode_keycode(KEYCODE_VERB) assert ch == 99 def test_keycode_in_upper_bits(self, encoder): """Keycode occupies bits 14-10 of the 15-bit value.""" _, value = encoder.encode_keycode(KEYCODE_VERB) extracted = (value >> 10) & 0x1F assert extracted == KEYCODE_VERB def test_lower_bits_zero(self, encoder): """Bits 9-0 are zero for a simple keypress.""" _, value = encoder.encode_keycode(KEYCODE_NOUN) assert (value & 0x3FF) == 0 def test_value_is_15_bit(self, encoder): """Encoded value fits in 15 bits.""" _, value = encoder.encode_keycode(0x1F) # max 5-bit keycode assert 0 <= value <= 0x7FFF class TestDigitEncoding: """Digit (0-9) keycode encoding.""" def test_all_digits_encode(self, encoder): """Each digit 0-9 produces a valid (channel, value) pair.""" for d in range(10): ch, val = encoder.encode_digit(d) assert ch == AGC_CH_INLINK assert 0 <= val <= 0x7FFF def test_digit_keycodes_unique(self, encoder): """Each digit maps to a distinct keycode/value.""" values = set() for d in range(10): _, val = encoder.encode_digit(d) values.add(val) assert len(values) == 10 def test_invalid_digit(self, encoder): with pytest.raises(ValueError): encoder.encode_digit(10) with pytest.raises(ValueError): encoder.encode_digit(-1) class TestVerbEncoding: """VERB command encoding (VERB key + 2 digit keys).""" def test_verb_sequence_length(self, encoder): """V37 produces 3 pairs: VERB + digit + digit.""" pairs = encoder.encode_verb(37) assert len(pairs) == 3 def test_verb_key_first(self, encoder): """First pair in the sequence is the VERB keycode.""" pairs = encoder.encode_verb(37) _, value = pairs[0] assert (value >> 10) & 0x1F == KEYCODE_VERB def test_verb_digits_correct(self, encoder): """Digits encode the verb number.""" pairs = encoder.encode_verb(37) _, d1_val = pairs[1] _, d2_val = pairs[2] # Digit 3 assert (d1_val >> 10) & 0x1F == KEYCODE_DIGITS[3] # Digit 7 assert (d2_val >> 10) & 0x1F == KEYCODE_DIGITS[7] def test_verb_zero_padded(self, encoder): """V06 encodes as VERB, 0, 6.""" pairs = encoder.encode_verb(6) _, d1_val = pairs[1] _, d2_val = pairs[2] assert (d1_val >> 10) & 0x1F == KEYCODE_DIGITS[0] assert (d2_val >> 10) & 0x1F == KEYCODE_DIGITS[6] def test_verb_boundary_values(self, encoder): pairs_0 = encoder.encode_verb(0) assert len(pairs_0) == 3 pairs_99 = encoder.encode_verb(99) assert len(pairs_99) == 3 def test_verb_out_of_range(self, encoder): with pytest.raises(ValueError): encoder.encode_verb(100) with pytest.raises(ValueError): encoder.encode_verb(-1) class TestNounEncoding: """NOUN command encoding.""" def test_noun_sequence_length(self, encoder): pairs = encoder.encode_noun(1) assert len(pairs) == 3 def test_noun_key_first(self, encoder): pairs = encoder.encode_noun(1) _, value = pairs[0] assert (value >> 10) & 0x1F == KEYCODE_NOUN def test_noun_digits(self, encoder): pairs = encoder.encode_noun(47) _, d1_val = pairs[1] _, d2_val = pairs[2] assert (d1_val >> 10) & 0x1F == KEYCODE_DIGITS[4] assert (d2_val >> 10) & 0x1F == KEYCODE_DIGITS[7] def test_noun_out_of_range(self, encoder): with pytest.raises(ValueError): encoder.encode_noun(100) class TestDataEncoding: """Signed/unsigned data entry encoding.""" def test_positive_data_starts_with_plus(self, encoder): pairs = encoder.encode_data(12345) _, sign_val = pairs[0] assert (sign_val >> 10) & 0x1F == KEYCODE_PLUS def test_negative_data_starts_with_minus(self, encoder): pairs = encoder.encode_data(-12345) _, sign_val = pairs[0] assert (sign_val >> 10) & 0x1F == KEYCODE_MINUS def test_signed_data_length(self, encoder): """Signed data: sign + 5 digits = 6 pairs.""" pairs = encoder.encode_data(12345) assert len(pairs) == 6 def test_unsigned_data_length(self, encoder): """Unsigned data: 5 digits only = 5 pairs.""" pairs = encoder.encode_data(12345, signed=False) assert len(pairs) == 5 def test_data_digits(self, encoder): """Verify digit sequence for +00042.""" pairs = encoder.encode_data(42) # Skip sign (index 0), check digits 0, 0, 0, 4, 2 expected_digits = [0, 0, 0, 4, 2] for i, expected_d in enumerate(expected_digits): _, val = pairs[i + 1] actual_keycode = (val >> 10) & 0x1F assert actual_keycode == KEYCODE_DIGITS[expected_d], ( f"digit {i}: expected {expected_d} (keycode {KEYCODE_DIGITS[expected_d]}), " f"got keycode {actual_keycode}" ) def test_data_zero(self, encoder): """Zero encodes as +00000.""" pairs = encoder.encode_data(0) assert len(pairs) == 6 # sign + 5 digits def test_data_max_value(self, encoder): pairs = encoder.encode_data(99999) assert len(pairs) == 6 def test_data_out_of_range(self, encoder): with pytest.raises(ValueError): encoder.encode_data(100000) with pytest.raises(ValueError): encoder.encode_data(-100000) class TestProceedEncoding: """PROCEED/ENTER key encoding.""" def test_proceed_single_pair(self, encoder): pairs = encoder.encode_proceed() assert len(pairs) == 1 def test_proceed_is_enter_key(self, encoder): pairs = encoder.encode_proceed() _, value = pairs[0] assert (value >> 10) & 0x1F == KEYCODE_ENTER class TestCommandDispatch: """High-level encode_command() dispatch.""" def test_verb_dispatch(self, encoder): pairs = encoder.encode_command("VERB", 37) assert len(pairs) == 3 def test_noun_dispatch(self, encoder): pairs = encoder.encode_command("NOUN", 1) assert len(pairs) == 3 def test_data_dispatch(self, encoder): pairs = encoder.encode_command("DATA", 42) assert len(pairs) == 6 def test_proceed_dispatch(self, encoder): pairs = encoder.encode_command("PROCEED") assert len(pairs) == 1 def test_case_insensitive(self, encoder): """Command type matching is case-insensitive.""" p1 = encoder.encode_command("verb", 37) p2 = encoder.encode_command("Verb", 37) p3 = encoder.encode_command("VERB", 37) assert p1 == p2 == p3 def test_unknown_command(self, encoder): with pytest.raises(ValueError, match="unknown command type"): encoder.encode_command("ABORT", 0) def test_missing_data_for_verb(self, encoder): with pytest.raises(ValueError, match="VERB requires"): encoder.encode_command("VERB") def test_missing_data_for_noun(self, encoder): with pytest.raises(ValueError, match="NOUN requires"): encoder.encode_command("NOUN") def test_missing_data_for_data(self, encoder): with pytest.raises(ValueError, match="DATA requires"): encoder.encode_command("DATA") class TestVerbNounConvenience: """encode_verb_noun() full command sequence.""" def test_full_sequence_length(self, encoder): """V37N01 ENTER = VERB+3+7 + NOUN+0+1 + ENTER = 7 pairs.""" pairs = encoder.encode_verb_noun(37, 1) assert len(pairs) == 7 def test_sequence_structure(self, encoder): """Verify key ordering: VERB, d, d, NOUN, d, d, ENTER.""" pairs = encoder.encode_verb_noun(16, 65) keycodes = [(v >> 10) & 0x1F for _, v in pairs] assert keycodes[0] == KEYCODE_VERB assert keycodes[1] == KEYCODE_DIGITS[1] assert keycodes[2] == KEYCODE_DIGITS[6] assert keycodes[3] == KEYCODE_NOUN assert keycodes[4] == KEYCODE_DIGITS[6] assert keycodes[5] == KEYCODE_DIGITS[5] assert keycodes[6] == KEYCODE_ENTER def test_all_pairs_use_inlink(self, encoder): pairs = encoder.encode_verb_noun(37, 1) for ch, _ in pairs: assert ch == AGC_CH_INLINK class TestPacketCompatibility: """Verify encoded values survive the AGC packet protocol roundtrip.""" def test_keycode_survives_packet_roundtrip(self, encoder): """Each (channel, value) pair can be packed/unpacked via form_io_packet.""" pairs = encoder.encode_verb_noun(37, 1) for channel, value in pairs: packet = form_io_packet(channel, value) got_ch, got_val, _ = parse_io_packet(packet) assert got_ch == channel assert got_val == value def test_data_value_survives_packet_roundtrip(self, encoder): """Data encoding survives the 15-bit packet protocol.""" pairs = encoder.encode_data(54321) for channel, value in pairs: packet = form_io_packet(channel, value) got_ch, got_val, _ = parse_io_packet(packet) assert got_ch == channel assert got_val == value