diff --git a/.gitignore b/.gitignore index 7f134e5..7f31157 100644 --- a/.gitignore +++ b/.gitignore @@ -144,6 +144,7 @@ celerybeat.pid # Environments .env +.env.* .envrc .venv env/ @@ -239,5 +240,8 @@ backend/src/services/linear/api/resolvers.py.backup backend/FILES_CREATED.md third_party/ slack_credentials.md -.env -.env.local +local_experiments/ +.claude/settings.local.json +.claude/** +!.claude/CLAUDE.md +!.claude/settings.json diff --git a/backend/src/services/calendar/api/methods.py b/backend/src/services/calendar/api/methods.py index 9146aa2..bd56dbe 100644 --- a/backend/src/services/calendar/api/methods.py +++ b/backend/src/services/calendar/api/methods.py @@ -9,6 +9,7 @@ import json import logging +from datetime import timezone from typing import Any, Callable, Awaitable, Optional from functools import wraps @@ -105,9 +106,73 @@ generate_resource_id, etags_match, REPLICA_NOW_RFC3339, + parse_rfc3339, ) +# ============================================================================ +# WATCH EXPIRATION PARSING +# ============================================================================ + + +def parse_watch_expiration(expiration: Any) -> Optional[int]: + """ + Parse expiration value for watch channels. + + Google Calendar API accepts expiration as milliseconds since Unix epoch. + Some clients may send ISO date strings which need to be converted. + + Args: + expiration: The expiration value (int, string int, or ISO date string) + + Returns: + Expiration in milliseconds since epoch, or None if not provided + + Raises: + ValidationError: If the expiration format is invalid + """ + if expiration is None: + return None + + # If already an int, return as-is + if isinstance(expiration, int): + return expiration + + # If it's a string, try to parse it + if isinstance(expiration, str): + expiration = expiration.strip() + if not expiration: + return None + + # Try parsing as integer (milliseconds) + try: + return int(expiration) + except ValueError: + pass + + # Try parsing as ISO date string + try: + dt = parse_rfc3339(expiration) + # If datetime is naive, assume UTC + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + # Convert to milliseconds since epoch + return int(dt.timestamp() * 1000) + except (ValueError, AttributeError): + pass + + # Invalid format + raise ValidationError( + f"Invalid expiration format: {expiration}. " + "Expected milliseconds since epoch or ISO 8601 date string." + ) + + raise ValidationError( + f"Invalid expiration type: {type(expiration).__name__}. " + "Expected integer or string." + ) + + # ============================================================================ # REQUEST UTILITIES # ============================================================================ @@ -1036,7 +1101,7 @@ async def calendar_list_watch(request: Request) -> JSONResponse: from ..database.schema import Channel resource_id = generate_resource_id() - expiration = body.get("expiration") + expiration_ms = parse_watch_expiration(body.get("expiration")) channel = Channel( id=channel_id, @@ -1044,7 +1109,7 @@ async def calendar_list_watch(request: Request) -> JSONResponse: resource_uri=f"/users/me/calendarList", type=channel_type, address=address, - expiration=int(expiration) if expiration else None, + expiration=expiration_ms, token=body.get("token"), params=body.get("params"), payload=body.get("payload", False), @@ -1961,7 +2026,7 @@ async def events_watch(request: Request) -> JSONResponse: from ..database.schema import Channel resource_id = generate_resource_id() - expiration = body.get("expiration") + expiration_ms = parse_watch_expiration(body.get("expiration")) channel = Channel( id=channel_id, @@ -1969,7 +2034,7 @@ async def events_watch(request: Request) -> JSONResponse: resource_uri=f"/calendars/{calendar_id}/events", type=channel_type, address=address, - expiration=int(expiration) if expiration else None, + expiration=expiration_ms, token=body.get("token"), params=body.get("params"), payload=body.get("payload", False), @@ -2400,7 +2465,7 @@ async def acl_watch(request: Request) -> JSONResponse: from ..database.schema import Channel resource_id = generate_resource_id() - expiration = body.get("expiration") + expiration_ms = parse_watch_expiration(body.get("expiration")) channel = Channel( id=channel_id, @@ -2408,7 +2473,7 @@ async def acl_watch(request: Request) -> JSONResponse: resource_uri=f"/calendars/{calendar_id}/acl", type=channel_type, address=address, - expiration=int(expiration) if expiration else None, + expiration=expiration_ms, token=body.get("token"), user_id=user_id, # Track ownership params=body.get("params"), @@ -2681,7 +2746,7 @@ async def settings_watch(request: Request) -> JSONResponse: from ..database.schema import Channel resource_id = generate_resource_id() - expiration = body.get("expiration") + expiration_ms = parse_watch_expiration(body.get("expiration")) channel = Channel( id=channel_id, @@ -2689,7 +2754,7 @@ async def settings_watch(request: Request) -> JSONResponse: resource_uri=f"/users/{user_id}/settings", type=channel_type, address=address, - expiration=int(expiration) if expiration else None, + expiration=expiration_ms, token=body.get("token"), params=body.get("params"), payload=body.get("payload", False), diff --git a/backend/src/services/calendar/database/operations.py b/backend/src/services/calendar/database/operations.py index 0a5ff1d..a5f8e67 100644 --- a/backend/src/services/calendar/database/operations.py +++ b/backend/src/services/calendar/database/operations.py @@ -603,6 +603,13 @@ def create_event( start_date = start.get("date") end_date = end.get("date") + # Extract organizer/creator fields from kwargs to allow override (e.g., for import) + # Use provided values if present, otherwise default to current user + # This matches Google Calendar API behavior where imports preserve original organizer + organizer_email = kwargs.pop("organizer_email", None) or user.email + organizer_display_name = kwargs.pop("organizer_display_name", None) or user.display_name + organizer_self = organizer_email == user.email + event = Event( id=event_id, calendar_id=calendar.id, @@ -623,9 +630,9 @@ def create_event( creator_display_name=user.display_name, creator_self=True, organizer_id=user_id, - organizer_email=user.email, - organizer_display_name=user.display_name, - organizer_self=True, + organizer_email=organizer_email, + organizer_display_name=organizer_display_name, + organizer_self=organizer_self, etag=generate_etag(f"{event_id}:1"), **{k: v for k, v in kwargs.items() if hasattr(Event, k)}, ) @@ -633,7 +640,10 @@ def create_event( # Add attendees if attendees: - for attendee_data in attendees: + for idx, attendee_data in enumerate(attendees): + # Validate email is required for attendees (per Google Calendar API) + if "email" not in attendee_data or not attendee_data["email"]: + raise RequiredFieldError(f"attendees[{idx}].email") attendee = EventAttendee( event_id=event_id, email=attendee_data["email"], @@ -1030,13 +1040,21 @@ def list_events( all_events.append(instance) + # Helper to normalize datetime to timezone-aware for comparison + def _normalize_dt(dt: Optional[datetime]) -> datetime: + if dt is None: + return datetime.min.replace(tzinfo=timezone.utc) + if dt.tzinfo is None: + return dt.replace(tzinfo=timezone.utc) + return dt + # Sort combined results if order_by == "startTime": - all_events.sort(key=lambda e: (e.start_datetime or datetime.min.replace(tzinfo=timezone.utc), e.id)) + all_events.sort(key=lambda e: (_normalize_dt(e.start_datetime), e.id)) elif order_by == "updated": - all_events.sort(key=lambda e: (e.updated_at or datetime.min.replace(tzinfo=timezone.utc), e.id), reverse=True) + all_events.sort(key=lambda e: (_normalize_dt(e.updated_at), e.id), reverse=True) else: - all_events.sort(key=lambda e: (e.start_datetime or datetime.min.replace(tzinfo=timezone.utc), e.id)) + all_events.sort(key=lambda e: (_normalize_dt(e.start_datetime), e.id)) # Apply pagination to combined results (offset already decoded above) paginated_events = all_events[offset:offset + max_results + 1] @@ -1127,7 +1145,10 @@ def update_event( ) # Add new attendees user = session.get(User, user_id) - for attendee_data in kwargs["attendees"]: + for idx, attendee_data in enumerate(kwargs["attendees"]): + # Validate email is required for attendees (per Google Calendar API) + if "email" not in attendee_data or not attendee_data["email"]: + raise RequiredFieldError(f"attendees[{idx}].email") attendee = EventAttendee( event_id=event_id, email=attendee_data["email"], @@ -2009,13 +2030,21 @@ def get_event_instances( all_instances.append(exc) # Expand recurrence rules to get instance dates - instance_dates = expand_recurrence( - recurrence=master.recurrence, - start=start_dt, - time_min=min_dt, - time_max=max_dt, - max_instances=max_results, - ) + try: + instance_dates = expand_recurrence( + recurrence=master.recurrence, + start=start_dt, + time_min=min_dt, + time_max=max_dt, + max_instances=max_results, + ) + except Exception as e: + # Log and return empty if recurrence expansion fails + # Keep broad exception to maintain graceful degradation (matching Google's behavior) + logger.warning( + "Failed to expand recurrence for event %s in get_instances: %s", master.id, e + ) + return [], None, None # Calculate event duration duration = timedelta(hours=1) # Default diff --git a/backend/src/services/calendar/database/schema.py b/backend/src/services/calendar/database/schema.py index 1edf548..dbd277a 100644 --- a/backend/src/services/calendar/database/schema.py +++ b/backend/src/services/calendar/database/schema.py @@ -8,6 +8,7 @@ String, Text, Integer, + BigInteger, Boolean, DateTime, ForeignKey, @@ -630,8 +631,8 @@ class Channel(Base): String(1000), nullable=False ) # Webhook callback URL expiration: Mapped[Optional[int]] = mapped_column( - Integer, nullable=True - ) # Unix timestamp ms + BigInteger, nullable=True + ) # Unix timestamp ms (requires BigInteger for ms since epoch) token: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) params: Mapped[Optional[dict[str, Any]]] = mapped_column(JSONB, nullable=True) # Whether payload is wanted for notifications diff --git a/examples/calendar/testsuites/calendar_bench.json b/examples/calendar/testsuites/calendar_bench.json index 4f53435..8410c33 100644 --- a/examples/calendar/testsuites/calendar_bench.json +++ b/examples/calendar/testsuites/calendar_bench.json @@ -10,7 +10,9 @@ "etag", "html_link", "ical_uid", - "sequence" + "sequence", + "start_datetime", + "end_datetime" ] }, "tests": [ @@ -151,17 +153,14 @@ "summary": { "contains": "Compost Communion" }, - "attendees": { - "contains": "dariush@test.com" - }, "calendar_id": { "eq": "cal_harvest_schedule" }, "start.dateTime": { - "contains": "2018-06-24T09:00" + "contains": "2018-06-23" }, "end.dateTime": { - "contains": "2018-06-24T11:00" + "contains": "2018-06-23" }, "start.timeZone": { "eq": "Asia/Tokyo" @@ -169,6 +168,16 @@ }, "expected_count": 1 }, + { + "diff_type": "added", + "entity": "calendar_event_attendees", + "where": { + "email": { + "eq": "dariush@test.com" + } + }, + "expected_count": 1 + }, { "diff_type": "changed", "entity": "calendar_events", @@ -649,9 +658,6 @@ "description": { "i_contains": "spare bulbs" }, - "attendees": { - "contains": "takeshi@test.com" - }, "calendar_id": { "eq": "cal_celluloid_dreams" }, @@ -667,6 +673,16 @@ }, "expected_count": 1 }, + { + "diff_type": "added", + "entity": "calendar_event_attendees", + "where": { + "email": { + "eq": "takeshi@test.com" + } + }, + "expected_count": 1 + }, { "diff_type": "changed", "entity": "calendar_events", @@ -682,7 +698,20 @@ } } }, - "expected_count": 1 + "expected_count": 1, + "ignore": [ + "attendees", + "color_id", + "description", + "end", + "recurrence", + "reminders", + "start", + "status", + "summary", + "transparency", + "visibility" + ] }, { "diff_type": "changed", @@ -780,10 +809,10 @@ "calendar_id": { "eq": "cal_symposium_curiosity" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-18T08:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-18T09:00" }, "start.timeZone": { @@ -802,11 +831,14 @@ "calendar_id": { "eq": "cal_symposium_curiosity" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-19T15:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-19T17:00" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -842,7 +874,20 @@ "contains": "Annex of Temporal Studies" } } - } + }, + "ignore": [ + "attendees", + "color_id", + "description", + "end", + "recurrence", + "reminders", + "start", + "status", + "summary", + "transparency", + "visibility" + ] }, { "diff_type": "added", @@ -900,19 +945,16 @@ "summary": { "contains": "DJ Nebula" }, - "attendees": { - "i_contains": "yuna@test.com" - }, "calendar_id": { "eq": "cal_thunderwave_festival" }, "location": { "contains": "Ethereal Meadow" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-17T05:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-17T07:00" }, "start.timeZone": { @@ -923,16 +965,20 @@ }, { "diff_type": "added", - "entity": "calendar_events", + "entity": "calendar_event_attendees", "where": { - "summary": { - "contains": "DJ Nebula" - }, - "attendees": { - "i_contains": "petro@test.com" - }, - "calendar_id": { - "eq": "cal_thunderwave_festival" + "email": { + "eq": "yuna@test.com" + } + }, + "expected_count": 1 + }, + { + "diff_type": "added", + "entity": "calendar_event_attendees", + "where": { + "email": { + "eq": "petro@test.com" } }, "expected_count": 1 @@ -1009,16 +1055,13 @@ "summary": { "contains": "Sasquatch Migration" }, - "attendees": { - "i_contains": "mateusz@test.com" - }, "calendar_id": { "eq": "cal_cryptozoology_summit" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-18T09:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-18T10:00" }, "start.timeZone": { @@ -1027,6 +1070,16 @@ }, "expected_count": 1 }, + { + "diff_type": "added", + "entity": "calendar_event_attendees", + "where": { + "email": { + "eq": "mateusz@test.com" + } + }, + "expected_count": 1 + }, { "diff_type": "added", "entity": "calendar_events", @@ -1037,11 +1090,14 @@ "calendar_id": { "eq": "cal_cryptozoology_summit" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-18T14:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-18T16:00" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -1117,11 +1173,14 @@ "summary": { "contains": "Paradox Prevention Seminar" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-20T14:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-20T16:00" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -1211,17 +1270,17 @@ "location": { "contains": "Dune Pavilion B" }, - "attendees": { - "contains": "ananya@test.com" - }, "calendar_id": { "eq": "cal_mirage_menagerie" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-23T14:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-23T14:15" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -1236,17 +1295,17 @@ "location": { "contains": "Dune Pavilion B" }, - "attendees": { - "contains": "ananya@test.com" - }, "calendar_id": { "eq": "cal_mirage_menagerie" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-23T14:15" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-23T14:30" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -1261,17 +1320,17 @@ "location": { "contains": "Dune Pavilion B" }, - "attendees": { - "contains": "ananya@test.com" - }, "calendar_id": { "eq": "cal_mirage_menagerie" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-23T14:30" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-23T14:45" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -1286,17 +1345,17 @@ "location": { "contains": "Dune Pavilion B" }, - "attendees": { - "contains": "ananya@test.com" - }, "calendar_id": { "eq": "cal_mirage_menagerie" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-23T14:45" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-23T15:00" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -1311,17 +1370,17 @@ "location": { "contains": "Dune Pavilion B" }, - "attendees": { - "contains": "ananya@test.com" - }, "calendar_id": { "eq": "cal_mirage_menagerie" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-23T15:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-23T15:15" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -1336,17 +1395,17 @@ "location": { "contains": "Dune Pavilion B" }, - "attendees": { - "contains": "ananya@test.com" - }, "calendar_id": { "eq": "cal_mirage_menagerie" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-23T15:15" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-23T15:30" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -1361,17 +1420,17 @@ "location": { "contains": "Dune Pavilion B" }, - "attendees": { - "contains": "ananya@test.com" - }, "calendar_id": { "eq": "cal_mirage_menagerie" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-23T15:30" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-23T15:45" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -1386,21 +1445,31 @@ "location": { "contains": "Dune Pavilion B" }, - "attendees": { - "contains": "ananya@test.com" - }, "calendar_id": { "eq": "cal_mirage_menagerie" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-23T15:45" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-23T16:00" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 }, + { + "diff_type": "added", + "entity": "calendar_event_attendees", + "where": { + "email": { + "eq": "ananya@test.com" + } + }, + "expected_count": 8 + }, { "diff_type": "added", "entity": "calendar_events", @@ -1411,7 +1480,7 @@ "calendar_id": { "eq": "cal_mirage_menagerie" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-18T15:00" }, "start.timeZone": { @@ -1430,11 +1499,14 @@ "calendar_id": { "eq": "cal_mirage_menagerie" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-23T19:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-23T20:00" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -1948,10 +2020,10 @@ "location": { "contains": "Willow Glade Observation Ring" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-19T19:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-19T20:00" }, "start.timeZone": { @@ -1970,10 +2042,10 @@ "summary": { "contains": "Bioluminescent Microscopy Workshop" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-23T19:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-23T20:00" }, "start.timeZone": { @@ -2077,10 +2149,10 @@ "calendar_id": { "eq": "cal_clockwork_tinkerers_guild" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-22T18:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-22T19:00" }, "start.timeZone": { @@ -2102,10 +2174,10 @@ "calendar_id": { "eq": "cal_clockwork_tinkerers_guild" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-29T19:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-29T20:00" }, "description": { @@ -2146,11 +2218,14 @@ "calendar_id": { "eq": "cal_clockwork_tinkerers_guild" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-07-06T18:00" }, "status": { "eq": "cancelled" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": { @@ -2168,10 +2243,10 @@ "calendar_id": { "eq": "cal_clockwork_tinkerers_guild" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-07-07T12:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-07-07T13:00" }, "start.timeZone": { @@ -2230,7 +2305,7 @@ "summary": { "contains": "Moon-Shell Rebinding" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-07-03T09:00" }, "status": { @@ -2238,6 +2313,9 @@ }, "recurrence": { "contains": "FREQ=MONTHLY" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -2265,11 +2343,14 @@ "summary": { "contains": "Moon-Shell Rebinding" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-09-04T09:00" }, "status": { "eq": "cancelled" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": { @@ -2284,11 +2365,14 @@ "summary": { "contains": "Moon-Shell Rebinding" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-08-07T11:00" }, "description": { "i_contains": "storm-surge delay" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -2300,8 +2384,11 @@ "summary": { "contains": "Ink Tide Inventory" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-07-15T16:00" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -2383,7 +2470,7 @@ "summary": { "contains": "Dawn Bell Rite" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-18T05:30" }, "status": { @@ -2391,6 +2478,9 @@ }, "recurrence": { "contains": "FREQ=DAILY" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -2424,11 +2514,14 @@ "summary": { "contains": "Dawn Bell Rite" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-23" }, "status": { "eq": "cancelled" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": { @@ -2446,11 +2539,14 @@ "summary": { "contains": "Dawn Bell Rite" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-20T06:30" }, "description": { "i_contains": "storm quiet hours" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -2486,15 +2582,22 @@ "summary": { "contains": "Glassmoth Conservatory Candle-Lighting" }, - "attendees": { - "contains": "ngozi@test.com" - }, "location": { "contains": "Hothouse Lantern Atrium" } }, "expected_count": 1 }, + { + "diff_type": "added", + "entity": "calendar_event_attendees", + "where": { + "email": { + "eq": "ngozi@test.com" + } + }, + "expected_count": 1 + }, { "diff_type": "added", "entity": "calendar_channels", @@ -2536,10 +2639,10 @@ "summary": { "contains": "Saffron Dusk Feather-Mending" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-22T18:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-22T19:00" }, "start.timeZone": { @@ -2599,10 +2702,10 @@ "summary": { "contains": "Quartzloom Spore Cataloging" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-21T09:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-21T10:00" }, "start.timeZone": { @@ -2666,28 +2769,29 @@ "contains": "2018-06-24T14:00" } }, - "start_datetime": { - "to": { - "contains": "2018-06-24T14:00" - } - }, "end": { "to": { "contains": "2018-06-24T15:00" } }, - "end_datetime": { - "to": { - "contains": "2018-06-24T15:00" - } - }, "location": { "to": { "contains": "Dome 3" } } }, - "expected_count": 1 + "expected_count": 1, + "ignore": [ + "attendees", + "color_id", + "description", + "recurrence", + "reminders", + "status", + "summary", + "transparency", + "visibility" + ] }, { "diff_type": "changed", @@ -2732,10 +2836,10 @@ "summary": { "contains": "Emberglass Kiln Glow" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-25T19:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-25T20:30" }, "start.timeZone": { @@ -2770,10 +2874,10 @@ "summary": { "contains": "Fogloom Archive Lantern Check" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-26T20:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-26T20:45" }, "start.timeZone": { @@ -2830,28 +2934,29 @@ "contains": "2018-06-27T15:00" } }, - "start_datetime": { - "to": { - "contains": "2018-06-27T15:00" - } - }, "end": { "to": { "contains": "2018-06-27T16:00" } }, - "end_datetime": { - "to": { - "contains": "2018-06-27T16:00" - } - }, "location": { "to": { "contains": "North Alcove" } } }, - "expected_count": 1 + "expected_count": 1, + "ignore": [ + "attendees", + "color_id", + "description", + "recurrence", + "reminders", + "status", + "summary", + "transparency", + "visibility" + ] } ], "metadata": { @@ -2925,16 +3030,31 @@ }, { "id": "test_26", - "name": "Mistforge Observatory - settings check only", - "prompt": "Please list my calendar settings so I can confirm my timezone and date/time formats before I reply to Sana (sana@test.com).", + "name": "Mistforge Observatory - settings check and watch", + "prompt": "Please list my calendar settings so I can confirm my timezone and date/time formats before I reply to Sana (sana@test.com). Also, set up a watch on my settings so I get notified of any changes.", "type": "actionEval", "seed_template": "calendar_default", "impersonate_user_id": "user_agent", - "assertions": [], + "assertions": [ + { + "diff_type": "added", + "entity": "calendar_channels", + "where": { + "resource_uri": { + "eq": "/users/user_agent/settings" + }, + "user_id": { + "eq": "user_agent" + } + }, + "expected_count": 1 + } + ], "metadata": { - "min_tool_calls": 1, + "min_tool_calls": 2, "tools_required": [ - "settings.list" + "settings.list", + "settings.watch" ] } }, @@ -2981,21 +3101,11 @@ "contains": "2018-06-28T06:00" } }, - "start_datetime": { - "to": { - "contains": "2018-06-28T06:00" - } - }, "end": { "to": { "contains": "2018-06-28T06:45" } }, - "end_datetime": { - "to": { - "contains": "2018-06-28T06:45" - } - }, "location": { "to": { "contains": "Pier 7 Scope" @@ -3007,7 +3117,17 @@ } } }, - "expected_count": 1 + "expected_count": 1, + "ignore": [ + "attendees", + "color_id", + "description", + "reminders", + "status", + "summary", + "transparency", + "visibility" + ] } ], "metadata": { @@ -3036,10 +3156,10 @@ "summary": { "contains": "Copperseed Archive dusting" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-28T09:30" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-28T10:00" }, "start.timeZone": { @@ -3198,10 +3318,10 @@ "summary": { "contains": "Emberwharf Tide Log" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-29T17:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-29T17:30" }, "start.timeZone": { @@ -3302,7 +3422,8 @@ } } }, - "expected_count": 1 + "expected_count": 1, + "ignore": ["status", "sequence", "updated_at"] }, { "diff_type": "added", @@ -3349,7 +3470,8 @@ } } }, - "expected_count": 1 + "expected_count": {"min": 1}, + "description": "All events on this calendar should be cleared (cancelled)" }, { "diff_type": "changed", @@ -3670,12 +3792,12 @@ } }, "expected_changes": { - "start_datetime": { + "start": { "to": { "contains": "2018-06-29T16:00" } }, - "end_datetime": { + "end": { "to": { "contains": "2018-06-29T17:00" } @@ -3686,7 +3808,18 @@ } } }, - "expected_count": 1 + "expected_count": 1, + "ignore": [ + "attendees", + "color_id", + "description", + "recurrence", + "reminders", + "status", + "summary", + "transparency", + "visibility" + ] } ], "metadata": { @@ -4025,10 +4158,10 @@ "summary": { "contains": "Ashline Relay Briefing" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-28T15:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-28T16:00" }, "start.timeZone": { @@ -4091,12 +4224,12 @@ "contains": "RRULE:FREQ=WEEKLY;BYDAY=MO" } }, - "start_datetime": { + "start": { "to": { "contains": "2018-07-02T19:00" } }, - "end_datetime": { + "end": { "to": { "contains": "2018-07-02T20:00" } @@ -4107,7 +4240,17 @@ } } }, - "expected_count": 1 + "expected_count": 1, + "ignore": [ + "attendees", + "color_id", + "description", + "reminders", + "status", + "summary", + "transparency", + "visibility" + ] }, { "diff_type": "added", @@ -4186,10 +4329,10 @@ "summary": { "contains": "Saltglass Alignment" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-30T22:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-30T22:30" }, "start.timeZone": { @@ -4285,10 +4428,10 @@ "location": { "contains": "Brine Archive Hall" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-30T14:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-30T15:00" }, "start.timeZone": { @@ -4355,10 +4498,10 @@ "summary": { "contains": "Bronze Fret Alignment" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-06-30T10:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-06-30T11:00" }, "start.timeZone": { @@ -4867,28 +5010,29 @@ "contains": "2018-07-01T09:00" } }, - "start_datetime": { - "to": { - "contains": "2018-07-01T09:00" - } - }, "end": { "to": { "contains": "2018-07-01T10:30" } }, - "end_datetime": { - "to": { - "contains": "2018-07-01T10:30" - } - }, "location": { "to": { "contains": "Forge Bay 2" } } }, - "expected_count": 1 + "expected_count": 1, + "ignore": [ + "attendees", + "color_id", + "description", + "recurrence", + "reminders", + "status", + "summary", + "transparency", + "visibility" + ] }, { "diff_type": "changed", @@ -5076,28 +5220,29 @@ "contains": "2018-07-02T20:00" } }, - "start_datetime": { - "to": { - "contains": "2018-07-02T20:00" - } - }, "end": { "to": { "contains": "2018-07-02T21:00" } }, - "end_datetime": { - "to": { - "contains": "2018-07-02T21:00" - } - }, "location": { "to": { "contains": "Upper Ring" } } }, - "expected_count": 1 + "expected_count": 1, + "ignore": [ + "attendees", + "color_id", + "description", + "recurrence", + "reminders", + "status", + "summary", + "transparency", + "visibility" + ] }, { "diff_type": "changed", @@ -5188,28 +5333,29 @@ "contains": "2018-07-03T10:00" } }, - "start_datetime": { - "to": { - "contains": "2018-07-03T10:00" - } - }, "end": { "to": { "contains": "2018-07-03T11:00" } }, - "end_datetime": { - "to": { - "contains": "2018-07-03T11:00" - } - }, "location": { "to": { "contains": "Glassbed Hall" } } }, - "expected_count": 1 + "expected_count": 1, + "ignore": [ + "attendees", + "color_id", + "description", + "recurrence", + "reminders", + "status", + "summary", + "transparency", + "visibility" + ] }, { "diff_type": "changed", @@ -5315,14 +5461,17 @@ "summary": { "contains": "Tideglass Ledger Seal" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-07-04T13:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-07-04T14:00" }, "location": { "contains": "Seawick Vault" + }, + "start.timeZone": { + "eq": "America/Los_Angeles" } }, "expected_count": 1 @@ -5379,7 +5528,18 @@ } } }, - "expected_count": 1 + "expected_count": 1, + "ignore": [ + "attendees", + "color_id", + "end", + "recurrence", + "reminders", + "start", + "status", + "transparency", + "visibility" + ] }, { "diff_type": "added", @@ -5506,7 +5666,7 @@ "end": { "contains": "2018-08-23" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-08-23" } }, @@ -5598,23 +5758,25 @@ "contains": "2018-09-17T10:00" } }, - "start_datetime": { - "to": { - "contains": "2018-09-17T10:00" - } - }, "end": { "to": { "contains": "2018-09-17T11:00" } - }, - "end_datetime": { - "to": { - "contains": "2018-09-17T11:00" - } } }, - "expected_count": 1 + "expected_count": 1, + "ignore": [ + "attendees", + "color_id", + "description", + "location", + "recurrence", + "reminders", + "status", + "summary", + "transparency", + "visibility" + ] } ], "metadata": { @@ -5838,10 +6000,10 @@ "calendar_id": { "eq": "test.user@test.com" }, - "start_datetime": { + "start.dateTime": { "contains": "2018-08-01T09:00" }, - "end_datetime": { + "end.dateTime": { "contains": "2018-08-01T09:30" }, "start.timeZone": {