Files
odysseus/tests/test_calendar_update_event_tz.py
T

202 lines
6.7 KiB
Python

"""update_event must anchor datetimes to the user tz, like create_event.
create_event parses a naive/natural-language dtstart in the USER's
timezone (parse_due_for_user -> stored naive-UTC, is_utc=True), but
update_event parsed args["dtstart"] with the raw server-local _parse_dt
and never refreshed is_utc. So updating an event to the same naive value
it was created with silently shifted it by the user's UTC offset (9h for a
Tokyo user) and left is_utc inconsistent. The do_manage_notes update path
was already fixed for the analogous issue.
"""
import json
import uuid
import pytest
import core.database as cdb
from core.database import CalendarEvent
from tests.helpers.sqlite_db import make_temp_sqlite
_TS, _ENGINE, _TMPDB = make_temp_sqlite(cdb.Base.metadata)
@pytest.fixture(autouse=True)
def _bind_temp_db(monkeypatch):
monkeypatch.setattr(cdb, "SessionLocal", _TS)
import routes.calendar_routes as cr
monkeypatch.setattr(cr, "SessionLocal", _TS, raising=False)
yield
@pytest.fixture
def tokyo_offset():
from routes.calendar_routes import set_user_tz_offset
set_user_tz_offset(540) # Tokyo, UTC+9
try:
yield
finally:
set_user_tz_offset(None)
async def test_update_event_dtstart_anchored_to_user_tz(tokyo_offset):
from src.tool_implementations import do_manage_calendar
owner = "tz-" + uuid.uuid4().hex[:6]
naive = "2026-06-10T14:00:00" # 14:00 Tokyo == 05:00 UTC
created = await do_manage_calendar(json.dumps({
"action": "create_event",
"summary": "Standup",
"dtstart": naive,
}), owner=owner)
assert created.get("exit_code", 0) == 0, created
assert created["all_day"] is False
uid = created["uid"]
db = _TS()
try:
ev = db.query(CalendarEvent).filter(CalendarEvent.uid == uid).first()
created_dtstart, created_is_utc = ev.dtstart, ev.is_utc
finally:
db.close()
# Update the same event to the SAME naive wall-clock value.
updated = await do_manage_calendar(json.dumps({
"action": "update_event",
"uid": uid,
"dtstart": naive,
}), owner=owner)
assert updated.get("exit_code", 0) == 0, updated
assert updated["all_day"] is False
db = _TS()
try:
ev = db.query(CalendarEvent).filter(CalendarEvent.uid == uid).first()
# Same input -> same stored moment and same is_utc flag as create.
assert ev.dtstart == created_dtstart
assert bool(ev.is_utc) == bool(created_is_utc)
# And concretely: 14:00 Tokyo is 05:00 UTC, stored naive-UTC.
assert ev.dtstart.hour == 5
assert bool(ev.is_utc) is True
finally:
db.close()
async def test_update_event_with_time_converts_all_day_event_to_timed(tokyo_offset):
from src.tool_implementations import do_manage_calendar
owner = "all-day-to-timed-" + uuid.uuid4().hex[:6]
created = await do_manage_calendar(json.dumps({
"action": "create_event",
"summary": "Summer festival",
"dtstart": "2026-08-28",
"all_day": True,
}), owner=owner)
assert created.get("exit_code", 0) == 0, created
assert created["all_day"] is True
updated = await do_manage_calendar(json.dumps({
"action": "update_event",
"uid": created["uid"],
"dtstart": "2026-08-28T15:30:00",
"dtend": "2026-08-28T16:30:00",
}), owner=owner)
assert updated.get("exit_code", 0) == 0, updated
assert updated["all_day"] is False
assert "has_reminder" in updated
assert updated["dtstart"] == "2026-08-28T06:30:00Z"
db = _TS()
try:
ev = db.query(CalendarEvent).filter(CalendarEvent.uid == created["uid"]).first()
assert bool(ev.all_day) is False
assert bool(ev.is_utc) is True
assert ev.dtstart.hour == 6
assert ev.dtstart.minute == 30
finally:
db.close()
async def test_create_all_day_date_preserves_literal_calendar_day(tokyo_offset):
from src.tool_implementations import do_manage_calendar
owner = "all-day-date-" + uuid.uuid4().hex[:6]
created = await do_manage_calendar(json.dumps({
"action": "create_event",
"summary": "My Birthday",
"dtstart": "2026-10-24",
"all_day": True,
"rrule": "FREQ=YEARLY",
}), owner=owner)
assert created.get("exit_code", 0) == 0, created
db = _TS()
try:
ev = db.query(CalendarEvent).filter(CalendarEvent.uid == created["uid"]).first()
assert bool(ev.all_day) is True
assert bool(ev.is_utc) is False
assert ev.dtstart.isoformat() == "2026-10-24T00:00:00"
assert ev.dtend.isoformat() == "2026-10-25T00:00:00"
finally:
db.close()
async def test_update_event_resolves_exact_title_when_model_sends_id(tokyo_offset):
from src.tool_implementations import do_manage_calendar
owner = "title-update-" + uuid.uuid4().hex[:6]
title = "Temporary calendar fixture"
created = await do_manage_calendar(json.dumps({
"action": "create_event",
"summary": title,
"dtstart": "2030-01-02T10:00",
"dtend": "2030-01-02T11:00",
}), owner=owner)
assert created.get("exit_code", 0) == 0, created
updated = await do_manage_calendar(json.dumps({
"action": "update_event",
"id": title,
"location": "Updated fixture location",
}), owner=owner)
assert updated.get("exit_code", 0) == 0, updated
db = _TS()
try:
event = db.query(CalendarEvent).filter(CalendarEvent.uid == created["uid"]).first()
assert event.location == "Updated fixture location"
finally:
db.close()
async def test_list_events_accepts_start_time_end_time_aliases():
from src.tool_implementations import do_manage_calendar
owner = "list-" + uuid.uuid4().hex[:6]
created = await do_manage_calendar(json.dumps({
"action": "create_event",
"summary": "July planning",
"dtstart": "2026-07-15T12:00:00Z",
"dtend": "2026-07-15T13:00:00Z",
}), owner=owner)
assert created.get("exit_code", 0) == 0, created
listed = await do_manage_calendar(json.dumps({
"action": "list_events",
"start_time": "2026-07-01T00:00:00Z",
"end_time": "2026-08-01T00:00:00Z",
}), owner=owner)
assert listed.get("exit_code", 0) == 0, listed
assert "between 2026-07-01 and 2026-08-01" in listed["response"]
assert [event["summary"] for event in listed["events"]] == ["July planning"]
async def test_list_events_query_without_range_does_not_default_to_two_weeks():
from src.tool_implementations import do_manage_calendar
listed = await do_manage_calendar(json.dumps({
"action": "list_events",
"query": "July",
}), owner="list-" + uuid.uuid4().hex[:6])
assert listed.get("exit_code") == 1, listed
assert "explicit start/end" in listed["error"]