mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 09:21:33 +00:00
Central is retired -- its NATS broker (nats://central.echo6.mesh:4222) no longer exists -- but SatpassConfig.feed_source still defaulted to "central", overriding the _SourcedFeed mixin default of "native". A fresh install that enabled satpass got a silently dead adapter. The dashboard reinforced it: Environment.tsx seeded feed_source 'central' and labelled the adapter "via Central". Also corrects comments that documented the opposite of the code: the adapter_config keys usgs_quake.global_mag_floor / regional_mag_floor were labelled "CENTRAL-PATH ONLY", but notifications/gating/quake.py reads them unconditionally in the native path. Following those comments would have led someone to delete live config keys. - config.py: SatpassConfig.feed_source "central" -> "native" + docstring - adapter_config/defaults.py: correct the two "CENTRAL-PATH ONLY" comments - Environment.tsx: seed 'native'; reword the satpass subtitle - tests: assert the native default; pin central explicitly where a test exercises the central path or the feed_source flip Suite: 2337 passed, 6 pre-existing failures (stale SCHEMA_VERSION x3, expired TLE fixtures x2, one order-dependent), no new failures. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
182 lines
7.1 KiB
Python
182 lines
7.1 KiB
Python
"""Config hot-reload tests for EnvironmentalStore.apply_config().
|
|
|
|
A config PUT to the "environmental" (or "generic_sources") section should
|
|
take effect LIVE -- only the adapter(s) whose own config actually changed are
|
|
rebuilt in place, on the SAME running store, with no container restart:
|
|
|
|
* an unchanged adapter's object identity is preserved (untouched);
|
|
* a changed adapter (still feed_source=="native") is rebuilt in place and
|
|
the NEW instance carries the new config values;
|
|
* store-level dedup/seen state (self._seen, self._seeded, ...) is keyed by
|
|
event source, not by adapter object, and survives a swap unchanged;
|
|
* flipping feed_source to/from "central" is the one case that still needs
|
|
a restart -- it must NOT hot-swap;
|
|
* rebuilding "nifc" cascades to also rebuild "firms" (it holds a hard
|
|
reference to the nifc adapter instance).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
|
|
from meshai.config import EnvironmentalConfig
|
|
from meshai.env.store import EnvironmentalStore
|
|
|
|
|
|
def _cfg(**overrides) -> EnvironmentalConfig:
|
|
"""A minimal EnvironmentalConfig with usgs_quake enabled+native (has a
|
|
plain feed_url field, no network I/O at construction) so tests have a
|
|
real adapter to diff/swap, plus whatever overrides the test needs.
|
|
"""
|
|
cfg = EnvironmentalConfig()
|
|
cfg.usgs_quake = dataclasses.replace(
|
|
cfg.usgs_quake, enabled=True, feed_source="native",
|
|
feed_url="https://example.invalid/quakes-a.geojson",
|
|
)
|
|
for key, value in overrides.items():
|
|
setattr(cfg, key, value)
|
|
return cfg
|
|
|
|
|
|
def test_unchanged_adapter_config_is_a_noop():
|
|
store = EnvironmentalStore(_cfg())
|
|
nws_before = store._adapters["nws"]
|
|
usgs_quake_before = store._adapters["usgs_quake"]
|
|
|
|
same_cfg = _cfg() # byte-identical field values, fresh dataclass instances
|
|
result = store.apply_config(same_cfg)
|
|
|
|
assert result["nws"] == "unchanged"
|
|
assert result["usgs_quake"] == "unchanged"
|
|
assert store._adapters["nws"] is nws_before
|
|
assert store._adapters["usgs_quake"] is usgs_quake_before
|
|
|
|
|
|
def test_changed_feed_url_swaps_adapter_and_preserves_dedup_state():
|
|
store = EnvironmentalStore(_cfg())
|
|
nws_before = store._adapters["nws"]
|
|
usgs_quake_before = store._adapters["usgs_quake"]
|
|
assert usgs_quake_before._feed_url == "https://example.invalid/quakes-a.geojson"
|
|
|
|
# Store-level dedup state, keyed by event SOURCE -- must survive a swap.
|
|
store._seen["usgs_quake"] = {"usgs_quake\x1eeid:us1000aaaa"}
|
|
store._seeded.add("usgs_quake")
|
|
|
|
new_cfg = _cfg()
|
|
new_cfg.usgs_quake = dataclasses.replace(
|
|
new_cfg.usgs_quake, feed_url="https://example.invalid/quakes-b.geojson")
|
|
|
|
result = store.apply_config(new_cfg)
|
|
|
|
assert result["usgs_quake"] == "reloaded"
|
|
assert result["nws"] == "unchanged"
|
|
|
|
usgs_quake_after = store._adapters["usgs_quake"]
|
|
assert usgs_quake_after is not usgs_quake_before, "changed adapter must be a NEW object"
|
|
assert usgs_quake_after._feed_url == "https://example.invalid/quakes-b.geojson", \
|
|
"the new instance must carry the new config value"
|
|
|
|
# Sibling adapter untouched (identity preserved).
|
|
assert store._adapters["nws"] is nws_before
|
|
|
|
# Store-level dedup/seen state survives the swap unchanged.
|
|
assert store._seen["usgs_quake"] == {"usgs_quake\x1eeid:us1000aaaa"}
|
|
assert "usgs_quake" in store._seeded
|
|
|
|
|
|
def test_disabling_an_adapter_removes_it_live():
|
|
store = EnvironmentalStore(_cfg())
|
|
assert "usgs_quake" in store._adapters
|
|
|
|
new_cfg = _cfg()
|
|
new_cfg.usgs_quake = dataclasses.replace(new_cfg.usgs_quake, enabled=False)
|
|
result = store.apply_config(new_cfg)
|
|
|
|
assert result["usgs_quake"] == "reloaded"
|
|
assert "usgs_quake" not in store._adapters
|
|
|
|
|
|
def test_feed_source_flip_to_central_requires_restart_and_does_not_swap():
|
|
store = EnvironmentalStore(_cfg())
|
|
nws_before = store._adapters["nws"]
|
|
|
|
new_cfg = _cfg()
|
|
new_cfg.nws = dataclasses.replace(new_cfg.nws, feed_source="central")
|
|
result = store.apply_config(new_cfg)
|
|
|
|
assert result["nws"] == "restart_required"
|
|
assert store._adapters["nws"] is nws_before, "must NOT hot-swap across the Central boundary"
|
|
|
|
# Repeated PUTs with the same (still-unapplied) value keep reporting
|
|
# restart_required rather than silently settling to "unchanged".
|
|
result2 = store.apply_config(new_cfg)
|
|
assert result2["nws"] == "restart_required"
|
|
assert store._adapters["nws"] is nws_before
|
|
|
|
|
|
def test_feed_source_flip_from_central_to_native_requires_restart():
|
|
# satpass now defaults to feed_source="native", so pin the starting state
|
|
# to "central" explicitly -- this exercises the flip, not the default.
|
|
base_cfg = _cfg()
|
|
base_cfg.satpass = dataclasses.replace(base_cfg.satpass, feed_source="central")
|
|
store = EnvironmentalStore(base_cfg)
|
|
assert "satpass" not in store._adapters # central -> no native instance
|
|
|
|
new_cfg = _cfg()
|
|
new_cfg.satpass = dataclasses.replace(
|
|
new_cfg.satpass, enabled=True, feed_source="native")
|
|
result = store.apply_config(new_cfg)
|
|
|
|
assert result["satpass"] == "restart_required"
|
|
assert result["satpass_tle"] == "restart_required"
|
|
assert "satpass" not in store._adapters
|
|
assert "satpass_tle" not in store._adapters
|
|
|
|
|
|
def test_nifc_rebuild_cascades_to_firms():
|
|
cfg = _cfg()
|
|
cfg.fires = dataclasses.replace(cfg.fires, enabled=True, feed_source="native")
|
|
cfg.firms = dataclasses.replace(
|
|
cfg.firms, enabled=True, feed_source="native", map_key="test-key")
|
|
|
|
store = EnvironmentalStore(cfg)
|
|
nifc_before = store._adapters["nifc"]
|
|
firms_before = store._adapters["firms"]
|
|
assert firms_before._fires_adapter is nifc_before
|
|
|
|
# Change something on the "fires" (nifc) config only; firms' OWN config
|
|
# is untouched.
|
|
new_cfg = _cfg()
|
|
new_cfg.fires = dataclasses.replace(cfg.fires, state="US-NV")
|
|
new_cfg.firms = cfg.firms # byte-identical
|
|
|
|
result = store.apply_config(new_cfg)
|
|
|
|
assert result["nifc"] == "reloaded"
|
|
assert result["firms"] == "reloaded", \
|
|
"firms must cascade-rebuild even though its own config didn't change"
|
|
|
|
nifc_after = store._adapters["nifc"]
|
|
firms_after = store._adapters["firms"]
|
|
assert nifc_after is not nifc_before
|
|
assert firms_after is not firms_before
|
|
assert firms_after._fires_adapter is nifc_after, \
|
|
"firms must hold the FRESH nifc reference, not the stale one"
|
|
|
|
|
|
def test_generic_sources_change_rebuilds_generic_http_only():
|
|
store = EnvironmentalStore(
|
|
_cfg(), generic_sources=[{"name": "a", "url": "https://a.example"}])
|
|
generic_before = store._adapters.get("generic_http")
|
|
nws_before = store._adapters["nws"]
|
|
|
|
same_cfg = _cfg()
|
|
result = store.apply_config(
|
|
same_cfg, generic_sources=[{"name": "a", "url": "https://a.example"}])
|
|
assert result["generic_http"] == "unchanged"
|
|
assert store._adapters.get("generic_http") is generic_before
|
|
|
|
result2 = store.apply_config(
|
|
same_cfg, generic_sources=[{"name": "a", "url": "https://a-changed.example"}])
|
|
assert result2["generic_http"] == "reloaded"
|
|
assert result2["nws"] == "unchanged"
|
|
assert store._adapters["nws"] is nws_before
|