mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
chore(central-ripout 2b): relocate satellite code to env/satellite/ (#162)
* chore(central-ripout 2b): create env/satellite package, move pass_predictor
pass_predictor.py was 100% live (no dead entrypoint) — SGP4 pass
computation used by both the native satpass adapter and the on-demand
!satpass command. Straight move, no code changes: meshai.central.pass_predictor
-> meshai.env.satellite.pass_predictor. Owner directive: satellite code gets
its own folder under the feed adapters, separate from env.satpass (the
adapter) to avoid colliding with env/satpass.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(central-ripout 2b): split satpass_handler.py -> env/satellite/pass_format.py
satpass_handler.py was a split file: live wire-formatting/gate logic plus
dead Central-envelope ingest machinery whose only caller was the
already-deleted central/consumer.py NATS bridge.
Moved (live, verified via rg — external callers in env/satpass.py and
commands/satpass_cmd.py, or transitively called by them):
gate_consolidated_pass, format_pass, _check_rate_cap, _upsert_satpass,
_attach_commit, _map_severity, _canonical_id, _azimuth_to_compass,
_short_sat_name, _collapse_compass, _region_paren, _is_synthetic_observer,
_format_time_12h/24h, _format_ampm, _tz_abbr, _date_label,
plus the _SHORT_SAT_NAMES/_SHORT_NAME_SUBSTR/_SYNTHETIC_OBSERVERS tables.
Dropped (dead — zero callers outside the already-deleted consumer.py and
handle_satpass/consolidate_satpass_pending themselves; verified with rg):
handle_satpass, consolidate_satpass_pending, _cleanup_pending,
load_pending_schedule, _log_event_returning_id, _coerce_float,
_coerce_int, _parse_iso_epoch, _now, CONSOLIDATION_DELAY,
_pending_consolidation_ids, drain_pending_consolidation_ids,
_elevation_bucket (already-orphaned pre-ripout: superseded by numeric
"max NN°" wire format, zero callers anywhere but its own tests),
SCHEMA_SATPASS_EVENTS/SCHEMA_SATPASS_PENDING (unused string constants —
actual schema lives in persistence/migrations/*.sql, never imported).
Also dropped now-unused `json`/`time`/`Any` imports.
Straight code move otherwise — no logic changes to any moved function.
Two docstrings updated for accuracy (module docstring, and
gate_consolidated_pass's docstring which referenced the now-deleted
Central consumer path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(central-ripout 2b): split tle_handler.py -> env/satellite/tle_store.py
tle_handler.py was a split file: live storage helpers plus a dead
Central-envelope ingest entrypoint whose only caller was the
already-deleted central/consumer.py NATS bridge.
Moved (live — used by env.tle_fetch, env.satpass, commands.satpass_cmd,
verified via rg): upsert_tle, get_fresh_tles, get_tle_by_norad,
search_tle_by_name.
Dropped (dead — handle_tle's only callers were tests and the deleted
consumer.py; verified with rg): handle_tle.
Straight code move otherwise — no logic changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(central-ripout 2b): repoint satellite consumers at env.satellite
Rewire the three production consumers (including their lazy/function-body
imports, not just module-top ones) to the new location:
- env/satpass.py: central.tle_handler -> env.satellite.tle_store,
central.pass_predictor -> env.satellite.pass_predictor,
central.satpass_handler -> env.satellite.pass_format
- env/tle_fetch.py: central.tle_handler.upsert_tle -> env.satellite.tle_store
- commands/satpass_cmd.py: all three, same mapping
Also refreshed docstrings that pointed at the old module paths or described
the now-fully-deleted Central consolidation path
(consolidate_satpass_pending / satpass_pending buffer) as a live
alternative, and updated central/__init__.py's module docstring to stop
listing the three relocated modules among central's remaining contents.
No behavior changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(central-ripout 2b): update satpass/tle test suite for the relocation
Repoints every remaining test import at the new env.satellite.* modules
and removes/adapts coverage for the Central envelope-ingest path deleted
in this pass (handle_satpass, consolidate_satpass_pending, handle_tle, and
the filtering/coercion/staleness logic that lived only inside them):
- test_satpass_native.py, test_tle_fetch.py, test_satpass_command.py:
import-path updates only (pass_predictor, tle_store). Also dropped
test_satpass_command.py's TestTLEUpsert.test_returns_none_always
(handle_tle-specific contract, no longer applicable) and rewrote its two
latest-wins tests to call upsert_tle directly — same behavior under test,
now exercised through the still-live primitive instead of the dead
wrapper.
- test_satpass_native.py: deleted test_central_consolidate_feeds_shared_gate_merged
(spied on consolidate_satpass_pending, which no longer exists). The
merge-across-observers logic it guarded is native-side (_consolidate)
and already covered by test_two_observers_consolidate_to_one_broadcast.
- test_satpass_handler.py: gutted to the one test that calls format_pass
directly (test_format_pass_worst_case_fits_140); the rest exercised
handle_satpass's observer/norad/elevation filters, which have no live
equivalent (the native adapter filters at the config level, not
per-envelope) and is redundant with test_satpass_native.py's dedup/wire
coverage via the real SatpassAdapter path.
- test_satpass_broadcast_safety.py: kept every test that calls format_pass
or gate_consolidated_pass-adjacent REGISTRY checks directly (wire format,
clean-format rules, REGISTRY defaults); deleted TestNoradIdTypeCoercion
and TestStalenessGuard (handle_satpass-only logic, no live equivalent)
and the 6 _elevation_bucket tests (_elevation_bucket itself was dead
before this pass too — zero callers anywhere but its own tests, already
superseded by the numeric "max NN°" wire format per its own docstring).
- test_satpass_persisted_timer.py: dropped test_due_at_persisted_on_normal_ingest
(handle_satpass-only); kept the two schema/migration tests, which don't
touch satpass_handler.
- Deleted outright (tested ONLY the dead Central envelope-ingest path, no
live equivalent to port to): test_satpass_event_path.py,
test_satpass_compass_fallback.py, test_satpass_wire_fields.py.
Full suite: 2059 passed, 0 failed (was 0 failed on main pre-change).
Satpass/TLE subset (99 tests across 8 files) verified green in isolation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ff127bee18
commit
41178831e4
18 changed files with 590 additions and 2483 deletions
|
|
@ -1,204 +1,29 @@
|
|||
"""v0.7 satpass_handler tests."""
|
||||
"""v0.7 satpass_handler tests.
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
The Central envelope-ingest path (`handle_satpass`,
|
||||
`consolidate_satpass_pending`, and the observer/norad/elevation/staleness
|
||||
filtering that lived inside them) was retired with the Central NATS
|
||||
consumer and deleted 2026-07 when the still-live wire-formatting + gate
|
||||
code (`format_pass`, `gate_consolidated_pass`) moved to
|
||||
`meshai.env.satellite.pass_format`. That live code's coverage now lives in
|
||||
tests/test_satpass_native.py (dedup, wire format, commit callback, via the
|
||||
native SatpassAdapter path) and tests/test_satpass_broadcast_safety.py
|
||||
(format_pass formatting rules called directly). What remains here is the
|
||||
one test that calls `format_pass` directly and doesn't fit either of those
|
||||
files' focus.
|
||||
"""
|
||||
|
||||
|
||||
def _envelope(norad_id=25544, sat_name="ISS", observer="Boise",
|
||||
max_el=75.0, aos="2026-06-12T03:32:00Z",
|
||||
los="2026-06-12T03:38:00Z", direction="NW-SE",
|
||||
aos_compass="SW", los_compass="NE"):
|
||||
"""Build a CloudEvents envelope for a satellite pass."""
|
||||
return {
|
||||
"specversion": "1.0",
|
||||
"type": "central.sat.pass",
|
||||
"source": "central",
|
||||
"id": f"pass-{norad_id}-{aos}",
|
||||
"data": {
|
||||
"adapter": "n2yo_visualpasses",
|
||||
"category": "pass.n2yo_visualpasses",
|
||||
"severity": 0,
|
||||
"data": {
|
||||
"norad_id": norad_id,
|
||||
"satellite_name": sat_name,
|
||||
"observer_name": observer,
|
||||
"max_elevation_deg": max_el,
|
||||
"aos_time": aos,
|
||||
"los_time": los,
|
||||
"azimuth_at_peak_compass": direction,
|
||||
"azimuth_at_aos_compass": aos_compass,
|
||||
"azimuth_at_los_compass": los_compass,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db():
|
||||
"""Mock database connection."""
|
||||
conn = MagicMock()
|
||||
conn.execute.return_value.fetchone.return_value = None
|
||||
conn.execute.return_value.lastrowid = 1
|
||||
with patch("meshai.central.satpass_handler.get_db", return_value=conn):
|
||||
yield conn
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_adapter_config():
|
||||
"""Mock adapter_config.satpass."""
|
||||
cfg = MagicMock()
|
||||
cfg.enabled = True
|
||||
cfg.observers = [] # empty = all observers
|
||||
cfg.min_elevation = 30
|
||||
cfg.norad_ids = [25544] # must be non-empty for opt-in
|
||||
cfg.dry_run = False
|
||||
cfg.max_broadcasts_per_hour = 4
|
||||
cfg.max_aos_horizon_hours = 24
|
||||
with patch("meshai.central.satpass_handler.adapter_config") as mock:
|
||||
mock.satpass = cfg
|
||||
from meshai.central.satpass_handler import handle_satpass
|
||||
if hasattr(handle_satpass, "_disabled_logged"):
|
||||
del handle_satpass._disabled_logged
|
||||
if hasattr(handle_satpass, "_no_norad_ids_logged"):
|
||||
del handle_satpass._no_norad_ids_logged
|
||||
yield cfg
|
||||
|
||||
|
||||
def _ingest_and_consolidate(env, subject, *, now, data=None):
|
||||
"""Drive the two-call async satpass contract (ingest -> consolidate).
|
||||
|
||||
handle_satpass() ingests the pass into satpass_pending and ALWAYS
|
||||
returns None; the consumer then runs consolidate_satpass_pending(),
|
||||
which returns the (wire, data) to broadcast (or None if suppressed).
|
||||
Returns the consolidation result so a test can assert on the real wire.
|
||||
"""
|
||||
from meshai.central.satpass_handler import (
|
||||
handle_satpass, consolidate_satpass_pending,
|
||||
drain_pending_consolidation_ids)
|
||||
drain_pending_consolidation_ids()
|
||||
assert handle_satpass(
|
||||
env, subject, data=data if data is not None else {}, now=now) is None
|
||||
for cid in drain_pending_consolidation_ids():
|
||||
res = consolidate_satpass_pending(cid)
|
||||
if res is not None:
|
||||
return res
|
||||
return None
|
||||
|
||||
|
||||
class TestSatpassHandler:
|
||||
"""Tests for handle_satpass function."""
|
||||
|
||||
def test_high_elevation_pass_broadcasts(self, mock_adapter_config):
|
||||
"""A pass with high elevation should broadcast (via consolidation)."""
|
||||
env = _envelope(max_el=75.0)
|
||||
result = _ingest_and_consolidate(env, "central.sat.pass.iss",
|
||||
now=1781235000)
|
||||
assert result is not None
|
||||
wire, _ = result
|
||||
assert "ISS" in wire
|
||||
|
||||
def test_low_elevation_pass_filtered(self, mock_db, mock_adapter_config):
|
||||
"""A pass below min_elevation should be filtered."""
|
||||
from meshai.central.satpass_handler import handle_satpass
|
||||
|
||||
mock_adapter_config.min_elevation = 30
|
||||
env = _envelope(max_el=25.0)
|
||||
result = handle_satpass(env, "central.sat.pass.iss", data={}, now=1718163120)
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_observer_filter_blocks_mismatch(self, mock_db, mock_adapter_config):
|
||||
"""A pass for non-configured observer should be filtered."""
|
||||
from meshai.central.satpass_handler import handle_satpass
|
||||
|
||||
mock_adapter_config.observers = ["Magic Valley"]
|
||||
env = _envelope(observer="Boise")
|
||||
result = handle_satpass(env, "central.sat.pass.iss", data={}, now=1718163120)
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_observer_filter_allows_match(self, mock_adapter_config):
|
||||
"""A pass for configured observer should broadcast."""
|
||||
mock_adapter_config.observers = ["Boise", "Magic Valley"]
|
||||
env = _envelope(observer="Boise", max_el=45.0)
|
||||
result = _ingest_and_consolidate(env, "central.sat.pass.iss",
|
||||
now=1781235000)
|
||||
assert result is not None
|
||||
|
||||
def test_norad_id_filter(self, mock_db, mock_adapter_config):
|
||||
"""NORAD ID filter should block non-matching satellites."""
|
||||
from meshai.central.satpass_handler import handle_satpass
|
||||
|
||||
mock_adapter_config.norad_ids = [25544] # ISS only
|
||||
env = _envelope(norad_id=12345, max_el=60.0)
|
||||
result = handle_satpass(env, "central.sat.pass.other", data={}, now=1718163120)
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_dedup_blocks_second_broadcast(self, mock_adapter_config):
|
||||
"""Second pass in same hour bucket should be deduplicated."""
|
||||
env = _envelope(max_el=60.0)
|
||||
|
||||
# First pass consolidates into a real broadcast.
|
||||
result1 = _ingest_and_consolidate(env, "central.sat.pass.iss",
|
||||
now=1781235000)
|
||||
assert result1 is not None
|
||||
|
||||
# Simulate the broadcast committing (marks satpass_events broadcast).
|
||||
_wire, data1 = result1
|
||||
data1["_on_broadcast_committed"](1781235010)
|
||||
|
||||
# Second pass in the same (norad, aos-hour) bucket must be deduped.
|
||||
result2 = _ingest_and_consolidate(env, "central.sat.pass.iss",
|
||||
now=1781235060)
|
||||
assert result2 is None
|
||||
|
||||
def test_wire_format(self, mock_adapter_config):
|
||||
"""Wire is a single clean line: short name, degrees, collapsed compass."""
|
||||
env = _envelope(sat_name="ISS", max_el=75, observer="Boise",
|
||||
direction="S", aos_compass="SW", los_compass="NE")
|
||||
result = _ingest_and_consolidate(env, "central.sat.pass.iss",
|
||||
now=1781235000)
|
||||
assert result is not None
|
||||
wire, _ = result
|
||||
|
||||
assert "\n" not in wire # single line now
|
||||
assert "ISS" in wire
|
||||
assert "max 75°" in wire # numeric elevation, not a bucket word
|
||||
assert "overhead" not in wire
|
||||
assert "min window" not in wire
|
||||
assert "SW→S→NE" in wire # aos -> peak -> los, collapsed
|
||||
|
||||
def test_commit_callback_attached(self, mock_adapter_config):
|
||||
"""Broadcast should attach commit callback (on the consolidation data)."""
|
||||
env = _envelope(max_el=60.0)
|
||||
result = _ingest_and_consolidate(env, "central.sat.pass.iss",
|
||||
now=1781235000)
|
||||
assert result is not None
|
||||
_wire, data = result
|
||||
assert "_on_broadcast_committed" in data
|
||||
assert "_broadcast_audit" in data
|
||||
assert data["_broadcast_audit"]["table"] == "satpass_events"
|
||||
|
||||
def test_wrong_adapter_ignored(self, mock_db, mock_adapter_config):
|
||||
"""Envelope with wrong adapter should be ignored."""
|
||||
from meshai.central.satpass_handler import handle_satpass
|
||||
|
||||
env = _envelope()
|
||||
env["data"]["adapter"] = "some_other_adapter"
|
||||
result = handle_satpass(env, "central.sat.pass.iss", data={}, now=1718163120)
|
||||
|
||||
assert result is None
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Budget-fit SAFETY CAP: a pathologically long satellite name must not push
|
||||
# the broadcast string past 140 chars. Calls format_pass directly (bypasses
|
||||
# the broken consolidation path).
|
||||
# the consolidation path entirely).
|
||||
# ============================================================================
|
||||
|
||||
def test_format_pass_worst_case_fits_140():
|
||||
from meshai.central.satpass_handler import format_pass
|
||||
from meshai.env.satellite.pass_format import format_pass
|
||||
wire = format_pass(
|
||||
sat_name=("NOAA-19 EXPERIMENTAL SUPER LONG SATELLITE DESIGNATION "
|
||||
"PAYLOAD REVISION X PROTOTYPE FLIGHT MODEL SERIAL 00042"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue