mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
* chore: excise the dead Central NATS consumer path
Central was retired and its database dropped 2026-07-15; its NATS broker no
longer exists. Verified against the live CT108 deployment: all 12 adapters
run feed_source=native, zero on central, and central.enabled is False
(default, never overridden). The consumer and its handlers were unreachable.
Removed:
- central/consumer.py and 6 dead handlers (nws, quake, swpc, nwis, avy,
incident) -- their handle_* entrypoints were reachable only from the
consumer's dispatch
- the Central wiring in main.py (init, guarded start, retry loop, stop path)
- the dead config surface: CentralConsumerConfig, EnvironmentalConfig.central,
adapter_config ("central","severity_thresholds") and its display block
- the nats-py dependency (consumer.py was its only importer)
- 19 test files that exercised only the dead path
KEPT -- these live under central/ but are imported directly by native
adapters, and deleting them would break production:
- wfigs_handler.py: firms_handler._handle_pass_boundary() calls its _render()
on the live FIRMS growth-fire path (env/firms.py -> ingest_hotspot_pixel)
- firms_handler, satpass_handler, tle_handler: split files whose handle_*
entrypoints are dead but whose engines are live. Left intact; splitting
them is separate work.
- pass_predictor, budget, idaho_gauge_sites: fully live.
The usgs_quake keys global_mag_floor / regional_mag_floor / regional_centroid
/ regional_radius_mi / broadcast_pager_alerts are NOT removed despite comments
labelling them "CENTRAL-PATH ONLY" -- notifications/gating/quake.py reads them
unconditionally in the native path. Those comments are corrected separately.
Test-count note: the suite drops ~425 tests. Most were migration PARITY tests
whose sole purpose was proving the native rewrite byte-matched the Central
handler (golden byte-parity, cross-source identity, gate-sequence replay).
With the handler deleted there is nothing left to compare against, so they
cannot exist. Native-only tests were kept and reworked where a test reached
for a central symbol incidentally. This is a real coverage loss, accepted
deliberately: the parity harness proved the refactor faithful, and git
history preserves the originals.
Suite: 1984 passed, 6 failed -- the same 6 pre-existing failures as main
(stale SCHEMA_VERSION x3, expired TLE fixtures x2, one order-dependent),
all being fixed on fix/green-test-suite. No new failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(nws): restore native-only golden coverage for the wire formatter
Commit ca751fb5 deleted the Central nws_handler parity harness along with
the handler itself, which took the ONLY tests that pinned formatters.nws
.format()'s literal wire output. Gate-sequence and schema-conformance
tests already survived natively; the formatter's actual rendered text did
not have any native-only regression net.
Add TestFormatterGolden to test_nws_refactor.py: 3 real-fixture cases plus
6 hand-built pathological cases mined from the deleted test_nws_handler.py
(SVR path-sampling, the "no dangling separator" regression, TOR on-ground
vs radar-indicated, FFW flood-cause detection). Every literal was verified
by temporarily restoring the pre-excision central.nws_handler._render()
from git history (ca751fb5^) in a throwaway, uncommitted script, confirming
byte-identical output against the current native format() for all 37 real
fixtures (nws/ + nws_last/) and all 9 pathological cases, then pinning the
confirmed-matching string as the literal -- not a blind snapshot of
current behavior.
quake/swpc/avalanche/hydro/incident/fire were checked and already carry
equivalent native-only golden coverage (added directly in ca751fb5), so no
changes were needed there.
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>
364 lines
11 KiB
Python
364 lines
11 KiB
Python
"""v0.6-3c API tests for adapter_config + adapter_meta routes.
|
|
|
|
Uses FastAPI TestClient against a tmp DB seeded by the conftest autouse
|
|
fixture. Covers: GET (list, per-adapter, single), PUT (incl. type
|
|
validation), POST reset, GET/PUT meta, cache invalidation propagation
|
|
to the accessor.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from meshai.adapter_config import adapter_config, invalidate_cache, REGISTRY
|
|
from meshai.dashboard.api.adapter_config_routes import router
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
app = FastAPI()
|
|
app.include_router(router, prefix="/api")
|
|
return TestClient(app)
|
|
|
|
|
|
# ============================================================================
|
|
# GET /api/adapter-config (grouped)
|
|
# ============================================================================
|
|
|
|
|
|
def test_list_returns_every_registry_key(client):
|
|
"""The grouped listing must return exactly one row per REGISTRY entry.
|
|
|
|
Previously hard-coded an exact total (59, then 96, then 94 -- the test
|
|
name still said 59 long after the asserted number had drifted twice)
|
|
that rotted every time a key was legitimately added or removed from
|
|
REGISTRY. Comparing against len(REGISTRY) directly is the actual
|
|
invariant under test and can't rot the same way.
|
|
"""
|
|
r = client.get("/api/adapter-config")
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
total = sum(len(v) for v in body.values())
|
|
assert total == len(REGISTRY)
|
|
|
|
|
|
def test_list_grouped_by_adapter(client):
|
|
"""wfigs's key set in the API must match REGISTRY exactly.
|
|
|
|
Previously hard-coded a 5-key set that missed max_declare_age_seconds
|
|
(added by the fire age-gate feature, commit 95b1a23e) -- comparing
|
|
against REGISTRY directly means a future added/removed wfigs key can't
|
|
silently desync this test again.
|
|
"""
|
|
r = client.get("/api/adapter-config")
|
|
body = r.json()
|
|
assert "wfigs" in body
|
|
keys = {row["key"] for row in body["wfigs"]}
|
|
expected = {key for (adapter, key) in REGISTRY if adapter == "wfigs"}
|
|
assert keys == expected
|
|
|
|
|
|
def test_list_includes_type_value_default_description(client):
|
|
r = client.get("/api/adapter-config")
|
|
body = r.json()
|
|
row = next(row for row in body["wfigs"] if row["key"] == "cooldown_seconds")
|
|
assert row["value"] == 28800
|
|
assert row["default"] == 28800
|
|
assert row["type"] == "int"
|
|
assert row["description"]
|
|
|
|
|
|
# ============================================================================
|
|
# GET /api/adapter-config/{adapter}
|
|
# ============================================================================
|
|
|
|
|
|
def test_per_adapter_list(client):
|
|
r = client.get("/api/adapter-config/usgs_quake")
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert isinstance(body, list)
|
|
keys = {row["key"] for row in body}
|
|
assert keys == {
|
|
"regional_centroid", "regional_radius_mi",
|
|
"broadcast_pager_alerts", "global_mag_floor",
|
|
"regional_mag_floor", "escalate_mag_floor",
|
|
}
|
|
|
|
|
|
def test_per_adapter_empty_for_itd_511(client):
|
|
"""itd_511 has zero config keys post-3a.1; returns empty list, not 404."""
|
|
r = client.get("/api/adapter-config/itd_511")
|
|
assert r.status_code == 200
|
|
assert len(r.json()) > 0 # itd_511 has adapter_config keys now
|
|
|
|
|
|
# ============================================================================
|
|
# GET /api/adapter-config/{adapter}/{key}
|
|
# ============================================================================
|
|
|
|
|
|
def test_get_single_key(client):
|
|
r = client.get("/api/adapter-config/usgs_quake/global_mag_floor")
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert body["value"] == 3.0
|
|
assert body["default"] == 3.0
|
|
assert body["type"] == "float"
|
|
|
|
|
|
def test_get_unknown_key_404(client):
|
|
r = client.get("/api/adapter-config/wfigs/no_such_key")
|
|
assert r.status_code == 404
|
|
|
|
|
|
# ============================================================================
|
|
# PUT /api/adapter-config/{adapter}/{key}
|
|
# ============================================================================
|
|
|
|
|
|
def test_put_updates_value(client):
|
|
r = client.put(
|
|
"/api/adapter-config/usgs_quake/global_mag_floor",
|
|
json={"value": 2.8},
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["value"] == 2.8
|
|
# GET reflects the new value.
|
|
g = client.get("/api/adapter-config/usgs_quake/global_mag_floor")
|
|
assert g.json()["value"] == 2.8
|
|
|
|
|
|
def test_put_invalidates_accessor_cache(client):
|
|
"""The handler-side accessor reads the new value WITHOUT a restart."""
|
|
# Prime the cache with the default.
|
|
assert adapter_config.usgs_quake.global_mag_floor == 3.0
|
|
# Mutate via API.
|
|
client.put(
|
|
"/api/adapter-config/usgs_quake/global_mag_floor",
|
|
json={"value": 2.5},
|
|
)
|
|
# Accessor returns the new value -- cache invalidation worked.
|
|
assert adapter_config.usgs_quake.global_mag_floor == 2.5
|
|
|
|
|
|
def test_put_int_validation(client):
|
|
"""int field rejects string body."""
|
|
r = client.put(
|
|
"/api/adapter-config/wfigs/cooldown_seconds",
|
|
json={"value": "two hours"},
|
|
)
|
|
assert r.status_code == 400
|
|
|
|
|
|
def test_put_int_rejects_float_with_fraction(client):
|
|
r = client.put(
|
|
"/api/adapter-config/wfigs/cooldown_seconds",
|
|
json={"value": 3600.5},
|
|
)
|
|
assert r.status_code == 400
|
|
|
|
|
|
def test_put_int_accepts_float_with_integer_value(client):
|
|
r = client.put(
|
|
"/api/adapter-config/wfigs/cooldown_seconds",
|
|
json={"value": 3600.0},
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["value"] == 3600
|
|
|
|
|
|
def test_put_float_accepts_int(client):
|
|
r = client.put(
|
|
"/api/adapter-config/usgs_quake/global_mag_floor",
|
|
json={"value": 4},
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["value"] == 4.0
|
|
|
|
|
|
def test_put_bool_rejects_int(client):
|
|
r = client.put(
|
|
"/api/adapter-config/wfigs/broadcast_on_acres",
|
|
json={"value": 1},
|
|
)
|
|
assert r.status_code == 400
|
|
|
|
|
|
def test_put_str_validation(client):
|
|
r = client.put(
|
|
"/api/adapter-config/geocoder/photon_url",
|
|
json={"value": 42},
|
|
)
|
|
assert r.status_code == 400
|
|
|
|
|
|
def test_put_json_accepts_list(client):
|
|
# broadcast_severities was removed in the config-schema cleanup; use the
|
|
# surviving nws json-list key tombstone_msgtypes to exercise list PUTs.
|
|
r = client.put(
|
|
"/api/adapter-config/nws/tombstone_msgtypes",
|
|
json={"value": ["Cancel"]},
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["value"] == ["Cancel"]
|
|
|
|
|
|
def test_put_json_accepts_dict(client):
|
|
# central/severity_thresholds was removed with the Central handler path;
|
|
# any surviving "json"-type key accepts dict values equally (the API
|
|
# only checks JSON-serializability, not shape) -- use reminders_wfigs/
|
|
# terminate_when (default is a list) to exercise the dict-value path.
|
|
r = client.put(
|
|
"/api/adapter-config/reminders_wfigs/terminate_when",
|
|
json={"value": {"routine_max": 0, "priority_max": 1, "immediate_min": 2}},
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["value"] == {"routine_max": 0, "priority_max": 1, "immediate_min": 2}
|
|
|
|
|
|
def test_put_json_accepts_none(client):
|
|
r = client.put(
|
|
"/api/adapter-config/firms/bbox",
|
|
json={"value": None},
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["value"] is None
|
|
|
|
|
|
def test_put_unknown_key_404(client):
|
|
r = client.put(
|
|
"/api/adapter-config/wfigs/no_such_key",
|
|
json={"value": 1},
|
|
)
|
|
assert r.status_code == 404
|
|
|
|
|
|
def test_put_missing_value_field(client):
|
|
r = client.put(
|
|
"/api/adapter-config/wfigs/cooldown_seconds",
|
|
json={},
|
|
)
|
|
assert r.status_code == 400
|
|
|
|
|
|
# ============================================================================
|
|
# POST /api/adapter-config/{adapter}/{key}/reset
|
|
# ============================================================================
|
|
|
|
|
|
def test_reset_restores_default(client):
|
|
# Mutate.
|
|
client.put(
|
|
"/api/adapter-config/usgs_quake/global_mag_floor",
|
|
json={"value": 2.8},
|
|
)
|
|
assert client.get("/api/adapter-config/usgs_quake/global_mag_floor").json()["value"] == 2.8
|
|
|
|
# Reset.
|
|
r = client.post("/api/adapter-config/usgs_quake/global_mag_floor/reset")
|
|
assert r.status_code == 200
|
|
assert r.json()["value"] == 3.0
|
|
# Accessor too.
|
|
invalidate_cache()
|
|
assert adapter_config.usgs_quake.global_mag_floor == 3.0
|
|
|
|
|
|
def test_reset_invalidates_cache(client):
|
|
"""Reset must invalidate the accessor cache the same as PUT."""
|
|
client.put(
|
|
"/api/adapter-config/usgs_quake/global_mag_floor",
|
|
json={"value": 2.8},
|
|
)
|
|
# Prime cache with the post-PUT value.
|
|
assert adapter_config.usgs_quake.global_mag_floor == 2.8
|
|
client.post("/api/adapter-config/usgs_quake/global_mag_floor/reset")
|
|
# Cache cleared -- next read returns the default.
|
|
assert adapter_config.usgs_quake.global_mag_floor == 3.0
|
|
|
|
|
|
def test_reset_unknown_key_404(client):
|
|
r = client.post("/api/adapter-config/wfigs/no_such_key/reset")
|
|
assert r.status_code == 404
|
|
|
|
|
|
# ============================================================================
|
|
# GET /api/adapter-meta
|
|
# ============================================================================
|
|
|
|
|
|
def test_list_meta(client):
|
|
r = client.get("/api/adapter-meta")
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert "wfigs" in body
|
|
assert body["wfigs"]["include_in_llm_context"] is True
|
|
# geocoder defaults to False (central adapter meta was removed with the
|
|
# Central handler path)
|
|
assert body["geocoder"]["include_in_llm_context"] is False
|
|
|
|
|
|
# ============================================================================
|
|
# PUT /api/adapter-meta/{adapter}
|
|
# ============================================================================
|
|
|
|
|
|
def test_put_meta_toggles_llm_context(client):
|
|
r = client.put(
|
|
"/api/adapter-meta/itd_511",
|
|
json={"include_in_llm_context": False},
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["include_in_llm_context"] is False
|
|
# GET reflects the change.
|
|
g = client.get("/api/adapter-meta").json()
|
|
assert g["itd_511"]["include_in_llm_context"] is False
|
|
|
|
|
|
def test_put_meta_updates_display_name(client):
|
|
r = client.put(
|
|
"/api/adapter-meta/wfigs",
|
|
json={"display_name": "Active wildfires (WFIGS)"},
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["display_name"] == "Active wildfires (WFIGS)"
|
|
|
|
|
|
def test_put_meta_partial_update(client):
|
|
"""Only the fields in the body change; others survive."""
|
|
original = client.get("/api/adapter-meta").json()["wfigs"]
|
|
r = client.put(
|
|
"/api/adapter-meta/wfigs",
|
|
json={"include_in_llm_context": False},
|
|
)
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert body["display_name"] == original["display_name"] # unchanged
|
|
assert body["include_in_llm_context"] is False
|
|
|
|
|
|
def test_put_meta_rejects_bad_bool(client):
|
|
r = client.put(
|
|
"/api/adapter-meta/wfigs",
|
|
json={"include_in_llm_context": "yes"},
|
|
)
|
|
assert r.status_code == 400
|
|
|
|
|
|
def test_put_meta_rejects_empty_display_name(client):
|
|
r = client.put(
|
|
"/api/adapter-meta/wfigs",
|
|
json={"display_name": " "},
|
|
)
|
|
assert r.status_code == 400
|
|
|
|
|
|
def test_put_meta_unknown_adapter_404(client):
|
|
r = client.put(
|
|
"/api/adapter-meta/nonexistent_adapter",
|
|
json={"include_in_llm_context": False},
|
|
)
|
|
assert r.status_code == 404
|