CU-03: Google Calendar integration: conflict blocking, buffers, two-way sync
Priority: P0 Accounts/sessions: P1 host (ravikantguptaofficial@gmail.com), signed into Calendo via Google AND its Google Calendar connected to Calendo; the same Google account signed into calendar.google.com and mail.google.com (Gmail) in the same browser profile. Parallel-safe: Yes — this suite never edits global host weekly availability; it only creates GCal busy events, one event type, and one booking, all RUNID-scoped. (Caveat: do not run two GCal suites that block the same hour simultaneously.) Exclusive (rewrites global host availability?): No. Estimated time: 35–45 minutes (Google API freeBusy/push-webhook propagation adds wait time). L3 reality checks: Heavy — (1) a real GCal BUSY event suppresses a Calendo slot, (2) a real Calendo booking creates a real GCal event, (3) reschedule moves the GCal event, (4) cancel deletes the GCal event, (5) an external GCal event appears in the Calendo dashboard Calendar tab (inbound two-way).
Goal
This suite proves Calendo's Google Calendar integration is real two-way sync, not a mock. It verifies that (a) when the host is genuinely busy in Google Calendar, Calendo stops offering that time to invitees (outbound conflict blocking via Google freeBusy); (b) per-event-type before/after buffers expand that blocking to adjacent slots; (c) a confirmed Calendo booking actually writes a Google Calendar event with the invitee and host as attendees; (d) rescheduling moves that event and cancelling deletes it; and (e) an event created directly in Google Calendar surfaces inside Calendo's dashboard Calendar view (inbound sync). This is the single most important integration for an indie Calendly competitor — if a host gets double-booked because Calendo ignored their real calendar, the product is unusable. Every step that touches Google must be confirmed in the real Google UI, not just in Calendo's screen.
Preconditions
- P1 is logged into Calendo. Open
https://calendo.dev/dashboard/— the dashboard (#appLayout,#welcomeHeader) must render without redirecting to/auth/login.html. If it redirects to login, STOP and flag a precondition failure per00-setup-preconditions.md. Do NOT perform a cold password/Google login mid-run. - P1's Google Calendar must already be Connected to Calendo (see verification in Step 2). If it is NOT connected, follow the one-time connect procedure in
00-setup-preconditions.md(summarized in the "If Google Calendar is not connected" block under Steps). Connecting requires clicking through the production OAuth consent screen including the "Google hasn't verified this app" red warning page (Advanced → Go to calendo.dev (unsafe)). - The browser profile must ALSO be signed into the same Google account at
https://calendar.google.comandhttps://mail.google.com. If either prompts for a password, STOP and flag a precondition failure — do not log in cold. - The host has a working default weekly availability schedule with normal business hours (e.g. weekday hours that include late morning). This suite does NOT modify it. If the host's booking page shows NO available slots at all on any upcoming weekday, flag it (an empty schedule makes conflict-blocking unverifiable) and stop.
- The host slug is read at runtime from the dashboard Overview share URL (
#bookingLinkValue, formathttps://calendo.dev/booking/?user=<HOST_SLUG>). Do not hardcode it.
Test data
- RUNID: pick a fresh UTC token at execution time, e.g.
20260601-1530. Embed it in every created artifact so reruns never collide and Gmail searches scope cleanly. - Event type: name
GCal Sync <RUNID>, duration30min. Slug auto-derives togcal-sync-<RUNID>(lowercase, spaces→hyphens). This event type will get buffers applied later. - Invitee (booking): name
Inv <RUNID>, emailravikantguptaofficial+inv-<RUNID>@gmail.com(plus-alias of P1's inbox so all booking emails land in one searchable Gmail). - GCal busy block (conflict test): title
CALENDO-BUSY-<RUNID>, placed over one of the host's available hours on the next eligible weekday (see Step 5 for picking the slot). - GCal external event (inbound sync test): title
CALENDO-EXT-<RUNID>, placed at any clearly-labeled future time the same week. - GCal event created BY Calendo (booking): Calendo writes its title as
GCal Sync <RUNID> with Inv <RUNID>— search GCal forGCal Sync <RUNID>.
Steps
Order matters throughout: you must establish the connection (Steps 1–2) and a clean baseline of offered slots (Steps 3–4) BEFORE creating the GCal busy block, so you can prove a specific slot disappeared. Capturing the "before" screenshot is mandatory.
- Action — Go to the dashboard (
https://calendo.dev/dashboard/). Confirm you are P1 (top-right account /#welcomeHeadershows the host name). Read and record the host slug from the Overview share URL (#bookingLinkValue, text likehttps://calendo.dev/booking/?user=<HOST_SLUG>). Expect — Dashboard loads;#bookingLinkValuecontains auser=slug. Record<HOST_SLUG>. [L1]
- Action — Open the Availability tab (left sidebar link
.sidebar-nav a[data-tab="availability"]), then open the Calendar settings sub-panel (#avail-panel-calendar-settings, heading "Calendar settings"). Look at the connections list (#calSettingsConnections). Expect — A connection card (.cal-connection-card) reading "Google Calendar" with status text "Checking 1 calendar" (.cal-connection-status) is present. The "+ Connect calendar account" button (#calSettingsConnectBtn) does NOT offer Google again (Google already connected). Capture screenshot:cu03-02-google-connected. [L1][L2] If Google Calendar is NOT connected: the connections list shows "No calendars connected." In that case, click "+ Connect calendar account" / the "Connect Google Calendar" link (a[href="/api/calendars/connect/google"]). On the Google consent flow: pick the P1 account; on the "Google hasn't verified this app" red warning page click "Advanced" then "Go to calendo.dev (unsafe)"; on the scopes screen (calendar.events + calendar.readonly) click Continue/Allow. You should be redirected back tohttps://calendo.dev/dashboard/?calendar=connectedand see a toast "Calendar connected!". Re-do Step 2 to confirm the card appears. If consent fails or asks for a cold password, flag a precondition failure and stop.
- Action — Create the event type. Go to the Overview tab (
.sidebar-nav a[data-tab="overview"]), click "+ New" (#overviewCreateEtBtn). In the edit modal (#editEtModal), set name =GCal Sync <RUNID>(#editEtName), duration = 30 min (#editEtDuration→ value30), then click Create (#editEtSaveBtn). Expect — Modal closes; the Overview event-types list (#overviewEventTypes) now containsGCal Sync <RUNID>. Note the booking URL:https://calendo.dev/booking/?user=<HOST_SLUG>&event=gcal-sync-<RUNID>. [L1][L2]
- Action (baseline) — In a new tab, open the public booking page for this event:
https://calendo.dev/booking/?user=<HOST_SLUG>&event=gcal-sync-<RUNID>. Wait for the calendar view (#booking-view). Click the first available day (.calendar-day.available) on a weekday that is at least 1–2 days out (to clear the host's min-notice window of ~4 hours and ensure a full day of slots). Record the date and the FULL ordered list of offered time slots (.time-slot) shown in the time panel (#time-slots-list). Pick ONE slot in the middle of the offered range as your target busy slot (e.g. an 11:00-ish slot) — record its exact start time and the slots immediately before and after it (you will use those for the buffer test in Step 9). Expect — At least 3 consecutive.time-slotentries are offered on that day. Capture screenshot:cu03-04-slots-before(must clearly show the target slot present). Record: target date, target slot time, the slot before it, the slot after it. [L1]
- Action (create real GCal conflict) — Open
https://calendar.google.com(signed in as P1). Navigate to the exact target date from Step 4. Create a new event titledCALENDO-BUSY-<RUNID>covering the target slot's wall-clock time in the host's timezone, set its visibility/status to Busy (default), and Save. (Cover at least the full 30-minute target slot; covering ±30 min around it is fine and makes the effect unambiguous.) Expect — TheCALENDO-BUSY-<RUNID>event is visible on the target date in Google Calendar, marked Busy. Capture screenshot:cu03-05-gcal-busy-created. [L3]
- Action (verify conflict blocks the slot) — Return to the booking-page tab. Hard-reload it (Calendo queries Google freeBusy live per slots request; allow up to ~30s and one extra reload if Google is slow to reflect). Re-open the same target date (
.calendar-day.available). Expect — The target slot from Step 4 is NO LONGER offered in#time-slots-list(it has been filtered out by the Google busy time). Other, non-overlapping slots that day are still offered. If the whole day vanished, that is acceptable only if the busy block plus buffers covers all remaining slots — but for a mid-day single-slot block, expect just the target (and immediately-overlapping) slots to disappear while earlier/later slots remain. Capture screenshot:cu03-06-slot-blocked-after(showing the target time absent). This is the core outbound-conflict proof. [L3→L1]
- Action (book a still-free slot → write to GCal) — On the same booking page, pick a clearly-free slot that does NOT overlap the busy block (and is comfortably away from it — choose one well before or well after
CALENDO-BUSY-<RUNID>). Click the day (.calendar-day.available), click the slot (.time-slot), then in the confirm form fill name =Inv <RUNID>(#input-name) and email =ravikantguptaofficial+inv-<RUNID>@gmail.com(#input-email). Click Confirm Booking (#btn-confirm). Record the chosen booking date/time. Expect — Confirmation view appears:.confirmation-titlereads "Booking Confirmed!". A manage block (#confirmation-manage) shows Reschedule (a[href*="reschedule.html?token="]) and Cancel booking (a[href*="cancel.html?token="]) links. Record thetoken=value from those hrefs (the cancel/reschedule token) — you will reuse it in Steps 11–12. Capture screenshot:cu03-07-booking-confirmed. [L1][L2]
- Action (apply buffers to the event type) — Back in the dashboard, Overview tab, click the
GCal Sync <RUNID>event type to edit it (opens#editEtModal/ the event edit panel). Expand the "Limits & Buffers" section (#epSectionLimits). Set Buffer before (min) =30(#editEtBufferBefore) and Buffer after (min) =30(#editEtBufferAfter). Save (#editEtSaveBtn). Expect — Save succeeds; the event-type summary reflects the buffer (e.g. "±30/30m buffer" in#epSummaryLimits). Capture screenshot:cu03-08-buffers-set. [L1][L2]
- Action (verify buffers suppress adjacent slots) — Reload the booking page (
https://calendo.dev/booking/?user=<HOST_SLUG>&event=gcal-sync-<RUNID>) and open the target date again. Compare against your Step 4 record of the slot-before and slot-after the (still-present)CALENDO-BUSY-<RUNID>block. Expect — With a 30-min before/after buffer, the slots immediately adjacent to the busy block (the slot-before and slot-after you recorded in Step 4) are now ALSO removed, because Calendo's overlap test is(slotStart − bufferBefore) < busy.end && (slotEnd + bufferAfter) > busy.start. So a wider window aroundCALENDO-BUSY-<RUNID>is blocked than in Step 6. Capture screenshot:cu03-09-buffer-blocked. Note: this also makes adjacency to the Step-7 booking itself buffered; that is expected and consistent. [L3→L1]
- Action (L3: confirm the booking created a real GCal event) — Go to
https://calendar.google.com, navigate to the date/time of the Step-7 booking. (Calendo writes the event on confirmation; if not visible within ~30s, refresh GCal once.) Expect — An event titledGCal Sync <RUNID> with Inv <RUNID>exists at exactly the booked start time, lasting 30 min. Open it: attendees includeravikantguptaofficial+inv-<RUNID>@gmail.com(the invitee) and the host. If the event type defaulted to Google Meet, a Google Meet link is attached. The description contains "Powered by Calendo" plus Cancel/Reschedule links. Capture screenshot:cu03-10-gcal-event-created. [L3]
- Action (reschedule → GCal event moves) — Open the reschedule page using the token from Step 7:
https://calendo.dev/booking/reschedule.html?token=<TOKEN>. Pick a new slot on a DIFFERENT day or clearly different time (free, non-overlapping with the busy block). Confirm the reschedule. Record the new date/time. Expect — Calendo confirms the reschedule (success message / updated time on the page). Capture screenshot:cu03-11-rescheduled. [L1][L2]
- Action (cancel → GCal event deletes) — Open the cancel page using the same token:
https://calendo.dev/booking/cancel.html?token=<TOKEN>. Confirm the cancellation (provide a reason if the field requires one). Expect — Calendo confirms the booking is cancelled. Capture screenshot:cu03-12-cancelled. [L1][L2]
- Action (inbound two-way: external GCal event shows in Calendo) — In
https://calendar.google.com, create a brand-new event titledCALENDO-EXT-<RUNID>at any clear future time this week (e.g. tomorrow 16:00 host time), Save. Then in the Calendo dashboard open the Calendar tab (.sidebar-nav a[data-tab="calendar"], panel#panel-calendar, grid#calendarViewGrid) and navigate to the week containing that event. (The dashboard Calendar view calls/api/calendar-view, which merges live Google events tagged as source "google"; allow one refresh.) Expect —CALENDO-EXT-<RUNID>appears on the Calendo Calendar grid at the matching time, labeled as a Google/external event (small "Google" source label). This proves inbound sync (external GCal → Calendo). Capture screenshot:cu03-13-inbound-ext-event. [L3→L1]
L3 reality checks
- Gmail (booking lifecycle emails): Open
https://mail.google.com. Searchinv-<RUNID>. Confirm, all addressed toravikantguptaofficial+inv-<RUNID>@gmail.com:- a booking confirmation email for
GCal Sync <RUNID>(subject typically contains "Confirmed" and the event name) — from Step 7; - a reschedule email reflecting the new time — from Step 11;
- a cancellation email — from Step 12. Also search
GCal Sync <RUNID>to catch any host-side notification in the same inbox. Open the confirmation email and verify the Cancel/Reschedule links point tocalendo.dev/booking/cancel.html?token=and.../reschedule.html?token=. Capture screenshot:cu03-L3-gmail-emails.
- a booking confirmation email for
- Google Calendar — busy block (conflict): the
CALENDO-BUSY-<RUNID>event you created is on the target date marked Busy (Step 5), and its presence is what removed the target slot in Calendo (Step 6). Screenshot pair:cu03-05-gcal-busy-created+cu03-06-slot-blocked-after. - Google Calendar — Calendo-created event (created): at the Step-7 booking time, event
GCal Sync <RUNID> with Inv <RUNID>exists with invitee+host attendees (Step 10). Screenshot:cu03-10-gcal-event-created. - Google Calendar — event moved (reschedule): Calendo reschedules by deleting the old GCal event and creating a NEW one at the new time. After Step 11, confirm in GCal that NO
GCal Sync <RUNID> with Inv <RUNID>event remains at the ORIGINAL Step-7 time, and exactly ONE such event now exists at the Step-11 new time. Capture screenshot:cu03-L3-gcal-moved. - Google Calendar — event deleted (cancel): after Step 12, confirm in GCal that NO
GCal Sync <RUNID> with Inv <RUNID>event exists at the Step-11 time (or anywhere) — the cancel deleted it (sendUpdates=all). Capture screenshot:cu03-L3-gcal-deleted. - Inbound sync:
CALENDO-EXT-<RUNID>created in GCal (Step 13) appears in the Calendo dashboard Calendar grid. Screenshot:cu03-13-inbound-ext-event.
Cleanup
Leave both the Calendo account and the Google Calendar clean for the next run.
- Google Calendar: delete
CALENDO-BUSY-<RUNID>(Step 5) andCALENDO-EXT-<RUNID>(Step 13). Confirm noGCal Sync <RUNID> with Inv <RUNID>event remains (Step 12 should have removed it; if it lingers, delete it manually). - Calendo booking: the test booking was cancelled in Step 12 — confirm it shows Cancelled (Bookings tab
.sidebar-nav a[data-tab="bookings"], filtered/searched byInv <RUNID>) and no longer occupies a slot. - Calendo event type: Overview tab → open
GCal Sync <RUNID>→ delete it (the event type's delete/remove action; confirm the destructive prompt). Verify it disappears from#overviewEventTypes. - Do NOT disconnect P1's Google Calendar — it is a persistent precondition for all GCal suites. Leave it Connected.
- Do NOT modify the host's weekly availability schedule (this suite never changed it).
Pass/Fail criteria
The run PASSES only if ALL are true:
- Step 2: Calendo shows Google Calendar as a connected
.cal-connection-card(or it was successfully connected via the documented consent click-through). - Step 4 → Step 6: a specific time slot that WAS offered before the GCal busy block is NO LONGER offered after
CALENDO-BUSY-<RUNID>exists, with at least one non-overlapping slot still offered (proving it was the conflict, not a total outage). - Step 7: booking confirmed in Calendo with a usable cancel/reschedule token.
- Step 9: after setting 30/30-min buffers, the slots immediately adjacent to the busy block are additionally removed (buffer expands blocking beyond Step 6).
- Step 10: a real Google Calendar event
GCal Sync <RUNID> with Inv <RUNID>exists at the booked time with the invitee AND host as attendees. - Step 11 (L3): in GCal, the event is gone from the original time and present exactly once at the new time.
- Step 12 (L3): in GCal, the event is fully deleted; Calendo shows the booking Cancelled.
- Step 13:
CALENDO-EXT-<RUNID>created in GCal appears in Calendo's dashboard Calendar tab (inbound sync). - L3 Gmail: confirmation, reschedule, and cancellation emails all arrived in the
+inv-<RUNID>inbox. - Cleanup completed: GCal test events deleted, event type deleted, booking cancelled, Google connection left intact.
Any single failure = FAIL for that line item; record which.
Evidence to capture
cu03-02-google-connected— Calendo shows Google connected.cu03-04-slots-before— booking page with the target slot present (baseline).cu03-05-gcal-busy-created—CALENDO-BUSY-<RUNID>in Google Calendar.cu03-06-slot-blocked-after— target slot absent after the busy block.cu03-07-booking-confirmed— Calendo "Booking Confirmed!" with cancel/reschedule links.cu03-08-buffers-set— event type with 30/30 buffers saved.cu03-09-buffer-blocked— adjacent slots removed by buffers.cu03-10-gcal-event-created— Calendo-created GCal event with attendees (invitee + host).cu03-11-rescheduled,cu03-12-cancelled— Calendo reschedule/cancel confirmations.cu03-L3-gcal-moved,cu03-L3-gcal-deleted— GCal showing the event moved, then deleted.cu03-13-inbound-ext-event— external GCal event surfaced in Calendo's Calendar tab.cu03-L3-gmail-emails— Gmail searchinv-<RUNID>showing confirmation + reschedule + cancellation.- Notes: recorded
<HOST_SLUG>, target date, target slot time, slot-before/slot-after times, booked time, rescheduled time, cancel/reschedule token.
Manual residue / cannot-verify
- Webhook push latency / cron-driven inbound cancellation: Calendo also has an inbound Google push-webhook path (
/api/calendars/webhook/google) that auto-cancels a Calendo booking when the corresponding GCal event is deleted in Google, plus a periodicsyncCalendarscron. This suite verifies inbound visibility (Step 13) but does NOT exercise the webhook-driven auto-cancel (deleting the Calendo-created GCal event by hand and waiting for Calendo to mark the booking cancelled). Timing is non-deterministic from the browser; hand this off to the human to spot-check or treat as covered by unit tests (tests/unit/worker/google-calendar-webhook.test.js). - freeBusy propagation timing: if Google is slow to reflect the new busy event, Step 6/9 may need extra reloads; a transient failure here is a timing artifact, not necessarily a product bug — note retries and re-test before declaring FAIL.
- OAuth consent screen state: whether the production app still shows the unverified-app warning depends on Google verification status (TBD per
docs/google-oauth-verification.html); the agent should record which consent screens appeared but cannot change verification state. - Watch-channel expiry/renewal (the
watch_expirationrefresh cron) is server-side and out of scope for a browser run.