Skip to content

test(calendar): prove non-Google sources are safe on an old client (#1396) - #1426

Draft
h4yfans wants to merge 1 commit into
h4yfans/calendar-provider-ui-shellfrom
h4yfans/calendar-cross-version-compat
Draft

test(calendar): prove non-Google sources are safe on an old client (#1396)#1426
h4yfans wants to merge 1 commit into
h4yfans/calendar-provider-ui-shellfrom
h4yfans/calendar-cross-version-compat

Conversation

@h4yfans

@h4yfans h4yfans commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Closes #1396. Phase 0 of the multi-provider calendar epic #1390. Top of the stack — the gate for every provider phase.

Stacked on #1425 (#1395) → #1424 (#1394) → #1423 (#1393) → #1422 (#1392) → #1407 (#1391). Base is h4yfans/calendar-provider-ui-shell.

The question

Device A runs a build that can connect CalDAV and creates provider='caldav' sources. Device B is still on a Google-only build and pulls them down.

The item handlers were already defensive in one direction — provider: data.provider ?? 'google' covers rows written before the column carried meaning. The other direction had never been verified, and it is the one that can lose user data: if the old client decided an unrecognised row was corrupt and cleaned it up, that delete would sync back and remotely destroy the connection the user had just made on A.

The answer: it does not misbehave

main/sync/calendar-cross-version-compat.test.ts (26 tests) drives the handlers directly — which is exactly what a Google-only build does with these rows — against ics, caldav and microsoft:

  • Stored verbatim. applyUpsert writes the provider through and never rewrites one it does not recognise; ?? 'google' only fires when the key is genuinely absent.
  • Not deleted, not archived, not "repaired." The row survives a first pull, a second pull that edits it, and every read path (all of which filter on archivedAt IS NULL). I checked for a sweep too: the only two deletes on calendar_sources are an explicit user-initiated disconnect — scoped by provider — and applying a remote tombstone. There is no orphan sweep or credential-reconcile pass over these tables.
  • Never written back rewritten. buildPushPayload round-trips provider unchanged, so an old client re-pushing a row it cannot use does not re-home the calendar.
  • External events apply normally. The missing-parent guard (Sync pull permanently skips tasks on FOREIGN KEY constraint failure → endless re-pull loop (real user affected) #837) keys on the parent row existing, not on the provider being one this build can sync.
  • Delete-clock behaviour is provider-independent. A stale tombstone is skipped and a causally-newer one applied, and a google delete and an unknown-provider delete are asserted to return the same result for the same clocks.

Payload round trip. provider is z.string().optional() rather than an enum, so an old client's zod does not strip a value it has never seen. That matters more than it looks: a stripped field comes back as the ?? 'google' fallback, which would silently re-home the calendar. Asserted for sources and bindings, plus a full local → buildPushPayload → schema parse → apply-on-a-second-db loop that checks provider, metadata and syncCursor all survive.

Consequence: no fan-out gate needed

The PR #754 sync-type-negotiation pattern was the fallback if the old client had misbehaved. It does not, so new-provider rows fan out to every device unconditionally and an old device simply carries them inert — nothing polls them, hasLocalAuth reads false, and they resolve themselves the moment that device updates.

Written up under apps/docs/src/architecture/sync-handlers.mdUnknown Calendar Providers Are Not Corruption.

Verification

  • pnpm --filter @memry/desktop typecheck — green
  • ESLint on the new file — 0 errors; Prettier clean
  • pnpm test:sync-server — 62 files, 941 tests passed
  • Desktop: main 6404 passed / 1 expected fail / 4 skipped (520 files) · shared 2455 · main-integration 9 · (renderer 6973 green as of feat(calendar): provider-aware settings and connect UI shell (#1395) #1425)
  • pnpm docs:impact --base origin/main --strict — green · pnpm docs:build — green

Owed before merge

The two-profile manual run is not done. The acceptance criteria call for dev:a on the new build and dev:b on the pre-change build, and I cannot drive two Electron profiles from here. What the tests establish is that the old client's code paths are safe with these rows; what the manual run would add is the end-to-end confirmation over a real sync round trip. Given this issue gates every provider phase, that run should happen before merge rather than after.

@github-actions github-actions Bot added documentation Improvements or additions to documentation test labels Aug 13, 2026
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit 08afc49.

@h4yfans
h4yfans force-pushed the h4yfans/calendar-provider-ui-shell branch from da8a305 to 2c46af1 Compare August 13, 2026 16:17
@h4yfans
h4yfans force-pushed the h4yfans/calendar-cross-version-compat branch from cf46ffa to af7f23b Compare August 13, 2026 16:17
@h4yfans
h4yfans force-pushed the h4yfans/calendar-provider-ui-shell branch from 2c46af1 to b03870c Compare August 13, 2026 16:19
@h4yfans
h4yfans force-pushed the h4yfans/calendar-cross-version-compat branch from af7f23b to 1de5e16 Compare August 13, 2026 16:19
@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…1396)

Device A on a newer build connects CalDAV and syncs `provider='caldav'` rows.
Device B is still on a Google-only build and pulls them down. The item
handlers were already defensive in the other direction — an absent `provider`
falls back to `'google'` — but this direction had never been verified, and it
is the one that can lose data: if the old client decided an unrecognised row
was corrupt and cleaned it up, that delete would sync back and remotely destroy
the connection the user just made.

Finding: it does not. `main/sync/calendar-cross-version-compat.test.ts` runs
the handlers — which is exactly what a Google-only build does — against `ics`,
`caldav` and `microsoft` rows and asserts they are:

- stored verbatim, never rewritten to `google`
- not archived, not deleted, not "repaired" on a second pull
- pushed back out with the provider intact
- accepted as parents for external events (the missing-parent guard keys on the
  row existing, not on the provider being syncable)
- subject to the same delete-clock rules as google — a stale tombstone is
  skipped, a causally-newer one applied, identically

The payload round trip is checked too: `provider` is `z.string().optional()`
rather than an enum, so an old client's zod does not strip a value it has never
seen — a stripped field would come back as the `?? 'google'` fallback and
silently re-home the calendar.

Consequence: no sync type negotiation gate is needed. The PR #754 pattern was
the fallback if the old client had misbehaved; it does not, so new-provider
rows fan out unconditionally and an old device carries them inert until it
updates.

Findings written up under `apps/docs/src/architecture/sync-handlers.md`.
@h4yfans
h4yfans force-pushed the h4yfans/calendar-provider-ui-shell branch from b03870c to fc57c6f Compare August 13, 2026 16:45
@h4yfans
h4yfans force-pushed the h4yfans/calendar-cross-version-compat branch from 1de5e16 to 08afc49 Compare August 13, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant