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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lekt8
2026-06-03 01:37:45 +08:00
committed by GitHub
parent 4019283eba
commit 80de69ebb0
2 changed files with 81 additions and 1 deletions

View File

@@ -315,9 +315,10 @@ Bulk delete/archive/mark emails. Use this for "delete all those" after listing e
{"action": "create_event", "summary": "<event title>", "dtstart": "<natural language or ISO datetime>"} {"action": "create_event", "summary": "<event title>", "dtstart": "<natural language or ISO datetime>"}
``` ```
Calendar event management (CalDAV). Actions: `list_events`, `create_event`, `update_event`, `delete_event`, `list_calendars`. \ 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"). \ `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`). \ 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. \ 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.""", `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.", "create_session": "- ```create_session``` — Create a new chat. Line 1 = chat name, line 2 = model name. Use for background/parallel work.",

View File

@@ -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()