Skip to content

feat(calendar): verify Google Calendar two-way sync via E2E - #291

Merged
h4yfans merged 7 commits into
mainfrom
worktree-calendar-e2e-comprehensive
Apr 20, 2026
Merged

feat(calendar): verify Google Calendar two-way sync via E2E#291
h4yfans merged 7 commits into
mainfrom
worktree-calendar-e2e-comprehensive

Conversation

@h4yfans

@h4yfans h4yfans commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator

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):

  • Phase A (2193947c) — Four test hooks on __memryTestHooks: seedGoogleCalendarTokens, createGoogleCalendarEventForE2E, deleteGoogleCalendarEventForE2E, getGooglePushChannelProbe. Fixes a stale DEFAULT_WEBHOOK_URL (sync.memry.io → sync.memrynote.com) that the MEMRY_CALENDAR_WEBHOOK_URL override had been masking.
  • Phase B import (4fad99a5) — New suite calendar-google-two-way-sync.e2e.ts: seeds tokens → connects via hook → creates event on Google via REST → calls syncGoogleCalendarSource → asserts the event landed in calendar_external_events. Introduces connectGoogleCalendarForE2E (mirrors CONNECT_PROVIDER post-OAuth side effects without driving the loopback popup) and syncGoogleCalendarSourceForE2E (bypasses syncGoogleCalendarNow's isMemryUserSignedIn() early-return).
  • Push-channel cleanup (6360ac91) — Removes the invented MEMRY_E2E_STAGING_USER_SETUP_TOKEN gate (nothing else sets it). Replaces with a plain test.skip(true, 'Requires staging Memry sync-auth bootstrap — tracked separately').
  • Write-back (7785c4db) — New suite calendar-google-writeback.e2e.ts: inserts a local calendar_events row → calls pushSourceToGoogleCalendar via hook → GETs the event from Google → asserts summary/start/end round-tripped. Three new hooks: createMemryEventForWriteBackE2E, pushMemryEventToGoogleForE2E, fetchGoogleEventForE2E. Uses targetCalendarId = primary so writes land on the primary calendar instead of auto-creating a "Memry" calendar on the test account.
  • Formatter refactor (d9071f36) — Noise commit tidying a handful of adjacent test files.

Architecture note

pushSourceToGoogleCalendar and syncGoogleCalendarSource are 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.insert round-trip). The write-back assertion compares against Math.floor(ms / 1000) * 1000 to match the real contract.

Test results

Suite Result
calendar-google-two-way-sync.e2e.ts (Google → Memry import) 3/3 green ~7s
calendar-google-writeback.e2e.ts (Memry → Google write-back) 3/3 green ~7s each, 3 separate runs
calendar-push-channels.e2e.ts (webhook round-trip) 2/2 skipped, clear reason

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:

  • Staging Memry sync-auth user with Ed25519 signing key wired into the E2E env. (Blocker for all items below.)
  • Push-channel round-trip (connect → channel registered → drained on disconnect)
  • Webhook-driven freshness (<10s Google edit → desktop visible)
  • Channel renewal near expiry
  • Bidirectional conflict resolution (glue layer around field-merge-calendar)
  • Token refresh mid-session

Test plan

  • Write-back suite passes 3× on staging (fresh Electron instance per run)
  • Import suite still passes post-merge with main (no regressions)
  • Push-channel suite skips cleanly with the honest skip reason
  • No production code touched — all changes are test hooks + E2E specs
  • Reviewer: verify the staging GOOGLE_CALENDAR_E2E_* credentials are present in CI secrets before enabling GOOGLE_CALENDAR_E2E=1 in CI workflows

h4yfans added 7 commits April 20, 2026 03:09
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
h4yfans merged commit ee094bf into main Apr 20, 2026
5 checks passed
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.
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.
@h4yfans
h4yfans deleted the worktree-calendar-e2e-comprehensive branch May 6, 2026 16:36
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