fix: register satpass adapter on EnvironmentalConfig

_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 <noreply@anthropic.com>
This commit is contained in:
Matt Johnson (via Claude) 2026-06-12 07:39:44 +00:00
commit aa2838fee3
5 changed files with 143 additions and 8 deletions

View file

@ -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).",
},

View file

@ -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):

View file

@ -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):

View file

@ -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 ----------------------------------------------------------

View file

@ -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"