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

Test data

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.

  1. Action — Go to the dashboard (https://calendo.dev/dashboard/). Confirm you are P1 (top-right account / #welcomeHeader shows the host name). Read and record the host slug from the Overview share URL (#bookingLinkValue, text like https://calendo.dev/booking/?user=<HOST_SLUG>). Expect — Dashboard loads; #bookingLinkValue contains a user= slug. Record <HOST_SLUG>. [L1]
  1. 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 to https://calendo.dev/dashboard/?calendar=connected and 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.
  1. 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 → value 30), then click Create (#editEtSaveBtn). Expect — Modal closes; the Overview event-types list (#overviewEventTypes) now contains GCal Sync <RUNID>. Note the booking URL: https://calendo.dev/booking/?user=<HOST_SLUG>&event=gcal-sync-<RUNID>. [L1][L2]
  1. 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-slot entries 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]
  1. 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 titled CALENDO-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 — The CALENDO-BUSY-<RUNID> event is visible on the target date in Google Calendar, marked Busy. Capture screenshot: cu03-05-gcal-busy-created. [L3]
  1. 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]
  1. 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-title reads "Booking Confirmed!". A manage block (#confirmation-manage) shows Reschedule (a[href*="reschedule.html?token="]) and Cancel booking (a[href*="cancel.html?token="]) links. Record the token= value from those hrefs (the cancel/reschedule token) — you will reuse it in Steps 11–12. Capture screenshot: cu03-07-booking-confirmed. [L1][L2]
  1. 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]
  1. 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 around CALENDO-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]
  1. 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 titled GCal Sync <RUNID> with Inv <RUNID> exists at exactly the booked start time, lasting 30 min. Open it: attendees include ravikantguptaofficial+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]
  1. 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]
  1. 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]
  1. Action (inbound two-way: external GCal event shows in Calendo) — In https://calendar.google.com, create a brand-new event titled CALENDO-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.) ExpectCALENDO-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

Cleanup

Leave both the Calendo account and the Google Calendar clean for the next run.

Pass/Fail criteria

The run PASSES only if ALL are true:

Any single failure = FAIL for that line item; record which.

Evidence to capture

Manual residue / cannot-verify