Skip to content

fix: conformance on Ox, Zimbra, SOGo, CCS and Bedework - #53

Merged
tobixen merged 12 commits into
mainfrom
fix/added-parameters
Nov 12, 2026
Merged

tobixen merged 12 commits into
mainfrom
fix/added-parameters

Conversation

@tobixen

@tobixen tobixen commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Disclaimer: AI-generated code and text below the line, generated by Claude Opus 5.5


Work towards a conformance suite that passes on every CalDAV test server. verify and the suite now tell what a server adds harmlessly (parameters that say nothing about the value, the organizer as an attendee, re-stamped occurrences) from real changes (a dropped start, a parameter added with a meaning such as a TZID or PARTSTAT=DECLINED). The CalDAV backend declares more of what servers lose, from caldav's hints: floating times stored in UTC (timezones.floating), a task start Zimbra keeps only with a due and a COMPLETED Ox keeps only with a STATUS (a capability may now name what a property requires), task alarms Ox rewrites (lossy, with a Loss.note saying how), and a new recurrence.rdate capability for the RDATE Zimbra drops while keeping the RRULE. A VTIMEZONE the server dropped (CCS) is put back on read, Bedework's 500 for a taken name maps to an emulated native id, and the test harness waits for Bedework 5 to settle after writes.

New capability listing.complete and error ListingLimitError: Stalwart cuts a long listing at its WebDav.maxResults with a 507, and caldav keeps none of it. Where caldav's limit hints say so, the backend declares the limit, and a 507 in a listing raises ListingLimitError instead of a bare BackendError.

With the caldav hints this needs (python-caldav/caldav#855 and python-caldav/caldav#856, merged; python-caldav/caldav#861 and python-caldav/caldav#862 for Robur; the task-alarm hint still to come), the suite passes on Zimbra 8.8, Ox, Cyrus, DAViCal, Davis, Baikal, Nextcloud, Radicale, Xandikos, CCS, Bedework 3 and Stalwart. Still failing: Bedework 5 stores every ATTENDEE twice; robur's own server errors (500 on PUT, dropped connections); ecloud's calendar-creation rate limit. CI runs released caldav, where the tests for the new hints are skipped.

SOGo adds RSVP=TRUE and PARTSTAT to an ATTENDEE that had none, Ox adds
EMAIL=, and Ox adds the organizer to a meeting's attendees, as an RFC
6638 scheduling server does.  verify and the conformance suite counted
that as a change, though nothing the client sent is lost.

A stored line now matches a sent one when it has the same value, every
sent parameter, and added only parameters that say nothing about the
value: X- ones, and CN, EMAIL, RSVP and SCHEDULE-* on an attendee or
organizer.  A parameter dropped or changed, one added with a meaning
(a TZID, PARTSTAT=DECLINED, RELTYPE), or a line more or less, still is
a difference.  The one line more allowed is an attendee that is the
stored organizer; each attendee sent must still be there.  The property
round trip in the conformance suite compares as verify does, instead
of line by line.

Prompt: ... and continue working on getting all the conformance tests passing
Followup-Prompt: I think ... LOSSY is when information is lost, not when information is added [answer to: should verification tolerate parameters a server adds, or should attendees count as LOSSY there?]

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-by: Tobias Brox <tobias@redpill-linpro.com>
Deleting an occurrence rewrites the object, and Ox then gives every
component a new DTSTAMP and LAST-MODIFIED.  Deleting a sibling read
before that raised ConflictError, as if another client had edited it.
The check now leaves out what verify already skips (DTSTAMP, SEQUENCE,
LAST-MODIFIED, UID), so only a real edit is a conflict.

Zimbra drops a task's DTSTART.  differences() compares a single start,
end or due time as an instant, and read it from the stored component
without checking it was there, so verify=True raised KeyError instead
of reporting the loss.

Prompt: ... and continue working on getting all the conformance tests passing

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-by: Tobias Brox <tobias@redpill-linpro.com>
Zimbra and SOGo keep a TZID but store a floating time in UTC.
timezones covers both kinds of time, so no level said that, and the
floating corpus event failed on these servers.  The new capability
says what becomes of a floating time only; the CalDAV backend declares
it lossy where caldav's save-load.event.floating hint is unsupported,
and unknown on a task list: the hint is probed on events, and SOGo
keeps a task's floating times.
Where timezones is lowered it converts floating times as before.

Prompt: ... and continue working on getting all the conformance tests passing
Followup-Prompt: [Floating times should get a new capability timezones.floating] and a tester probe and new caldav hint

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-by: Tobias Brox <tobias@redpill-linpro.com>
Zimbra drops a task's DTSTART where it has no DUE, and Ox drops its
COMPLETED where it has no STATUS; nothing said so, and the writes lost
them silently.  A LOSSY capability may now name what a property
requires, and the loss planner reports the property as lost where none
of those is there.  The CalDAV backend declares task.start that way
where caldav's new hint save-load.todo.dtstart-without-due is
unsupported, and task.completed where
save-load.todo.completed-without-status is; both come from one table.

Prompt: ... and continue working on getting all the conformance tests passing
Followup-Prompt: [Floating times should get a new capability timezones.floating] and a tester probe and new caldav hint

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-by: Tobias Brox <tobias@redpill-linpro.com>
Bedework 5 makes an object readable, and findable, a few seconds after
the PUT is answered.  wait_out_search_cache becomes wait_for_server: a
GET or multiget of a recently written object waits out caldav's
synchronous-write delay, and searches wait the same delay where the
server has no search-cache delay of its own.  Only reads of fresh
objects wait, not every write as in caldav's own tests.

Prompt: [get the conformance tests to pass on Bedework 5]

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-by: Tobias Brox <tobias@redpill-linpro.com>
Ox keeps a task's VALARM but makes its trigger absolute and replaces
its description, as caldav's new save-load.todo.alarm hint says.  On a
task list where the hint is broken, alarms are LOSSY: the loss policy
hears of it, the alarm is sent as it is, and verify lets a rewritten
VALARM pass, though not a dropped one.  Where the hint is unsupported
they are dropped.  A calendar holding events too keeps FULL.

The loss gets a note saying how the server changes the value, as the
backend cannot tell what it will store; stored is then the value as
sent, and the warn policy shows the note.

Prompt: [we probably need] another probe in the caldav-server-tester library on whether task lists accept alarms
Followup-Prompt: save-load.todo.alarm is fine, and it's possible to use the behaviour or extra_keys to elaborate why it's broken.
Followup-Prompt: [Loss should get a separate note field]

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-by: Tobias Brox <tobias@redpill-linpro.com>
CCS stores an event with TZID=Europe/Oslo but drops its VTIMEZONE,
which RFC 5545 section 3.2.19 requires, so the conformance round trip
of an event series in a time zone failed there.  A CalDAV read now adds
the missing VTIMEZONE from zoneinfo; the server's bytes stay in .wire as
received.

Prompt: [pick from the task list: "calendaring: remaining conformance failures on Ox, Zimbra and others"]
Followup-Prompt: ... First priority is to get the conformance tests passing for all the caldav servers available ...
Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-by: Tobias Brox <tobias@redpill-linpro.com>
Bedework (3 and 5) refuses a new object under a taken name with 500,
not 412, so caldav's hint for it becomes save.if-none-match
"ungraceful".  That maps to identity.client-native-id "emulated", as
for SOGo: caldav GETs the name before the PUT.  The comment said only
"overwrites".

Prompt: [pick from the task list: "calendaring: remaining conformance failures on Ox, Zimbra and others"]
Followup-Prompt: ... First priority is to get the conformance tests passing for all the caldav servers available ...
Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-by: Tobias Brox <tobias@redpill-linpro.com>
Zimbra 8.8 and 10.1 drop every RDATE, timed or all-day, and keep an
RRULE, so the all-day corpus event with an RDATE failed verify there.
recurrence covers RRULE, RDATE and EXDATE alike; declaring it lossy
would have said the RRULE is lost too.  The new capability says what
becomes of an RDATE alone: where it is unsupported the loss planner
drops an event's RDATE, and the overrides of the occurrences only it
gives, and reports them.  The CalDAV backend reads caldav's new
save-load.event.recurrences.rdate hint, and declares it unknown on a
task list, as the hint is probed on events.

Prompt: [about Zimbra 10.1 dropping RDATE only inside the suite's run:] Keep digging.
Followup-Prompt: new feature [answer to: should calendaring get a new recurrence.rdate feature, or declare all of RECURRENCE lossy on Zimbra?]

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-by: Tobias Brox <tobias@redpill-linpro.com>
Loss.sent and Loss.stored are typed as any value, and mypy refused
_alarms() taking only a string.  It now takes what a Loss holds.

Prompt: [fix issues found during the review process]

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-by: Tobias Brox <tobias@redpill-linpro.com>
The VTIMEZONE test read .end off an icalendar.Component, which mypy
rejects in CI ("Component" has no attribute "end"). An isinstance
assert on icalendar.Todo narrows the type.

Prompt: Look through the last two PRs.  Pipeline seems to have failed

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-by: Tobias Brox <tobias@redpill-linpro.com>
Stalwart cuts a long listing at its WebDav.maxResults with a 507, and
caldav keeps none of it, so a listing there failed as a bare
BackendError.  listing.complete is now unsupported where caldav's
search, propfind or multiget limit hint says so, and the 507 raises
ListingLimitError, which the conformance test accepts only there.

Prompt: Continue working on getting those [calendaring] conformance tests passing.
Prompts [Declare the limit as a problem and let it fail] as for now, and it should be raised with the Stalwart.  (...)

Assisted-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-by: Tobias Brox <tobias@redpill-linpro.com>
@tobixen
tobixen merged commit 29cff31 into main Nov 12, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant