meshai/work/tests/test_tle_fetch.py

230 lines
8.1 KiB
Python
Raw Permalink Normal View History

feat(phase4c): native SGP4 satpass — standalone satellite passes, no Central (#39) meshai can now predict + broadcast satellite passes locally without Central. Data plumbing (4c-1): - env/tle_fetch.py: keyless Celestrak GP fetcher (GROUP/CATNR, FORMAT=tle) → upserts the existing sat_tles table via a shared upsert_tle() helper extracted into tle_handler (Central ingest refactored to call it, unchanged) - observer_locations table (v23, SCHEMA_VERSION 22->23) + persistence helpers; seeded from SatpassConfig.observers in main._init_components - SatpassConfig: observers, tle_groups, norad_ids, tle_refresh_seconds, min_elevation_deg, window_hours Predictor + source-agnostic gate (4c-2): - extracted gate_consolidated_pass(consolidated, *, now) from consolidate_satpass_pending: dedup-vs-satpass_events + rate cap + format_pass + deferred commit. Central path byte-identical (114 tests unchanged) - env/satpass.py: native adapter predicts passes for each sat x observer via pass_predictor.compute_passes, consolidates IN-MEMORY per canonical hour bucket (earliest AOS / latest LOS / max-el observer supplies peak_compass + entry/exit observers), runs the shared gate, emits sat_pass. Commit rides event.data so satpass_events dedups across ticks — NO satpass_pending, NO Central-consumer timer dependency (works with Central off) - registered in env/store.py gated on enabled and feed_source==native 32 new tests (tle_fetch 16, observer_locations 14... satpass_native 8, minus overlaps); full suite 10-failure baseline (1672 passed). Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 02:25:55 -06:00
"""Tests for the native Celestrak TLE fetcher (env.tle_fetch).
Covers epoch parsing, 3-line block parsing, upsert into the shared
sat_tles table, latest-epoch-wins on re-fetch, and malformed-block
tolerance. HTTP is monkeypatched no network.
"""
from __future__ import annotations
fix(persistence): derive SCHEMA_VERSION from migrations; unbreak the red suite (#140) * fix(persistence): derive SCHEMA_VERSION from the migrations directory db.py hardcoded SCHEMA_VERSION = 26 while migrations/ had already reached v29 (v27 dispatcher floor-drop counter, v28 mesh_observations, v29 IPAWS). The migration runner globs the directory and applies every vN.sql it finds regardless of the constant, so a fresh DB actually landed at 29 while the constant claimed 26 -- a three-version drift that three tests were correctly catching. Derive it from the highest vN.sql present instead of bumping the literal, so it cannot drift again the next time someone adds a migration. Falls back to 0 if the directory is missing so import never fails; the migrations dir sits alongside db.py and ships with the package (Dockerfile COPYs meshai/). Adds a regression guard asserting the constant matches the highest migration file, and updates three tests that hardcoded 26 as a literal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(tle): make TLE fixtures time-relative so they cannot expire The ISS fixtures hardcoded epochs of 2026-06-30/07-01/07-02. tle_handler sets STALE_DAYS = 14 and get_tle_by_norad() filters on epoch >= now - 14d, so the fixtures silently aged out on 2026-07-02 and the tests began failing -- a time bomb, not a regression. Compute epochs relative to wall-clock now (base = now - 2d, +/-1d for newer/older) with correct TLE epoch-field encoding and mod-10 checksum. STALE_DAYS is untouched -- widening it in product code would have changed production behavior to paper over a test bug. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(fire-tracker): pin config + history db to tmp_path load_config() defaults HistoryConfig.database to the relative path "conversations.db", resolved against the process CWD, so every test calling load_config() with no override shares one file for the whole session. The conftest DB-isolation fixture only covers MESHAI_DB_PATH, not this. Point both the config dir and the history database at the test's tmp_path. NOTE: this does NOT resolve the order-dependent failure -- the test still passes standalone and fails in a full run, so the polluting state lives somewhere other than config/history. Left failing rather than weakened; root cause still unidentified. 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>
2026-07-17 13:03:40 -06:00
import datetime as _dt
feat(phase4c): native SGP4 satpass — standalone satellite passes, no Central (#39) meshai can now predict + broadcast satellite passes locally without Central. Data plumbing (4c-1): - env/tle_fetch.py: keyless Celestrak GP fetcher (GROUP/CATNR, FORMAT=tle) → upserts the existing sat_tles table via a shared upsert_tle() helper extracted into tle_handler (Central ingest refactored to call it, unchanged) - observer_locations table (v23, SCHEMA_VERSION 22->23) + persistence helpers; seeded from SatpassConfig.observers in main._init_components - SatpassConfig: observers, tle_groups, norad_ids, tle_refresh_seconds, min_elevation_deg, window_hours Predictor + source-agnostic gate (4c-2): - extracted gate_consolidated_pass(consolidated, *, now) from consolidate_satpass_pending: dedup-vs-satpass_events + rate cap + format_pass + deferred commit. Central path byte-identical (114 tests unchanged) - env/satpass.py: native adapter predicts passes for each sat x observer via pass_predictor.compute_passes, consolidates IN-MEMORY per canonical hour bucket (earliest AOS / latest LOS / max-el observer supplies peak_compass + entry/exit observers), runs the shared gate, emits sat_pass. Commit rides event.data so satpass_events dedups across ticks — NO satpass_pending, NO Central-consumer timer dependency (works with Central off) - registered in env/store.py gated on enabled and feed_source==native 32 new tests (tle_fetch 16, observer_locations 14... satpass_native 8, minus overlaps); full suite 10-failure baseline (1672 passed). Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 02:25:55 -06:00
import pytest
from meshai.env.tle_fetch import (
TLEFetchAdapter,
parse_tle_block,
parse_tle_epoch,
)
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>
2026-07-17 15:46:36 -06:00
from meshai.env.satellite.tle_store import get_tle_by_norad
feat(phase4c): native SGP4 satpass — standalone satellite passes, no Central (#39) meshai can now predict + broadcast satellite passes locally without Central. Data plumbing (4c-1): - env/tle_fetch.py: keyless Celestrak GP fetcher (GROUP/CATNR, FORMAT=tle) → upserts the existing sat_tles table via a shared upsert_tle() helper extracted into tle_handler (Central ingest refactored to call it, unchanged) - observer_locations table (v23, SCHEMA_VERSION 22->23) + persistence helpers; seeded from SatpassConfig.observers in main._init_components - SatpassConfig: observers, tle_groups, norad_ids, tle_refresh_seconds, min_elevation_deg, window_hours Predictor + source-agnostic gate (4c-2): - extracted gate_consolidated_pass(consolidated, *, now) from consolidate_satpass_pending: dedup-vs-satpass_events + rate cap + format_pass + deferred commit. Central path byte-identical (114 tests unchanged) - env/satpass.py: native adapter predicts passes for each sat x observer via pass_predictor.compute_passes, consolidates IN-MEMORY per canonical hour bucket (earliest AOS / latest LOS / max-el observer supplies peak_compass + entry/exit observers), runs the shared gate, emits sat_pass. Commit rides event.data so satpass_events dedups across ticks — NO satpass_pending, NO Central-consumer timer dependency (works with Central off) - registered in env/store.py gated on enabled and feed_source==native 32 new tests (tle_fetch 16, observer_locations 14... satpass_native 8, minus overlaps); full suite 10-failure baseline (1672 passed). Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 02:25:55 -06:00
from meshai.config import SatpassConfig
from meshai.persistence import get_db
fix(persistence): derive SCHEMA_VERSION from migrations; unbreak the red suite (#140) * fix(persistence): derive SCHEMA_VERSION from the migrations directory db.py hardcoded SCHEMA_VERSION = 26 while migrations/ had already reached v29 (v27 dispatcher floor-drop counter, v28 mesh_observations, v29 IPAWS). The migration runner globs the directory and applies every vN.sql it finds regardless of the constant, so a fresh DB actually landed at 29 while the constant claimed 26 -- a three-version drift that three tests were correctly catching. Derive it from the highest vN.sql present instead of bumping the literal, so it cannot drift again the next time someone adds a migration. Falls back to 0 if the directory is missing so import never fails; the migrations dir sits alongside db.py and ships with the package (Dockerfile COPYs meshai/). Adds a regression guard asserting the constant matches the highest migration file, and updates three tests that hardcoded 26 as a literal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(tle): make TLE fixtures time-relative so they cannot expire The ISS fixtures hardcoded epochs of 2026-06-30/07-01/07-02. tle_handler sets STALE_DAYS = 14 and get_tle_by_norad() filters on epoch >= now - 14d, so the fixtures silently aged out on 2026-07-02 and the tests began failing -- a time bomb, not a regression. Compute epochs relative to wall-clock now (base = now - 2d, +/-1d for newer/older) with correct TLE epoch-field encoding and mod-10 checksum. STALE_DAYS is untouched -- widening it in product code would have changed production behavior to paper over a test bug. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(fire-tracker): pin config + history db to tmp_path load_config() defaults HistoryConfig.database to the relative path "conversations.db", resolved against the process CWD, so every test calling load_config() with no override shares one file for the whole session. The conftest DB-isolation fixture only covers MESHAI_DB_PATH, not this. Point both the config dir and the history database at the test's tmp_path. NOTE: this does NOT resolve the order-dependent failure -- the test still passes standalone and fails in a full run, so the polluting state lives somewhere other than config/history. Left failing rather than weakened; root cause still unidentified. 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>
2026-07-17 13:03:40 -06:00
# -- time-relative TLE fixture generation --------------------------------
#
# These used to be hardcoded 3-line sets with a fixed 2026-06-30..07-02
# epoch. get_tle_by_norad() filters on `epoch >= now() - STALE_DAYS`
# (STALE_DAYS=14, real wall-clock `now`), so a fixed-date fixture ages out
# 14 days after it's written and silently starts failing
# test_tick_upserts_into_sat_tles / test_latest_epoch_wins_on_refetch.
# Deriving the epoch from the real current time at test-collection time
# means these can never expire again.
def _tle_epoch_field(dt: _dt.datetime) -> str:
"""Build a TLE line-1 epoch field (cols 19-32): YYDDD.DDDDDDDD."""
yy = dt.year % 100
start_of_year = _dt.datetime(dt.year, 1, 1, tzinfo=_dt.timezone.utc)
doy_frac = (dt - start_of_year).total_seconds() / 86400.0 + 1.0
doy_int = int(doy_frac)
frac = doy_frac - doy_int
return f"{yy:02d}{doy_int:03d}.{round(frac * 1e8):08d}"
def _tle_checksum(line_without_checksum_digit: str) -> int:
"""Standard TLE mod-10 checksum: sum of digits, '-' counts as 1,
everything else 0."""
total = 0
for ch in line_without_checksum_digit:
if ch.isdigit():
total += int(ch)
elif ch == "-":
total += 1
return total % 10
def _iss_tle_block(dt: _dt.datetime, element_set_num: str, revnum: str) -> str:
"""Build a valid 3-line ISS (ZARYA) TLE with the given epoch. All
other orbital elements are fixed (arbitrary but internally
consistent) only epoch/checksum/element-set/revnum vary, mirroring
the original hand-written fixtures."""
line1_body = (f"1 25544U 98067A {_tle_epoch_field(dt)} .00016717 "
f"00000-0 10270-3 0 {element_set_num}")
line1 = f"{line1_body}{_tle_checksum(line1_body)}"
line2 = (f"2 25544 51.6400 208.9163 0007417 17.6777 85.6621 "
f"15.54225995 {revnum}")
return f"ISS (ZARYA)\n{line1}\n{line2}\n"
# Base epoch: 2 days ago. Comfortably inside the 14-day STALE_DAYS window
# no matter when the suite runs, and never in the future.
_NOW = _dt.datetime.now(_dt.timezone.utc)
_BASE_EPOCH = _NOW - _dt.timedelta(days=2)
# A valid ISS 3-line set at the base epoch.
ISS_TLE = _iss_tle_block(_BASE_EPOCH, "900", "12345")
# Same satellite, a NEWER epoch (1 day after base).
ISS_TLE_NEWER = _iss_tle_block(_BASE_EPOCH + _dt.timedelta(days=1), "901", "12347")
# Same satellite, an OLDER epoch (1 day before base).
ISS_TLE_OLDER = _iss_tle_block(_BASE_EPOCH - _dt.timedelta(days=1), "900", "12343")
# ISO epoch string parse_tle_epoch() will produce for ISS_TLE's line 1 --
# computed via the real parser (not re-derived independently) so
# assertions can't drift from the actual parsing behavior under test.
ISS_TLE_EPOCH_ISO = parse_tle_epoch(ISS_TLE.splitlines()[1])
feat(phase4c): native SGP4 satpass — standalone satellite passes, no Central (#39) meshai can now predict + broadcast satellite passes locally without Central. Data plumbing (4c-1): - env/tle_fetch.py: keyless Celestrak GP fetcher (GROUP/CATNR, FORMAT=tle) → upserts the existing sat_tles table via a shared upsert_tle() helper extracted into tle_handler (Central ingest refactored to call it, unchanged) - observer_locations table (v23, SCHEMA_VERSION 22->23) + persistence helpers; seeded from SatpassConfig.observers in main._init_components - SatpassConfig: observers, tle_groups, norad_ids, tle_refresh_seconds, min_elevation_deg, window_hours Predictor + source-agnostic gate (4c-2): - extracted gate_consolidated_pass(consolidated, *, now) from consolidate_satpass_pending: dedup-vs-satpass_events + rate cap + format_pass + deferred commit. Central path byte-identical (114 tests unchanged) - env/satpass.py: native adapter predicts passes for each sat x observer via pass_predictor.compute_passes, consolidates IN-MEMORY per canonical hour bucket (earliest AOS / latest LOS / max-el observer supplies peak_compass + entry/exit observers), runs the shared gate, emits sat_pass. Commit rides event.data so satpass_events dedups across ticks — NO satpass_pending, NO Central-consumer timer dependency (works with Central off) - registered in env/store.py gated on enabled and feed_source==native 32 new tests (tle_fetch 16, observer_locations 14... satpass_native 8, minus overlaps); full suite 10-failure baseline (1672 passed). Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 02:25:55 -06:00
def _adapter(**overrides) -> TLEFetchAdapter:
cfg = SatpassConfig(enabled=True, feed_source="native",
tle_groups=[], norad_ids=[25544], **overrides)
return TLEFetchAdapter(cfg)
# -- epoch parsing ------------------------------------------------------------
def test_parse_tle_epoch_iso():
iso = parse_tle_epoch(
"1 25544U 98067A 26182.50000000 .00016717 00000-0 10270-3 0 9008")
assert iso.startswith("2026-07-01T12:00:00")
assert "+00:00" in iso
def test_parse_tle_epoch_two_digit_year_window():
# YY=98 -> 1998 (>= 57 maps to 1900s).
iso = parse_tle_epoch("1 25544U 98067A 98001.00000000 .0 0 0 0 1")
assert iso.startswith("1998-01-01")
# -- block parsing ------------------------------------------------------------
def test_parse_tle_block_basic():
recs = parse_tle_block(ISS_TLE)
assert len(recs) == 1
r = recs[0]
assert r["norad_id"] == 25544
assert r["name"] == "ISS (ZARYA)"
assert r["line1"].startswith("1 25544")
assert r["line2"].startswith("2 25544")
fix(persistence): derive SCHEMA_VERSION from migrations; unbreak the red suite (#140) * fix(persistence): derive SCHEMA_VERSION from the migrations directory db.py hardcoded SCHEMA_VERSION = 26 while migrations/ had already reached v29 (v27 dispatcher floor-drop counter, v28 mesh_observations, v29 IPAWS). The migration runner globs the directory and applies every vN.sql it finds regardless of the constant, so a fresh DB actually landed at 29 while the constant claimed 26 -- a three-version drift that three tests were correctly catching. Derive it from the highest vN.sql present instead of bumping the literal, so it cannot drift again the next time someone adds a migration. Falls back to 0 if the directory is missing so import never fails; the migrations dir sits alongside db.py and ships with the package (Dockerfile COPYs meshai/). Adds a regression guard asserting the constant matches the highest migration file, and updates three tests that hardcoded 26 as a literal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(tle): make TLE fixtures time-relative so they cannot expire The ISS fixtures hardcoded epochs of 2026-06-30/07-01/07-02. tle_handler sets STALE_DAYS = 14 and get_tle_by_norad() filters on epoch >= now - 14d, so the fixtures silently aged out on 2026-07-02 and the tests began failing -- a time bomb, not a regression. Compute epochs relative to wall-clock now (base = now - 2d, +/-1d for newer/older) with correct TLE epoch-field encoding and mod-10 checksum. STALE_DAYS is untouched -- widening it in product code would have changed production behavior to paper over a test bug. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(fire-tracker): pin config + history db to tmp_path load_config() defaults HistoryConfig.database to the relative path "conversations.db", resolved against the process CWD, so every test calling load_config() with no override shares one file for the whole session. The conftest DB-isolation fixture only covers MESHAI_DB_PATH, not this. Point both the config dir and the history database at the test's tmp_path. NOTE: this does NOT resolve the order-dependent failure -- the test still passes standalone and fails in a full run, so the polluting state lives somewhere other than config/history. Left failing rather than weakened; root cause still unidentified. 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>
2026-07-17 13:03:40 -06:00
assert r["epoch"] == ISS_TLE_EPOCH_ISO
feat(phase4c): native SGP4 satpass — standalone satellite passes, no Central (#39) meshai can now predict + broadcast satellite passes locally without Central. Data plumbing (4c-1): - env/tle_fetch.py: keyless Celestrak GP fetcher (GROUP/CATNR, FORMAT=tle) → upserts the existing sat_tles table via a shared upsert_tle() helper extracted into tle_handler (Central ingest refactored to call it, unchanged) - observer_locations table (v23, SCHEMA_VERSION 22->23) + persistence helpers; seeded from SatpassConfig.observers in main._init_components - SatpassConfig: observers, tle_groups, norad_ids, tle_refresh_seconds, min_elevation_deg, window_hours Predictor + source-agnostic gate (4c-2): - extracted gate_consolidated_pass(consolidated, *, now) from consolidate_satpass_pending: dedup-vs-satpass_events + rate cap + format_pass + deferred commit. Central path byte-identical (114 tests unchanged) - env/satpass.py: native adapter predicts passes for each sat x observer via pass_predictor.compute_passes, consolidates IN-MEMORY per canonical hour bucket (earliest AOS / latest LOS / max-el observer supplies peak_compass + entry/exit observers), runs the shared gate, emits sat_pass. Commit rides event.data so satpass_events dedups across ticks — NO satpass_pending, NO Central-consumer timer dependency (works with Central off) - registered in env/store.py gated on enabled and feed_source==native 32 new tests (tle_fetch 16, observer_locations 14... satpass_native 8, minus overlaps); full suite 10-failure baseline (1672 passed). Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 02:25:55 -06:00
def test_parse_tle_block_skips_malformed():
# First triple is garbage (line1 doesn't start with "1 "); second is valid.
block = (
"GARBAGE SAT\n"
"not a real line1\n"
"also not line2\n"
+ ISS_TLE
)
recs = parse_tle_block(block)
norads = [r["norad_id"] for r in recs]
assert 25544 in norads
# The garbage entry must not have produced a record.
assert all(isinstance(n, int) for n in norads)
# -- fetch + upsert -----------------------------------------------------------
def test_tick_upserts_into_sat_tles(monkeypatch):
adapter = _adapter()
monkeypatch.setattr(adapter, "_fetch", lambda url: ISS_TLE)
changed = adapter.tick(now=1_000_000)
assert changed is True
row = get_tle_by_norad(25544)
assert row is not None
assert row["name"] == "ISS (ZARYA)"
assert row["line1"].startswith("1 25544")
assert row["line2"].startswith("2 25544")
fix(persistence): derive SCHEMA_VERSION from migrations; unbreak the red suite (#140) * fix(persistence): derive SCHEMA_VERSION from the migrations directory db.py hardcoded SCHEMA_VERSION = 26 while migrations/ had already reached v29 (v27 dispatcher floor-drop counter, v28 mesh_observations, v29 IPAWS). The migration runner globs the directory and applies every vN.sql it finds regardless of the constant, so a fresh DB actually landed at 29 while the constant claimed 26 -- a three-version drift that three tests were correctly catching. Derive it from the highest vN.sql present instead of bumping the literal, so it cannot drift again the next time someone adds a migration. Falls back to 0 if the directory is missing so import never fails; the migrations dir sits alongside db.py and ships with the package (Dockerfile COPYs meshai/). Adds a regression guard asserting the constant matches the highest migration file, and updates three tests that hardcoded 26 as a literal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(tle): make TLE fixtures time-relative so they cannot expire The ISS fixtures hardcoded epochs of 2026-06-30/07-01/07-02. tle_handler sets STALE_DAYS = 14 and get_tle_by_norad() filters on epoch >= now - 14d, so the fixtures silently aged out on 2026-07-02 and the tests began failing -- a time bomb, not a regression. Compute epochs relative to wall-clock now (base = now - 2d, +/-1d for newer/older) with correct TLE epoch-field encoding and mod-10 checksum. STALE_DAYS is untouched -- widening it in product code would have changed production behavior to paper over a test bug. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(fire-tracker): pin config + history db to tmp_path load_config() defaults HistoryConfig.database to the relative path "conversations.db", resolved against the process CWD, so every test calling load_config() with no override shares one file for the whole session. The conftest DB-isolation fixture only covers MESHAI_DB_PATH, not this. Point both the config dir and the history database at the test's tmp_path. NOTE: this does NOT resolve the order-dependent failure -- the test still passes standalone and fails in a full run, so the polluting state lives somewhere other than config/history. Left failing rather than weakened; root cause still unidentified. 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>
2026-07-17 13:03:40 -06:00
assert row["epoch"] == ISS_TLE_EPOCH_ISO
feat(phase4c): native SGP4 satpass — standalone satellite passes, no Central (#39) meshai can now predict + broadcast satellite passes locally without Central. Data plumbing (4c-1): - env/tle_fetch.py: keyless Celestrak GP fetcher (GROUP/CATNR, FORMAT=tle) → upserts the existing sat_tles table via a shared upsert_tle() helper extracted into tle_handler (Central ingest refactored to call it, unchanged) - observer_locations table (v23, SCHEMA_VERSION 22->23) + persistence helpers; seeded from SatpassConfig.observers in main._init_components - SatpassConfig: observers, tle_groups, norad_ids, tle_refresh_seconds, min_elevation_deg, window_hours Predictor + source-agnostic gate (4c-2): - extracted gate_consolidated_pass(consolidated, *, now) from consolidate_satpass_pending: dedup-vs-satpass_events + rate cap + format_pass + deferred commit. Central path byte-identical (114 tests unchanged) - env/satpass.py: native adapter predicts passes for each sat x observer via pass_predictor.compute_passes, consolidates IN-MEMORY per canonical hour bucket (earliest AOS / latest LOS / max-el observer supplies peak_compass + entry/exit observers), runs the shared gate, emits sat_pass. Commit rides event.data so satpass_events dedups across ticks — NO satpass_pending, NO Central-consumer timer dependency (works with Central off) - registered in env/store.py gated on enabled and feed_source==native 32 new tests (tle_fetch 16, observer_locations 14... satpass_native 8, minus overlaps); full suite 10-failure baseline (1672 passed). Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 02:25:55 -06:00
def test_latest_epoch_wins_on_refetch(monkeypatch):
adapter = _adapter()
monkeypatch.setattr(adapter, "_fetch", lambda url: ISS_TLE)
assert adapter.tick(now=1_000_000) is True
first = get_tle_by_norad(25544)["epoch"]
# A newer epoch replaces it.
adapter._last_tick = 0 # bypass interval gate for the test
monkeypatch.setattr(adapter, "_fetch", lambda url: ISS_TLE_NEWER)
assert adapter.tick(now=2_000_000) is True
newer = get_tle_by_norad(25544)["epoch"]
assert newer > first
# An older epoch is ignored (no write).
adapter._last_tick = 0
monkeypatch.setattr(adapter, "_fetch", lambda url: ISS_TLE_OLDER)
changed = adapter.tick(now=3_000_000)
assert changed is False
assert get_tle_by_norad(25544)["epoch"] == newer
def test_malformed_block_does_not_crash_tick(monkeypatch):
adapter = _adapter()
monkeypatch.setattr(
adapter, "_fetch",
lambda url: "COMPLETE GARBAGE\nno lines here\n")
# Should complete without raising and write nothing.
changed = adapter.tick(now=1_000_000)
assert changed is False
assert get_tle_by_norad(25544) is None
def test_fetch_error_is_isolated(monkeypatch):
adapter = _adapter()
def boom(url):
raise RuntimeError("HTTP 503")
monkeypatch.setattr(adapter, "_fetch", boom)
changed = adapter.tick(now=1_000_000)
assert changed is False
assert adapter.health_status["last_error"] is not None
assert adapter.health_status["consecutive_errors"] == 1
def test_storage_only_no_events(monkeypatch):
adapter = _adapter()
monkeypatch.setattr(adapter, "_fetch", lambda url: ISS_TLE)
adapter.tick(now=1_000_000)
assert adapter.get_events() == []
assert adapter.to_event({}) is None
def test_interval_gate_skips_early_ticks(monkeypatch):
adapter = _adapter(tle_refresh_seconds=21600)
calls = []
monkeypatch.setattr(adapter, "_fetch",
lambda url: calls.append(url) or ISS_TLE)
adapter.tick(now=1_000_000.0) # first tick fetches
adapter.tick(now=1_000_100.0) # 100s later, within interval -> skipped
assert len(calls) == 1
def test_no_targets_configured_is_noop():
cfg = SatpassConfig(enabled=True, feed_source="native",
tle_groups=[], norad_ids=[])
adapter = TLEFetchAdapter(cfg)
assert adapter.tick(now=1_000_000) is False
def test_group_and_catnr_urls():
cfg = SatpassConfig(tle_groups=["weather"], norad_ids=[25544])
adapter = TLEFetchAdapter(cfg)
urls = [u for _, u in adapter._targets()]
assert any("GROUP=weather&FORMAT=tle" in u for u in urls)
assert any("CATNR=25544&FORMAT=tle" in u for u in urls)
assert all(u.startswith("https://celestrak.org/NORAD/elements/gp.php") for u in urls)