feat(calendar): verify Google Calendar two-way sync via E2E - #291
Merged
Conversation
Adds the four test hooks that calendar-push-channels.e2e.ts depends on, unblocking the currently-skipped push-channel round-trip suite: - seedGoogleCalendarTokens: exchanges refresh→access token, derives account email via userinfo, stores refresh token in keytar, inserts a kind='account' calendar_sources row so push runtime can resolve it. - getGooglePushChannelProbe: wraps getGooglePushRuntime().getActiveChannelCount(). - createGoogleCalendarEventForE2E: direct events.insert call against Google Calendar API using seeded credentials. - deleteGoogleCalendarEventForE2E: direct events.delete, treats 410 Gone as success for idempotent cleanup. Also fixes a stale DEFAULT_WEBHOOK_URL that pointed at the defunct sync.memry.io domain instead of sync.memrynote.com. The env-var override MEMRY_CALENDAR_WEBHOOK_URL masked this in every real deployment, so it never surfaced as a bug. Hooks cache OAuth credentials in a module-level variable, scoped to a single test process. Intentional: simpler than threading through parameters and test-runs are short-lived. Typecheck passes (only pre-existing unrelated packages/storage-data error remains).
Adds Phase B two-way sync verification: a flag-gated E2E suite that proves the
Google → Memry import path works end-to-end against real Google Calendar APIs,
without requiring the Memry staging sync-auth infrastructure that push channels
depend on.
Phase A hook defect fix:
seedGoogleCalendarTokens was calling oauth2/v2/userinfo to derive the account
email, but the E2E refresh token is granted only calendar scopes — userinfo
returns 401 even though token refresh itself works. Switched to
calendar/v3/calendars/primary which returns id=<email> under the scope we
already need. Side benefit: doubles as a scope health-check.
New hooks in test-hooks.ts:
- connectGoogleCalendarForE2E replicates CONNECT_PROVIDER's post-OAuth side
effects (account + calendar source upserts, sync runner start) using the
seeded refresh token, so tests don't need to drive the real OAuth loopback.
- syncGoogleCalendarSourceForE2E calls syncGoogleCalendarSource directly;
bypasses syncGoogleCalendarNow's isMemryUserSignedIn early-return so the
Google pull path is verifiable without Memry sync auth.
- listCalendarExternalEventsForE2E probes calendar_external_events so the
test can assert imports landed locally.
- seedGoogleCalendarTokens also populates GOOGLE_CALENDAR_CLIENT_ID/_SECRET
env vars that the production token manager reads, so sync-service can
refresh access tokens without callers setting both E2E-prefixed and
non-prefixed variants.
New test: calendar-google-two-way-sync.e2e.ts
Seeds tokens → connects via hook → creates event directly on Google →
calls sync hook → asserts event persisted in calendar_external_events.
Cleans up the Google event in finally. Green 3/3 runs against staging in
~7s each.
calendar-push-channels.e2e.ts
Push-channel round-trip requires a staging Memry sync-auth setup that
isn't provisioned yet — POSTing to /calendar/channels needs a valid bearer
token. Added a second gate (MEMRY_E2E_STAGING_USER_SETUP_TOKEN) so the
suite skips with a clear reason instead of timing out on channel-count
polls. UI selectors also updated to match reality (⌘+, → Integrations →
Connect/Disconnect; the original "Settings" button never existed).
This commit enhances the formatting of several test files by adjusting the indentation and line breaks for better readability. Key changes include: - Simplifying function definitions in `google-channel-manager.test.ts`. - Formatting mock return values in `provider-auth-transfer.test.ts` for clarity. - Organizing import statements in `sync-service.test.ts` and `generated-rpc.ts` for consistency. - Streamlining object structures in `linking-service.test.ts` and `linking-service.ts` to improve visual clarity. These changes do not alter any functionality but improve the maintainability of the test code.
MEMRY_E2E_STAGING_USER_SETUP_TOKEN was referenced only inside the push-channel e2e to gate the whole suite, but nothing else in the codebase ever sets or reads it — it was a placeholder left behind after the two-way-sync work split the testable vs non-testable paths. Replaces the fake conditional with a plain test.skip(true, '…'). Honest > clever. The underlying blocker is unchanged: push-channel round-trip requires the desktop to be signed into Memry's sync-server (authed POST /calendar/channels + staging-visible webhook endpoint), and the E2E harness today bootstraps against a local test sync server. Until a pre-provisioned staging Memry user with its Ed25519 signing key is wired into the E2E env, these tests cannot execute end-to-end. The data-plane pull path is covered without Memry auth by calendar-google-two-way-sync.e2e.ts; the data-plane push path by calendar-google-writeback.e2e.ts (incoming commit).
Closes the second direction of Google Calendar two-way sync. Counterpart to
the direct-pull test that already proves Google → Memry import; this suite
proves Memry → Google push against the real Google Calendar API, without
needing the Memry staging sync-auth that push-channel webhooks depend on.
Same pattern as the import suite: bypass the guard paths that require Memry
sync auth (syncLocalSourceToGoogleCalendar → isMemryUserSignedIn early-return)
by calling pushSourceToGoogleCalendar directly — it has no auth guard because
it's the inner mechanism, with the guard sitting at the outer orchestrator.
New hooks in test-hooks.ts:
- createMemryEventForWriteBackE2E inserts a minimal calendar_events row.
targetCalendarId is set to the user's primary so the push routes there
instead of falling through to ensureMemryCalendarSource (which would
auto-create a "Memry" calendar on the test Google account and leave
state to clean up between runs).
- pushMemryEventToGoogleForE2E wraps pushSourceToGoogleCalendar with
sourceType='event' and returns the binding's remoteCalendarId /
remoteEventId so the test can assert end-state directly on Google.
- fetchGoogleEventForE2E: GET /calendar/v3/calendars/:id/events/:id,
returns summary + start/end. Lets the test verify the write actually
landed with the right fields, not just that the binding was created.
New test: calendar-google-writeback.e2e.ts
seed tokens → connect → create local calendar_events row → push via hook →
GET from Google → assert summary + start + end → cleanup (delete Google event).
Start/end compared at whole-second precision since Google Calendar drops
sub-second precision on the events.insert round-trip.
3/3 separate Playwright runs green against staging, ~7s each. Together with
calendar-google-two-way-sync.e2e.ts this proves two-way sync operational at
the data-plane layer. Push-channel/webhook round-trip remains gated on the
staging sync-auth user — the one piece of infra that unblocks the rest.
… form Commit d9071f3 ran a prettier/formatter pass that reflowed these generated files into the rest of the codebase's style (multi-line imports, multi-line arrow bodies). But they're machine-generated by: apps/desktop/scripts/generate-rpc-bindings.ts apps/desktop/scripts/generate-ipc-invoke-map.js …which emit compact, canonical form. The `ipc:check` pre-typecheck hook compares on-disk against generator output and failed in CI (the refactor commit landed on the worktree branch before this PR, so main-branch CI never saw it — the drift first surfaced on this PR's CI run). Fix: run both generators and commit the result. Same approach as prior cleanup commits: - 47928bf chore(calendar): regenerate IPC + typecheck fix after rich-field contract shift - 625f021 chore(calendar): clear lint warnings + reset generated IPC to canonical form No behavior change — the generated types and runtime bindings are equivalent; only formatting differs.
h4yfans
added a commit
that referenced
this pull request
Apr 20, 2026
Wires the GOOGLE_CALENDAR_E2E_* repo secrets (configured 2026-04-20) into the Playwright Electron job and flips GOOGLE_CALENDAR_E2E=1 so the two Calendar suites landed in #291 actually exercise the real Google Calendar API in CI: - calendar-google-two-way-sync.e2e.ts (Google → Memry import) - calendar-google-writeback.e2e.ts (Memry → Google write-back) Without this, both suites' `CREDS_PRESENT` gate was false in CI and they skipped silently — the tests were landed but not actually running. Why per-step env (not workflow-level): The secrets are only needed for the Playwright step. Scoping to the step limits exposure and makes it easy to remove if we ever split the E2E job. Forks are still safe: Forked repos can't access secrets, so the env vars will be empty strings in their CI. The Calendar suites' CREDS_PRESENT check demands non-empty values and short-circuits the whole describe block via test.skip, so forks' E2E jobs continue to pass without the Calendar suites executing. Trigger scope unchanged: e2e.yml still runs only on push-to-main + manual workflow_dispatch — the Calendar suites will gate main-branch health post-merge, not individual PRs. Adding a `pull_request:` trigger is a bigger policy decision (cost and Google API usage) and is intentionally out of scope here; tracked as a follow-up.
3 tasks
h4yfans
added a commit
that referenced
this pull request
May 6, 2026
h4yfans
added a commit
that referenced
this pull request
May 6, 2026
Wires the GOOGLE_CALENDAR_E2E_* repo secrets (configured 2026-04-20) into the Playwright Electron job and flips GOOGLE_CALENDAR_E2E=1 so the two Calendar suites landed in #291 actually exercise the real Google Calendar API in CI: - calendar-google-two-way-sync.e2e.ts (Google → Memry import) - calendar-google-writeback.e2e.ts (Memry → Google write-back) Without this, both suites' `CREDS_PRESENT` gate was false in CI and they skipped silently — the tests were landed but not actually running. Why per-step env (not workflow-level): The secrets are only needed for the Playwright step. Scoping to the step limits exposure and makes it easy to remove if we ever split the E2E job. Forks are still safe: Forked repos can't access secrets, so the env vars will be empty strings in their CI. The Calendar suites' CREDS_PRESENT check demands non-empty values and short-circuits the whole describe block via test.skip, so forks' E2E jobs continue to pass without the Calendar suites executing. Trigger scope unchanged: e2e.yml still runs only on push-to-main + manual workflow_dispatch — the Calendar suites will gate main-branch health post-merge, not individual PRs. Adding a `pull_request:` trigger is a bigger policy decision (cost and Google API usage) and is intentionally out of scope here; tracked as a follow-up.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds automated E2E verification that Google Calendar two-way sync is operational in both directions. Covers the data-plane layer (import + write-back) without requiring the Memry sync-server auth infra that push-channel webhooks depend on.
What's shipped (5 commits):
2193947c) — Four test hooks on__memryTestHooks:seedGoogleCalendarTokens,createGoogleCalendarEventForE2E,deleteGoogleCalendarEventForE2E,getGooglePushChannelProbe. Fixes a staleDEFAULT_WEBHOOK_URL(sync.memry.io → sync.memrynote.com) that theMEMRY_CALENDAR_WEBHOOK_URLoverride had been masking.4fad99a5) — New suitecalendar-google-two-way-sync.e2e.ts: seeds tokens → connects via hook → creates event on Google via REST → callssyncGoogleCalendarSource→ asserts the event landed incalendar_external_events. IntroducesconnectGoogleCalendarForE2E(mirrorsCONNECT_PROVIDERpost-OAuth side effects without driving the loopback popup) andsyncGoogleCalendarSourceForE2E(bypassessyncGoogleCalendarNow'sisMemryUserSignedIn()early-return).6360ac91) — Removes the inventedMEMRY_E2E_STAGING_USER_SETUP_TOKENgate (nothing else sets it). Replaces with a plaintest.skip(true, 'Requires staging Memry sync-auth bootstrap — tracked separately').7785c4db) — New suitecalendar-google-writeback.e2e.ts: inserts a localcalendar_eventsrow → callspushSourceToGoogleCalendarvia hook → GETs the event from Google → asserts summary/start/end round-tripped. Three new hooks:createMemryEventForWriteBackE2E,pushMemryEventToGoogleForE2E,fetchGoogleEventForE2E. UsestargetCalendarId = primaryso writes land on the primary calendar instead of auto-creating a "Memry" calendar on the test account.d9071f36) — Noise commit tidying a handful of adjacent test files.Architecture note
pushSourceToGoogleCalendarandsyncGoogleCalendarSourceare the inner mechanisms; their auth guard (isMemryUserSignedIn) sits on the outer orchestrators (syncLocalSourceToGoogleCalendar,syncGoogleCalendarNow). The hooks call the inner mechanisms directly — same pattern for both directions — which is what lets the data plane verify end-to-end without staging sync-auth.Google Calendar stores event times at whole-second precision (it drops ms on
events.insertround-trip). The write-back assertion compares againstMath.floor(ms / 1000) * 1000to match the real contract.Test results
calendar-google-two-way-sync.e2e.ts(Google → Memry import)calendar-google-writeback.e2e.ts(Memry → Google write-back)calendar-push-channels.e2e.ts(webhook round-trip)All three suites run against real Google Calendar APIs using the E2E refresh token in
.env.e2e. The push-channel suite skips cleanly until staging sync-auth provisioning lands.What this does NOT cover (gap list)
One piece of infra unlocks everything below:
field-merge-calendar)Test plan
GOOGLE_CALENDAR_E2E_*credentials are present in CI secrets before enablingGOOGLE_CALENDAR_E2E=1in CI workflows