init commit

This commit is contained in:
2026-01-26 15:08:24 -06:00
commit 67225a725a
33 changed files with 3350 additions and 0 deletions

175
tests/test_state_manager.py Normal file
View File

@@ -0,0 +1,175 @@
"""Tests for the state manager."""
import json
import tempfile
from datetime import datetime, timedelta
from pathlib import Path
import pytest
from app.models.alerts import AlertType, TriggeredAlert
from app.services.state_manager import StateManager
def make_triggered_alert(
alert_type: AlertType = AlertType.TEMPERATURE_LOW,
forecast_hour: str = "2024-01-15-12",
) -> TriggeredAlert:
"""Create a test triggered alert."""
return TriggeredAlert(
alert_type=alert_type,
title="Test Alert",
message="Test message",
forecast_hour=forecast_hour,
value=25.0,
threshold=32.0,
)
class TestStateManager:
"""Test state manager functionality."""
def test_load_creates_empty_state_if_no_file(self, tmp_path: Path) -> None:
"""Test that loading from nonexistent file creates empty state."""
state_file = tmp_path / "state.json"
manager = StateManager(str(state_file))
state = manager.load()
assert len(state.sent_alerts) == 0
def test_save_creates_file(self, tmp_path: Path) -> None:
"""Test that saving creates the state file."""
state_file = tmp_path / "subdir" / "state.json"
manager = StateManager(str(state_file))
alert = make_triggered_alert()
manager.record_sent(alert)
manager.save()
assert state_file.exists()
def test_save_and_load_round_trip(self, tmp_path: Path) -> None:
"""Test that state survives save/load cycle."""
state_file = tmp_path / "state.json"
manager = StateManager(str(state_file))
alert = make_triggered_alert()
manager.record_sent(alert)
manager.save()
# Create new manager and load
manager2 = StateManager(str(state_file))
state = manager2.load()
assert len(state.sent_alerts) == 1
assert alert.dedup_key in state.sent_alerts
def test_filter_duplicates_removes_known_alerts(self, tmp_path: Path) -> None:
"""Test that known alerts are filtered out."""
state_file = tmp_path / "state.json"
manager = StateManager(str(state_file))
# Record an alert as sent
alert1 = make_triggered_alert(forecast_hour="2024-01-15-12")
manager.record_sent(alert1)
# Try to send the same alert again plus a new one
alert2 = make_triggered_alert(forecast_hour="2024-01-15-12") # Duplicate
alert3 = make_triggered_alert(forecast_hour="2024-01-15-13") # New
filtered = manager.filter_duplicates([alert2, alert3])
assert len(filtered) == 1
assert filtered[0].forecast_hour == "2024-01-15-13"
def test_different_alert_types_not_duplicates(self, tmp_path: Path) -> None:
"""Test that different alert types for same hour aren't duplicates."""
state_file = tmp_path / "state.json"
manager = StateManager(str(state_file))
alert1 = make_triggered_alert(
alert_type=AlertType.TEMPERATURE_LOW, forecast_hour="2024-01-15-12"
)
manager.record_sent(alert1)
alert2 = make_triggered_alert(
alert_type=AlertType.PRECIPITATION, forecast_hour="2024-01-15-12"
)
filtered = manager.filter_duplicates([alert2])
assert len(filtered) == 1
def test_purge_old_records(self, tmp_path: Path) -> None:
"""Test that old records are purged."""
state_file = tmp_path / "state.json"
manager = StateManager(str(state_file), dedup_window_hours=24)
# Manually create an old record
alert = make_triggered_alert()
manager.state.sent_alerts[alert.dedup_key] = manager.state.sent_alerts.get(
alert.dedup_key
) or type(
"SentAlertRecord",
(),
{
"dedup_key": alert.dedup_key,
"alert_type": alert.alert_type.value,
"sent_at": datetime.now() - timedelta(hours=48),
"forecast_hour": alert.forecast_hour,
"to_dict": lambda self: {
"dedup_key": self.dedup_key,
"alert_type": self.alert_type,
"sent_at": self.sent_at.isoformat(),
"forecast_hour": self.forecast_hour,
},
},
)()
# Use the actual model
from app.models.state import SentAlertRecord
manager.state.sent_alerts[alert.dedup_key] = SentAlertRecord(
dedup_key=alert.dedup_key,
alert_type=alert.alert_type.value,
sent_at=datetime.now() - timedelta(hours=48),
forecast_hour=alert.forecast_hour,
)
purged = manager.purge_old_records()
assert purged == 1
assert len(manager.state.sent_alerts) == 0
def test_atomic_write_creates_temp_file(self, tmp_path: Path) -> None:
"""Test that atomic write doesn't leave temp files on success."""
state_file = tmp_path / "state.json"
manager = StateManager(str(state_file))
alert = make_triggered_alert()
manager.record_sent(alert)
manager.save()
# Check no temp files remain
temp_files = list(tmp_path.glob("state_*.tmp"))
assert len(temp_files) == 0
def test_load_handles_corrupt_json(self, tmp_path: Path) -> None:
"""Test that corrupt JSON is handled gracefully."""
state_file = tmp_path / "state.json"
state_file.write_text("not valid json {{{")
manager = StateManager(str(state_file))
state = manager.load()
# Should return empty state
assert len(state.sent_alerts) == 0
def test_dedup_key_format(self) -> None:
"""Test that dedup key has expected format."""
alert = make_triggered_alert(
alert_type=AlertType.TEMPERATURE_LOW, forecast_hour="2024-01-15-12"
)
assert alert.dedup_key == "temperature_low:2024-01-15-12"