Files
meshcore-bot/tests/unit/test_gwx_day_night.py
agessaman 17a93104cc fix(gwx): label current conditions by is_day, moon at night, name tomorrow's weather
At 5:58 am wx 98258 (Open-Meteo provider) replied "Tonight: ☀️Clear … | H:68°F L:50°F | Tomorrow: 🌫️ H:67°F L:51°F". Any hour before 06:00 was labeled "Tonight", which next to today's high/low and a "Tomorrow" for the following day read like last night's forecast, and code 0 always showed a sun.

I now request Open-Meteo's is_day and use it for both the label and the emoji, falling back to 06:00–18:00 local when it's absent. Night before noon is "Overnight" (the NWS name for that period, new key in every catalog), night after noon stays "Tonight". Clear and mostly clear at night show 🌙. Tomorrow's segment adds its description when it fits and keeps the emoji-only form otherwise.

Goldens change for the new request field, Overnight on the fixtures captured after midnight, 🌙 on clear night fixtures, and tomorrow's description.
2026-10-02 07:04:52 -07:00

96 lines
3.7 KiB
Python

"""gwx names the current period and picks its clear-sky emoji from the sun at the location."""
import configparser
import copy
import json
from pathlib import Path
from unittest.mock import Mock, patch
import pytest
from modules.commands.alternatives.wx_international import GlobalWxCommand
from modules.i18n import Translator
MODULE = "modules.commands.alternatives.wx_international"
SEATTLE = json.loads((Path(__file__).resolve().parents[1] / "fixtures" / "open_meteo" / "seattle.json").read_text())
def _reply(time, is_day=..., code=0, language="en", budget=None):
config = configparser.ConfigParser()
config.read_dict({"Weather": {"temperature_unit": "fahrenheit"}, "Gwx_Command": {}, "Bot": {}})
bot = Mock()
bot.config = config
bot.translator = Translator(language)
cmd = GlobalWxCommand(bot)
data = copy.deepcopy(SEATTLE)
data["current"]["time"] = time
data["current"]["weather_code"] = code
if is_day is not ...:
data["current"]["is_day"] = is_day
response = Mock(ok=True)
response.json.return_value = data
kwargs = {}
if budget is not None:
cmd.get_max_message_length = Mock(return_value=budget)
kwargs["message"] = Mock()
with patch(f"{MODULE}.requests.get", return_value=response) as get:
text = cmd.get_open_meteo_weather(47.6, -122.3, **kwargs)
return text, get.call_args.kwargs["params"]
def test_is_day_is_requested():
_, params = _reply("2026-10-02T05:58", is_day=0)
assert "is_day" in params["current"].split(",")
@pytest.mark.parametrize(("time", "is_day", "label", "emoji"), [
("2026-10-02T05:58", 0, "Overnight", "🌙"),
("2026-10-02T00:30", 0, "Overnight", "🌙"),
("2026-10-02T21:00", 0, "Tonight", "🌙"),
("2026-10-02T19:30", 0, "Tonight", "🌙"),
("2026-10-02T07:30", 1, "Today", "☀️"),
# is_day wins over the hour: dark at 06:30 in October, still light at 18:30 in June.
("2026-10-02T06:30", 0, "Overnight", "🌙"),
("2026-06-21T18:30", 1, "Today", "☀️"),
])
def test_label_and_emoji_follow_is_day(time, is_day, label, emoji):
text, _ = _reply(time, is_day=is_day)
assert text.startswith(f"{label}: {emoji}Clear ")
@pytest.mark.parametrize(("time", "label", "emoji"), [
("2026-10-02T05:58", "Overnight", "🌙"),
("2026-10-02T12:00", "Today", "☀️"),
("2026-10-02T20:00", "Tonight", "🌙"),
])
def test_without_is_day_the_local_hour_decides(time, label, emoji):
text, _ = _reply(time)
assert text.startswith(f"{label}: {emoji}Clear ")
@pytest.mark.parametrize("code", [2, 3, 45, 61])
def test_cloud_and_rain_emoji_are_the_same_at_night(code):
day, _ = _reply("2026-10-02T12:00", is_day=1, code=code)
night, _ = _reply("2026-10-02T21:00", is_day=0, code=code)
assert day.split(":", 1)[1].split()[0] == night.split(":", 1)[1].split()[0]
def test_overnight_is_translated():
text, _ = _reply("2026-10-02T05:58", is_day=0, language="de")
assert text.startswith("Nachts: 🌙")
def test_tomorrow_names_its_weather_when_it_fits():
text, _ = _reply("2026-10-02T05:58", is_day=0, budget=300)
tomorrow = text.split(" | ")[-1]
code = SEATTLE["daily"]["weather_code"][1]
description = Translator("en").translate(f"commands.gwx.weather_descriptions.{code}")
assert tomorrow.startswith("Tomorrow: ") and f"{description} H:" in tomorrow
def test_tomorrow_falls_back_to_the_emoji_when_the_description_does_not_fit():
emoji = GlobalWxCommand._get_weather_emoji(None, SEATTLE["daily"]["weather_code"][1])
tails = {_reply("2026-10-02T05:58", is_day=0, budget=b)[0].split(" | ")[-1] for b in range(100, 200)}
# Some budget has room for tomorrow but not its description.
assert any(t.startswith(f"Tomorrow: {emoji} H:") for t in tails)