sync(chunk 1): VTODO mapper with an unknown-property round-trip
A hand-rolled content-line model instead of ical4j: a raw (name, params, value) tree is what the round-trip requirement wants, and a typed model normalises away exactly what has to survive. lib-recur already does RRULE and java.time is native at minSdk 29, so the 2.2 MB of zone data and the registry shims buy nothing. Deviates from SYNC.md's library table — see SYNC-PLAN.md decision 4. - domain/ical: parser, serialiser, value codecs. No Android, no data types, so the floret-kit extraction stays a file move. - data/tasks/ical/VTodoMapper: VTODO <-> TaskEntity. Claims a property only when it can reproduce it exactly; everything else round-trips verbatim through TaskEntity.unknown_properties, which already existed at v1 — no migration needed, and BEGIN/END lines carry the nesting the plan thought needed a second table. - 19 fixtures as the specification, with the canonical comparison harness from SYNC.md: no property lost, modulo the enumerated allowlist. - ICalendarWriter now delegates folding and escaping rather than carrying its own copy. VALARM ownership settled: neither side writes the other's alarms. VALARMs round-trip in the residue, local reminders stay in task_alarms. The setAlarm collision was the provider's; the two stores are now disjoint.
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
# VTODO fixture corpus
|
||||
|
||||
The mapper's **specification**, not its regression net. Every file here is a
|
||||
shape that was found in the wild and that a plausible implementation gets wrong.
|
||||
A clean, server-generated VTODO catches nothing and is deliberately absent.
|
||||
|
||||
`RoundTripTest` asserts every fixture survives `parse → map → write → serialise`
|
||||
with no property lost, compared as a canonical multiset modulo the allowlist in
|
||||
`docs/SYNC-PLAN.md` § chunk 1 (`PRODID`, `DTSTAMP`, `LAST-MODIFIED`, `SEQUENCE`,
|
||||
VTIMEZONE bodies, fold positions, parameter quoting).
|
||||
|
||||
| File | What it is for |
|
||||
|---|---|
|
||||
| `nextcloud-shared-confidential.ics` | A `CLASS:CONFIDENTIAL` task as Nextcloud rewrites it on GET from a **shared** calendar — `DUE`, `STATUS`, `COMPLETED`, `PERCENT-COMPLETE`, `PRIORITY` and `RELATED-TO` already stripped, ETag left untouched. Re-PUTting this destroys the owner's task |
|
||||
| `recurring-master-overrides.ics` | Master plus two `RECURRENCE-ID` overrides in one resource, sharing a UID |
|
||||
| `valarm-unknown-nested.ics` | Unknown properties **inside** a `VALARM` — the case a flat property-per-row model cannot represent |
|
||||
| `unknown-component.ics` | An entirely unknown sub-component |
|
||||
| `unknown-tzid.ics` | A `TZID` no device tzdb knows, with its own `VTIMEZONE` |
|
||||
| `uid-special-chars.ics` | A UID containing `/` and `@` — the href sanitiser must not assume it is filename-safe |
|
||||
| `rrule-due-no-dtstart.ics` | `RRULE` + `DUE` and no `DTSTART`: no well-defined `RECURRENCE-ID`, undefined in RFC 5545, ubiquitous in the wild |
|
||||
| `moz-alarm-props.ics` | `X-MOZ-LASTACK` / `X-MOZ-SNOOZE-TIME`. Dropping them causes documented Thunderbird alarm storms |
|
||||
| `apple-sort-order.ics` | `X-APPLE-SORT-ORDER` — preserve, never interpret |
|
||||
| `floating-time.ics` | `DTSTART` with neither `Z` nor `TZID`. Readable, not reproducible: it must round-trip from the residue, not be rewritten as a guess |
|
||||
| `completion-model-a.ics` | Completion as a `RECURRENCE-ID` override, master stays open — jtx Board, Thunderbird. **The model we write** |
|
||||
| `completion-model-b.ics` | Master's `DUE` advanced in place, completion cleared — tasks.org, Evolution, Nextcloud in practice |
|
||||
| `completion-model-c.ics` | `STATUS:COMPLETED` on the master, killing the series — Nextcloud Tasks ≤ 0.17. **Always a bug**; we read it, we never write it |
|
||||
| `completion-model-d.ics` | The completed occurrence detached as a new task with a new UID — OpenTasks |
|
||||
| `quoted-param-colon.ics` | A quoted parameter value containing a colon and a comma. The value separator is the first colon *outside* quotes |
|
||||
| `mixed-value-types.ics` | `DTSTART;VALUE=DATE` with a timed `DUE`. One `is_all_day` flag cannot author both, so the odd one out must stay in the residue rather than be flattened to a date |
|
||||
| `malformed-scalars.ics` | `PRIORITY:11`, `PERCENT-COMPLETE:150`, `SEQUENCE:x`, `STATUS:X-DEFERRED`, `CLASS:X-INTERNAL` — values no spec allows. Clamping or defaulting any of them is a silent rewrite of somebody's data |
|
||||
| `two-timezones.ics` | `DTSTART` and `DUE` in different zones. One `timezone` column cannot author both |
|
||||
| `folded-utf8.ics` | A line folded mid-emoji-adjacent, to prove folding counts octets and splits on character boundaries |
|
||||
Reference in New Issue
Block a user