@vantageos/vantage-crm-mcp 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.
Files changed (48) hide show
  1. package/README.md +260 -0
  2. package/dist/convex/crm/_helpers.js +24 -0
  3. package/dist/convex/crm/activities.js +220 -0
  4. package/dist/convex/crm/briefing.js +198 -0
  5. package/dist/convex/crm/calendarCron.js +92 -0
  6. package/dist/convex/crm/calendarCronDispatch.js +83 -0
  7. package/dist/convex/crm/calendarSync.js +294 -0
  8. package/dist/convex/crm/companies.js +323 -0
  9. package/dist/convex/crm/contacts.js +346 -0
  10. package/dist/convex/crm/deals.js +481 -0
  11. package/dist/convex/crm/emailActions.js +158 -0
  12. package/dist/convex/crm/emailCron.js +210 -0
  13. package/dist/convex/crm/emailCronDispatch.js +76 -0
  14. package/dist/convex/crm/emailSync.js +260 -0
  15. package/dist/convex/crm/onboarding.js +185 -0
  16. package/dist/convex/crm/stats.js +75 -0
  17. package/dist/convex/crm/tasks.js +109 -0
  18. package/dist/convex/crons.js +25 -0
  19. package/dist/convex/integrations.js +183 -0
  20. package/dist/convex/lib/auditLog.js +109 -0
  21. package/dist/convex/lib/auth.js +372 -0
  22. package/dist/convex/lib/rbac.js +123 -0
  23. package/dist/convex/lib/workspace.js +171 -0
  24. package/dist/convex/organizations.js +192 -0
  25. package/dist/convex/schema.js +690 -0
  26. package/dist/convex/users.js +217 -0
  27. package/dist/convex/workspaces.js +603 -0
  28. package/dist/mcp-server/lib/convexClient.js +50 -0
  29. package/dist/mcp-server/lib/scopeEnforcement.js +76 -0
  30. package/dist/mcp-server/registry.js +116 -0
  31. package/dist/mcp-server/server.js +97 -0
  32. package/dist/mcp-server/tests/registry.test.js +163 -0
  33. package/dist/mcp-server/tests/scopeEnforcement.test.js +137 -0
  34. package/dist/mcp-server/tests/security.test.js +257 -0
  35. package/dist/mcp-server/tests/tools.test.js +272 -0
  36. package/dist/mcp-server/tools/activities.js +207 -0
  37. package/dist/mcp-server/tools/admin.js +190 -0
  38. package/dist/mcp-server/tools/companies.js +233 -0
  39. package/dist/mcp-server/tools/contacts.js +306 -0
  40. package/dist/mcp-server/tools/customFields.js +222 -0
  41. package/dist/mcp-server/tools/customObjects.js +235 -0
  42. package/dist/mcp-server/tools/deals.js +297 -0
  43. package/dist/mcp-server/tools/rbac.js +177 -0
  44. package/dist/mcp-server/tools/search.js +155 -0
  45. package/dist/mcp-server/tools/workflows.js +234 -0
  46. package/dist/mcp-server/transport/http.js +257 -0
  47. package/dist/mcp-server/transport/stdio.js +90 -0
  48. package/package.json +45 -0
@@ -0,0 +1,257 @@
1
+ "use strict";
2
+ /**
3
+ * MCP Server — Security & Compliance Tests
4
+ *
5
+ * Tests:
6
+ * BL-1: Multi-tenant workspace isolation cross-check (IDOR prevention)
7
+ * SHOULD-FIX-1: NOT_IMPLEMENTED stable envelope (not throw)
8
+ * SHOULD-FIX-2: purge_archived_records 30-day grace (Convex-side enforced)
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ const vitest_1 = require("vitest");
45
+ // ---------------------------------------------------------------------------
46
+ // Mock convexClient for tool-layer tests
47
+ // ---------------------------------------------------------------------------
48
+ vitest_1.vi.mock('../lib/convexClient', () => ({
49
+ getConvexClient: vitest_1.vi.fn(() => ({
50
+ query: vitest_1.vi.fn().mockResolvedValue([]),
51
+ mutation: vitest_1.vi.fn().mockResolvedValue({ purged: 0, skippedGrace: 0 }),
52
+ })),
53
+ withEnvelope: async (fn) => {
54
+ try {
55
+ const data = await fn();
56
+ return { success: true, data };
57
+ }
58
+ catch (e) {
59
+ const message = e instanceof Error ? e.message : String(e);
60
+ const code = e instanceof Error && e.name !== 'Error' ? e.name : 'CONVEX_ERROR';
61
+ return { success: false, error: { code, message } };
62
+ }
63
+ },
64
+ ok: (data) => ({ success: true, data }),
65
+ err: (code, message) => ({ success: false, error: { code, message } }),
66
+ }));
67
+ // ---------------------------------------------------------------------------
68
+ // BL-1: requireWorkspaceMatchesToken — extracted logic test
69
+ // ---------------------------------------------------------------------------
70
+ /**
71
+ * Inline the helper to test the isolation logic in isolation from the HTTP server.
72
+ * The actual function is in mcp-server/transport/http.ts.
73
+ */
74
+ function requireWorkspaceMatchesToken(tokenContext, args) {
75
+ const tokenWorkspaceId = tokenContext.workspaceId ?? tokenContext.orgId;
76
+ const argsWorkspaceId = args['workspaceId'];
77
+ if (argsWorkspaceId !== undefined &&
78
+ argsWorkspaceId !== null &&
79
+ tokenWorkspaceId !== null &&
80
+ argsWorkspaceId !== tokenWorkspaceId) {
81
+ const err = new Error(`Token workspace (${tokenWorkspaceId}) does not match args.workspaceId (${String(argsWorkspaceId)}). Cross-tenant access denied.`);
82
+ err.name = 'WorkspaceMismatchError';
83
+ err.code = 'FORBIDDEN_WORKSPACE_MISMATCH';
84
+ throw err;
85
+ }
86
+ }
87
+ (0, vitest_1.describe)('BL-1: multi-tenant workspace isolation', () => {
88
+ (0, vitest_1.it)('allows matching workspaceId', () => {
89
+ (0, vitest_1.expect)(() => requireWorkspaceMatchesToken({ workspaceId: 'ws_abc', orgId: 'org_xyz' }, { workspaceId: 'ws_abc' })).not.toThrow();
90
+ });
91
+ (0, vitest_1.it)('allows request without workspaceId in args (e.g. getById tools)', () => {
92
+ (0, vitest_1.expect)(() => requireWorkspaceMatchesToken({ workspaceId: 'ws_abc', orgId: 'org_xyz' }, { contactId: 'contact_123' })).not.toThrow();
93
+ });
94
+ (0, vitest_1.it)('REJECTS token workspace A vs args workspace B — IDOR cross-tenant', () => {
95
+ (0, vitest_1.expect)(() => requireWorkspaceMatchesToken({ workspaceId: 'ws_workspaceA', orgId: 'org_orgX' }, { workspaceId: 'ws_workspaceB' })).toThrow('Cross-tenant access denied');
96
+ });
97
+ (0, vitest_1.it)('rejection error has FORBIDDEN_WORKSPACE_MISMATCH code', () => {
98
+ try {
99
+ requireWorkspaceMatchesToken({ workspaceId: 'ws_A', orgId: 'org_X' }, { workspaceId: 'ws_B' });
100
+ vitest_1.expect.fail('should have thrown');
101
+ }
102
+ catch (e) {
103
+ (0, vitest_1.expect)(e.code).toBe('FORBIDDEN_WORKSPACE_MISMATCH');
104
+ (0, vitest_1.expect)(e.message).toContain('ws_A');
105
+ (0, vitest_1.expect)(e.message).toContain('ws_B');
106
+ }
107
+ });
108
+ (0, vitest_1.it)('falls back to orgId when workspaceId is null on token', () => {
109
+ (0, vitest_1.expect)(() => requireWorkspaceMatchesToken({ workspaceId: null, orgId: 'org_abc' }, { workspaceId: 'org_abc' })).not.toThrow();
110
+ });
111
+ (0, vitest_1.it)('rejects when orgId fallback does not match args.workspaceId', () => {
112
+ (0, vitest_1.expect)(() => requireWorkspaceMatchesToken({ workspaceId: null, orgId: 'org_abc' }, { workspaceId: 'org_xyz' })).toThrow('Cross-tenant access denied');
113
+ });
114
+ (0, vitest_1.it)('allows when both token and args workspaceId are null/undefined', () => {
115
+ (0, vitest_1.expect)(() => requireWorkspaceMatchesToken({ workspaceId: null, orgId: 'org_abc' }, { contactId: 'x' })).not.toThrow();
116
+ });
117
+ });
118
+ // ---------------------------------------------------------------------------
119
+ // SHOULD-FIX-1: NOT_IMPLEMENTED stable envelope shape
120
+ // ---------------------------------------------------------------------------
121
+ const admin_1 = require("../tools/admin");
122
+ const customFields_1 = require("../tools/customFields");
123
+ const rbac_1 = require("../tools/rbac");
124
+ const workflows_1 = require("../tools/workflows");
125
+ (0, vitest_1.describe)('SHOULD-FIX-1: NOT_IMPLEMENTED stable envelope', () => {
126
+ (0, vitest_1.it)('list_audit_log returns NOT_IMPLEMENTED envelope (not throw)', async () => {
127
+ const result = (await (0, admin_1.list_audit_log)({
128
+ workspaceId: 'ws_test',
129
+ }));
130
+ (0, vitest_1.expect)(result.success).toBe(false);
131
+ (0, vitest_1.expect)(result.error?.code).toBe('NOT_IMPLEMENTED');
132
+ (0, vitest_1.expect)(result.error?.message).toMatch(/V0\.2/);
133
+ });
134
+ (0, vitest_1.it)('get_entity_history returns NOT_IMPLEMENTED envelope (not throw)', async () => {
135
+ const result = (await (0, admin_1.get_entity_history)({
136
+ workspaceId: 'ws_test',
137
+ entityType: 'contact',
138
+ entityId: 'cnt_123',
139
+ }));
140
+ (0, vitest_1.expect)(result.success).toBe(false);
141
+ (0, vitest_1.expect)(result.error?.code).toBe('NOT_IMPLEMENTED');
142
+ });
143
+ (0, vitest_1.it)('get_actor_history returns NOT_IMPLEMENTED envelope (not throw)', async () => {
144
+ const result = (await (0, admin_1.get_actor_history)({
145
+ workspaceId: 'ws_test',
146
+ actorId: 'user_abc',
147
+ }));
148
+ (0, vitest_1.expect)(result.success).toBe(false);
149
+ (0, vitest_1.expect)(result.error?.code).toBe('NOT_IMPLEMENTED');
150
+ });
151
+ (0, vitest_1.it)('delete_custom_field returns NOT_IMPLEMENTED envelope (not throw)', async () => {
152
+ const result = (await (0, customFields_1.delete_custom_field)({
153
+ definitionId: 'def_123',
154
+ }));
155
+ (0, vitest_1.expect)(result.success).toBe(false);
156
+ (0, vitest_1.expect)(result.error?.code).toBe('NOT_IMPLEMENTED');
157
+ });
158
+ (0, vitest_1.it)('update_member_role returns NOT_IMPLEMENTED envelope (not throw)', async () => {
159
+ const result = (await (0, rbac_1.update_member_role)({
160
+ workspaceId: 'ws_test',
161
+ userId: 'user_abc',
162
+ newRole: 'editor',
163
+ }));
164
+ (0, vitest_1.expect)(result.success).toBe(false);
165
+ (0, vitest_1.expect)(result.error?.code).toBe('NOT_IMPLEMENTED');
166
+ });
167
+ (0, vitest_1.it)('update_member_permissions returns NOT_IMPLEMENTED envelope (not throw)', async () => {
168
+ const result = (await (0, rbac_1.update_member_permissions)({
169
+ workspaceId: 'ws_test',
170
+ userId: 'user_abc',
171
+ permissions: { canExportData: true },
172
+ }));
173
+ (0, vitest_1.expect)(result.success).toBe(false);
174
+ (0, vitest_1.expect)(result.error?.code).toBe('NOT_IMPLEMENTED');
175
+ });
176
+ (0, vitest_1.it)('replay_execution returns NOT_IMPLEMENTED envelope (not throw)', async () => {
177
+ const result = (await (0, workflows_1.replay_execution)({
178
+ executionId: 'exec_xyz',
179
+ }));
180
+ (0, vitest_1.expect)(result.success).toBe(false);
181
+ (0, vitest_1.expect)(result.error?.code).toBe('NOT_IMPLEMENTED');
182
+ });
183
+ (0, vitest_1.it)('NOT_IMPLEMENTED tools do NOT throw — verified for all 7', async () => {
184
+ const calls = [
185
+ () => (0, admin_1.list_audit_log)({ workspaceId: 'ws_test' }),
186
+ () => (0, admin_1.get_entity_history)({ workspaceId: 'ws_test', entityType: 'deal', entityId: 'e' }),
187
+ () => (0, admin_1.get_actor_history)({ workspaceId: 'ws_test', actorId: 'a' }),
188
+ () => (0, customFields_1.delete_custom_field)({ definitionId: 'def_x' }),
189
+ () => (0, rbac_1.update_member_role)({ workspaceId: 'ws_test', userId: 'u', newRole: 'viewer' }),
190
+ () => (0, rbac_1.update_member_permissions)({ workspaceId: 'ws_test', userId: 'u', permissions: {} }),
191
+ () => (0, workflows_1.replay_execution)({ executionId: 'exec_x' }),
192
+ ];
193
+ for (const call of calls) {
194
+ await (0, vitest_1.expect)(call()).resolves.toMatchObject({
195
+ success: false,
196
+ error: { code: 'NOT_IMPLEMENTED' },
197
+ });
198
+ }
199
+ });
200
+ });
201
+ // ---------------------------------------------------------------------------
202
+ // SHOULD-FIX-2: purge_archived_records grace — Convex-side guard
203
+ // ---------------------------------------------------------------------------
204
+ /**
205
+ * The actual 30-day grace is enforced in convex/crm/admin.ts purgeArchivedRecords.
206
+ * This test validates the guard logic (extracted for unit testing without Convex runtime).
207
+ * Integration: see comment at bottom for Convex direct-call validation strategy.
208
+ */
209
+ function assertPurgeGrace(archivedAt) {
210
+ const GRACE_PERIOD_MS = 30 * 24 * 60 * 60 * 1000;
211
+ if (Date.now() - archivedAt < GRACE_PERIOD_MS) {
212
+ throw new Error('PURGE_GRACE_NOT_ELAPSED');
213
+ }
214
+ }
215
+ (0, vitest_1.describe)('SHOULD-FIX-2: purge 30-day grace server-side enforcement', () => {
216
+ (0, vitest_1.it)('rejects record archived less than 30 days ago', () => {
217
+ const archivedAt = Date.now() - 10 * 24 * 60 * 60 * 1000; // 10 days ago
218
+ (0, vitest_1.expect)(() => assertPurgeGrace(archivedAt)).toThrow('PURGE_GRACE_NOT_ELAPSED');
219
+ });
220
+ (0, vitest_1.it)('rejects record archived 29 days ago', () => {
221
+ const archivedAt = Date.now() - 29 * 24 * 60 * 60 * 1000;
222
+ (0, vitest_1.expect)(() => assertPurgeGrace(archivedAt)).toThrow('PURGE_GRACE_NOT_ELAPSED');
223
+ });
224
+ (0, vitest_1.it)('rejects record archived exactly 1 second less than 30 days ago', () => {
225
+ const archivedAt = Date.now() - (30 * 24 * 60 * 60 * 1000 - 1000);
226
+ (0, vitest_1.expect)(() => assertPurgeGrace(archivedAt)).toThrow('PURGE_GRACE_NOT_ELAPSED');
227
+ });
228
+ (0, vitest_1.it)('allows record archived exactly 30 days ago', () => {
229
+ const archivedAt = Date.now() - 30 * 24 * 60 * 60 * 1000;
230
+ (0, vitest_1.expect)(() => assertPurgeGrace(archivedAt)).not.toThrow();
231
+ });
232
+ (0, vitest_1.it)('allows record archived 31 days ago', () => {
233
+ const archivedAt = Date.now() - 31 * 24 * 60 * 60 * 1000;
234
+ (0, vitest_1.expect)(() => assertPurgeGrace(archivedAt)).not.toThrow();
235
+ });
236
+ (0, vitest_1.it)('allows record archived 90 days ago', () => {
237
+ const archivedAt = Date.now() - 90 * 24 * 60 * 60 * 1000;
238
+ (0, vitest_1.expect)(() => assertPurgeGrace(archivedAt)).not.toThrow();
239
+ });
240
+ /**
241
+ * Convex-direct call guard:
242
+ * convex/crm/admin.ts purgeArchivedRecords iterates all archived records and
243
+ * skips (does NOT delete) any record where Date.now() - archivedAt < GRACE_PERIOD_MS.
244
+ * The returned { skippedGrace: N } count confirms grace enforcement for callers
245
+ * that bypass the MCP layer. No Zod dependency in the Convex handler.
246
+ */
247
+ (0, vitest_1.it)('purge_archived_records MCP tool routes to Convex mutation (not throwing)', async () => {
248
+ const { purge_archived_records } = await Promise.resolve().then(() => __importStar(require('../tools/admin')));
249
+ const result = (await purge_archived_records({
250
+ workspaceId: 'ws_test',
251
+ entityType: 'contact',
252
+ olderThanDays: 30,
253
+ }));
254
+ // With mocked Convex client returning { purged: 0, skippedGrace: 0 }
255
+ (0, vitest_1.expect)(result.success).toBe(true);
256
+ });
257
+ });
@@ -0,0 +1,272 @@
1
+ "use strict";
2
+ /**
3
+ * MCP Server — Tool Handler Unit Tests
4
+ *
5
+ * Tests Zod validation + envelope behavior for one tool per category (10 tools).
6
+ * Does NOT make live Convex calls — validates input schema rejection.
7
+ *
8
+ * Scope enforcement integration tests are in scopeEnforcement.test.ts.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const vitest_1 = require("vitest");
12
+ // ---------------------------------------------------------------------------
13
+ // Mock the Convex client so handlers don't make live network calls
14
+ // ---------------------------------------------------------------------------
15
+ vitest_1.vi.mock('../lib/convexClient', () => ({
16
+ getConvexClient: vitest_1.vi.fn(() => ({
17
+ query: vitest_1.vi.fn().mockResolvedValue([]),
18
+ mutation: vitest_1.vi.fn().mockResolvedValue('mock_id_123'),
19
+ action: vitest_1.vi.fn().mockResolvedValue(null),
20
+ })),
21
+ withEnvelope: async (fn) => {
22
+ try {
23
+ const data = await fn();
24
+ return { success: true, data };
25
+ }
26
+ catch (e) {
27
+ const message = e instanceof Error ? e.message : String(e);
28
+ return { success: false, error: { code: 'ERROR', message } };
29
+ }
30
+ },
31
+ ok: (data) => ({ success: true, data }),
32
+ err: (code, message) => ({ success: false, error: { code, message } }),
33
+ }));
34
+ /**
35
+ * Wrap a handler call to catch ZodError thrown before withEnvelope.
36
+ * In production, handlers throw ZodError synchronously on bad input.
37
+ * The MCP server catches these in CallTool. Tests simulate that behavior.
38
+ */
39
+ async function callHandler(fn, args) {
40
+ try {
41
+ return (await fn(args));
42
+ }
43
+ catch (e) {
44
+ const message = e instanceof Error ? e.message : String(e);
45
+ return { success: false, error: { code: 'ZOD_ERROR', message } };
46
+ }
47
+ }
48
+ // ---------------------------------------------------------------------------
49
+ // Imports after mock
50
+ // ---------------------------------------------------------------------------
51
+ const contacts_1 = require("../tools/contacts");
52
+ const companies_1 = require("../tools/companies");
53
+ const deals_1 = require("../tools/deals");
54
+ const activities_1 = require("../tools/activities");
55
+ const customFields_1 = require("../tools/customFields");
56
+ const customObjects_1 = require("../tools/customObjects");
57
+ const workflows_1 = require("../tools/workflows");
58
+ const search_1 = require("../tools/search");
59
+ const admin_1 = require("../tools/admin");
60
+ const rbac_1 = require("../tools/rbac");
61
+ // ---------------------------------------------------------------------------
62
+ // Contacts
63
+ // ---------------------------------------------------------------------------
64
+ (0, vitest_1.describe)('tool/contacts', () => {
65
+ (0, vitest_1.it)('create_contact: rejects missing required fields', async () => {
66
+ const result = await callHandler(contacts_1.create_contact, { workspaceId: 'ws1' });
67
+ (0, vitest_1.expect)(result.success).toBe(false);
68
+ (0, vitest_1.expect)(result.error?.message).toMatch(/firstName|lastName/);
69
+ });
70
+ (0, vitest_1.it)('create_contact: accepts valid args', async () => {
71
+ const result = await callHandler(contacts_1.create_contact, {
72
+ workspaceId: 'ws1',
73
+ firstName: 'John',
74
+ lastName: 'Doe',
75
+ email: 'john@example.com',
76
+ type: 'lead',
77
+ });
78
+ (0, vitest_1.expect)(result.success).toBe(true);
79
+ });
80
+ (0, vitest_1.it)('get_contact: rejects missing contactId', async () => {
81
+ const result = await callHandler(contacts_1.get_contact, {});
82
+ (0, vitest_1.expect)(result.success).toBe(false);
83
+ (0, vitest_1.expect)(result.error?.message).toMatch(/contactId/);
84
+ });
85
+ (0, vitest_1.it)('get_contact: rejects invalid email in create', async () => {
86
+ const result = await callHandler(contacts_1.create_contact, {
87
+ workspaceId: 'ws1',
88
+ firstName: 'Bad',
89
+ lastName: 'Email',
90
+ email: 'not-an-email',
91
+ });
92
+ (0, vitest_1.expect)(result.success).toBe(false);
93
+ (0, vitest_1.expect)(result.error?.message).toMatch(/email/i);
94
+ });
95
+ });
96
+ // ---------------------------------------------------------------------------
97
+ // Companies
98
+ // ---------------------------------------------------------------------------
99
+ (0, vitest_1.describe)('tool/companies', () => {
100
+ (0, vitest_1.it)('create_company: rejects missing name', async () => {
101
+ const result = await callHandler(companies_1.create_company, { workspaceId: 'ws1' });
102
+ (0, vitest_1.expect)(result.success).toBe(false);
103
+ });
104
+ (0, vitest_1.it)('create_company: accepts valid args', async () => {
105
+ const result = await callHandler(companies_1.create_company, {
106
+ workspaceId: 'ws1',
107
+ name: 'Acme Corp',
108
+ industry: 'Technology',
109
+ });
110
+ (0, vitest_1.expect)(result.success).toBe(true);
111
+ });
112
+ });
113
+ // ---------------------------------------------------------------------------
114
+ // Deals
115
+ // ---------------------------------------------------------------------------
116
+ (0, vitest_1.describe)('tool/deals', () => {
117
+ (0, vitest_1.it)('create_deal: rejects missing title', async () => {
118
+ const result = await callHandler(deals_1.create_deal, { workspaceId: 'ws1', stage: 'Lead' });
119
+ (0, vitest_1.expect)(result.success).toBe(false);
120
+ });
121
+ (0, vitest_1.it)('create_deal: accepts valid args', async () => {
122
+ const result = await callHandler(deals_1.create_deal, {
123
+ workspaceId: 'ws1',
124
+ title: 'Enterprise Deal',
125
+ stage: 'Prospecting',
126
+ value: 50000,
127
+ });
128
+ (0, vitest_1.expect)(result.success).toBe(true);
129
+ });
130
+ (0, vitest_1.it)('move_deal_stage: rejects probability > 100', async () => {
131
+ const result = await callHandler(deals_1.move_deal_stage, {
132
+ dealId: 'deal_123',
133
+ newStage: 'Closed Won',
134
+ probability: 150,
135
+ });
136
+ (0, vitest_1.expect)(result.success).toBe(false);
137
+ });
138
+ (0, vitest_1.it)('move_deal_stage: accepts valid args', async () => {
139
+ const result = await callHandler(deals_1.move_deal_stage, {
140
+ dealId: 'deal_123',
141
+ newStage: 'Negotiation',
142
+ probability: 75,
143
+ });
144
+ (0, vitest_1.expect)(result.success).toBe(true);
145
+ });
146
+ });
147
+ // ---------------------------------------------------------------------------
148
+ // Activities
149
+ // ---------------------------------------------------------------------------
150
+ (0, vitest_1.describe)('tool/activities', () => {
151
+ (0, vitest_1.it)('create_activity: rejects invalid type', async () => {
152
+ const result = await callHandler(activities_1.create_activity, {
153
+ workspaceId: 'ws1',
154
+ type: 'invalid_type',
155
+ subject: 'Test',
156
+ });
157
+ (0, vitest_1.expect)(result.success).toBe(false);
158
+ });
159
+ (0, vitest_1.it)('create_activity: accepts valid task type', async () => {
160
+ const result = await callHandler(activities_1.create_activity, {
161
+ workspaceId: 'ws1',
162
+ type: 'task',
163
+ subject: 'Follow up',
164
+ dueAt: Date.now() + 86400000,
165
+ });
166
+ (0, vitest_1.expect)(result.success).toBe(true);
167
+ });
168
+ });
169
+ // ---------------------------------------------------------------------------
170
+ // Custom Fields
171
+ // ---------------------------------------------------------------------------
172
+ (0, vitest_1.describe)('tool/customFields', () => {
173
+ (0, vitest_1.it)('add_custom_field: rejects invalid key format', async () => {
174
+ const result = await callHandler(customFields_1.add_custom_field, {
175
+ workspaceId: 'ws1',
176
+ entityType: 'contact',
177
+ key: 'Invalid Key!',
178
+ label: 'Test Field',
179
+ fieldType: 'text',
180
+ });
181
+ (0, vitest_1.expect)(result.success).toBe(false);
182
+ });
183
+ (0, vitest_1.it)('add_custom_field: accepts valid snake_case key', async () => {
184
+ const result = await callHandler(customFields_1.add_custom_field, {
185
+ workspaceId: 'ws1',
186
+ entityType: 'contact',
187
+ key: 'lead_source',
188
+ label: 'Lead Source',
189
+ fieldType: 'select',
190
+ options: ['inbound', 'outbound', 'referral'],
191
+ });
192
+ (0, vitest_1.expect)(result.success).toBe(true);
193
+ });
194
+ });
195
+ // ---------------------------------------------------------------------------
196
+ // Custom Objects
197
+ // ---------------------------------------------------------------------------
198
+ (0, vitest_1.describe)('tool/customObjects', () => {
199
+ (0, vitest_1.it)('create_record: rejects missing fields', async () => {
200
+ const result = await callHandler(customObjects_1.create_record, { workspaceId: 'ws1', objectType: 'project' });
201
+ (0, vitest_1.expect)(result.success).toBe(false);
202
+ });
203
+ (0, vitest_1.it)('create_record: accepts valid args', async () => {
204
+ const result = await callHandler(customObjects_1.create_record, {
205
+ workspaceId: 'ws1',
206
+ objectType: 'project',
207
+ fields: { name: 'Q4 Campaign', budget: 50000 },
208
+ });
209
+ (0, vitest_1.expect)(result.success).toBe(true);
210
+ });
211
+ });
212
+ // ---------------------------------------------------------------------------
213
+ // Workflows
214
+ // ---------------------------------------------------------------------------
215
+ (0, vitest_1.describe)('tool/workflows', () => {
216
+ (0, vitest_1.it)('create_workflow: rejects missing trigger', async () => {
217
+ const result = await callHandler(workflows_1.create_workflow, {
218
+ workspaceId: 'ws1',
219
+ name: 'Lead nurture',
220
+ actions: [],
221
+ });
222
+ (0, vitest_1.expect)(result.success).toBe(false);
223
+ });
224
+ (0, vitest_1.it)('create_workflow: accepts valid args', async () => {
225
+ const result = await callHandler(workflows_1.create_workflow, {
226
+ workspaceId: 'ws1',
227
+ name: 'Lead nurture',
228
+ trigger: { type: 'contact_created', config: {} },
229
+ actions: [{ type: 'send_email', config: { templateId: 'welcome' }, order: 0 }],
230
+ });
231
+ (0, vitest_1.expect)(result.success).toBe(true);
232
+ });
233
+ });
234
+ // ---------------------------------------------------------------------------
235
+ // Search / Analytics
236
+ // ---------------------------------------------------------------------------
237
+ (0, vitest_1.describe)('tool/search', () => {
238
+ (0, vitest_1.it)('pipeline_value: rejects missing workspaceId', async () => {
239
+ const result = await callHandler(search_1.pipeline_value, {});
240
+ (0, vitest_1.expect)(result.success).toBe(false);
241
+ });
242
+ (0, vitest_1.it)('pipeline_value: accepts valid args', async () => {
243
+ const result = await callHandler(search_1.pipeline_value, { workspaceId: 'ws1' });
244
+ (0, vitest_1.expect)(result.success).toBe(true);
245
+ });
246
+ });
247
+ // ---------------------------------------------------------------------------
248
+ // Admin
249
+ // ---------------------------------------------------------------------------
250
+ (0, vitest_1.describe)('tool/admin', () => {
251
+ (0, vitest_1.it)('list_workspace_limits: rejects missing workspaceId', async () => {
252
+ const result = await callHandler(admin_1.list_workspace_limits, {});
253
+ (0, vitest_1.expect)(result.success).toBe(false);
254
+ });
255
+ (0, vitest_1.it)('list_workspace_limits: accepts valid args', async () => {
256
+ const result = await callHandler(admin_1.list_workspace_limits, { workspaceId: 'ws1' });
257
+ (0, vitest_1.expect)(result.success).toBe(true);
258
+ });
259
+ });
260
+ // ---------------------------------------------------------------------------
261
+ // RBAC
262
+ // ---------------------------------------------------------------------------
263
+ (0, vitest_1.describe)('tool/rbac', () => {
264
+ (0, vitest_1.it)('list_workspace_members: rejects missing workspaceId', async () => {
265
+ const result = await callHandler(rbac_1.list_workspace_members, {});
266
+ (0, vitest_1.expect)(result.success).toBe(false);
267
+ });
268
+ (0, vitest_1.it)('list_workspace_members: accepts valid workspaceId', async () => {
269
+ const result = await callHandler(rbac_1.list_workspace_members, { workspaceId: 'ws1' });
270
+ (0, vitest_1.expect)(result.success).toBe(true);
271
+ });
272
+ });