mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
refactor: move source tree into work/, multi-stage Docker build, fix satpass
- Move all application source (meshai/, dashboard-frontend/, tests/, config/, docs/, Dockerfile, etc.) into work/ directory - Add Node.js multi-stage build to Dockerfile for frontend compilation; remove compiled static assets from git tracking - Fix satpass missing time windows: consolidation was splitting wire on newline and only putting line 1 in event.title, dropping the time window line that the composer uses for precomposed broadcasts - Fix satpass burst flooding: stagger consolidation timers (+60s per pending pass) so Central batch publishes don't blast the mesh - Update CI workflow build context to work/ - Anchor lib/ and data/ gitignore patterns to repo root to prevent false matches on nested directories - Add dashboard-frontend/node_modules/ to .dockerignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7128b432ee
commit
2e1fb325f7
283 changed files with 109402 additions and 483 deletions
344
work/tests/test_adapter_config_api.py
Normal file
344
work/tests/test_adapter_config_api.py
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
"""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
|
||||
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_all_59_keys(client):
|
||||
r = client.get("/api/adapter-config")
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
# 14 adapters with at least one key (itd_511 has zero -- not in the
|
||||
# grouped dict because the SQL only returns rows that exist).
|
||||
total = sum(len(v) for v in body.values())
|
||||
assert total == 92
|
||||
|
||||
|
||||
def test_list_grouped_by_adapter(client):
|
||||
r = client.get("/api/adapter-config")
|
||||
body = r.json()
|
||||
assert "wfigs" in body
|
||||
keys = {row["key"] for row in body["wfigs"]}
|
||||
assert keys == {"cooldown_seconds", "anchor_max_mi", "freshness_seconds",
|
||||
"broadcast_on_acres", "broadcast_on_contained"}
|
||||
|
||||
|
||||
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):
|
||||
r = client.put(
|
||||
"/api/adapter-config/nws/broadcast_severities",
|
||||
json={"value": ["Extreme"]},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["value"] == ["Extreme"]
|
||||
|
||||
|
||||
def test_put_json_accepts_dict(client):
|
||||
r = client.put(
|
||||
"/api/adapter-config/central/severity_thresholds",
|
||||
json={"value": {"routine_max": 0, "priority_max": 1, "immediate_min": 2}},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
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
|
||||
# central / geocoder default to False
|
||||
assert body["central"]["include_in_llm_context"] is False
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue