central/tests
malice 8e388dabd5
v0.12.1: n2yo_visualpasses adapter (server-side visible-pass alerts)
## Architectural placement — complement, not replacement

| | satpass_predict (v0.11.1) | **n2yo_visualpasses (this PR)** |
|---|---|---|
| Computes from | Raw TLEs via local SGP4 | n2yo's pre-computed visualpasses endpoint |
| Magnitude data? | ✗ (SGP4 alone can't compute illumination) | ✓ (server-side sun-geometry) |
| Sun illumination filter? | ✗ | ✓ (n2yo returns sunlit passes only) |
| Cost per (observer, sat) pair | Local compute, free | One n2yo API transaction |
| Failure mode | TLE drift over time | Quota exhaustion, vendor outage |

Both adapters serve the same operator question ("when is sat X overhead at site Y?") but with different data sources. Matt's stated use case is to have **both** running so a vendor outage or quota burn on n2yo doesn't blind him to passes that satpass_predict can still propagate locally.

## Subject collision is intentional

Both adapters emit on `central.sat.pass.us.<state_lower>.<observer_slug>`. A consumer subscribing to e.g. `central.sat.pass.us.id.boise` receives events from **both** adapters. Disambiguation lives in `data.category`:

- `pass.satpass_predict` → local SGP4
- `pass.n2yo_visualpasses` → n2yo API

The v0.10.8 category-discriminated `Nats-Msg-Id` keeps both adapters' JetStream dedup windows separate even when they emit for the same (observer, satellite, AOS) tuple (which they will, by design, for sunlit passes).

This is documented explicitly in the new `### n2yo_visualpasses` subsection of `docs/CONSUMER-INTEGRATION.md` so future consumer integrators don't get surprised.

## Quota math

Default settings ship a curated **6 observers × 6 sats** configuration:

- **Observers** (ID + UT): Filer (primary), Boise, Idaho Falls, Ogden, Salt Lake City, Provo
- **Satellites** (curated for amateur observation): ISS (25544), NOAA-15 (25338), NOAA-18 (28654), NOAA-19 (33591), SO-50 (27607), AO-91 (43017)

At 1h cadence: **6 × 6 × 24 = 864 transactions/day**, comfortably under n2yo's free-tier **1000/day cap** with ~13% headroom for retries or expansion. Operator can extend either dimension if they upgrade quota.

## API key plumbing (tomtom_flow pattern)

Exact mirror of the v0.9.3 tomtom_flow precedent — confirmed during recon to be the established pattern:

```python
requires_api_key = "n2yo"          # class attr, GUI surfaces "requires X" warning
api_key_field = "api_key_alias"    # class attr, GUI renders api_key_select dropdown
# Settings field:
api_key_alias: str = "n2yo"
```

Cached `_api_key` populated via `ConfigStore.get_api_key(alias)` in `startup()` and `apply_config()`. Missing-key path: log INFO, return immediately (zero events, no exception). The live key is scrubbed from log strings via a `_redact()` helper before they hit journald.

**`python -m set_api_key` does not exist** — that was a speculative invocation in the spec. The actual flow is GUI-based: Matt adds the `n2yo` alias via the `/api-keys` page, then enables the adapter via `/adapters/n2yo_visualpasses/edit`.

## Diff size — flag for review

**+848 / −1 = +847 net** across 8 files. Spec budget was ≤600 lines. **Over by ~247** (~41%, similar shape to v0.12.0's overage).

| File | Lines | Notes |
|---|---|---|
| `src/central/adapters/n2yo_visualpasses.py` | 330 | **Under** the ≤350 adapter cap ✓ |
| `tests/test_n2yo_visualpasses.py` | 411 | The bulk of the overage |
| `sql/migrations/040_add_n2yo_visualpasses_adapter.sql` | 45 | Heavy comment block; could trim ~15 lines |
| `docs/CONSUMER-INTEGRATION.md` | 40 | Required by `test_consumer_doc` |
| Partials (event_rows + event_summaries) | 13 | |
| `tests/test_events_feed_frontend.py` | 8 | _SAMPLE_INNER + _EXPECTED_SUBJECT |
| `src/central/gui/routes.py` | 1 | ADAPTER_GROUPS extension |

**Test breakdown** (31 tests in 8 classes):
- 9 severity-bucketing tests — spec called out 4 boundaries (-3.1, -2.9, -0.5, 2.5); the extra 5 pin inclusive-vs-exclusive at -3.0, -1.0, 2.0 boundaries + the ranges in between. Useful regression guards but not strictly spec-required.
- 4 settings-default tests — pin the curated 6×6 set + quota math.
- 4 adapter-class-attrs tests — pin requires_api_key/api_key_field/data_class/default_cadence_s wiring.
- 3 subject_for tests — happy path + UT-state lowercasing + unknown fallback.
- 1 _pass_to_event shape test.
- 7 poll-loop tests — missing key, empty observers, empty norad_ids, happy path, empty passes array, fetch-failure-doesn't-kill-poll, multi-obs-multi-sat 6×6 aggregate.
- 1 HTTP-layer test — 401 → None (the one test that goes through the real session.get mock).
- 2 static-isolation tests — acceptance bar #2 (no hardcoded keys) and #4 (no absolute paths).

I can trim the test file to ~250 lines by dropping the non-strictly-spec-mandated tests (settings defaults, class attrs, extra severity boundaries, extra subject_for variants). **Flag for your call:** keep the comprehensive suite, or trim to spec minimum?

## Test plan

- [x] `pytest tests/test_n2yo_visualpasses.py` — **31/31 pass** (all offline, zero n2yo API hits).
- [x] `pytest tests/test_events_feed_frontend.py` — **122/122 pass** (fixture coverage extended).
- [x] `pytest tests/test_consumer_doc.py` — **6/6 pass** (new `### n2yo_visualpasses` subsection accepted).
- [x] Full sweep `pytest tests/` (excluding postgres-dep files) — **1243 passed, 1 skipped, 0 failures**.
- [x] Ruff: **clean on new files** (`n2yo_visualpasses.py`, `test_n2yo_visualpasses.py`). The pre-existing F841 warnings in routes.py / test_events_feed_frontend.py / supervisor.py are unchanged from v0.11.3-pre.
- [x] **No hardcoded API key in diff** — `git diff main..HEAD | grep -iE 'apiKey=[A-Z0-9]{6,}|api_key.*=.*"[A-Z0-9]{6,}'` returns empty.
- [x] **No absolute paths in test code** — `TestStaticIsolation` enforces this at runtime.

## Deploy plan

1. Squash-merge PR #N → tag v0.12.1 at merge SHA → push tag.
2. `ssh central`, `git pull` on `/opt/central`. **No `uv sync`** (aiohttp already in venv from earlier adapters).
3. **Matt adds the n2yo API key via GUI `/api-keys` page** (Add → alias `n2yo` → paste key). Do this **before** enabling the adapter — missing-key path is graceful but the adapter logs INFO and skips polling until the key lands.
4. Apply migration 040 manually via psql (per option C established pattern):
   `sudo -u postgres psql central -f /opt/central/sql/migrations/040_add_n2yo_visualpasses_adapter.sql`
   **Do NOT** run `central-migrate` — orphan migrations 032-039 stay deferred for the morning queue.
5. `sudo systemctl restart central-supervisor` (picks up the new adapter via discovery) + `sudo systemctl restart central-gui` (picks up new partials + ADAPTER_GROUPS change).
6. **No** `central-archive` restart (CENTRAL_SAT pre-existed; only the adapter row is new).
7. Verify: `config.adapters` has `n2yo_visualpasses` row with `enabled=false`; `config.api_keys` has alias `n2yo`; supervisor log shows the adapter discovered but not polling (matches `enabled=false`).
8. Matt enables via `/adapters/n2yo_visualpasses/edit` when ready. First poll happens within 1h; events surface at `/events` filtered by adapter=n2yo_visualpasses.

## Halt acknowledgment

Per spec acceptance bar #6: **squash-merge NOT authorized**. Branch + PR open. Halting for line-by-line review.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-09 16:00:55 -06:00
..
fixtures v0.11.0: new celestrak_tle adapter + CENTRAL_SAT satellite-tracking stream (#100) 2026-06-09 00:54:19 -06:00
__init__.py
conftest.py feat(nwis): site + stats enrichment — named location + WaterWatch normalcy band (v0.8.0) 2026-05-25 15:30:19 +00:00
README.md chore: normalize line endings to LF 2026-05-16 22:26:12 +00:00
test_adapters.py fix(4-1): resolve api_key alias from per-adapter settings, not class attr 2026-05-19 23:08:11 +00:00
test_api_key_resolver.py fix(4-1): resolve api_key alias from per-adapter settings, not class attr 2026-05-19 23:08:11 +00:00
test_api_keys.py feat(gui): implement first-run setup wizard (1b-8) (#24) 2026-05-17 22:06:22 -06:00
test_apply_enrichment_coordless.py fix(3-M.b): apply_enrichment always attaches _enriched for declared adapters 2026-05-21 04:04:25 +00:00
test_archive_bbox_filter.py v0.10.2: monitoring-area bbox enforced at supervisor publish (was archive-only) (#PR_NUMBER_PLACEHOLDER) 2026-06-05 20:34:10 -06:00
test_archive_multi_stream.py feat(2-E.5): single-source-of-truth stream registry 2026-05-19 07:37:01 +00:00
test_audit.py feat(gui): add auth core, setup gate, and first-run operator creation 2026-05-17 05:30:49 +00:00
test_auth.py feat(gui): implement first-run setup wizard (1b-8) (#24) 2026-05-17 22:06:22 -06:00
test_avalanche_org.py v0.10.11: extend avalanche_org adapter — tombstones, geo.bbox, hyphen slugs (#99) 2026-06-08 23:08:22 -06:00
test_backend_settings_schema.py fix(3-L.5): per-backend settings schemas (fixes build_enrichers TypeError) 2026-05-20 23:10:10 +00:00
test_bootstrap_config.py chore: housekeeping - orphan branch + three stale tests (#22) 2026-05-17 18:14:58 -06:00
test_celestrak_tle.py v0.11.1: satpass_predict adapter (server-side pass alerts for fixed observers) (#101) 2026-06-09 01:16:43 -06:00
test_config_source.py chore(M-b): clear get_settings lru_cache in test fixtures (fixes order-dependent crypto failures + 3 latent siblings) 2026-05-21 15:51:51 +00:00
test_config_store.py chore(M-b): clear get_settings lru_cache in test fixtures (fixes order-dependent crypto failures + 3 latent siblings) 2026-05-21 15:51:51 +00:00
test_consumer_doc.py v0.10.3: rip out state_511_atis adapter (superseded by itd_511 v0.10.0; Castle Rock legacy shape EOL per sister-site discovery) (#88) 2026-06-06 14:44:00 -06:00
test_crypto.py chore: normalize line endings to LF 2026-05-16 22:26:12 +00:00
test_csrf_handler.py feat(gui): implement first-run setup wizard (1b-8) (#24) 2026-05-17 22:06:22 -06:00
test_csrf_race_condition.py feat(gui): implement first-run setup wizard (1b-8) (#24) 2026-05-17 22:06:22 -06:00
test_dashboard.py feat(2-E.5): single-source-of-truth stream registry 2026-05-19 07:37:01 +00:00
test_dedup_mixin.py fix(wzdx): drop 'unknown' direction from subject + extract dedup mixin (v0.9.1) 2026-05-25 21:18:21 +00:00
test_enrichment_config_plumbing.py fix(3-L.5): per-backend settings schemas (fixes build_enrichers TypeError) 2026-05-20 23:10:10 +00:00
test_enrichment_framework.py feat(3-J): enrichment framework + GeocoderEnricher + NoOpBackend + FIRMS pilot 2026-05-20 04:39:49 +00:00
test_enrichment_locations_coverage.py feat(gui-bugs): fix eonet dashboard exception + out-of-range map bbox 2026-05-24 22:38:13 +00:00
test_enrichment_mile_marker.py v0.10.6: extract mile_marker from itd_511 comment field as _enriched.mile_marker (#94) 2026-06-07 21:38:04 -06:00
test_eonet.py v0.9.20: regional subject routing on quake / fire / hydro / disaster adapters 2026-05-27 23:50:30 -06:00
test_events_adapter_column.py docs: add test database setup, restore geom to test fixture 2026-05-17 18:26:48 +00:00
test_events_bbox_guard.py feat(map-rework): fit-to-results, marker clustering, map-filter toggle, shape/opacity encoding (v0.7.2) 2026-05-25 01:20:04 +00:00
test_events_feed.py feat(api): add paginated events feed JSON endpoint (#25) 2026-05-17 22:31:00 -06:00
test_events_feed_frontend.py v0.12.1: n2yo_visualpasses adapter (server-side visible-pass alerts) 2026-06-09 16:00:55 -06:00
test_events_filtering.py Hide tombstones from default events view + show-removed toggle (v0.9.11) 2026-05-26 22:14:38 +00:00
test_events_pagination.py feat(layout-pagination): collapse legend, stabilize rows, real offset paginator (v0.7.3) 2026-05-25 02:04:23 +00:00
test_events_retention.py v0.9.13: per-stream archived-events retention sweep 2026-05-27 02:31:11 +00:00
test_fire_fused.py v0.9.14: fused FIRMS+WFIGS fire view 2026-05-27 03:49:30 +00:00
test_firms.py v0.10.2: monitoring-area bbox enforced at supervisor publish (was archive-only) (#PR_NUMBER_PLACEHOLDER) 2026-06-05 20:34:10 -06:00
test_form_descriptors.py v0.11.3: fix GUI adapter-edit 500 on list[int] settings fields 2026-06-09 13:26:57 -06:00
test_gdacs.py fix(2-E): use canonical removed-event subject pattern 2026-05-19 07:08:15 +00:00
test_geocoder_enricher.py feat(3-J): enrichment framework + GeocoderEnricher + NoOpBackend + FIRMS pilot 2026-05-20 04:39:49 +00:00
test_gui_adapter_edit.py v0.10.3: rip out state_511_atis adapter (superseded by itd_511 v0.10.0; Castle Rock legacy shape EOL per sister-site discovery) (#88) 2026-06-06 14:44:00 -06:00
test_gui_scaffold.py fix(tests): update tests for lazy app loading and 302 redirect 2026-05-17 06:14:25 +00:00
test_inciweb.py fix(2-C): wire dedup into poll loop, add conditional fetch 2026-05-19 03:53:10 +00:00
test_itd_511.py v0.10.6: extract mile_marker from itd_511 comment field as _enriched.mile_marker (#94) 2026-06-07 21:38:04 -06:00
test_itd_511_cameras.py v0.10.0: ITD 511 official API adapter (events + advisories + cameras) (#85) 2026-06-03 22:36:26 -06:00
test_migrate.py v0.9.18: reconcile schema_migrations drift + add --check drift detection 2026-05-27 06:40:38 +00:00
test_models.py v0.10.8: discriminate Nats-Msg-Id by event.category to prevent incident+perimeter dedup collision (#96) 2026-06-08 01:12:22 -06:00
test_monitoring_area.py v0.10.9: widen monitoring-area default to cover all of Idaho (49.0N) (#97) 2026-06-08 01:42:59 -06:00
test_n2yo_visualpasses.py v0.12.1: n2yo_visualpasses adapter (server-side visible-pass alerts) 2026-06-09 16:00:55 -06:00
test_navi_backend.py feat(3-K.5): operator-settable EnrichmentConfig (config plumbing) 2026-05-20 18:52:22 +00:00
test_nominatim_backend.py feat(3-K): real geocoder backends + producer-doc reframe + consumer-doc enrichment 2026-05-20 16:10:44 +00:00
test_nwis.py v0.9.20: regional subject routing on quake / fire / hydro / disaster adapters 2026-05-27 23:50:30 -06:00
test_nwis_enrichment.py feat(nwis): site + stats enrichment — named location + WaterWatch normalcy band (v0.8.0) 2026-05-25 15:30:19 +00:00
test_nws_normalization.py v0.10.7: fix NWS SAME state-FIPS parse + 5-digit ANSI county form (#95) 2026-06-08 00:30:13 -06:00
test_photon_backend.py feat(3-K): real geocoder backends + producer-doc reframe + consumer-doc enrichment 2026-05-20 16:10:44 +00:00
test_preview_hook.py fix(2-G.5): preview_for_settings contract in adapter docstring + distinguish [] from None 2026-05-19 17:55:39 +00:00
test_producer_doc.py feat(3-K): real geocoder backends + producer-doc reframe + consumer-doc enrichment 2026-05-20 16:10:44 +00:00
test_region_picker.py feat(gui): generic adapter edit form 2026-05-18 23:16:37 +00:00
test_requires_api_key.py fix(2-A3b): complete error-render path, fix link, add supervisor tests 2026-05-19 02:17:29 +00:00
test_resend.py v0.10.5.2: fix BY_START_TIME feedback loop in Re-send (snapshot last_seq boundary) (#93) 2026-06-06 22:36:04 -06:00
test_sat_common.py v0.12.0: sat_positions adapter (live global satellite positions) + sat_common refactor 2026-06-09 15:23:32 -06:00
test_sat_positions.py v0.12.0: sat_positions adapter (live global satellite positions) + sat_common refactor 2026-06-09 15:23:32 -06:00
test_satpass_predict.py v0.12.0: sat_positions adapter (live global satellite positions) + sat_common refactor 2026-06-09 15:23:32 -06:00
test_session_auth.py feat(gui): implement first-run setup wizard (1b-8) (#24) 2026-05-17 22:06:22 -06:00
test_setup_gate.py feat(gui): implement first-run setup wizard (1b-8) (#24) 2026-05-17 22:06:22 -06:00
test_stream_registry.py feat(2-E.5): single-source-of-truth stream registry 2026-05-19 07:37:01 +00:00
test_streams.py feat(gui): implement first-run setup wizard (1b-8) (#24) 2026-05-17 22:06:22 -06:00
test_subject_helpers.py v0.9.20: regional subject routing on quake / fire / hydro / disaster adapters 2026-05-27 23:50:30 -06:00
test_supervisor_hotreload.py chore(lint-cleanup): remove 10 pre-existing ruff issues in 4 test files 2026-05-21 18:20:18 +00:00
test_supervisor_integration.py chore(lint-cleanup): remove 10 pre-existing ruff issues in 4 test files 2026-05-21 18:20:18 +00:00
test_supervisor_publish_filter.py v0.10.2: monitoring-area bbox enforced at supervisor publish (was archive-only) (#PR_NUMBER_PLACEHOLDER) 2026-06-05 20:34:10 -06:00
test_swpc.py feat(2-D): add NOAA SWPC space weather adapters (alerts, kindex, protons) 2026-05-19 05:55:29 +00:00
test_telemetry_separation.py v0.12.0: sat_positions adapter (live global satellite positions) + sat_common refactor 2026-06-09 15:23:32 -06:00
test_tomtom_flow.py v0.10.2: monitoring-area bbox enforced at supervisor publish (was archive-only) (#PR_NUMBER_PLACEHOLDER) 2026-06-05 20:34:10 -06:00
test_tomtom_flow_passthrough.py feat(tomtom_flow): Navi passthrough endpoint /api/traffic/flow (v0.9.4) 2026-05-26 00:04:02 +00:00
test_tomtom_incidents.py fix(gui): generic model_list editor for list-of-model adapters + TomTom bbox validation & quota (v0.9.9) 2026-05-26 05:57:34 +00:00
test_usgs_quake.py v0.9.20: regional subject routing on quake / fire / hydro / disaster adapters 2026-05-27 23:50:30 -06:00
test_wfigs.py v0.10.4: switch wfigs_incidents to non-Current endpoint w/ WF active-only filter (resurrects IMT-managed fires like Blue Ridge) (#89) 2026-06-06 18:10:16 -06:00
test_wizard.py fix(wizard): eliminate all hardcoded field.name branches 2026-05-19 01:01:56 +00:00
test_wzdx.py WZDx: poll-time state allowlist with Idaho-region default (v0.9.17) 2026-05-27 05:57:57 +00:00

Central Tests

Test Database

Some tests (notably test_config_store.py) require a real PostgreSQL database. By default, tests connect to:

postgresql://central_test:testpass@localhost/central_test

If your test database uses different credentials, set the CENTRAL_TEST_DB_DSN environment variable:

export CENTRAL_TEST_DB_DSN="postgresql://myuser:mypass@localhost/mydb"
uv run pytest tests/test_config_store.py