From f94cf209531fb1285ada2d67ee6c1a02d5ebe58d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 23:36:18 +0000 Subject: [PATCH] fix(satpass): coerce norad_ids to int set at comparison site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GUI saves norad_ids as JSON strings (["25544"]), wire delivers norad_id as int. Membership test `25544 in ["25544"]` was False — opt-in list silently matched nothing. Build allow_set as {int(x) for x in norad_ids_raw if str(x).isdigit()}, accept both string and int shapes forever. Garbage entries silently skipped. satpass_cmd already coerces via [int(x) for x in cfg_ids]. Co-Authored-By: Claude Opus 4.6 --- meshai/central/satpass_handler.py | 9 ++-- tests/test_satpass_broadcast_safety.py | 74 ++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/meshai/central/satpass_handler.py b/meshai/central/satpass_handler.py index 7df7a7a..011b339 100644 --- a/meshai/central/satpass_handler.py +++ b/meshai/central/satpass_handler.py @@ -277,13 +277,16 @@ def handle_satpass(envelope: dict, subject: str, return None # OPT-IN NORAD ID filter: empty list = broadcast NOTHING - norad_ids = getattr(cfg, "norad_ids", []) or [] - if not norad_ids: + norad_ids_raw = getattr(cfg, "norad_ids", []) or [] + if not norad_ids_raw: if not getattr(handle_satpass, "_no_norad_ids_logged", False): logger.info("satpass: no norad_ids configured; pass broadcasts disabled") handle_satpass._no_norad_ids_logged = True return None - if norad_id not in norad_ids: + # Coerce to int set — GUI may save as strings (["25544"]), wire + # delivers int. Accept both shapes forever. + allow_set = {int(x) for x in norad_ids_raw if str(x).strip().isdigit()} + if norad_id not in allow_set: logger.debug("satpass_handler: norad_id %d not in configured list", norad_id) return None diff --git a/tests/test_satpass_broadcast_safety.py b/tests/test_satpass_broadcast_safety.py index 39268bd..f3f9b68 100644 --- a/tests/test_satpass_broadcast_safety.py +++ b/tests/test_satpass_broadcast_safety.py @@ -451,3 +451,77 @@ class TestRegistryKeys: spec = REGISTRY[("satpass", "norad_ids")] assert "broadcast nothing" in spec["description"].lower() or \ "opt-in" in spec["description"].lower() + + +class TestNoradIdTypeCoercion: + """norad_ids may arrive as strings from the GUI or ints from code. + The handler must accept both shapes forever.""" + + def test_string_norad_ids_matches_int_wire(self): + """norad_ids=["25544"] must match wire norad_id 25544 (int).""" + from meshai.central.satpass_handler import handle_satpass + _clear_handler_flags() + _enable_satpass_db(norad_ids=["25544"], dry_run=False) + + env = _envelope(norad_id=25544, max_el=80.0) + result = handle_satpass(env, "test.subject", data={}) + assert result is not None, "string norad_id should match int wire" + + def test_mixed_int_and_string_norad_ids(self): + """norad_ids=[25544, "22825"] must match both NORAD IDs.""" + from meshai.central.satpass_handler import handle_satpass + _clear_handler_flags() + _enable_satpass_db(norad_ids=[25544, "22825"], dry_run=False) + + # int in list, int on wire + env_iss = _envelope(norad_id=25544, max_el=65.0) + result_iss = handle_satpass(env_iss, "test.subject", data={}) + assert result_iss is not None, "int norad_id in mixed list should match" + + # string in list, int on wire + env_noaa = _envelope(norad_id=22825, sat_name="NOAA 15", max_el=65.0, + aos="2026-06-12T05:32:00Z", los="2026-06-12T05:38:00Z") + result_noaa = handle_satpass(env_noaa, "test.subject", data={}) + assert result_noaa is not None, "string norad_id in mixed list should match int wire" + + def test_garbage_entries_skipped_without_crash(self): + """Non-numeric entries in norad_ids must be silently skipped.""" + from meshai.central.satpass_handler import handle_satpass + _clear_handler_flags() + _enable_satpass_db(norad_ids=["25544", "not_a_number", "", None, "abc123"], + dry_run=False) + + env = _envelope(norad_id=25544, max_el=80.0) + # Must not raise, and the valid entry should still match + result = handle_satpass(env, "test.subject", data={}) + assert result is not None, "valid entry should match despite garbage siblings" + + def test_all_garbage_norad_ids_matches_nothing(self): + """If every entry is garbage, allow_set is empty and nothing matches.""" + from meshai.central.satpass_handler import handle_satpass + _clear_handler_flags() + _enable_satpass_db(norad_ids=["abc", "", "xyz"], dry_run=False) + + env = _envelope(norad_id=25544, max_el=80.0) + result = handle_satpass(env, "test.subject", data={}) + assert result is None, "all-garbage norad_ids should match nothing" + + def test_pure_int_norad_ids_still_works(self): + """norad_ids=[25544] (pure int) must continue to work.""" + from meshai.central.satpass_handler import handle_satpass + _clear_handler_flags() + _enable_satpass_db(norad_ids=[25544], dry_run=False) + + env = _envelope(norad_id=25544, max_el=80.0) + result = handle_satpass(env, "test.subject", data={}) + assert result is not None, "pure int norad_id should still match" + + def test_string_norad_id_rejects_non_matching(self): + """norad_ids=["25544"] must NOT match wire norad_id 99999.""" + from meshai.central.satpass_handler import handle_satpass + _clear_handler_flags() + _enable_satpass_db(norad_ids=["25544"], dry_run=False) + + env = _envelope(norad_id=99999, max_el=80.0) + result = handle_satpass(env, "test.subject", data={}) + assert result is None, "non-matching norad_id should be rejected"