"""Regression tests: calendar tool respects the user's configured timezone.""" from datetime import timezone from unittest.mock import AsyncMock, patch import pytest @pytest.mark.asyncio async def test_create_event_all_day_bare_date_interprets_local_midnight(): """Bare date like '2026-09-30' for a NY user = 2026-09-30 00:00 NY, stored as 2026-09-30 04:00 UTC — NOT 2026-09-30 00:00 UTC (which would be 2026-09-29 19:00 NY and land on the wrong day).""" from fabledassistant.services.tools.calendar import create_event_tool captured = {} async def fake_create_event(**kwargs): captured.update(kwargs) event = AsyncMock() event.to_dict.return_value = {"id": 1, **{k: v for k, v in kwargs.items() if not callable(v)}} return event with patch( "fabledassistant.services.tools.calendar.get_user_tz", AsyncMock(return_value=__import__("zoneinfo").ZoneInfo("America/New_York")), ), patch( "fabledassistant.services.tools.calendar.events_create_event", side_effect=fake_create_event, ): result = await create_event_tool( user_id=1, arguments={"title": "Birthday", "start": "2026-09-30"}, ) assert result["success"] is True start_dt = captured["start_dt"] assert start_dt.tzinfo is not None # Converted to UTC: 00:00 NY (EDT, UTC-4) == 04:00 UTC on the same day utc = start_dt.astimezone(timezone.utc) assert (utc.year, utc.month, utc.day) == (2026, 9, 30) assert utc.hour == 4 # EDT offset; would be 5 in EST assert captured["all_day"] is True @pytest.mark.asyncio async def test_create_event_naive_datetime_is_user_local(): """'2026-03-15T14:30' for a NY user means 2:30pm NY, not 2:30pm UTC.""" from fabledassistant.services.tools.calendar import create_event_tool captured = {} async def fake_create_event(**kwargs): captured.update(kwargs) event = AsyncMock() event.to_dict.return_value = {"id": 1} return event with patch( "fabledassistant.services.tools.calendar.get_user_tz", AsyncMock(return_value=__import__("zoneinfo").ZoneInfo("America/New_York")), ), patch( "fabledassistant.services.tools.calendar.events_create_event", side_effect=fake_create_event, ): await create_event_tool( user_id=1, arguments={"title": "Meeting", "start": "2026-03-15T14:30"}, ) utc = captured["start_dt"].astimezone(timezone.utc) # 14:30 NY on 2026-03-15 (after DST start) = 18:30 UTC assert (utc.year, utc.month, utc.day, utc.hour, utc.minute) == (2026, 3, 15, 18, 30) @pytest.mark.asyncio async def test_create_event_explicit_offset_is_respected(): """Model-supplied timezone offsets must not be overridden by user TZ.""" from fabledassistant.services.tools.calendar import create_event_tool captured = {} async def fake_create_event(**kwargs): captured.update(kwargs) event = AsyncMock() event.to_dict.return_value = {"id": 1} return event with patch( "fabledassistant.services.tools.calendar.get_user_tz", AsyncMock(return_value=__import__("zoneinfo").ZoneInfo("America/New_York")), ), patch( "fabledassistant.services.tools.calendar.events_create_event", side_effect=fake_create_event, ): await create_event_tool( user_id=1, arguments={"title": "Meeting", "start": "2026-03-15T14:30+00:00"}, ) utc = captured["start_dt"].astimezone(timezone.utc) assert (utc.hour, utc.minute) == (14, 30) @pytest.mark.asyncio async def test_list_events_bare_date_range_covers_local_day(): """date_from/date_to as bare dates should cover the full LOCAL day.""" from fabledassistant.services.tools.calendar import list_events_tool captured = {} async def fake_list_events(*, user_id, date_from, date_to): captured["date_from"] = date_from captured["date_to"] = date_to return [] with patch( "fabledassistant.services.tools.calendar.get_user_tz", AsyncMock(return_value=__import__("zoneinfo").ZoneInfo("America/New_York")), ), patch( "fabledassistant.services.tools.calendar.events_list_events", side_effect=fake_list_events, ): await list_events_tool( user_id=1, arguments={"date_from": "2026-09-30", "date_to": "2026-09-30"}, ) df = captured["date_from"].astimezone(timezone.utc) dt = captured["date_to"].astimezone(timezone.utc) # 2026-09-30 00:00 NY (EDT) = 04:00 UTC same day assert (df.year, df.month, df.day, df.hour) == (2026, 9, 30, 4) # 2026-09-30 23:59:59 NY (EDT) = 03:59:59 UTC next day assert (dt.year, dt.month, dt.day, dt.hour) == (2026, 10, 1, 3)