@volter/twin-calcom 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +90 -0
- package/client/calcom-mirror.css +29 -0
- package/client/calcom-mirror.tsx +125 -0
- package/dist/client/calcom-mirror.bundle.js +321 -0
- package/dist/client/calcom-mirror.css +29 -0
- package/dist/client/calcom-mirror.d.ts +13 -0
- package/dist/client/calcom-mirror.js +61 -0
- package/dist/client/calcom-mirror.tsx +125 -0
- package/dist/src/calcom-budget.d.ts +52 -0
- package/dist/src/calcom-budget.js +145 -0
- package/dist/src/calcom-capabilities.d.ts +3 -0
- package/dist/src/calcom-capabilities.js +521 -0
- package/dist/src/calcom-conformance.d.ts +15 -0
- package/dist/src/calcom-conformance.js +86 -0
- package/dist/src/calcom-connector.d.ts +90 -0
- package/dist/src/calcom-connector.js +229 -0
- package/dist/src/calcom-mirror-ui.d.ts +23 -0
- package/dist/src/calcom-mirror-ui.js +100 -0
- package/dist/src/calcom-perform-harness.d.ts +5 -0
- package/dist/src/calcom-perform-harness.js +29 -0
- package/dist/src/calcom-server.d.ts +14 -0
- package/dist/src/calcom-server.js +31 -0
- package/dist/src/calcom-twin.d.ts +16 -0
- package/dist/src/calcom-twin.js +718 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +31 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.js +69 -0
- package/package.json +72 -0
- package/src/calcom-budget.ts +171 -0
- package/src/calcom-capabilities.ts +529 -0
- package/src/calcom-conformance.ts +92 -0
- package/src/calcom-connector.ts +258 -0
- package/src/calcom-mirror-ui.ts +110 -0
- package/src/calcom-perform-harness.ts +24 -0
- package/src/calcom-server.ts +39 -0
- package/src/calcom-twin.ts +675 -0
- package/src/cli.ts +29 -0
- package/src/index.ts +101 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
// Cal.com capability manifest — the EXPECTED REAL-PRODUCT SURFACE (the target), authored
|
|
2
|
+
// top-down from what Cal.com v2 actually does — NOT from what this twin has built. This is the
|
|
3
|
+
// honest denominator: most entries start as `todo` and coverage reads LOW until the twin truly
|
|
4
|
+
// reaches 100% of Cal.com. `verify()` (required to count as done) is ground truth; every
|
|
5
|
+
// `expected:'done'` is genuinely claimed, so a broken one shows as a regression. Grow this
|
|
6
|
+
// toward Cal.com's *full* v2 surface every cycle.
|
|
7
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { createElement } from 'react';
|
|
11
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
12
|
+
import { checkCapabilities, uiDataCoupled, verifyBoundary, isInfrastructureError, harnessError } from '@volter/world-tooling';
|
|
13
|
+
import { BookingDetail, BookingsList, EventTypesList } from "../client/calcom-mirror.js";
|
|
14
|
+
import { bookingTone, buildCalcomMirrorClient, createCalcomMirrorServer } from "./calcom-mirror-ui.js";
|
|
15
|
+
import { handleCalcomTwinRequest } from "./calcom-twin.js";
|
|
16
|
+
import { pullCalcomAll } from "./calcom-connector.js";
|
|
17
|
+
import { performPending } from "./calcom-perform-harness.js";
|
|
18
|
+
// ── UI verify: the built mirror bundle backs `calcom.ui.booking_detail`'s bundle+render
|
|
19
|
+
// hybrid below (NOT a marker-grep capability of its own — TWIN-14/B4 killed those). ──
|
|
20
|
+
let bundle = null;
|
|
21
|
+
const mirrorBundle = () => (bundle ??= buildCalcomMirrorClient());
|
|
22
|
+
const AT = '2026-07-01T00:00:00.000Z';
|
|
23
|
+
async function withRoot(steps) {
|
|
24
|
+
const root = mkdtempSync(join(tmpdir(), 'calcom-cap-'));
|
|
25
|
+
const h = (s) => handleCalcomTwinRequest({ method: s.m, path: s.p, body: s.b === undefined ? undefined : JSON.stringify(s.b), root, occurredAt: AT });
|
|
26
|
+
try {
|
|
27
|
+
return await verifyBoundary('calcom.withRoot', () => steps(h));
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
rmSync(root, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const ok = (r) => r.status >= 200 && r.status < 300;
|
|
34
|
+
const env = (r) => r.body.status;
|
|
35
|
+
const data = (r) => r.body.data;
|
|
36
|
+
// ── shorthands ──
|
|
37
|
+
const done = (id, area, title, dimension, tier, verify) => ({ id, area, title, dimension, tier, expected: 'done', verify });
|
|
38
|
+
const todo = (id, area, title, dimension, tier) => ({ id, area, title, dimension, tier, expected: 'todo' });
|
|
39
|
+
// A fake connector executor for the connector verify (offline, no network).
|
|
40
|
+
const fakeExecute = async (method, path) => {
|
|
41
|
+
if (path === '/bookings')
|
|
42
|
+
return { status: 'success', data: [{ uid: 'pulled1', id: 1, title: 'Pulled', status: 'accepted', start: AT, end: AT, duration: 30, eventTypeId: 7001, attendees: [], hosts: [] }] };
|
|
43
|
+
if (path === '/event-types')
|
|
44
|
+
return { status: 'success', data: [{ id: 9, title: 'Demo', slug: 'demo', lengthInMinutes: 45, ownerId: 5000 }] };
|
|
45
|
+
return { status: 'success', data: {} };
|
|
46
|
+
};
|
|
47
|
+
export const CALCOM_CAPABILITIES = [
|
|
48
|
+
// ── Me / profile (core) ──
|
|
49
|
+
done('calcom.me.get', 'me', 'GET /v2/me — the API-key owner profile', 'api', 'core', () => withRoot(async (h) => { const r = await h({ m: 'GET', p: '/v2/me' }); return ok(r) && env(r) === 'success' && data(r).id === 5000 && data(r).organization.isPlatform === true; })),
|
|
50
|
+
done('calcom.me.update', 'me', 'PATCH /v2/me — update profile (folds, then GET reflects it)', 'api', 'niche', () => withRoot(async (h) => {
|
|
51
|
+
const p = await h({ m: 'PATCH', p: '/v2/me', b: { name: 'New Name', timeZone: 'Europe/London' } });
|
|
52
|
+
if (!ok(p) || data(p).name !== 'New Name' || data(p).timeZone !== 'Europe/London' || data(p).id !== 5000)
|
|
53
|
+
return false;
|
|
54
|
+
const g = await h({ m: 'GET', p: '/v2/me' });
|
|
55
|
+
if (data(g).name !== 'New Name' || data(g).timeZone !== 'Europe/London')
|
|
56
|
+
return false;
|
|
57
|
+
const bad = await h({ m: 'PATCH', p: '/v2/me', b: {} });
|
|
58
|
+
return bad.status === 400 && env(bad) === 'error';
|
|
59
|
+
})),
|
|
60
|
+
// ── Slots / availability (core) ──
|
|
61
|
+
done('calcom.slots.list', 'slots', 'GET /v2/slots — availability keyed by date', 'api', 'core', () => withRoot(async (h) => { const r = await h({ m: 'GET', p: '/v2/slots?eventTypeId=7001&start=2026-07-01' }); const d = data(r); return ok(r) && Array.isArray(d['2026-07-01']) && d['2026-07-01'][0].start.includes('T16:00'); })),
|
|
62
|
+
done('calcom.slots.reserve', 'slots', 'POST /v2/slots/reservations — reserve a slot', 'api', 'common', () => withRoot(async (h) => { const r = await h({ m: 'POST', p: '/v2/slots/reservations', b: { eventTypeId: 7001, slotStart: '2026-07-01T16:00:00.000Z' } }); return ok(r) && env(r) === 'success' && typeof data(r).reservationUid === 'string'; })),
|
|
63
|
+
done('calcom.slots.reserve.requires_input', 'slots', 'POST /v2/slots/reservations rejects missing eventTypeId (400)', 'api', 'common', () => withRoot(async (h) => { const r = await h({ m: 'POST', p: '/v2/slots/reservations', b: { slotStart: 'x' } }); return r.status === 400 && env(r) === 'error'; })),
|
|
64
|
+
done('calcom.slots.reserve.get', 'slots', 'GET /v2/slots/reservations/{uid} — read a reservation', 'api', 'niche', () => withRoot(async (h) => { const c = await h({ m: 'POST', p: '/v2/slots/reservations', b: { eventTypeId: 7001, slotStart: '2026-07-01T16:00:00.000Z' } }); const uid = data(c).reservationUid; const g = await h({ m: 'GET', p: `/v2/slots/reservations/${uid}` }); return ok(g) && data(g).reservationUid === uid; })),
|
|
65
|
+
done('calcom.slots.reserve.delete', 'slots', 'DELETE /v2/slots/reservations/{uid} — release a reservation (then GET → 404)', 'api', 'niche', () => withRoot(async (h) => {
|
|
66
|
+
const c = await h({ m: 'POST', p: '/v2/slots/reservations', b: { eventTypeId: 7001, slotStart: '2026-07-01T16:00:00.000Z' } });
|
|
67
|
+
const uid = data(c).reservationUid;
|
|
68
|
+
const d = await h({ m: 'DELETE', p: `/v2/slots/reservations/${uid}` });
|
|
69
|
+
const g = await h({ m: 'GET', p: `/v2/slots/reservations/${uid}` });
|
|
70
|
+
const miss = await h({ m: 'DELETE', p: '/v2/slots/reservations/slot_zzz' });
|
|
71
|
+
return ok(d) && g.status === 404 && miss.status === 404;
|
|
72
|
+
})),
|
|
73
|
+
// ── Bookings (core) ──
|
|
74
|
+
done('calcom.bookings.create', 'bookings', 'POST /v2/bookings — create a booking (accepted)', 'api', 'core', () => withRoot(async (h) => { const r = await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', eventTypeId: 7001, attendee: { name: 'Jo', email: 'jo@x.test', timeZone: 'UTC' } } }); const d = data(r); return r.status === 201 && d.status === 'accepted' && d.attendees[0].email === 'jo@x.test' && d.hosts[0].mandatory === true && d.end === '2026-07-01T16:30:00.000Z'; })),
|
|
75
|
+
done('calcom.bookings.create.requires_start', 'bookings', 'POST /v2/bookings rejects a missing/invalid start (400)', 'api', 'core', () => withRoot(async (h) => { const r = await h({ m: 'POST', p: '/v2/bookings', b: { attendee: { name: 'a', email: 'a@b.test' } } }); return r.status === 400 && env(r) === 'error'; })),
|
|
76
|
+
done('calcom.bookings.create.requires_attendee', 'bookings', 'POST /v2/bookings rejects a missing attendee (400)', 'api', 'core', () => withRoot(async (h) => { const r = await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z' } }); return r.status === 400; })),
|
|
77
|
+
done('calcom.bookings.get', 'bookings', 'GET /v2/bookings/{uid} — read a created booking', 'api', 'core', () => withRoot(async (h) => { const c = await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', attendee: { name: 'a', email: 'a@b.test' } } }); const uid = data(c).uid; const g = await h({ m: 'GET', p: `/v2/bookings/${uid}` }); return ok(g) && data(g).uid === uid; })),
|
|
78
|
+
done('calcom.bookings.get.unknown_404', 'bookings', 'GET /v2/bookings/{uid} unknown uid → 404', 'api', 'core', () => withRoot(async (h) => { const r = await h({ m: 'GET', p: '/v2/bookings/booking_nope' }); return r.status === 404 && env(r) === 'error'; })),
|
|
79
|
+
done('calcom.bookings.list', 'bookings', 'GET /v2/bookings — list bookings', 'api', 'core', () => withRoot(async (h) => { await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', attendee: { name: 'a', email: 'a@b.test' } } }); const r = await h({ m: 'GET', p: '/v2/bookings' }); return ok(r) && Array.isArray(data(r)) && data(r).length === 1; })),
|
|
80
|
+
done('calcom.bookings.list.filter_status', 'bookings', 'GET /v2/bookings?status= — filter by status', 'api', 'common', () => withRoot(async (h) => { await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', attendee: { name: 'a', email: 'a@b.test' } } }); const acc = await h({ m: 'GET', p: '/v2/bookings?status=accepted' }); const can = await h({ m: 'GET', p: '/v2/bookings?status=cancelled' }); return data(acc).length === 1 && data(can).length === 0; })),
|
|
81
|
+
done('calcom.bookings.cancel', 'bookings', 'POST /v2/bookings/{uid}/cancel — transitions to cancelled', 'api', 'core', () => withRoot(async (h) => { const c = await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', attendee: { name: 'a', email: 'a@b.test' } } }); const uid = data(c).uid; const x = await h({ m: 'POST', p: `/v2/bookings/${uid}/cancel`, b: {} }); const g = await h({ m: 'GET', p: `/v2/bookings/${uid}` }); return ok(x) && data(x).status === 'cancelled' && data(g).status === 'cancelled'; })),
|
|
82
|
+
done('calcom.bookings.cancel.double_400', 'bookings', 'POST /v2/bookings/{uid}/cancel twice → 400 (already cancelled)', 'api', 'common', () => withRoot(async (h) => { const c = await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', attendee: { name: 'a', email: 'a@b.test' } } }); const uid = data(c).uid; await h({ m: 'POST', p: `/v2/bookings/${uid}/cancel`, b: {} }); const x2 = await h({ m: 'POST', p: `/v2/bookings/${uid}/cancel`, b: {} }); return x2.status === 400 && env(x2) === 'error'; })),
|
|
83
|
+
done('calcom.bookings.cancel.unknown_404', 'bookings', 'POST cancel on unknown uid → 404', 'api', 'common', () => withRoot(async (h) => { const r = await h({ m: 'POST', p: '/v2/bookings/booking_nope/cancel', b: {} }); return r.status === 404; })),
|
|
84
|
+
done('calcom.bookings.reschedule', 'bookings', 'POST /v2/bookings/{uid}/reschedule — new booking, old cancelled', 'api', 'common', () => withRoot(async (h) => { const c = await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', attendee: { name: 'a', email: 'a@b.test' } } }); const uid = data(c).uid; const r = await h({ m: 'POST', p: `/v2/bookings/${uid}/reschedule`, b: { start: '2026-07-02T16:00:00.000Z' } }); const old = await h({ m: 'GET', p: `/v2/bookings/${uid}` }); return r.status === 201 && data(r).start === '2026-07-02T16:00:00.000Z' && data(r).status === 'accepted' && data(old).status === 'cancelled'; })),
|
|
85
|
+
done('calcom.bookings.confirm', 'bookings', 'POST /v2/bookings/{uid}/confirm — pending → accepted (and 400 on a non-pending)', 'api', 'common', () => withRoot(async (h) => {
|
|
86
|
+
// a requiresConfirmation event type produces a PENDING booking (the real positive path)
|
|
87
|
+
const evt = await h({ m: 'POST', p: '/v2/event-types', b: { title: 'Gated', slug: 'gated', lengthInMinutes: 30, requiresConfirmation: true } });
|
|
88
|
+
const c = await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', eventTypeId: data(evt).id, attendee: { name: 'a', email: 'a@b.test' } } });
|
|
89
|
+
if (data(c).status !== 'pending')
|
|
90
|
+
return false;
|
|
91
|
+
const uid = data(c).uid;
|
|
92
|
+
const r = await h({ m: 'POST', p: `/v2/bookings/${uid}/confirm`, b: {} });
|
|
93
|
+
if (!ok(r) || data(r).status !== 'accepted')
|
|
94
|
+
return false;
|
|
95
|
+
const g = await h({ m: 'GET', p: `/v2/bookings/${uid}` });
|
|
96
|
+
if (data(g).status !== 'accepted')
|
|
97
|
+
return false;
|
|
98
|
+
// negative: confirming an already-accepted booking → 400
|
|
99
|
+
const again = await h({ m: 'POST', p: `/v2/bookings/${uid}/confirm`, b: {} });
|
|
100
|
+
return again.status === 400;
|
|
101
|
+
})),
|
|
102
|
+
done('calcom.bookings.decline', 'bookings', 'POST /v2/bookings/{uid}/decline — pending → rejected (and 400 on a non-pending)', 'api', 'common', () => withRoot(async (h) => {
|
|
103
|
+
const evt = await h({ m: 'POST', p: '/v2/event-types', b: { title: 'Gated', slug: 'gated', lengthInMinutes: 30, requiresConfirmation: true } });
|
|
104
|
+
const c = await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', eventTypeId: data(evt).id, attendee: { name: 'a', email: 'a@b.test' } } });
|
|
105
|
+
if (data(c).status !== 'pending')
|
|
106
|
+
return false;
|
|
107
|
+
const uid = data(c).uid;
|
|
108
|
+
const r = await h({ m: 'POST', p: `/v2/bookings/${uid}/decline`, b: {} });
|
|
109
|
+
if (!ok(r) || data(r).status !== 'rejected')
|
|
110
|
+
return false;
|
|
111
|
+
const g = await h({ m: 'GET', p: `/v2/bookings/${uid}` });
|
|
112
|
+
if (data(g).status !== 'rejected')
|
|
113
|
+
return false;
|
|
114
|
+
const again = await h({ m: 'POST', p: `/v2/bookings/${uid}/decline`, b: {} });
|
|
115
|
+
return again.status === 400;
|
|
116
|
+
})),
|
|
117
|
+
done('calcom.bookings.requires_confirmation_pending', 'bookings', 'A booking on a requiresConfirmation event type starts pending', 'api', 'common', () => withRoot(async (h) => { const evt = await h({ m: 'POST', p: '/v2/event-types', b: { title: 'G', slug: 'g', lengthInMinutes: 30, requiresConfirmation: true } }); const c = await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', eventTypeId: data(evt).id, attendee: { name: 'a', email: 'a@b.test' } } }); return c.status === 201 && data(c).status === 'pending'; })),
|
|
118
|
+
done('calcom.bookings.create.unknown_event_type_404', 'bookings', 'POST /v2/bookings with an unknown eventTypeId → 404', 'api', 'common', () => withRoot(async (h) => { const r = await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', eventTypeId: 424242, attendee: { name: 'a', email: 'a@b.test' } } }); return r.status === 404 && env(r) === 'error'; })),
|
|
119
|
+
todo('calcom.bookings.create_instant', 'bookings', 'POST /v2/bookings instant team meeting', 'api', 'niche'),
|
|
120
|
+
todo('calcom.bookings.create_recurring', 'bookings', 'POST /v2/bookings recurring series', 'api', 'niche'),
|
|
121
|
+
todo('calcom.bookings.seated', 'bookings', 'Seated bookings (seatUid, per-seat attendees)', 'api', 'niche'),
|
|
122
|
+
done('calcom.bookings.mark_absent', 'bookings', 'POST /v2/bookings/{uid}/mark-absent — flags attendee no-show + noShowHost', 'api', 'niche', () => withRoot(async (h) => {
|
|
123
|
+
const c = await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', attendee: { name: 'a', email: 'a@b.test' } } });
|
|
124
|
+
const uid = data(c).uid;
|
|
125
|
+
const m = await h({ m: 'POST', p: `/v2/bookings/${uid}/mark-absent`, b: { noShowHost: true, attendees: [{ email: 'a@b.test', absent: true }] } });
|
|
126
|
+
if (!ok(m) || data(m).noShowHost !== true || data(m).attendees[0].absent !== true)
|
|
127
|
+
return false;
|
|
128
|
+
const g = await h({ m: 'GET', p: `/v2/bookings/${uid}` });
|
|
129
|
+
if (data(g).noShowHost !== true || data(g).attendees[0].absent !== true)
|
|
130
|
+
return false;
|
|
131
|
+
const miss = await h({ m: 'POST', p: '/v2/bookings/booking_nope/mark-absent', b: {} });
|
|
132
|
+
return miss.status === 404;
|
|
133
|
+
})),
|
|
134
|
+
done('calcom.bookings.recordings', 'bookings', 'GET /v2/bookings/{uid}/recordings — empty array on a fresh local booking; 404 on unknown', 'api', 'niche', () => withRoot(async (h) => {
|
|
135
|
+
const c = await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', attendee: { name: 'a', email: 'a@b.test' } } });
|
|
136
|
+
const r = await h({ m: 'GET', p: `/v2/bookings/${data(c).uid}/recordings` });
|
|
137
|
+
const miss = await h({ m: 'GET', p: '/v2/bookings/booking_nope/recordings' });
|
|
138
|
+
return ok(r) && env(r) === 'success' && Array.isArray(data(r)) && data(r).length === 0 && miss.status === 404;
|
|
139
|
+
})),
|
|
140
|
+
done('calcom.bookings.references', 'bookings', 'GET /v2/bookings/{uid}/references — empty array on a fresh local booking; 404 on unknown', 'api', 'niche', () => withRoot(async (h) => {
|
|
141
|
+
const c = await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', attendee: { name: 'a', email: 'a@b.test' } } });
|
|
142
|
+
const r = await h({ m: 'GET', p: `/v2/bookings/${data(c).uid}/references` });
|
|
143
|
+
const miss = await h({ m: 'GET', p: '/v2/bookings/booking_nope/references' });
|
|
144
|
+
return ok(r) && env(r) === 'success' && Array.isArray(data(r)) && data(r).length === 0 && miss.status === 404;
|
|
145
|
+
})),
|
|
146
|
+
todo('calcom.bookings.reassign', 'bookings', 'POST /v2/bookings/{uid}/reassign (round-robin)', 'api', 'niche'),
|
|
147
|
+
// ── Event types (core) ──
|
|
148
|
+
done('calcom.event_types.create', 'event-types', 'POST /v2/event-types — create', 'api', 'core', () => withRoot(async (h) => { const r = await h({ m: 'POST', p: '/v2/event-types', b: { title: 'Intro', slug: 'intro', lengthInMinutes: 15 } }); const d = data(r); return r.status === 201 && d.lengthInMinutes === 15 && d.slug === 'intro' && String(d.bookingUrl).includes('intro'); })),
|
|
149
|
+
done('calcom.event_types.create.requires_fields', 'event-types', 'POST /v2/event-types rejects missing required fields (400)', 'api', 'core', () => withRoot(async (h) => { const r = await h({ m: 'POST', p: '/v2/event-types', b: { title: 'x' } }); return r.status === 400 && env(r) === 'error'; })),
|
|
150
|
+
done('calcom.event_types.get', 'event-types', 'GET /v2/event-types/{id} — read', 'api', 'core', () => withRoot(async (h) => { const c = await h({ m: 'POST', p: '/v2/event-types', b: { title: 'Intro', slug: 'intro', lengthInMinutes: 15 } }); const id = data(c).id; const g = await h({ m: 'GET', p: `/v2/event-types/${id}` }); return ok(g) && data(g).id === id; })),
|
|
151
|
+
done('calcom.event_types.get.unknown_404', 'event-types', 'GET /v2/event-types/{id} unknown → 404', 'api', 'common', () => withRoot(async (h) => { const r = await h({ m: 'GET', p: '/v2/event-types/999999' }); return r.status === 404; })),
|
|
152
|
+
done('calcom.event_types.list', 'event-types', 'GET /v2/event-types — list', 'api', 'core', () => withRoot(async (h) => { await h({ m: 'POST', p: '/v2/event-types', b: { title: 'a', slug: 'a', lengthInMinutes: 15 } }); const r = await h({ m: 'GET', p: '/v2/event-types' }); return ok(r) && data(r).length === 1; })),
|
|
153
|
+
done('calcom.event_types.update', 'event-types', 'PATCH /v2/event-types/{id} — update', 'api', 'common', () => withRoot(async (h) => { const c = await h({ m: 'POST', p: '/v2/event-types', b: { title: 'a', slug: 'a', lengthInMinutes: 15 } }); const id = data(c).id; const p = await h({ m: 'PATCH', p: `/v2/event-types/${id}`, b: { title: 'b' } }); return ok(p) && data(p).title === 'b'; })),
|
|
154
|
+
done('calcom.event_types.delete', 'event-types', 'DELETE /v2/event-types/{id} — delete (then 404)', 'api', 'common', () => withRoot(async (h) => { const c = await h({ m: 'POST', p: '/v2/event-types', b: { title: 'a', slug: 'a', lengthInMinutes: 15 } }); const id = data(c).id; const d = await h({ m: 'DELETE', p: `/v2/event-types/${id}` }); const g = await h({ m: 'GET', p: `/v2/event-types/${id}` }); return ok(d) && g.status === 404; })),
|
|
155
|
+
done('calcom.event_types.bulk', 'event-types', 'GET /v2/event-types?username=&eventSlug= — public lookup (eventSlug needs username → 400)', 'api', 'common', () => withRoot(async (h) => {
|
|
156
|
+
await h({ m: 'POST', p: '/v2/event-types', b: { title: 'Intro', slug: 'intro', lengthInMinutes: 15 } });
|
|
157
|
+
// by username: the seeded admin's event types incl. the implicit default
|
|
158
|
+
const byUser = await h({ m: 'GET', p: '/v2/event-types?username=twin-calcom-admin' });
|
|
159
|
+
if (!ok(byUser) || !data(byUser).some((e) => e.slug === 'intro') || !data(byUser).some((e) => e.slug === 'consultation'))
|
|
160
|
+
return false;
|
|
161
|
+
// by username + eventSlug: exactly the one
|
|
162
|
+
const one = await h({ m: 'GET', p: '/v2/event-types?username=twin-calcom-admin&eventSlug=intro' });
|
|
163
|
+
if (!ok(one) || data(one).length !== 1 || data(one)[0].slug !== 'intro')
|
|
164
|
+
return false;
|
|
165
|
+
// unknown username → empty
|
|
166
|
+
const none = await h({ m: 'GET', p: '/v2/event-types?username=ghost' });
|
|
167
|
+
if (!ok(none) || data(none).length !== 0)
|
|
168
|
+
return false;
|
|
169
|
+
// eventSlug without username → 400
|
|
170
|
+
const bad = await h({ m: 'GET', p: '/v2/event-types?eventSlug=intro' });
|
|
171
|
+
return bad.status === 400 && env(bad) === 'error';
|
|
172
|
+
})),
|
|
173
|
+
done('calcom.event_types.team', 'event-types', 'POST/GET /v2/teams/{id}/event-types — team event types carry teamId+schedulingType', 'api', 'common', () => withRoot(async (h) => {
|
|
174
|
+
const t = await h({ m: 'POST', p: '/v2/teams', b: { name: 'Sales' } });
|
|
175
|
+
const tid = data(t).id;
|
|
176
|
+
const c = await h({ m: 'POST', p: `/v2/teams/${tid}/event-types`, b: { title: 'Team Demo', slug: 'team-demo', lengthInMinutes: 45, schedulingType: 'roundRobin' } });
|
|
177
|
+
if (c.status !== 201 || data(c).teamId !== tid || data(c).schedulingType !== 'roundRobin' || data(c).lengthInMinutes !== 45)
|
|
178
|
+
return false;
|
|
179
|
+
const lst = await h({ m: 'GET', p: `/v2/teams/${tid}/event-types` });
|
|
180
|
+
if (!ok(lst) || data(lst).length !== 1 || data(lst)[0].slug !== 'team-demo')
|
|
181
|
+
return false;
|
|
182
|
+
// the personal list does NOT include the team event type
|
|
183
|
+
const personal = await h({ m: 'GET', p: '/v2/event-types' });
|
|
184
|
+
if (data(personal).length !== 0)
|
|
185
|
+
return false;
|
|
186
|
+
const bad = await h({ m: 'POST', p: '/v2/teams/team_999/event-types', b: { title: 'x', slug: 'x', lengthInMinutes: 30 } });
|
|
187
|
+
return bad.status === 404;
|
|
188
|
+
})),
|
|
189
|
+
done('calcom.event_types.locations', 'event-types', 'Event-type locations round-trip (integration/phone/link)', 'api', 'niche', () => withRoot(async (h) => {
|
|
190
|
+
const locs = [{ type: 'integration', integration: 'cal-video' }, { type: 'phone', phone: '+15551234' }];
|
|
191
|
+
const c = await h({ m: 'POST', p: '/v2/event-types', b: { title: 'L', slug: 'l', lengthInMinutes: 30, locations: locs } });
|
|
192
|
+
if (c.status !== 201 || !Array.isArray(data(c).locations) || data(c).locations.length !== 2 || data(c).locations[1].phone !== '+15551234')
|
|
193
|
+
return false;
|
|
194
|
+
const g = await h({ m: 'GET', p: `/v2/event-types/${data(c).id}` });
|
|
195
|
+
return data(g).locations[0].integration === 'cal-video';
|
|
196
|
+
})),
|
|
197
|
+
done('calcom.event_types.booking_fields', 'event-types', 'Custom booking fields round-trip on an event type', 'api', 'niche', () => withRoot(async (h) => {
|
|
198
|
+
const fields = [{ type: 'text', slug: 'company', label: 'Company', required: true }];
|
|
199
|
+
const c = await h({ m: 'POST', p: '/v2/event-types', b: { title: 'B', slug: 'b', lengthInMinutes: 30, bookingFields: fields } });
|
|
200
|
+
if (c.status !== 201 || data(c).bookingFields.length !== 1 || data(c).bookingFields[0].slug !== 'company')
|
|
201
|
+
return false;
|
|
202
|
+
const g = await h({ m: 'GET', p: `/v2/event-types/${data(c).id}` });
|
|
203
|
+
return data(g).bookingFields[0].required === true;
|
|
204
|
+
})),
|
|
205
|
+
done('calcom.event_types.seats_config', 'event-types', 'Seats config (seatsPerTimeSlot + show flags) round-trips', 'api', 'niche', () => withRoot(async (h) => {
|
|
206
|
+
const c = await h({ m: 'POST', p: '/v2/event-types', b: { title: 'S', slug: 's', lengthInMinutes: 30, seatsPerTimeSlot: 5, seatsShowAttendees: true } });
|
|
207
|
+
if (c.status !== 201 || data(c).seatsPerTimeSlot !== 5 || data(c).seatsShowAttendees !== true)
|
|
208
|
+
return false;
|
|
209
|
+
// default event types are not seated
|
|
210
|
+
const d = await h({ m: 'POST', p: '/v2/event-types', b: { title: 'P', slug: 'p', lengthInMinutes: 30 } });
|
|
211
|
+
return data(d).seatsPerTimeSlot === null;
|
|
212
|
+
})),
|
|
213
|
+
done('calcom.event_types.buffers', 'event-types', 'before/after buffers + minimumBookingNotice round-trip', 'api', 'niche', () => withRoot(async (h) => {
|
|
214
|
+
const c = await h({ m: 'POST', p: '/v2/event-types', b: { title: 'Bf', slug: 'bf', lengthInMinutes: 30, beforeEventBuffer: 10, afterEventBuffer: 15, minimumBookingNotice: 120 } });
|
|
215
|
+
if (c.status !== 201 || data(c).beforeEventBuffer !== 10 || data(c).afterEventBuffer !== 15 || data(c).minimumBookingNotice !== 120)
|
|
216
|
+
return false;
|
|
217
|
+
const d = await h({ m: 'POST', p: '/v2/event-types', b: { title: 'Nd', slug: 'nd', lengthInMinutes: 30 } });
|
|
218
|
+
return data(d).beforeEventBuffer === 0 && data(d).minimumBookingNotice === 0;
|
|
219
|
+
})),
|
|
220
|
+
todo('calcom.event_types.webhooks', 'event-types', 'Per-event-type webhooks', 'api', 'niche'),
|
|
221
|
+
// ── Schedules / availability (common) ──
|
|
222
|
+
done('calcom.schedules.create', 'schedules', 'POST /v2/schedules — create a schedule', 'api', 'core', () => withRoot(async (h) => { const r = await h({ m: 'POST', p: '/v2/schedules', b: { name: 'Working', timeZone: 'UTC', isDefault: true } }); const d = data(r); return r.status === 201 && d.timeZone === 'UTC' && Array.isArray(d.availability) && d.isDefault === true; })),
|
|
223
|
+
done('calcom.schedules.create.requires_fields', 'schedules', 'POST /v2/schedules rejects missing required fields (400)', 'api', 'common', () => withRoot(async (h) => { const r = await h({ m: 'POST', p: '/v2/schedules', b: { name: 'x' } }); return r.status === 400; })),
|
|
224
|
+
done('calcom.schedules.get', 'schedules', 'GET /v2/schedules/{id} — read', 'api', 'common', () => withRoot(async (h) => { const c = await h({ m: 'POST', p: '/v2/schedules', b: { name: 'W', timeZone: 'UTC', isDefault: true } }); const id = data(c).id; const g = await h({ m: 'GET', p: `/v2/schedules/${id}` }); return ok(g) && data(g).id === id; })),
|
|
225
|
+
done('calcom.schedules.list', 'schedules', 'GET /v2/schedules — list', 'api', 'common', () => withRoot(async (h) => { await h({ m: 'POST', p: '/v2/schedules', b: { name: 'W', timeZone: 'UTC', isDefault: true } }); const r = await h({ m: 'GET', p: '/v2/schedules' }); return ok(r) && data(r).length === 1; })),
|
|
226
|
+
done('calcom.schedules.update', 'schedules', 'PATCH /v2/schedules/{id} — update', 'api', 'common', () => withRoot(async (h) => { const c = await h({ m: 'POST', p: '/v2/schedules', b: { name: 'W', timeZone: 'UTC', isDefault: true } }); const id = data(c).id; const p = await h({ m: 'PATCH', p: `/v2/schedules/${id}`, b: { name: 'Renamed' } }); return ok(p) && data(p).name === 'Renamed'; })),
|
|
227
|
+
done('calcom.schedules.delete', 'schedules', 'DELETE /v2/schedules/{id} — delete (then 404)', 'api', 'niche', () => withRoot(async (h) => { const c = await h({ m: 'POST', p: '/v2/schedules', b: { name: 'W', timeZone: 'UTC', isDefault: true } }); const id = data(c).id; const d = await h({ m: 'DELETE', p: `/v2/schedules/${id}` }); const g = await h({ m: 'GET', p: `/v2/schedules/${id}` }); return ok(d) && g.status === 404; })),
|
|
228
|
+
done('calcom.schedules.overrides', 'schedules', 'Date-specific availability overrides round-trip (create + PATCH)', 'api', 'niche', () => withRoot(async (h) => {
|
|
229
|
+
const ov = [{ date: '2026-12-25', startTime: '00:00', endTime: '00:00' }]; // a closed day
|
|
230
|
+
const c = await h({ m: 'POST', p: '/v2/schedules', b: { name: 'W', timeZone: 'UTC', isDefault: true, overrides: ov } });
|
|
231
|
+
if (c.status !== 201 || !Array.isArray(data(c).overrides) || data(c).overrides[0].date !== '2026-12-25')
|
|
232
|
+
return false;
|
|
233
|
+
const id = data(c).id;
|
|
234
|
+
const newOv = [{ date: '2027-01-01', startTime: '10:00', endTime: '14:00' }];
|
|
235
|
+
const p = await h({ m: 'PATCH', p: `/v2/schedules/${id}`, b: { overrides: newOv } });
|
|
236
|
+
const g = await h({ m: 'GET', p: `/v2/schedules/${id}` });
|
|
237
|
+
return ok(p) && data(g).overrides.length === 1 && data(g).overrides[0].date === '2027-01-01';
|
|
238
|
+
})),
|
|
239
|
+
todo('calcom.availability.default', 'schedules', 'GET default availability for the user', 'api', 'niche'),
|
|
240
|
+
// ── Teams + memberships (common) ──
|
|
241
|
+
done('calcom.teams.create', 'teams', 'POST /v2/teams — create a team', 'api', 'common', () => withRoot(async (h) => { const r = await h({ m: 'POST', p: '/v2/teams', b: { name: 'Sales' } }); return r.status === 201 && data(r).name === 'Sales' && typeof data(r).slug === 'string'; })),
|
|
242
|
+
done('calcom.teams.create.requires_name', 'teams', 'POST /v2/teams rejects a missing name (400)', 'api', 'common', () => withRoot(async (h) => { const r = await h({ m: 'POST', p: '/v2/teams', b: {} }); return r.status === 400; })),
|
|
243
|
+
done('calcom.teams.get', 'teams', 'GET /v2/teams/{id} — read', 'api', 'common', () => withRoot(async (h) => { const c = await h({ m: 'POST', p: '/v2/teams', b: { name: 'Sales' } }); const id = data(c).id; const g = await h({ m: 'GET', p: `/v2/teams/${id}` }); return ok(g) && data(g).id === id; })),
|
|
244
|
+
done('calcom.teams.list', 'teams', 'GET /v2/teams — list', 'api', 'common', () => withRoot(async (h) => { await h({ m: 'POST', p: '/v2/teams', b: { name: 'Sales' } }); const r = await h({ m: 'GET', p: '/v2/teams' }); return ok(r) && data(r).length === 1; })),
|
|
245
|
+
done('calcom.memberships.create', 'teams', 'POST /v2/teams/{id}/memberships — add a member', 'api', 'common', () => withRoot(async (h) => { const t = await h({ m: 'POST', p: '/v2/teams', b: { name: 'Sales' } }); const tid = data(t).id; const m = await h({ m: 'POST', p: `/v2/teams/${tid}/memberships`, b: { userId: 99, role: 'ADMIN' } }); return m.status === 201 && data(m).role === 'ADMIN' && data(m).userId === 99; })),
|
|
246
|
+
done('calcom.memberships.create.requires_user', 'teams', 'POST membership rejects a missing userId (400)', 'api', 'niche', () => withRoot(async (h) => { const t = await h({ m: 'POST', p: '/v2/teams', b: { name: 'Sales' } }); const tid = data(t).id; const m = await h({ m: 'POST', p: `/v2/teams/${tid}/memberships`, b: {} }); return m.status === 400; })),
|
|
247
|
+
done('calcom.memberships.list', 'teams', 'GET /v2/teams/{id}/memberships — list members', 'api', 'common', () => withRoot(async (h) => { const t = await h({ m: 'POST', p: '/v2/teams', b: { name: 'Sales' } }); const tid = data(t).id; await h({ m: 'POST', p: `/v2/teams/${tid}/memberships`, b: { userId: 99 } }); const r = await h({ m: 'GET', p: `/v2/teams/${tid}/memberships` }); return ok(r) && data(r).length === 1; })),
|
|
248
|
+
done('calcom.teams.get.unknown_404', 'teams', 'GET /v2/teams/{id} unknown → 404', 'api', 'niche', () => withRoot(async (h) => { const r = await h({ m: 'GET', p: '/v2/teams/999999' }); return r.status === 404; })),
|
|
249
|
+
done('calcom.memberships.update', 'teams', 'PATCH a membership role/acceptance — folds, then list reflects it', 'api', 'niche', () => withRoot(async (h) => {
|
|
250
|
+
const t = await h({ m: 'POST', p: '/v2/teams', b: { name: 'Sales' } });
|
|
251
|
+
const tid = data(t).id;
|
|
252
|
+
const m = await h({ m: 'POST', p: `/v2/teams/${tid}/memberships`, b: { userId: 99, role: 'MEMBER' } });
|
|
253
|
+
const mid = data(m).id;
|
|
254
|
+
const p = await h({ m: 'PATCH', p: `/v2/teams/${tid}/memberships/${mid}`, b: { role: 'ADMIN', accepted: false } });
|
|
255
|
+
if (!ok(p) || data(p).role !== 'ADMIN' || data(p).accepted !== false)
|
|
256
|
+
return false;
|
|
257
|
+
const g = await h({ m: 'GET', p: `/v2/teams/${tid}/memberships/${mid}` });
|
|
258
|
+
if (data(g).role !== 'ADMIN')
|
|
259
|
+
return false;
|
|
260
|
+
// the list endpoint reflects the patch too (not just the single-GET projection)
|
|
261
|
+
const lst = await h({ m: 'GET', p: `/v2/teams/${tid}/memberships` });
|
|
262
|
+
const inList = data(lst).find((m) => m.id === mid);
|
|
263
|
+
if (!inList || inList.role !== 'ADMIN' || inList.accepted !== false)
|
|
264
|
+
return false;
|
|
265
|
+
const miss = await h({ m: 'PATCH', p: `/v2/teams/${tid}/memberships/mem_999`, b: { role: 'ADMIN' } });
|
|
266
|
+
return miss.status === 404;
|
|
267
|
+
})),
|
|
268
|
+
done('calcom.memberships.delete', 'teams', 'DELETE a membership — gone from the list (and 404 on re-fetch)', 'api', 'niche', () => withRoot(async (h) => {
|
|
269
|
+
const t = await h({ m: 'POST', p: '/v2/teams', b: { name: 'Sales' } });
|
|
270
|
+
const tid = data(t).id;
|
|
271
|
+
const m = await h({ m: 'POST', p: `/v2/teams/${tid}/memberships`, b: { userId: 99 } });
|
|
272
|
+
const mid = data(m).id;
|
|
273
|
+
const d = await h({ m: 'DELETE', p: `/v2/teams/${tid}/memberships/${mid}` });
|
|
274
|
+
const lst = await h({ m: 'GET', p: `/v2/teams/${tid}/memberships` });
|
|
275
|
+
const g = await h({ m: 'GET', p: `/v2/teams/${tid}/memberships/${mid}` });
|
|
276
|
+
return ok(d) && data(lst).length === 0 && g.status === 404;
|
|
277
|
+
})),
|
|
278
|
+
done('calcom.teams.update', 'teams', 'PATCH /v2/teams/{id} — rename folds, then GET reflects it', 'api', 'niche', () => withRoot(async (h) => {
|
|
279
|
+
const t = await h({ m: 'POST', p: '/v2/teams', b: { name: 'Sales' } });
|
|
280
|
+
const id = data(t).id;
|
|
281
|
+
const p = await h({ m: 'PATCH', p: `/v2/teams/${id}`, b: { name: 'Revenue' } });
|
|
282
|
+
if (!ok(p) || data(p).name !== 'Revenue')
|
|
283
|
+
return false;
|
|
284
|
+
const g = await h({ m: 'GET', p: `/v2/teams/${id}` });
|
|
285
|
+
const bad = await h({ m: 'PATCH', p: `/v2/teams/${id}`, b: {} });
|
|
286
|
+
return data(g).name === 'Revenue' && bad.status === 400;
|
|
287
|
+
})),
|
|
288
|
+
done('calcom.teams.delete', 'teams', 'DELETE /v2/teams/{id} — delete (then GET → 404)', 'api', 'niche', () => withRoot(async (h) => {
|
|
289
|
+
const t = await h({ m: 'POST', p: '/v2/teams', b: { name: 'Sales' } });
|
|
290
|
+
const id = data(t).id;
|
|
291
|
+
const d = await h({ m: 'DELETE', p: `/v2/teams/${id}` });
|
|
292
|
+
const g = await h({ m: 'GET', p: `/v2/teams/${id}` });
|
|
293
|
+
const lst = await h({ m: 'GET', p: '/v2/teams' });
|
|
294
|
+
return ok(d) && g.status === 404 && data(lst).length === 0;
|
|
295
|
+
})),
|
|
296
|
+
// ── Webhooks (registration CRUD — NOT signing) (common) ──
|
|
297
|
+
done('calcom.webhooks.create', 'webhooks', 'POST /v2/webhooks — register a webhook', 'api', 'common', () => withRoot(async (h) => { const r = await h({ m: 'POST', p: '/v2/webhooks', b: { subscriberUrl: 'https://x.test/h', triggers: ['BOOKING_CREATED'] } }); return r.status === 201 && data(r).subscriberUrl === 'https://x.test/h' && data(r).active === true; })),
|
|
298
|
+
done('calcom.webhooks.create.requires_fields', 'webhooks', 'POST /v2/webhooks rejects missing url/triggers (400)', 'api', 'common', () => withRoot(async (h) => { const r = await h({ m: 'POST', p: '/v2/webhooks', b: { subscriberUrl: 'x' } }); return r.status === 400; })),
|
|
299
|
+
done('calcom.webhooks.list', 'webhooks', 'GET /v2/webhooks — list registered webhooks', 'api', 'common', () => withRoot(async (h) => { await h({ m: 'POST', p: '/v2/webhooks', b: { subscriberUrl: 'https://x.test/h', triggers: ['BOOKING_CREATED'] } }); const r = await h({ m: 'GET', p: '/v2/webhooks' }); return ok(r) && data(r).length === 1; })),
|
|
300
|
+
done('calcom.webhooks.update', 'webhooks', 'PATCH /v2/webhooks/{id} — update', 'api', 'niche', () => withRoot(async (h) => { const c = await h({ m: 'POST', p: '/v2/webhooks', b: { subscriberUrl: 'https://x.test/h', triggers: ['BOOKING_CREATED'] } }); const id = data(c).id; const p = await h({ m: 'PATCH', p: `/v2/webhooks/${id}`, b: { active: false } }); return ok(p) && data(p).active === false; })),
|
|
301
|
+
done('calcom.webhooks.delete', 'webhooks', 'DELETE /v2/webhooks/{id} — delete (then 404)', 'api', 'niche', () => withRoot(async (h) => { const c = await h({ m: 'POST', p: '/v2/webhooks', b: { subscriberUrl: 'https://x.test/h', triggers: ['BOOKING_CREATED'] } }); const id = data(c).id; const d = await h({ m: 'DELETE', p: `/v2/webhooks/${id}` }); const g = await h({ m: 'GET', p: `/v2/webhooks/${id}` }); return ok(d) && g.status === 404; })),
|
|
302
|
+
done('calcom.webhooks.get', 'webhooks', 'GET /v2/webhooks/{id} — read one (and 404 on unknown)', 'api', 'niche', () => withRoot(async (h) => {
|
|
303
|
+
const c = await h({ m: 'POST', p: '/v2/webhooks', b: { subscriberUrl: 'https://x.test/h', triggers: ['BOOKING_CREATED'] } });
|
|
304
|
+
const id = data(c).id;
|
|
305
|
+
const g = await h({ m: 'GET', p: `/v2/webhooks/${id}` });
|
|
306
|
+
const miss = await h({ m: 'GET', p: '/v2/webhooks/wh_999' });
|
|
307
|
+
return ok(g) && data(g).id === id && data(g).subscriberUrl === 'https://x.test/h' && miss.status === 404;
|
|
308
|
+
})),
|
|
309
|
+
// ── Envelope / protocol fidelity (core) ──
|
|
310
|
+
done('calcom.protocol.success_envelope', 'protocol', 'Responses wrap data in { status:"success", data }', 'api', 'core', () => withRoot(async (h) => { const r = await h({ m: 'GET', p: '/v2/me' }); const b = r.body; return b.status === 'success' && b.data !== undefined; })),
|
|
311
|
+
done('calcom.protocol.error_envelope', 'protocol', 'Errors return { status:"error", error:{ code, message } }', 'api', 'core', () => withRoot(async (h) => { const r = await h({ m: 'GET', p: '/v2/bookings/booking_nope' }); const b = r.body; return r.status === 404 && b.status === 'error' && typeof b.error.code === 'string' && typeof b.error.message === 'string'; })),
|
|
312
|
+
done('calcom.protocol.unmodeled_404', 'protocol', 'Unmodeled op fails like the vendor (404), never a fake success', 'api', 'core', () => withRoot(async (h) => { const r = await h({ m: 'GET', p: '/v2/nonsense' }); return r.status === 404 && env(r) === 'error'; })),
|
|
313
|
+
done('calcom.protocol.read_only', 'protocol', 'read-only twin rejects writes (405)', 'api', 'common', () => {
|
|
314
|
+
const root = mkdtempSync(join(tmpdir(), 'calcom-ro-'));
|
|
315
|
+
try {
|
|
316
|
+
return handleCalcomTwinRequest({ method: 'POST', path: '/v2/bookings', body: '{}', root, readOnly: true }).then((r) => r.status === 405);
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
rmSync(root, { recursive: true, force: true });
|
|
320
|
+
}
|
|
321
|
+
}),
|
|
322
|
+
todo('calcom.protocol.api_version', 'protocol', 'cal-api-version header pins per-endpoint behavior', 'api', 'niche'),
|
|
323
|
+
done('calcom.protocol.pagination', 'protocol', 'take/skip pagination on list endpoints (+ sibling pagination meta)', 'api', 'common', () => withRoot(async (h) => {
|
|
324
|
+
for (let i = 0; i < 3; i++)
|
|
325
|
+
await h({ m: 'POST', p: '/v2/bookings', b: { start: `2026-07-0${i + 1}T16:00:00.000Z`, attendee: { name: `a${i}`, email: `a${i}@b.test` } } });
|
|
326
|
+
// no take/skip → full array, no pagination meta (back-compat)
|
|
327
|
+
const all = await h({ m: 'GET', p: '/v2/bookings' });
|
|
328
|
+
if (data(all).length !== 3 || all.body.pagination !== undefined)
|
|
329
|
+
return false;
|
|
330
|
+
// take=2 → first page of 2 + meta
|
|
331
|
+
const p1 = await h({ m: 'GET', p: '/v2/bookings?take=2' });
|
|
332
|
+
const meta1 = p1.body.pagination;
|
|
333
|
+
if (data(p1).length !== 2 || meta1.totalCount !== 3 || meta1.take !== 2 || meta1.skip !== 0 || meta1.hasMore !== true)
|
|
334
|
+
return false;
|
|
335
|
+
// take=2&skip=2 → the remaining 1, hasMore=false
|
|
336
|
+
const p2 = await h({ m: 'GET', p: '/v2/bookings?take=2&skip=2' });
|
|
337
|
+
const meta2 = p2.body.pagination;
|
|
338
|
+
return data(p2).length === 1 && meta2.hasMore === false && meta2.skip === 2;
|
|
339
|
+
})),
|
|
340
|
+
todo('calcom.protocol.rate_limit', 'protocol', '429 rate-limit responses + headers', 'api', 'niche'),
|
|
341
|
+
// ── Connector (lifecycle) ──
|
|
342
|
+
done('calcom.connector.pull', 'connector', 'PULL real bookings + event types into the observed log (idempotent)', 'connector', 'common', async () => {
|
|
343
|
+
const root = mkdtempSync(join(tmpdir(), 'calcom-cap-conn-'));
|
|
344
|
+
try {
|
|
345
|
+
const { twinResources } = await import('@volter/world-core');
|
|
346
|
+
const r1 = await pullCalcomAll(fakeExecute, root);
|
|
347
|
+
if (r1.bookings !== 1 || r1.eventTypes !== 1)
|
|
348
|
+
return false;
|
|
349
|
+
const before = twinResources('calcom', root).length;
|
|
350
|
+
await pullCalcomAll(fakeExecute, root);
|
|
351
|
+
const booking = twinResources('calcom', root).find((x) => x.id === 'pulled1');
|
|
352
|
+
return !!booking && twinResources('calcom', root).length === before;
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
if (isInfrastructureError(err))
|
|
356
|
+
throw harnessError('calcom.connector.pull', err);
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
rmSync(root, { recursive: true, force: true });
|
|
361
|
+
}
|
|
362
|
+
}),
|
|
363
|
+
done('calcom.connector.push', 'connector', 'PUSH a pending local create on success; an error response does NOT confirm', 'connector', 'common', async () => {
|
|
364
|
+
const { deployableEntries } = await import('@volter/world-core');
|
|
365
|
+
// (a) an ERROR response must leave the action pending (no false confirm)
|
|
366
|
+
const r1 = mkdtempSync(join(tmpdir(), 'calcom-cap-push-err-'));
|
|
367
|
+
try {
|
|
368
|
+
await handleCalcomTwinRequest({ method: 'POST', path: '/v2/event-types', body: JSON.stringify({ title: 'L', slug: 'l', lengthInMinutes: 30 }), root: r1, occurredAt: AT });
|
|
369
|
+
const errExec = async () => ({ status: 'error', error: { code: 'BadRequestException', message: 'nope' } });
|
|
370
|
+
const pushedErr = await performPending(errExec, { root: r1 });
|
|
371
|
+
if (pushedErr !== 0)
|
|
372
|
+
return false; // must NOT confirm on error
|
|
373
|
+
if (deployableEntries('calcom', r1).length === 0)
|
|
374
|
+
return false; // still pending
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
if (isInfrastructureError(err))
|
|
378
|
+
throw harnessError('calcom.connector.push', err);
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
finally {
|
|
382
|
+
rmSync(r1, { recursive: true, force: true });
|
|
383
|
+
}
|
|
384
|
+
// (b) a SUCCESS response confirms the action and hits the right endpoint
|
|
385
|
+
const r2 = mkdtempSync(join(tmpdir(), 'calcom-cap-push-ok-'));
|
|
386
|
+
try {
|
|
387
|
+
await handleCalcomTwinRequest({ method: 'POST', path: '/v2/event-types', body: JSON.stringify({ title: 'L', slug: 'l', lengthInMinutes: 30 }), root: r2, occurredAt: AT });
|
|
388
|
+
const calls = [];
|
|
389
|
+
const okExec = async (m, p) => { calls.push(`${m} ${p}`); return { status: 'success', data: { id: 1 } }; };
|
|
390
|
+
const pushed = await performPending(okExec, { root: r2 });
|
|
391
|
+
return pushed > 0 && calls.includes('POST /event-types') && deployableEntries('calcom', r2).length === 0;
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
if (isInfrastructureError(err))
|
|
395
|
+
throw harnessError('calcom.connector.push', err);
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
finally {
|
|
399
|
+
rmSync(r2, { recursive: true, force: true });
|
|
400
|
+
}
|
|
401
|
+
}),
|
|
402
|
+
todo('calcom.connector.pull_schedules', 'connector', 'PULL schedules + teams from the real account', 'connector', 'niche'),
|
|
403
|
+
// ── UI mirror (Cal.com has a real dashboard) ──
|
|
404
|
+
// TWIN-14 (B4) convergence — was a LOCAL uiDataCoupled(markers, seed, fetchPath, component,
|
|
405
|
+
// expectRows, idKey) helper (spinning up a real mirror HTTP server); now converged onto the
|
|
406
|
+
// SHARED @volter/world-tooling factory (seed/fetch/render/assert over the twin handler
|
|
407
|
+
// directly — the same data-coupling every other pack's migration uses). Semantics kept
|
|
408
|
+
// verbatim: seed 2 distinct bookings → fetch the SAME /v2/bookings the dashboard reads →
|
|
409
|
+
// render the dashboard's OWN BookingsList over the fetched rows → assert one `list-row` per
|
|
410
|
+
// seeded booking carrying its uid. Failable: an unseeded root's list is empty → 0 rows.
|
|
411
|
+
done('calcom.ui.bookings', 'ui', 'Bookings dashboard renders real twin bookings (data-coupled)', 'ui', 'core', uiDataCoupled({
|
|
412
|
+
withRoot,
|
|
413
|
+
seed: async (h) => {
|
|
414
|
+
await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', attendee: { name: 'A', email: 'a@x.test' } } });
|
|
415
|
+
await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-02T16:00:00.000Z', attendee: { name: 'B', email: 'b@x.test' } } });
|
|
416
|
+
},
|
|
417
|
+
fetch: async (h) => data(await h({ m: 'GET', p: '/v2/bookings' })),
|
|
418
|
+
render: (rows) => renderToStaticMarkup(createElement(BookingsList, { rows })),
|
|
419
|
+
assert: (rows, markup) => rows.length === 2 && !!markup && (markup.match(/list-row/g) ?? []).length === 2
|
|
420
|
+
&& rows.every((r) => markup.includes(String(r.uid))),
|
|
421
|
+
})),
|
|
422
|
+
// Same convergence for the Event Types screen: seed 1 distinct event type → fetch
|
|
423
|
+
// /v2/event-types → render the dashboard's OWN EventTypesList → assert its list-row + id.
|
|
424
|
+
done('calcom.ui.event_types', 'ui', 'Event Types screen renders real twin event types (data-coupled)', 'ui', 'common', uiDataCoupled({
|
|
425
|
+
withRoot,
|
|
426
|
+
seed: (h) => h({ m: 'POST', p: '/v2/event-types', b: { title: 'Intro', slug: 'intro', lengthInMinutes: 15 } }),
|
|
427
|
+
fetch: async (h) => data(await h({ m: 'GET', p: '/v2/event-types' })),
|
|
428
|
+
render: (rows) => renderToStaticMarkup(createElement(EventTypesList, { rows })),
|
|
429
|
+
assert: (rows, markup) => rows.length === 1 && !!markup && markup.includes('list-row') && markup.includes(String(rows[0].id)),
|
|
430
|
+
})),
|
|
431
|
+
// TWIN-14 (B4) migration — was a marker-grep over 'pill-ok'/'pill-bad'/'pill-warn' (bundle
|
|
432
|
+
// literals that happen to exist even in a dead twin); now uiDataCoupled: seed a booking in
|
|
433
|
+
// each of the three distinct statuses the mirror's PILL_CLASS maps (accepted/pending via a
|
|
434
|
+
// requiresConfirmation event type/cancelled) → fetch /v2/bookings → run each seeded status
|
|
435
|
+
// through the mirror's OWN `bookingTone` (exactly the function the pill component calls) →
|
|
436
|
+
// assert accepted→ok, pending→warn, cancelled→bad. Failable: an unseeded root finds no row
|
|
437
|
+
// for a status ⇒ `bookingTone(undefined)` → '' ⇒ none of the three tones match.
|
|
438
|
+
done('calcom.ui.status_pill', 'ui', 'Booking status pills map accepted/pending/cancelled to ok/warn/bad tones (data-coupled)', 'ui', 'niche', uiDataCoupled({
|
|
439
|
+
withRoot,
|
|
440
|
+
seed: async (h) => {
|
|
441
|
+
await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', attendee: { name: 'a', email: 'a@x.test' } } });
|
|
442
|
+
const evt = await h({ m: 'POST', p: '/v2/event-types', b: { title: 'Gated', slug: 'gated', lengthInMinutes: 30, requiresConfirmation: true } });
|
|
443
|
+
await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-02T16:00:00.000Z', eventTypeId: data(evt).id, attendee: { name: 'b', email: 'b@x.test' } } });
|
|
444
|
+
const c = await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-03T16:00:00.000Z', attendee: { name: 'c', email: 'c@x.test' } } });
|
|
445
|
+
await h({ m: 'POST', p: `/v2/bookings/${data(c).uid}/cancel`, b: {} });
|
|
446
|
+
},
|
|
447
|
+
fetch: async (h) => {
|
|
448
|
+
const rows = data(await h({ m: 'GET', p: '/v2/bookings' }));
|
|
449
|
+
return {
|
|
450
|
+
accepted: bookingTone(rows.find((r) => r.status === 'accepted')?.status),
|
|
451
|
+
pending: bookingTone(rows.find((r) => r.status === 'pending')?.status),
|
|
452
|
+
cancelled: bookingTone(rows.find((r) => r.status === 'cancelled')?.status),
|
|
453
|
+
};
|
|
454
|
+
},
|
|
455
|
+
assert: (tones) => tones.accepted === 'ok' && tones.pending === 'warn' && tones.cancelled === 'bad',
|
|
456
|
+
})),
|
|
457
|
+
// TWIN-14 (B4) migration — was a marker-grep over 'nav-item'/'Cal.com twin'; now
|
|
458
|
+
// uiDataCoupled following the sentry/supabase `ui.nav` reframe: the nav shell itself has no
|
|
459
|
+
// seedable data, so the honest proof is that BOTH nav-backed screens (Bookings / Event
|
|
460
|
+
// Types) read live seeded data — seed one distinct booking + one distinct event type →
|
|
461
|
+
// fetch each nav destination's own API → assert both seeded markers survive. Failable: an
|
|
462
|
+
// unseeded root has neither.
|
|
463
|
+
done('calcom.ui.nav', 'ui', 'Dashboard nav (Bookings / Event Types sections) — every nav destination reads live data (data-coupled)', 'ui', 'niche', uiDataCoupled({
|
|
464
|
+
withRoot,
|
|
465
|
+
seed: async (h) => {
|
|
466
|
+
await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', attendee: { name: 'Nav Check', email: 'nav-check@x.test' } } });
|
|
467
|
+
await h({ m: 'POST', p: '/v2/event-types', b: { title: 'Nav Check Event', slug: 'nav-check', lengthInMinutes: 20 } });
|
|
468
|
+
},
|
|
469
|
+
fetch: async (h) => {
|
|
470
|
+
const bookings = data(await h({ m: 'GET', p: '/v2/bookings' }));
|
|
471
|
+
const eventTypes = data(await h({ m: 'GET', p: '/v2/event-types' }));
|
|
472
|
+
return {
|
|
473
|
+
bookings: bookings.some((b) => (b.attendees?.[0]?.email) === 'nav-check@x.test'),
|
|
474
|
+
eventTypes: eventTypes.some((e) => e.slug === 'nav-check'),
|
|
475
|
+
};
|
|
476
|
+
},
|
|
477
|
+
assert: (hits) => hits.bookings && hits.eventTypes,
|
|
478
|
+
})),
|
|
479
|
+
done('calcom.ui.booking_detail', 'ui', 'Booking detail renders the real booking host+attendees (data-coupled)', 'ui', 'common', async () => {
|
|
480
|
+
const js = await mirrorBundle();
|
|
481
|
+
if (!['booking-detail', 'people-table', 'list-row'].every((m) => js.includes(m)))
|
|
482
|
+
return false;
|
|
483
|
+
const root = mkdtempSync(join(tmpdir(), 'calcom-uibd-'));
|
|
484
|
+
const server = await createCalcomMirrorServer({ root, port: 0 });
|
|
485
|
+
try {
|
|
486
|
+
const h = (s) => handleCalcomTwinRequest({ method: s.m, path: s.p, body: s.b === undefined ? undefined : JSON.stringify(s.b), root, occurredAt: AT });
|
|
487
|
+
await h({ m: 'POST', p: '/v2/bookings', b: { start: '2026-07-01T16:00:00.000Z', attendee: { name: 'Jo', email: 'jo@x.test' } } });
|
|
488
|
+
const res = await fetch(`http://127.0.0.1:${server.port}/v2/bookings`);
|
|
489
|
+
const body = (await res.json());
|
|
490
|
+
const rows = Array.isArray(body.data) ? body.data : [];
|
|
491
|
+
if (rows.length !== 1)
|
|
492
|
+
return false;
|
|
493
|
+
const markup = renderToStaticMarkup(createElement(BookingDetail, { rows }));
|
|
494
|
+
// one list-row per person: 1 host + 1 attendee = 2; both the host's name and the attendee's
|
|
495
|
+
// email (real seeded data) plus the host/attendee role labels and booking uid must render.
|
|
496
|
+
const rowCount = (markup.match(/list-row/g) ?? []).length;
|
|
497
|
+
return rowCount === 2 && markup.includes('jo@x.test') && markup.includes('Cal.com Twin Admin')
|
|
498
|
+
&& markup.includes('host') && markup.includes('attendee') && markup.includes(String(rows[0].uid));
|
|
499
|
+
}
|
|
500
|
+
catch (err) {
|
|
501
|
+
if (isInfrastructureError(err))
|
|
502
|
+
throw harnessError('calcom.ui.booking_detail', err);
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
finally {
|
|
506
|
+
server.stop();
|
|
507
|
+
rmSync(root, { recursive: true, force: true });
|
|
508
|
+
}
|
|
509
|
+
}),
|
|
510
|
+
todo('calcom.ui.availability_editor', 'ui', 'Availability/schedule editor screen', 'ui', 'niche'),
|
|
511
|
+
todo('calcom.ui.teams', 'ui', 'Teams + members screen', 'ui', 'niche'),
|
|
512
|
+
// ── Out of scope (genuinely impossible for a local twin) ──
|
|
513
|
+
// ── Pull-surface coverage audit gaps (TWIN-46 / G2) — filed as manifest todos, which is
|
|
514
|
+
// what the demand-ordered build list is drawn from. See pull-audit.json (repo root) for the
|
|
515
|
+
// per-pack pull-vs-read-surface evidence.
|
|
516
|
+
todo('calcom.connector.pull_webhooks', 'connector', 'Connector: pull registered webhooks (GET /v2/webhooks) from the real account', 'connector', 'common'),
|
|
517
|
+
todo('calcom.connector.pull_me', 'connector', 'Connector: pull the API-key-owner profile (GET /v2/me) from the real account', 'connector', 'core'),
|
|
518
|
+
];
|
|
519
|
+
export async function calcomCapabilities() {
|
|
520
|
+
return checkCapabilities('calcom', CALCOM_CAPABILITIES);
|
|
521
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type CalcomViolation = {
|
|
2
|
+
type: string;
|
|
3
|
+
id: string;
|
|
4
|
+
field: string;
|
|
5
|
+
reason: string;
|
|
6
|
+
};
|
|
7
|
+
export type CalcomConformanceReport = {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
resourcesChecked: number;
|
|
10
|
+
fieldsChecked: number;
|
|
11
|
+
violations: CalcomViolation[];
|
|
12
|
+
};
|
|
13
|
+
export declare function checkCalcomConformance(opts?: {
|
|
14
|
+
root?: string;
|
|
15
|
+
}): CalcomConformanceReport;
|