✓ The required accounts are already signed in in this browser — you do NOT need any passwords
To open Calendo, go to https://calendo.dev/auth/login.html and click "Sign in with Google" (Gmail) or "Sign in with Microsoft" (Outlook), then choose the account this suite needs. The sessions are already authenticated, so it signs straight in.
  • Primary host (P1): ravikantguptaofficial@gmail.com — "Sign in with Google"
  • Teammate / 2nd account (P3): everythingaichannelemail@gmail.com — "Sign in with Google"
  • Outlook host: ravikant0909@outlook.com — "Sign in with Microsoft"
Pick a fresh RUNID (a timestamp, e.g. 20260603-1400) and embed it in everything you create. This suite is safe to run in parallel with the other suites — if a time slot was just taken by another run, simply choose the next available one. Clean up what you create at the end, then report results using RESULTS-TEMPLATE.

CU-06: Availability engine: weekly rules, overrides, holidays, slot-debug

Priority: P0 Accounts/sessions: P1 host (ravikantguptaofficial@gmail.com) signed into Calendo at https://calendo.dev/dashboard/ via persisted session. No invitee account needed — bookings/L3 not in scope for this suite. Parallel-safe: No. Exclusive (rewrites global host availability?): YES. This suite rewrites P1's GLOBAL default weekly schedule ("Working Hours") plus its overrides/holidays/meeting-limits, which changes slot availability for every host-writing suite. It MUST run alone, and MUST restore the documented baseline at the end (see Cleanup). Estimated time: 35 minutes L3 reality checks: None. All assertions are [L1] UI / [L2] Calendo persistence + slot-debug API. No Google/Outlook calendar or Gmail verification is required for this suite. (Slot-debug reasons 8 google_calendar and 9 outlook_calendar are NOT exercised here — see Manual residue.)

Goal

This suite proves Calendo's availability engine correctly governs which times a public invitee can book. A host sets a known weekly recurring schedule (Mon–Fri 09:00–17:00) and the public booking page must offer ONLY those windows; a date override that blocks a weekday must zero out that day; an override that opens a normally-off day (Saturday) must surface slots; an enabled holiday preset must auto-block the holiday date; per-event-type buffers and min-notice must shrink the offered slots; multiple named schedules must drive different event types independently; and the /api/slot-debug endpoint must explain WHY any specific time is unavailable across its nine reason codes. This is the core scheduling correctness guarantee — if it's wrong, the product double-books or hides valid times. Because it rewrites the host's global schedule, it is the canonical "exclusive" suite that all other host-writing suites depend on for a clean baseline.

Preconditions

Test data

RUNID convention: pick one fresh UTC token at execution start, format YYYYMMDD-HHMM (example 20260601-1530). Embed RUNID in every created artifact so reruns never collide.

Steps

Order matters throughout: each phase mutates the schedule, so you must capture the original baseline FIRST (Step 1), then run phases sequentially, then restore in Cleanup. Several saves auto-persist (overrides) while others require an explicit Save button (weekly rules, holidays, meeting limits) — follow exactly.

Phase A — Establish and verify the known weekly schedule

  1. Action: Go to https://calendo.dev/dashboard/, click the sidebar "Availability" link (.sidebar-nav a[data-tab="availability"]). Confirm panel #panel-availability gets class active. Ensure the "Schedules" sub-tab is active (.avail-sub-tab with text "Schedules"; its panel #avail-panel-schedules has class active). In #scheduleSelect, confirm "Working Hours" is selected. Read and RECORD the current state for cleanup: the day circles that are active (#weeklyScheduleEditor [data-day-circle].active), each day's time-select values (.avail-time-select data-name="start-N-0"/end-N-0), the value of #totalWeeklyHours, the timezone shown in #scheduleTimezoneDisplay, plus any items in #overridesList, #meetingLimitsList (Advanced settings sub-tab), and #holidaysList. Expect: You can see and record a complete baseline snapshot. Capture screenshot: cu06-01-baseline. [L2]
  1. Action: Set the schedule to exactly Mon–Fri 09:00–17:00 and weekends off. In #weeklyScheduleEditor, for each weekday Mon(1)–Fri(5): ensure its circle ([data-day-circle="1"][data-day-circle="5"]) is active; if a day shows "Unavailable", click its circle to enable it (defaults to 09:00–17:00). For each weekday confirm the first window's start select (.avail-time-select[data-name="start-N-0"]) = 09:00 and end (end-N-0) = 17:00; if a day has extra windows, remove them via the window's × ([data-remove-window="N-i"]). Ensure Saturday(6) and Sunday(0) circles ([data-day-circle="6"], [data-day-circle="0"]) are NOT active (click to disable if they show times). Expect: Mon–Fri each show one 09:00–17:00 window; Sat/Sun show "Unavailable"; #totalWeeklyHours reads 40.0 hrs/week. Capture screenshot: cu06-02-weekly-set. [L1]
  1. Action: Click "Save Schedule" (#saveScheduleBtn). Wait for the toast "Schedule saved". Expect: A success toast appears; button returns to label "Save Schedule". [L1]
  1. Action: Reload the page (F5), navigate back to Availability, confirm "Working Hours" selected. Expect: Mon–Fri still 09:00–17:00, Sat/Sun off, #totalWeeklyHours = 40.0 hrs/week — i.e. the save persisted across reload. [L2]
  1. Action: Open the public booking page in a new tab: https://calendo.dev/booking/?user=<HOST_SLUG>&event=30-minute-meeting. Wait for #booking-view to be visible (give the calendar ~1.5s to render). Expect: The month calendar shows weekday cells with class .calendar-day.available (count > 0) and weekend cells with class .calendar-day.unavailable (count > 0). [L2]
  1. Action: Click the first available weekday cell (.calendar-day.available). Expect: Time slots (.time-slot) appear; the earliest visible slot corresponds to 09:00 host time (rendered in the invitee's timezone) and the latest fits before 17:00 (a 30-min event => last start 16:30 host time). Hover/read a couple of slots to confirm they fall inside 09:00–16:30 host-equivalent. Capture screenshot: cu06-03-booking-weekday-slots. [L2]
  1. Action: Navigate the booking calendar to land on a Saturday or Sunday cell. Expect: Weekend cells carry .calendar-day.unavailable and NOT .available; clicking does not surface .time-slot elements. [L2]

Phase B — Date override that BLOCKS a weekday

  1. Action: Back in the dashboard Availability "Schedules" sub-panel, in the date-override mini calendar (#overrideCalendar), use the nav arrows ([data-cal-nav="1"]) to reach the month containing BLOCK_DATE, then click that day cell ([data-cal-date="<BLOCK_DATE>"]). The override dialog #overrideDialog opens. Expect: Dialog title "Add Override"; #overrideDialogDate shows the long-form date of BLOCK_DATE; "Block entire day" (#overrideBlockDay) is checked. [L1]
  1. Action: Leave "Block entire day" checked and click "Save Override" (#saveOverride). Expect: Toast "Override saved" (overrides auto-save via PUT). The BLOCK_DATE cell in #overrideCalendar now shows the blocked styling (class has-override), and #overridesList lists the date with a "Blocked" badge (.avail-override-blocked). Capture screenshot: cu06-04-block-override-saved. [L1]
  1. Action: Reload the booking page tab at ...&event=30-minute-meeting, navigate to the month of BLOCK_DATE, and locate that date's cell. Expect: BLOCK_DATE is .calendar-day.unavailable (not .available); clicking it surfaces no .time-slot. The surrounding weekdays remain available. Capture screenshot: cu06-05-booking-block-date. [L2]

Phase C — Date override that OPENS a normally-off Saturday

  1. Action: In #overrideCalendar, reach the month of OPEN_DATE (a Saturday) and click its cell ([data-cal-date="<OPEN_DATE>"]). In the dialog, UNCHECK "Block entire day" (#overrideBlockDay) — the custom-hours block (#overrideCustomHours) appears. Set #overrideStart = 10:00 and #overrideEnd = 12:00. Click "Save Override" (#saveOverride). Expect: Toast "Override saved"; OPEN_DATE cell shows custom-hours styling (class has-override-custom); #overridesList shows OPEN_DATE with "10:00 AM - 12:00 PM". [L1]
  1. Action: Reload the booking page, navigate to the month of OPEN_DATE. Expect: OPEN_DATE (a Saturday, normally closed) is now .calendar-day.available. Click it; .time-slot elements appear, bounded by 10:00–12:00 host time (for a 30-min event, exactly four starts: 10:00, 10:30, 11:00, 11:30 host time). Capture screenshot: cu06-06-booking-open-saturday. [L2]

Phase D — Holiday preset auto-blocks a holiday

  1. Action: In the Availability tab, click the "Advanced settings" sub-tab (.avail-sub-tab text "Advanced settings"; panel #avail-panel-advanced-settings becomes active). In the Holidays section set #holidayCountry = "United States" (value US) and #holidayYear = 2026, then click "Load Presets" (#loadHolidayPresetsBtn). Expect: Toast "Loaded N holidays for US 2026"; #holidaysList populates with .holiday-toggle-row entries (e.g. "Independence Day", "Thanksgiving Day", "Christmas Day"), each with an enabled toggle ([data-toggle-holiday]). [L1]
  1. Action: Identify an UPCOMING US holiday at least a few days out (e.g. Independence Day 2026 = 2026-07-04, or the next holiday after today). Confirm its toggle is ON (checked). Click "Save Holidays" (#saveHolidaysBtn). Expect: Toast "Holidays saved". RECORD which holiday date you will assert as HOLIDAY_DATE. Capture screenshot: cu06-07-holidays-saved. [L1]
  1. Action: Switch back to the "Schedules" sub-tab; in #overrideCalendar navigate to the month of HOLIDAY_DATE. Expect: The HOLIDAY_DATE cell shows blocked styling (class has-override) with a tooltip "Holiday: <name>" (holidays render red even without an explicit override). [L1]
  1. Action: Reload the booking page, navigate to the month of HOLIDAY_DATE. Expect: HOLIDAY_DATE is .calendar-day.unavailable; no .time-slot on click — the holiday auto-blocked the day. Capture screenshot: cu06-08-booking-holiday-blocked. [L2]

Phase E — Buffer + min-notice interaction (per-event-type)

  1. Action: Create a dedicated event type so buffers/min-notice don't pollute the default. Go to the dashboard Overview (sidebar "Overview"), click "+ New" (#overviewCreateEtBtn); the edit modal #editEtModal opens. Set #editEtName = Buf-<RUNID>, #editEtDuration = 30. Open the "Limits & Buffers" section and set #editEtBufferBefore = 30, #editEtBufferAfter = 30, #editEtMinNotice = 48. Leave its availability schedule = default ("Working Hours") via #etSchedule. Click the save button (#editEtSaveBtn, labeled "Create"). Expect: Modal closes; Buf-<RUNID> appears in #overviewEventTypes. Record slug buf-<runid>. [L1]
  1. Action: Open https://calendo.dev/booking/?user=<HOST_SLUG>&event=buf-<runid>. Expect: Because min-notice is 48h, all slots within the next 48 hours are suppressed; the earliest bookable day/time is ≥48h from now. Compare against the 30-minute-meeting page (default 4h notice) opened side by side: the Buf-<RUNID> page offers strictly FEWER near-term slots. [L2]
  1. Action: On the Buf-<RUNID> page, pick any open weekday and inspect its .time-slot list. Expect: With 30-min buffers before/after, adjacent slots are spaced so a booked slot would block its neighbors; the slot grid is sparser than the buffer-free default event. (Exact count depends on existing bookings; the key assertion is fewer slots than the no-buffer default for the same day.) Capture screenshot: cu06-09-buffer-minnotice-slots. [L2]

Phase F — Multiple schedules drive different event types

  1. Action: Go to Availability → "Schedules" sub-tab, click "+ New Schedule" (#createScheduleBtn); form #newScheduleForm appears. Set #newScheduleName = Eve-<RUNID>, #newScheduleTimezone = America/New_York. Click the form's "Create" submit (#scheduleCreateForm button[type="submit"]). Expect: Form hides; #scheduleSelect now contains and has selected Eve-<RUNID>; the new schedule starts empty (all days "Unavailable", #totalWeeklyHours = 0.0 hrs/week). The "Delete Schedule" button (#deleteScheduleBtn) is now visible (non-default). [L1]
  1. Action: With Eve-<RUNID> selected, enable a DISTINCT window: click the Wednesday circle ([data-day-circle="3"]) to enable, set its window to start 18:00 / end 21:00 (.avail-time-select[data-name="start-3-0"]/end-3-0). Click "Save Schedule" (#saveScheduleBtn). Expect: Toast "Schedule saved"; #totalWeeklyHours reflects 3.0 hrs/week. [L1]
  1. Action: Create an event type bound to this schedule. Overview → "+ New" (#overviewCreateEtBtn); set #editEtName = EvtSched-<RUNID>, #editEtDuration = 30, and set #etSchedule to Eve-<RUNID>. Click "Create" (#editEtSaveBtn). Expect: Modal closes; EvtSched-<RUNID> appears in overview. Record slug evtsched-<runid>. [L1]
  1. Action: Open https://calendo.dev/booking/?user=<HOST_SLUG>&event=evtsched-<runid> and navigate to an upcoming Wednesday. Expect: Only Wednesdays offer slots, and those slots fall in the 18:00–21:00 New York window (rendered in invitee tz), NOT the default 09:00–17:00 LA window. Cross-check: the 30-minute-meeting page still shows Mon–Fri 09:00–17:00 LA — proving the two event types use independent schedules. Capture screenshot: cu06-10-multi-schedule-booking. [L2]

Phase G — Meeting limits (daily/weekly)

  1. Action: Re-select "Working Hours" in #scheduleSelect. Go to "Advanced settings" sub-tab. In Meeting limits, click "+ Add a meeting limit" (#addMeetingLimitBtn). A .meeting-limit-row appears with type select ([data-limit-type="0"]) defaulting to "Daily" and a max input ([data-limit-max="0"]) defaulting to 5. Leave type "daily", set max = 1. Click "Save Limits" (#saveMeetingLimitsBtn). Expect: Toast "Meeting limits saved". [L1]
  1. Action: Reload the page, return to Availability → Advanced settings → Meeting limits. Expect: The daily-max-1 limit persisted (row present, type "Daily", max 1) — confirming [L2] persistence of the meeting-limits write. (Full behavioral enforcement — a day going empty after 1 booking — is covered by unit tests tests/unit/worker/availability-meeting-limits.test.js; in-browser we verify persistence and that slot-debug reports daily_cap when applicable in Phase H.) Capture screenshot: cu06-11-meeting-limit-saved. [L2]

Phase H — Slot-debug "why is this time unavailable?" (all 9 reasons)

The endpoint is GET https://calendo.dev/api/slot-debug/<HOST_SLUG>/<EVENT_SLUG>?time=<ISO>&timezone=<TZ>. It returns JSON { slot, date, time, timezone, available, reasons: [{reason, detail}] }. Drive it by typing the URL into the browser address bar (you are authenticated, but this is a public endpoint) and reading the rendered JSON. The nine reason codes the engine can emit are: holiday, date_override, no_schedule, outside_hours, min_notice, max_future, booking_conflict, daily_cap, google_calendar, outlook_calendar.

  1. Action: Probe an OPEN time. Open …/api/slot-debug/<HOST_SLUG>/30-minute-meeting?time=<DEBUG_OK_TIME>&timezone=America/Los_Angeles. Expect: "available": true and "reasons": []. [L2]
  1. Action: Probe a weekend (no-schedule) time, e.g. an upcoming Sunday 14:00: …/30-minute-meeting?time=<SUNDAY>T14:00:00&timezone=America/Los_Angeles. Expect: available:false; a reason with "reason":"no_schedule" (detail mentions no availability rules for that weekday). [L2]
  1. Action: Probe an out-of-hours weekday time, e.g. a normal Tuesday at 20:00: …/30-minute-meeting?time=<TUESDAY>T20:00:00&timezone=America/Los_Angeles. Expect: available:false; reason "outside_hours" (detail cites scheduled hours 09:00-17:00). [L2]
  1. Action: Probe the BLOCK override date at 14:00: …/30-minute-meeting?time=<BLOCK_DATE>T14:00:00&timezone=America/Los_Angeles. Expect: available:false; reason "date_override" (detail: date blocked by a date override). [L2]
  1. Action: Probe a HOLIDAY date at 14:00: …/30-minute-meeting?time=<HOLIDAY_DATE>T14:00:00&timezone=America/Los_Angeles. Expect: available:false; reason "holiday" (detail names the holiday). [L2]
  1. Action: Probe a too-soon time using the high-min-notice event Buf-<RUNID>: a valid open weekday/time only ~2 hours from now: …/buf-<runid>?time=<SOON_ISO>&timezone=America/Los_Angeles. Expect: available:false; reason "min_notice" (detail: requires 48h notice). [L2]
  1. Action: Probe a far-future time beyond max_future_days (default 60) on the default event, e.g. ~120 days out at 14:00: …/30-minute-meeting?time=<FAR_ISO>&timezone=America/Los_Angeles. Expect: available:false; reason "max_future" (detail: more than 60 days in the future). [L2]
  1. Action: (booking_conflict) If a confirmed booking exists at a known time for 30-minute-meeting, probe that exact slot. Expect: available:false; reason "booking_conflict" with the conflicting booking's times. If no booking exists in this clean run, RECORD that booking_conflict was not exercised in-browser (it is covered by unit tests tests/unit/worker/availability.test.js); do not fabricate. [L2]
  1. Action: (daily_cap) The daily-max-1 limit set in Phase G is a SCHEDULE-level meeting limit (it empties days in slot computation), distinct from the event-type max_bookings_per_day cap that slot-debug's daily_cap reason reads. If P1's 30-minute-meeting has a max_bookings_per_day set AND that many confirmed bookings exist on a day, probe a time on that day to elicit "daily_cap". Otherwise RECORD that daily_cap was not reproducible in-browser this run (covered by unit tests). [L2]
  1. Action: RECORD the slot-debug coverage matrix: which of the nine reasons were observed live ({no_schedule, outside_hours, date_override, holiday, min_notice, max_future} are reliably reproducible; {booking_conflict, daily_cap} depend on existing data; {google_calendar, outlook_calendar} are out of scope for this suite). Capture screenshot of the slot-debug JSON for at least four distinct reasons: cu06-12-slotdebug-reasons. [L2]

L3 reality checks

None — see Pass/Fail. This suite makes no external calendar or email assertions. (Slot-debug reasons google_calendar and outlook_calendar would require connected external calendars with conflicting events; they are deliberately deferred to the calendar-integration suites and listed in Manual residue.)

Cleanup

Restore P1 to the documented baseline so other host-writing suites see Mon–Fri 09:00–17:00 with no test residue. Do these in order:

  1. Remove test overrides. Availability → Schedules sub-tab → #overridesList: click the × ([data-remove-override="<BLOCK_DATE>"]) for BLOCK_DATE and for OPEN_DATE; each auto-saves ("Override removed" toast). Confirm #overridesList shows "No date overrides" (unless the recorded baseline had overrides — in that case restore exactly those).
  2. Clear holidays (only if the baseline had none). Advanced settings → Holidays → click "Clear All" (#clearHolidaysBtn), then "Save Holidays" (#saveHolidaysBtn). If the baseline had holidays, re-load and re-save that exact set instead.
  3. Remove the meeting limit. Advanced settings → Meeting limits → click the × ([data-remove-limit="0"]) on the daily-max-1 row, then "Save Limits" (#saveMeetingLimitsBtn). Confirm "No meeting limits configured" (unless baseline had limits — restore those).
  4. Delete the test event types. Overview → for Buf-<RUNID> and EvtSched-<RUNID>, open each and delete it (event-type delete control / confirm dialog #dialogConfirm). Confirm both are gone from #overviewEventTypes.
  5. Delete the test schedule. Availability → select Eve-<RUNID> in #scheduleSelect → click "Delete Schedule" (#deleteScheduleBtn) → confirm in #confirmDialog via #dialogConfirm. Confirm #scheduleSelect no longer lists Eve-<RUNID> and falls back to "Working Hours".
  6. Restore the default weekly schedule. Select "Working Hours"; set days/windows back to the Step 1 baseline snapshot (default = Mon–Fri 09:00–17:00, weekends off, America/Los_Angeles); click "Save Schedule" (#saveScheduleBtn). Confirm #totalWeeklyHours = 40.0 hrs/week (or the recorded baseline value).
  7. Final verification. Reload, open …/api/slot-debug/<HOST_SLUG>/30-minute-meeting?time=<DEBUG_OK_TIME>&timezone=America/Los_Angeles and confirm available:true, reasons:[]. Capture screenshot: cu06-13-baseline-restored.

Pass/Fail criteria

The run PASSES only if ALL of the following hold:

FAILS if any availability mutation does not reflect on the public booking page, any slot-debug probe returns the wrong reason or a 4xx/5xx, persistence does not survive reload, or cleanup leaves residue / a non-baseline default schedule.

Evidence to capture

Manual residue / cannot-verify