mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
* fix(fixtures): rewrite capture_fixtures.py to use ephemeral push subscribe
Previous script hardcoded stream="CENTRAL" which does not exist — Central
partitions into domain streams (CENTRAL_QUAKE, CENTRAL_SPACE, etc.). It
also called pull_subscribe_bind() without await, making the fetch a no-op.
Fix: mirror the proven CentralConsumer.start() pattern — use
js.subscribe(subject, cb=..., AckPolicy.NONE, no durable) which auto-
discovers the correct stream via the subject filter, identical to how the
live consumer binds. Messages are funnelled through asyncio.Queue with
an idle-timeout to detect drain completion.
Adds live captured fixtures:
- tests/fixtures/quake/ — 3 envelopes (CENTRAL_QUAKE stream, mode=all)
- tests/fixtures/swpc/ — 40 envelopes (CENTRAL_SPACE, mode=all, proton_flux history)
- tests/fixtures/swpc_last/ — 23 envelopes (mode=last: 21 alert variants + kindex + proton_flux)
Avalanche: confirmed empty off-season (CENTRAL_AVY stream, 0 messages).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(phase1): migrate quake, swpc(Kp+flare), avalanche + staged-cutover gate
First hazards on the source-agnostic formatter+decider path, behind a
staged-cutover gate so deploy = shadow-only (old path still broadcasts; new
path dry-run-diffed) until MESHAI_CUTOVER_CATEGORIES flips a category live.
- formatters/{quake,swpc,avalanche}.py + gating/{quake,swpc,avalanche}.py:
source-agnostic format(event,*,now,budget) + decide(data,*,source,now)->GateResult.
quake (earthquake_event, tier-b: render PAGER + live update-prefix), swpc
(geomagnetic_storm + rf_propagation_alert, Kp+flare only; proton/solar_radiation
_storm stays legacy; geomag 600s window re-homed off the module-global into
gating/swpc with a deferred stamp; tier-b scale-based severity), avalanche
(avalanche_warning/watch; centralseverity->NAADS 1-5 remap; synthetic fixtures
off-season).
- central/{quake,swpc,avy}_handler.py bridges: cutover -> new decide()+canonical
data; not-cutover -> exact legacy behavior. env/{usgs_quake,swpc,avalanche}.py
emit canonical Event.data (avalanche stops precomposing). env/store.py generic
native decider hook (cutover-gated).
- notifications/cutover.py (is_cutover via MESHAI_CUTOVER_CATEGORIES); composer
dispatch + shadow hooks are cutover-aware (shadow no-ops once a category is live).
- scripts/capture_fixtures.py fixed (per-domain streams e.g. CENTRAL_QUAKE, await
bind); real quake/swpc fixtures captured; avalanche synthetic.
Tests: +~150 (quake/swpc/avalanche parity+cross-source+gate-sequence+cutover);
0 new failures (34 baseline, 1426 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
76 lines
3.2 KiB
Python
76 lines
3.2 KiB
Python
"""Phase-0 scaffold tests: formatter registry empty + dispatch correctness.
|
|
|
|
(a) With an empty registry, get_formatter returns None for real categories.
|
|
(b) A registered dummy formatter is called verbatim — multi-line output is
|
|
returned as-is (no Mode-B single-line cap applied).
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from meshai.notifications.formatters import FORMATTERS, get_formatter, register
|
|
from meshai.notifications.events import make_event
|
|
from meshai.notifications.renderers.composer import compose_mesh_message
|
|
|
|
|
|
# ── (a) empty registry returns None for known categories ──────────────────────
|
|
|
|
@pytest.mark.parametrize("category", [
|
|
"weather_warning",
|
|
# earthquake_event removed: Phase-1 registers formatters.quake for it.
|
|
"wildfire_incident",
|
|
"road_closure",
|
|
"battery_critical",
|
|
])
|
|
def test_get_formatter_returns_none_while_registry_empty(category):
|
|
"""Un-migrated categories must still return None from get_formatter."""
|
|
# earthquake_event is now migrated (Phase 1); the remaining categories
|
|
# here have no formatter yet and must still fall through to Mode-B.
|
|
assert category not in FORMATTERS, (
|
|
f"Category {category!r} should not be in FORMATTERS yet (not migrated)"
|
|
)
|
|
assert get_formatter(category) is None
|
|
|
|
|
|
# ── (b) registered dummy formatter is dispatched verbatim ────────────────────
|
|
|
|
_SYNTHETIC_CATEGORY = "_test_scaffold_dummy_category_phase0"
|
|
_MULTILINE_OUTPUT = "Line one\nLine two\nLine three"
|
|
|
|
|
|
def _dummy_formatter(event, *, now: float, budget: int) -> str:
|
|
"""Returns a fixed multi-line string to prove verbatim passthrough."""
|
|
return _MULTILINE_OUTPUT
|
|
|
|
|
|
def test_registered_formatter_returns_verbatim_multiline(monkeypatch):
|
|
"""Register a dummy for a synthetic category; compose_mesh_message must
|
|
return the dummy's multi-line output verbatim — newlines preserved — not
|
|
re-processed through Mode-B's single-line budget loop.
|
|
|
|
Cutover gate: the formatter is only dispatched when the category appears in
|
|
MESHAI_CUTOVER_CATEGORIES. This test sets the var to verify the formatter
|
|
IS invoked once cut over (the complementary not-cutover case is covered by
|
|
test_cutover_gate.py).
|
|
"""
|
|
from meshai.notifications.cutover import _clear_cache as _cutover_clear
|
|
# Mark synthetic category as cut over for this test.
|
|
monkeypatch.setenv("MESHAI_CUTOVER_CATEGORIES", _SYNTHETIC_CATEGORY)
|
|
_cutover_clear()
|
|
# Register the dummy (clean up afterwards to avoid cross-test pollution).
|
|
register(_SYNTHETIC_CATEGORY, _dummy_formatter)
|
|
try:
|
|
event = make_event(
|
|
source="test",
|
|
category=_SYNTHETIC_CATEGORY,
|
|
severity="routine",
|
|
title="First line\nSecond line", # multi-line title
|
|
)
|
|
result = compose_mesh_message(event)
|
|
assert result == _MULTILINE_OUTPUT, (
|
|
f"Expected verbatim multi-line output, got: {result!r}"
|
|
)
|
|
# Verify newlines are preserved (Mode-B would strip them).
|
|
assert "\n" in result, "Newlines must survive the formatter dispatch path"
|
|
finally:
|
|
FORMATTERS.pop(_SYNTHETIC_CATEGORY, None)
|
|
_cutover_clear() # restore cache for subsequent tests
|