Close the remaining M1 gaps
Eight items were still open at the end of M1: two integration tests that had only been run by hand, and six known gaps. Recurrence overrides now reach Google. Google addresses an exception through the series rather than as an event of its own, so the master is sent first and each override is then matched to its instance by original start time and patched. Matching needs the two sides' spellings reduced to one key: iCalendar writes a zoned local time, Google an absolute offset. An override matching no occurrence is counted rather than forced — that means a stale RECURRENCE-ID left behind by an edited RRULE, and inventing an event for it would put something in the calendar the series does not contain. Reading one component apart from another needed a view `properties` cannot give: it flattens every VEVENT together, which is right for the UID a series shares and wrong for an override, whose SUMMARY and the master's are then indistinguishable. `Calendar::events` splits them. A TZID now travels with the VTIMEZONE that defines it, derived from the zone's own transition table as the yearly rule it implies. This changes the content hash of every zoned recurring event, so the first cycle after this re-pushes them. A Google authorisation is filed under the account it was granted for rather than the endpoint that asked for it, so two endpoints on one account no longer need a login each. The account is read from the primary calendar's id, which needs no scope beyond the calendar one already granted. Authorisations written by the previous scheme are still honoured, and move across at the next login. An unreachable Google endpoint no longer ends the cycle — one lapsed token used to stop the CalDAV side too. It is named, only the aggregates depending on it stand down, and the run exits non-zero so a partial cycle cannot pass for success. The CalDAV leg cannot be narrowed the same way: pimsync is one process covering every pair, so a failure does not say which pair it belongs to. `--dry-run` now pulls for real, into a throwaway copy of the local mirrors and through a pimsync configuration that only ever reads from a server. What it reports is measured against the calendars as they are now rather than against whatever the last real cycle left behind. `calcalist prune` reports local mirrors of endpoints the configuration no longer names, and removes them under --force. The integration tests run against a real Radicale server and a real iCal feed: convergence and idempotence, the dry run, prune, and the scheduling rule asserted on the bytes that actually reached the server. The plan asked for an SMTP sink for that last one; Radicale implements no RFC 6638 scheduling, so a quiet SMTP port would have proved nothing about the transform. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
06e661b0c4
commit
623505b9c8
14 changed files with 2008 additions and 144 deletions
66
TODO.md
66
TODO.md
|
|
@ -14,8 +14,7 @@ sync semantics these items implement.
|
|||
|
||||
## M1 — bidirectional sync
|
||||
|
||||
Functionally complete. Everything below is done except the two automated
|
||||
integration tests, which are listed with the remaining gaps at the end.
|
||||
Complete. Every item from the plan is built, tested and verified live.
|
||||
|
||||
### Core modules
|
||||
|
||||
|
|
@ -61,6 +60,26 @@ integration tests, which are listed with the remaining gaps at the end.
|
|||
- [x] `calcalist status` — endpoints, aggregates, how many events are mirrored, and the
|
||||
exact `@marker` names an event can carry, so routing is discoverable without
|
||||
opening the config file
|
||||
- [x] Recurrence overrides are pushed to Google. Google addresses an exception
|
||||
through the series rather than as an event of its own, so the master goes up
|
||||
first and each override is then matched to its instance by original start
|
||||
time and patched. An override matching no occurrence is counted, not forced.
|
||||
- [x] A `TZID` now travels with the `VTIMEZONE` that defines it, derived from the
|
||||
zone's own transition table as the yearly rule it implies. A zone that does
|
||||
not shift gets one fixed observance.
|
||||
- [x] One Google authorisation covers every endpoint on that account. Tokens are
|
||||
keyed by the account, discovered at login from the primary calendar's id —
|
||||
which needs no scope beyond the calendar one already granted. An endpoint
|
||||
names its `account` only when calcalist is logged in to more than one.
|
||||
- [x] A Google endpoint that cannot be reached no longer ends the cycle. It is
|
||||
named, only the aggregates depending on it stand down, and the run exits
|
||||
non-zero so a lapsed token cannot pass for success.
|
||||
- [x] `calcalist prune` — reports local mirrors of endpoints the configuration no
|
||||
longer names, and removes them under `--force`
|
||||
- [x] `--dry-run` pulls for real, into a throwaway copy of the local mirrors and
|
||||
through a pimsync configuration that only ever reads from a server. What it
|
||||
reports is measured against the calendars as they are now, and nothing
|
||||
outside the copy is written.
|
||||
|
||||
### Tests
|
||||
|
||||
|
|
@ -93,33 +112,30 @@ calendar:
|
|||
- The mass-deletion guard refusing a 100% removal until `--force`
|
||||
- `pimsync check` validating the generated config against a live CalDAV server
|
||||
|
||||
## Remaining
|
||||
### Integration
|
||||
|
||||
### Tests not yet automated
|
||||
- [x] Against a real Radicale server and a real iCal feed, over pimsync: two
|
||||
sources converge on a CalDAV target, and the next cycle is a no-op.
|
||||
- [x] Safety, against the same server: no live `ATTENDEE` or `ORGANIZER` reaches
|
||||
the aggregate, the guest list survives as inert data, the alarm survives
|
||||
intact, and the source keeps its scheduling properties.
|
||||
|
||||
- [ ] Integration against Radicale plus a WebCal fixture, asserting convergence and
|
||||
idempotence. Done by hand twice; not yet a test that runs in CI.
|
||||
- [ ] Safety integration: a real CalDAV server with an SMTP sink, proving no mail is
|
||||
emitted on mirror writes or mirror deletions.
|
||||
The plan asked for an SMTP sink here. Radicale implements no RFC 6638
|
||||
scheduling, so a quiet SMTP port would have proved nothing about the
|
||||
transform — no server in reach of a test sends calendar mail at all. What
|
||||
is asserted instead is the bytes that reached the server, which is the
|
||||
thing the transform is actually responsible for. Proving the Google half
|
||||
was settled separately, from the API discovery document.
|
||||
- [x] `--dry-run` reaches the servers, reports what it found, and leaves both the
|
||||
target and the local mirrors untouched.
|
||||
- [x] A retired endpoint's mirror is reported by `prune` and removed under
|
||||
`--force`.
|
||||
|
||||
### Known gaps
|
||||
## Known gaps
|
||||
|
||||
- [ ] A recurring series' *exceptions* are not pushed to Google. Pulling them works.
|
||||
Google models them as separate events against an already existing series, so
|
||||
pushing needs `events.instances` plus a patch per exception. Reported per sync
|
||||
rather than dropped silently.
|
||||
- [ ] Each Google endpoint needs its own `google login`, even for the same account and
|
||||
OAuth client, because tokens are keyed by endpoint id. Two endpoints on one
|
||||
account should share a credential.
|
||||
- [ ] A failed Google pull aborts the whole cycle, including the CalDAV side. Safe —
|
||||
reconciling against a stale snapshot could read as mass deletion — but a lapsed
|
||||
token stops everything. Skipping only the affected aggregates would be better.
|
||||
- [ ] Removing an endpoint from the config leaves its vdir behind, holding events
|
||||
nothing manages any more.
|
||||
- [ ] `--dry-run` skips the pull entirely, so it reports against whatever the last
|
||||
real cycle left behind rather than against current remote state.
|
||||
- [ ] A `TZID` is emitted without an accompanying `VTIMEZONE`. Tolerated by the servers
|
||||
tested so far, and confined to recurring events, but not strictly conformant.
|
||||
- [ ] A `pimsync sync` that fails takes the whole CalDAV leg with it. Unlike the
|
||||
Google side this cannot be narrowed: pimsync is one process covering every
|
||||
pair, so a failure does not say which pair it belongs to.
|
||||
|
||||
## M2 — interface and packaging
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue