mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-08-25 03:50:03 +00:00
Builds on the merged rain/snow nowcast (#193): ten enhancements plus end-to-end, proactive, and live-smoke test coverage. All new behavior is config-gated or additive, so existing deployments are unaffected by default. Enhancements - Precip amount estimate "(est 0.2 in)" on the command and the proactive push; snow shown as real depth (Open-Meteo snowfall, cm); freezing rain tagged "in ice". - Bare country / US state resolves to its capital with a heads-up (self-contained modules/region_capitals.py; no pycountry/us dependency). - join_location() dedupes "Spain, Spain" / city-states. - !snow alias + neutral !nowcast; each looks for its own precip family across the window, else falls back with a "No snow, but rain ..." cross-type line. - Keyword-aware help (help rain / help snow). - Precip probability shown "(..., 70%)"; the proactive incoming alert is gated to >= [Weather_Service] rain_nowcast_min_probability (default 50). - Rain<->snow changeover line when the window holds both families. - Borderline temperature tag (30-38F). - Short-lived series cache shared by the command and the proactive poll. Tests - test_rain_command_e2e.py: drives RainCommand.execute() end to end and asserts the exact rendered reply across dry/incoming/raining, snow depth, cross-type mismatch, changeover, ice, temp tag, region capitals, config toggles, DM budget, and keyword-aware help. - test_rain_proactive_e2e.py: drives Weather_Service._check_rain_nowcast() for the probability gate, snow depth, ending notice, and once-per-episode dedup. - test_rain_live_smoke.py: opt-in (RAIN_LIVE_SMOKE=1) live Open-Meteo check for upstream schema drift; skipped in CI. - Shared scaffolding in tests/unit/_rain_harness.py. New config: [Rain_Command] show_amount/amount_unit/show_probability/show_temp/ cache_seconds/zip_city_lookup; [Weather_Service] rain_nowcast_min_probability/ rain_nowcast_cache_seconds/rain_nowcast_show_amount/rain_nowcast_amount_unit. ruff + mypy clean.
80 lines
3.3 KiB
Python
80 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Live smoke test against the real Open-Meteo API.
|
|
|
|
Opt-in (network): SKIPPED unless RAIN_LIVE_SMOKE is set, so CI and the normal
|
|
offline suite never depend on it. The mocked suites can't catch upstream schema
|
|
drift (Open-Meteo renaming/dropping a field, or no longer serving 15-min
|
|
probability/temperature); this one can, and it exercises the real fetch end to
|
|
end through the command. It does NOT geocode (Nominatim) — coordinates are fixed
|
|
and only the label is stubbed.
|
|
|
|
RAIN_LIVE_SMOKE=1 .venv/bin/python -m pytest tests/unit/test_rain_live_smoke.py -o addopts="" -s -q
|
|
"""
|
|
|
|
import os
|
|
|
|
import pytest
|
|
import requests
|
|
|
|
from modules.commands.rain_command import fetch_precip_series
|
|
from tests.unit._rain_harness import build_cmd, render
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
os.environ.get("RAIN_LIVE_SMOKE") in (None, "", "0"),
|
|
reason="live network test; set RAIN_LIVE_SMOKE=1 to run",
|
|
)
|
|
|
|
# (label, lat, lon) — a geographic spread so at least one usually has weather.
|
|
PLACES = [
|
|
("Nashville, TN", 36.1627, -86.7816),
|
|
("London, UK", 51.5072, -0.1276),
|
|
("Seattle, WA", 47.6062, -122.3321),
|
|
]
|
|
|
|
# Keys the analysis + formatting rely on; prob/temp are the newer dependencies.
|
|
REQUIRED_KEYS = {"times", "precip", "snow", "prob", "temp", "codes", "now", "step"}
|
|
|
|
|
|
def test_live_fetch_schema_is_intact():
|
|
"""The real API still returns every field we consume, at aligned lengths,
|
|
with 15-min probability + temperature populated (the features added last)."""
|
|
session = requests.Session()
|
|
try:
|
|
for label, lat, lon in PLACES:
|
|
series = fetch_precip_series(session, lat, lon, timeout=15)
|
|
assert series is not None, f"{label}: no series returned"
|
|
missing = REQUIRED_KEYS - series.keys()
|
|
assert not missing, f"{label}: missing keys {missing}"
|
|
|
|
n = len(series["times"])
|
|
assert n > 0, f"{label}: empty time series"
|
|
assert series["step"] in (15, 60)
|
|
for k in ("precip", "codes"):
|
|
assert len(series[k]) == n, f"{label}: {k} length {len(series[k])} != times {n}"
|
|
|
|
prob_ok = sum(p is not None for p in series["prob"])
|
|
temp_ok = sum(t is not None for t in series["temp"])
|
|
assert prob_ok > 0, f"{label}: probability series all None — upstream drift?"
|
|
assert temp_ok > 0, f"{label}: temperature series all None — upstream drift?"
|
|
|
|
print(
|
|
f"\n{label:14s} step={series['step']}min n={n} "
|
|
f"prob_nonnull={prob_ok} temp_nonnull={temp_ok} now={series['now']}"
|
|
)
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
def test_live_command_renders_for_real_places():
|
|
"""Drive execute() with a real fetch for each place and print the real reply.
|
|
Asserts only invariants (non-empty, fits budget, no stray 'None')."""
|
|
print("\n--- live !rain / !snow ---")
|
|
for label, lat, lon in PLACES:
|
|
for word in ("rain", "snow"):
|
|
# series=None -> real Open-Meteo fetch; location is fixed (no geocoding).
|
|
cmd, cap = build_cmd(series=None, coords=(lat, lon), label=label)
|
|
resp = render(cmd, cap, f"!{word}")
|
|
assert resp, f"{label}: empty !{word} reply"
|
|
assert "None" not in resp, f"{label}: stray None in !{word} reply: {resp!r}"
|
|
print(f"!{word:4s} {label:14s} -> {resp}")
|