From 80de69ebb07c5ccc4008dc0a85d63ba8a8aac564 Mon Sep 17 00:00:00 2001 From: lekt8 Date: Wed, 3 Jun 2026 01:37:45 +0800 Subject: [PATCH] feat: document rrule in the manage_calendar tool schema (#1320) (#1324) * feat: document rrule in the manage_calendar tool schema (#1320) The create_event handler already persists `rrule` (a single event carrying an iCalendar RRULE), but the manage_calendar tool schema didn't list it, so the agent had no documented way to make a recurring event and took a roundabout path. Add `rrule?` to the create_event field list with examples (FREQ=WEEKLY;BYDAY=MO etc.) and an explicit note to create ONE event with the rule rather than looping. Covered by tests/test_calendar_rrule.py: do_manage_calendar create_event with an rrule stores one event with that recurrence; without it, the event is single. Co-Authored-By: Claude Opus 4.8 (1M context) * test: restore SessionLocal via monkeypatch in #1320 rrule test (review) Per review: the test patched core.database.SessionLocal at module import and never restored it, which could leak the temp DB into later tests in the same process. Move the patch into an autouse monkeypatch fixture so it is restored after each test. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- src/agent_loop.py | 3 +- tests/test_calendar_rrule.py | 79 ++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 tests/test_calendar_rrule.py diff --git a/src/agent_loop.py b/src/agent_loop.py index 6c039b8..c9ca135 100644 --- a/src/agent_loop.py +++ b/src/agent_loop.py @@ -315,9 +315,10 @@ Bulk delete/archive/mark emails. Use this for "delete all those" after listing e {"action": "create_event", "summary": "", "dtstart": ""} ``` Calendar event management (CalDAV). Actions: `list_events`, `create_event`, `update_event`, `delete_event`, `list_calendars`. \ -For `create_event`: {summary, dtstart, dtend?, duration?, calendar?, location?, description?, reminder_minutes?}. \ +For `create_event`: {summary, dtstart, dtend?, duration?, calendar?, location?, description?, reminder_minutes?, rrule?}. \ `dtstart` accepts natural language ("tomorrow at 1pm", "in 2 hours", "next monday 9am") or ISO ("2026-05-12T13:00:00"). \ If `dtend` omitted, defaults to dtstart+1h (or +1d when `all_day: true`). \ +For a RECURRING event pass `rrule` as an iCalendar RRULE string, e.g. `"FREQ=WEEKLY;BYDAY=MO"` (every Monday), `"FREQ=DAILY;COUNT=10"`, or `"FREQ=MONTHLY;BYMONTHDAY=1"` — create ONE event with the rrule, do not loop creating many events. \ If the user asks for a reminder/alarm before the event, pass `reminder_minutes` as an integer; do not write reminder text into the event description and do NOT also call `manage_notes` for the same reminder because calendar reminders are routed through Notes automatically. \ `calendar` accepts a name ("Main") or short-id prefix.""", "create_session": "- ```create_session``` — Create a new chat. Line 1 = chat name, line 2 = model name. Use for background/parallel work.", diff --git a/tests/test_calendar_rrule.py b/tests/test_calendar_rrule.py new file mode 100644 index 0000000..40047b7 --- /dev/null +++ b/tests/test_calendar_rrule.py @@ -0,0 +1,79 @@ +"""Issue #1320 — the agent's manage_calendar tool can create a recurring event. + +The create_event handler already persists `rrule`, but it wasn't documented in the +tool schema, so the agent took "a roundabout way". This pins the end-to-end path: +calling do_manage_calendar with an rrule stores a single event carrying that RRULE. +""" + +import json +import tempfile +import uuid + +import pytest +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import NullPool + +import core.database as cdb +from core.database import CalendarEvent + +_TMPDB = tempfile.NamedTemporaryFile(suffix=".db", delete=False) +_ENGINE = create_engine( + f"sqlite:///{_TMPDB.name}", + connect_args={"check_same_thread": False}, + poolclass=NullPool, +) +cdb.Base.metadata.create_all(_ENGINE) +_TS = sessionmaker(bind=_ENGINE, autoflush=False, autocommit=False) + + +@pytest.fixture(autouse=True) +def _bind_temp_db(monkeypatch): + # do_manage_calendar does `from core.database import SessionLocal` at call + # time, so patch the module attribute to our temp DB — via monkeypatch so it + # is RESTORED after each test and can't leak into later tests in the process. + monkeypatch.setattr(cdb, "SessionLocal", _TS) + yield + + +async def test_create_event_with_rrule_persists_recurrence(): + from src.tool_implementations import do_manage_calendar + + owner = "tester-" + uuid.uuid4().hex[:6] + rrule = "FREQ=WEEKLY;BYDAY=MO" + res = await do_manage_calendar(json.dumps({ + "action": "create_event", + "summary": "Standup", + "dtstart": "2026-06-08T09:00:00Z", + "rrule": rrule, + }), owner=owner) + assert res.get("exit_code", 0) == 0, res + uid = res.get("uid") + assert uid, res + + db = _TS() + try: + ev = db.query(CalendarEvent).filter(CalendarEvent.uid == uid).first() + assert ev is not None + assert ev.rrule == rrule # ONE event carrying the recurrence rule + assert ev.summary == "Standup" + finally: + db.close() + + +async def test_create_event_without_rrule_is_single(): + from src.tool_implementations import do_manage_calendar + + owner = "tester-" + uuid.uuid4().hex[:6] + res = await do_manage_calendar(json.dumps({ + "action": "create_event", + "summary": "One-off", + "dtstart": "2026-06-09T10:00:00Z", + }), owner=owner) + assert res.get("exit_code", 0) == 0, res + db = _TS() + try: + ev = db.query(CalendarEvent).filter(CalendarEvent.uid == res["uid"]).first() + assert ev is not None and (ev.rrule or "") == "" + finally: + db.close()