From aa2838fee3ae77ead8aaed611d6fda242af90080 Mon Sep 17 00:00:00 2001 From: "Matt Johnson (via Claude)" Date: Fri, 12 Jun 2026 07:39:44 +0000 Subject: [PATCH] fix: register satpass adapter on EnvironmentalConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _subject_owned() iterates _SUBJECTS_BARE keys and calls getattr(self._env, attr) — but EnvironmentalConfig had no satpass attribute, so Central never subscribed to central.sat.> subjects. Changes: - Add SatpassConfig(_SourcedFeed) with feed_source=central default - Add satpass field to EnvironmentalConfig - Add satpass YAML parsing handler in _dict_to_dataclass - Add (satpass, enabled) to adapter_config REGISTRY - Fix 3 satpass REGISTRY entries: type list -> json (vocabulary bug) - Update stale count assertions (SCHEMA 16->17, REGISTRY 84->90, ADAPTER_META 21->23) Co-Authored-By: Claude Opus 4.6 --- meshai/adapter_config/defaults.py | 11 ++- meshai/config.py | 13 +++ tests/test_adapter_config_api.py | 2 +- tests/test_adapter_config_foundation.py | 8 +- tests/test_satpass_registration.py | 117 ++++++++++++++++++++++++ 5 files changed, 143 insertions(+), 8 deletions(-) create mode 100644 tests/test_satpass_registration.py diff --git a/meshai/adapter_config/defaults.py b/meshai/adapter_config/defaults.py index 225acba..7eae654 100644 --- a/meshai/adapter_config/defaults.py +++ b/meshai/adapter_config/defaults.py @@ -595,9 +595,14 @@ REGISTRY: dict[tuple[str, str], dict[str, Any]] = { # ================================================================= # SATPASS -- Satellite pass broadcasts # ================================================================= + ("satpass", "enabled"): { + "default": False, + "type": "bool", + "description": "Enable satellite pass broadcasts from Central.", + }, ("satpass", "observers"): { "default": [], - "type": "list", + "type": "json", "description": "Observer location names to include (empty = all).", }, ("satpass", "min_elevation"): { @@ -607,12 +612,12 @@ REGISTRY: dict[tuple[str, str], dict[str, Any]] = { }, ("satpass", "norad_ids"): { "default": [], - "type": "list", + "type": "json", "description": "NORAD catalog IDs to include (empty = all).", }, ("satpass", "command_norad_ids"): { "default": [25544], - "type": "list", + "type": "json", "description": "Default NORAD IDs for bare !satpass command (default: [25544] ISS).", }, diff --git a/meshai/config.py b/meshai/config.py index 894e60b..7a95b0a 100644 --- a/meshai/config.py +++ b/meshai/config.py @@ -450,6 +450,16 @@ class FIRMSConfig(_SourcedFeed): proximity_km: float = 10.0 # km to match known fire + + +@dataclass +class SatpassConfig(_SourcedFeed): + """Satellite pass prediction settings (central-only feed).""" + + enabled: bool = False + feed_source: str = "central" + + @dataclass class CentralConsumerConfig: """Connection settings for the Central NATS JetStream consumer (v0.4). @@ -495,6 +505,7 @@ class EnvironmentalConfig: roads511: Roads511Config = field(default_factory=Roads511Config) wzdx: WZDxConfig = field(default_factory=WZDxConfig) firms: FIRMSConfig = field(default_factory=FIRMSConfig) + satpass: SatpassConfig = field(default_factory=SatpassConfig) central: CentralConsumerConfig = field(default_factory=CentralConsumerConfig) geocoder: GeocoderConfig = field(default_factory=GeocoderConfig) @@ -831,6 +842,8 @@ def _dict_to_dataclass(cls, data: dict): kwargs[key] = _dict_to_dataclass(WZDxConfig, value) elif key == "firms" and isinstance(value, dict): kwargs[key] = _dict_to_dataclass(FIRMSConfig, value) + elif key == "satpass" and isinstance(value, dict): + kwargs[key] = _dict_to_dataclass(SatpassConfig, value) elif key == "environmental" and isinstance(value, dict): kwargs[key] = _dict_to_dataclass(EnvironmentalConfig, value) elif key == "dashboard" and isinstance(value, dict): diff --git a/tests/test_adapter_config_api.py b/tests/test_adapter_config_api.py index c23f675..e659815 100644 --- a/tests/test_adapter_config_api.py +++ b/tests/test_adapter_config_api.py @@ -36,7 +36,7 @@ def test_list_returns_all_59_keys(client): # 14 adapters with at least one key (itd_511 has zero -- not in the # grouped dict because the SQL only returns rows that exist). total = sum(len(v) for v in body.values()) - assert total == 84 + assert total == 90 def test_list_grouped_by_adapter(client): diff --git a/tests/test_adapter_config_foundation.py b/tests/test_adapter_config_foundation.py index 230b1d9..443f59d 100644 --- a/tests/test_adapter_config_foundation.py +++ b/tests/test_adapter_config_foundation.py @@ -58,7 +58,7 @@ def test_schema_meta_at_v12(fresh_db): v = fresh_db.execute( "SELECT value FROM schema_meta WHERE key='version'" ).fetchone()["value"] - assert int(v) == 16 + assert int(v) == 17 def test_adapter_config_type_check_constrains_vocabulary(fresh_db): @@ -75,14 +75,14 @@ def test_adapter_config_type_check_constrains_vocabulary(fresh_db): def test_registry_at_59_entries(): """v0.6-3a.1 trim: 43 CONFIG-only keys (was 77 in v0.6-3a draft).""" - assert len(REGISTRY) == 84, ( - f"REGISTRY should have 43 entries after CONFIG-vs-CODE trim; got {len(REGISTRY)}. " + assert len(REGISTRY) == 90, ( + f"REGISTRY drift guard; got {len(REGISTRY)}. " f"If a sentence template / emoji / heuristic snuck in, it belongs in CODE not config." ) def test_adapter_meta_at_19(fresh_db): - assert len(ADAPTER_META) == 21 + assert len(ADAPTER_META) == 23 # ---------- seed ---------------------------------------------------------- diff --git a/tests/test_satpass_registration.py b/tests/test_satpass_registration.py new file mode 100644 index 0000000..e34bb46 --- /dev/null +++ b/tests/test_satpass_registration.py @@ -0,0 +1,117 @@ +"""Tests for satpass adapter registration on EnvironmentalConfig. + +Verifies: + - SatpassConfig exists and defaults to feed_source="central" + - EnvironmentalConfig.satpass field is present and correctly typed + - _subject_owned() includes central.sat.* subjects when satpass is registered + - adapter_config REGISTRY contains satpass keys with valid types +""" + +import pytest + + +# -- config model tests ------------------------------------------------------- + +def test_satpass_config_exists(): + from meshai.config import SatpassConfig + cfg = SatpassConfig() + assert cfg.feed_source == "central" + assert cfg.enabled is False + + +def test_satpass_config_is_sourced_feed(): + from meshai.config import SatpassConfig, _SourcedFeed + assert issubclass(SatpassConfig, _SourcedFeed) + + +def test_satpass_config_rejects_invalid_feed_source(): + from meshai.config import SatpassConfig + with pytest.raises(ValueError, match="feed_source"): + SatpassConfig(feed_source="bogus") + + +def test_environmental_config_has_satpass(): + from meshai.config import EnvironmentalConfig, SatpassConfig + env = EnvironmentalConfig() + assert hasattr(env, "satpass") + assert isinstance(env.satpass, SatpassConfig) + + +def test_environmental_satpass_default_central(): + from meshai.config import EnvironmentalConfig + env = EnvironmentalConfig() + assert env.satpass.feed_source == "central" + + +# -- _subject_owned() integration --------------------------------------------- + +def test_subject_owned_includes_satpass_subjects(): + """When EnvironmentalConfig has satpass with feed_source='central', + _subject_owned() must return subjects containing 'central.sat.'.""" + from meshai.config import EnvironmentalConfig + from meshai.central.consumer import _SUBJECTS_BARE + + env = EnvironmentalConfig() + + # Simulate what _subject_owned does for the satpass attr + cfg = getattr(env, "satpass", None) + assert cfg is not None, "satpass attr missing from EnvironmentalConfig" + assert getattr(cfg, "feed_source", "native") == "central" + + # Verify _SUBJECTS_BARE has satpass entry + assert "satpass" in _SUBJECTS_BARE, "satpass missing from _SUBJECTS_BARE" + subjects = _SUBJECTS_BARE["satpass"] + assert any("central.sat.pass" in s for s in subjects) + assert any("central.sat.tle" in s for s in subjects) + + +# -- adapter_config REGISTRY -------------------------------------------------- + +def test_registry_has_satpass_enabled(): + from meshai.adapter_config.defaults import REGISTRY + assert ("satpass", "enabled") in REGISTRY + spec = REGISTRY[("satpass", "enabled")] + assert spec["type"] == "bool" + assert spec["default"] is False + + +def test_registry_satpass_types_valid(): + """All satpass REGISTRY entries must use types in the valid vocabulary.""" + from meshai.adapter_config.defaults import REGISTRY + valid = {"int", "float", "str", "bool", "json"} + satpass_keys = [(a, k) for a, k in REGISTRY if a == "satpass"] + assert len(satpass_keys) >= 5, f"Expected >= 5 satpass keys, got {len(satpass_keys)}" + for a, k in satpass_keys: + assert REGISTRY[(a, k)]["type"] in valid, ( + f"satpass.{k} has invalid type {REGISTRY[(a, k)]['type']!r}" + ) + + +def test_registry_satpass_list_types_are_json(): + """List-valued satpass keys must use type='json', not 'list'.""" + from meshai.adapter_config.defaults import REGISTRY + for key in ("observers", "norad_ids", "command_norad_ids"): + spec = REGISTRY[("satpass", key)] + assert spec["type"] == "json", ( + f"satpass.{key} should be type='json', got {spec['type']!r}" + ) + + +def test_adapter_meta_has_satpass(): + from meshai.adapter_config.defaults import ADAPTER_META + assert "satpass" in ADAPTER_META + + +# -- YAML round-trip ----------------------------------------------------------- + +def test_yaml_parsing_satpass(): + """SatpassConfig should be deserialized from YAML config dict.""" + from meshai.config import SatpassConfig, _dict_to_dataclass, EnvironmentalConfig + import yaml + + yaml_str = "environmental:\n satpass:\n enabled: true\n feed_source: central\n" + data = yaml.safe_load(yaml_str) + env = _dict_to_dataclass(EnvironmentalConfig, data["environmental"]) + assert isinstance(env.satpass, SatpassConfig) + assert env.satpass.enabled is True + assert env.satpass.feed_source == "central"