Files
Lamont/tests/unit/test_reminders.py
Chelsea Lee fbdf33e894
Some checks failed
CI / test (push) Has been cancelled
CI / compose-smoke (push) Has been cancelled
Build reusable bot framework
2026-07-19 21:53:24 -05:00

221 lines
6.6 KiB
Python

from datetime import datetime, timezone
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from modules.reminders.commands import _errorMessage, handleReminder, validateReminder
from modules.reminders.service import _nextRun, normalizeRecurrence
@pytest.mark.parametrize(
"command",
[
{"action": "list"},
{"action": "cancel", "reminder_id": "reminder-1"},
{"action": "set_timezone", "timezone": "America/Chicago"},
{
"action": "create",
"message": "call home",
"run_at": "2999-03-08T09:00:00-05:00",
"recurrence": {"frequency": "weekly", "interval": 2},
},
{"needs_clarification": "What time should I use?"},
],
)
def test_reminder_validator_accepts_supported_commands(command):
assert validateReminder(command) == []
def test_reminder_validator_reports_all_invalid_create_fields():
errors = validateReminder(
{
"action": "create",
"message": " ",
"run_at": "2026-03-08T09:00:00",
"recurrence": {"frequency": "hourly"},
}
)
assert "create requires a reminder message" in errors
assert "run_at must include a timezone offset" in errors
assert "recurrence frequency must be daily or weekly" in errors
@pytest.mark.parametrize(
("value", "expected"),
[
(None, None),
("once", None),
("daily", {"frequency": "daily", "interval": 1}),
(
{"frequency": "WEEKLY", "interval": "3"},
{"frequency": "weekly", "interval": 3},
),
],
)
def test_recurrence_normalization(value, expected):
assert normalizeRecurrence(value) == expected
@pytest.mark.parametrize(
"value",
[
"hourly",
{"frequency": "daily", "interval": 0},
{"frequency": "weekly", "interval": 366},
{"frequency": "daily", "interval": "many"},
],
)
def test_recurrence_normalization_rejects_invalid_values(value):
with pytest.raises(ValueError):
normalizeRecurrence(value)
@pytest.mark.parametrize(
("scheduled_for", "expected"),
[
# America/Chicago enters daylight time on March 8, 2026.
(
datetime(2026, 3, 7, 15, 0, tzinfo=timezone.utc),
datetime(2026, 3, 8, 14, 0, tzinfo=timezone.utc),
),
# It returns to standard time on November 1, 2026.
(
datetime(2026, 10, 31, 14, 0, tzinfo=timezone.utc),
datetime(2026, 11, 1, 15, 0, tzinfo=timezone.utc),
),
],
)
def test_next_run_preserves_local_wall_clock_across_dst(scheduled_for, expected):
result = _nextRun(
scheduled_for,
{"frequency": "daily", "interval": 1},
"America/Chicago",
now=scheduled_for,
)
assert result == expected
assert result.astimezone(__import__("zoneinfo").ZoneInfo("America/Chicago")).hour == 9
def test_next_run_skips_missed_intervals_after_downtime():
scheduled_for = datetime(2026, 3, 7, 15, 0, tzinfo=timezone.utc)
result = _nextRun(
scheduled_for,
{"frequency": "daily", "interval": 1},
"America/Chicago",
now=datetime(2026, 3, 9, 14, 1, tzinfo=timezone.utc),
)
assert result == datetime(2026, 3, 10, 14, 0, tzinfo=timezone.utc)
def _context(response, status):
api = SimpleNamespace(
request=AsyncMock(return_value=(response, status)),
timezone="UTC",
)
return SimpleNamespace(api=api, timezone="UTC", reply=AsyncMock())
@pytest.mark.asyncio
async def test_create_handler_reports_success_and_api_error():
context = _context(
{
"message": "call home",
"next_run_at": "2099-01-01T12:00:00+00:00",
"recurrence": {"frequency": "daily"},
},
201,
)
parsed = {
"action": "create",
"message": "call home",
"run_at": "2099-01-01T12:00:00Z",
"recurrence": {"frequency": "daily"},
}
await handleReminder(context, parsed)
assert "recurring" in context.reply.await_args.args[0]
context.api.request.assert_awaited_once_with(
"post",
"/api/reminders",
{
"message": "call home",
"run_at": "2099-01-01T12:00:00Z",
"recurrence": {"frequency": "daily"},
},
)
context = _context({"error": "database unavailable"}, 503)
await handleReminder(context, parsed)
assert "database unavailable" in context.reply.await_args.args[0]
@pytest.mark.asyncio
async def test_list_handler_formats_results_and_empty_state():
context = _context(
{
"reminders": [
{
"id": "reminder-one",
"next_run_at": "2099-01-01T12:00:00+00:00",
"message": "call home",
}
]
},
200,
)
await handleReminder(context, {"action": "list"})
reply = context.reply.await_args.args[0]
assert "Active reminders" in reply and "reminder-one" in reply
context = _context({"reminders": []}, 200)
await handleReminder(context, {"action": "list"})
assert context.reply.await_args.args[0] == "You have no active reminders."
context = _context({"error": "offline"}, 503)
await handleReminder(context, {"action": "list"})
assert "offline" in context.reply.await_args.args[0]
@pytest.mark.asyncio
async def test_cancel_and_timezone_handlers_update_context():
context = _context({"message": "call home"}, 200)
await handleReminder(
context,
{"action": "cancel", "reminder_id": "reminder-one"},
)
assert context.reply.await_args.args[0] == "Cancelled reminder: call home"
context.api.request.assert_awaited_once_with(
"delete", "/api/reminders/reminder-one"
)
context = _context({"error": "not found"}, 404)
await handleReminder(
context,
{"action": "cancel", "reminder_id": "missing"},
)
assert "not found" in context.reply.await_args.args[0]
context = _context({"timezone": "America/Chicago"}, 200)
await handleReminder(
context,
{"action": "set_timezone", "timezone": "America/Chicago"},
)
assert context.timezone == "America/Chicago"
assert context.api.timezone == "America/Chicago"
context = _context({}, 400)
await handleReminder(
context,
{"action": "set_timezone", "timezone": "bad"},
)
assert "couldn't update" in context.reply.await_args.args[0]
def test_error_message_handles_structured_and_unstructured_results():
assert _errorMessage({"error": "detail"}, "fallback") == "fallback detail"
assert _errorMessage("not an object", "fallback") == "fallback"