@elevasis/core 0.12.0 → 0.14.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 (50) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +9 -2
  3. package/dist/organization-model/index.d.ts +1 -1
  4. package/dist/organization-model/index.js +9 -2
  5. package/dist/test-utils/index.d.ts +480 -389
  6. package/dist/test-utils/index.js +28 -2
  7. package/package.json +1 -1
  8. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +2324 -0
  9. package/src/auth/multi-tenancy/credentials/__tests__/encryption.test.ts +217 -216
  10. package/src/auth/multi-tenancy/credentials/server/encryption.ts +5 -19
  11. package/src/auth/multi-tenancy/credentials/server/kek-loader.ts +3 -13
  12. package/src/auth/multi-tenancy/permissions.ts +12 -5
  13. package/src/business/acquisition/activity-events.test.ts +250 -0
  14. package/src/business/acquisition/activity-events.ts +84 -0
  15. package/src/business/acquisition/api-schemas.test.ts +1180 -0
  16. package/src/business/acquisition/api-schemas.ts +456 -235
  17. package/src/business/acquisition/crm-state-actions.test.ts +160 -0
  18. package/src/business/acquisition/derive-actions.test.ts +518 -0
  19. package/src/business/acquisition/derive-actions.ts +103 -0
  20. package/src/business/acquisition/index.ts +51 -11
  21. package/src/business/acquisition/stateful.ts +30 -0
  22. package/src/business/acquisition/types.ts +44 -77
  23. package/src/execution/engine/index.ts +4 -1
  24. package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +1 -2
  25. package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +363 -361
  26. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.test.ts +162 -186
  27. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.test.ts +316 -338
  28. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +204 -210
  29. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.test.ts +88 -0
  30. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +141 -134
  31. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +76 -75
  32. package/src/execution/engine/tools/integration/service.test.ts +34 -9
  33. package/src/execution/engine/tools/integration/service.ts +6 -3
  34. package/src/execution/engine/tools/lead-service-types.ts +90 -30
  35. package/src/execution/engine/tools/platform/acquisition/types.ts +266 -260
  36. package/src/execution/engine/tools/registry.ts +5 -4
  37. package/src/execution/engine/tools/tool-maps.ts +43 -21
  38. package/src/execution/engine/workflow/types.ts +11 -0
  39. package/src/organization-model/contracts.ts +4 -4
  40. package/src/organization-model/domains/navigation.ts +62 -62
  41. package/src/organization-model/domains/sales.ts +272 -0
  42. package/src/organization-model/organization-graph.mdx +2 -2
  43. package/src/organization-model/published.ts +21 -21
  44. package/src/organization-model/resolve.ts +21 -8
  45. package/src/platform/constants/versions.ts +1 -1
  46. package/src/reference/_generated/contracts.md +2324 -0
  47. package/src/scaffold-registry/index.ts +10 -9
  48. package/src/scaffold-registry/schema.ts +68 -62
  49. package/src/supabase/database.types.ts +2958 -2884
  50. package/src/test-utils/rls/RLSTestContext.ts +585 -553
@@ -0,0 +1,1180 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { LEAD_GEN_PIPELINE_DEFINITIONS, LEAD_GEN_STAGE_CATALOG } from '../../organization-model/domains/sales'
3
+ import {
4
+ AddCompaniesToListRequestSchema,
5
+ AddContactsToListRequestSchema,
6
+ AcqArtifactOwnerKindSchema,
7
+ AcqContactResponseSchema,
8
+ AcqContactStatusSchema,
9
+ AcqEmailValidSchema,
10
+ AcqListResponseSchema,
11
+ CreateArtifactRequestSchema,
12
+ CreateCompanyRequestSchema,
13
+ CreateContactRequestSchema,
14
+ CreateDealNoteRequestSchema,
15
+ CreateDealTaskRequestSchema,
16
+ CreateListRequestSchema,
17
+ DealDetailResponseSchema,
18
+ DealListResponseSchema,
19
+ DealNoteResponseSchema,
20
+ DealStageSchema,
21
+ DealTaskResponseSchema,
22
+ ExecuteActionRequestSchema,
23
+ IcpRubricSchema,
24
+ ListArtifactsQuerySchema,
25
+ ListCompaniesQuerySchema,
26
+ ListContactsQuerySchema,
27
+ ListDealsQuerySchema,
28
+ ListDealTasksDueQuerySchema,
29
+ ListMembersQuerySchema,
30
+ ListStatusSchema,
31
+ PipelineStageSchema,
32
+ ScrapingConfigSchema,
33
+ TransitionItemRequestSchema,
34
+ UpdateCompanyRequestSchema,
35
+ UpdateContactRequestSchema,
36
+ UpdateListConfigRequestSchema,
37
+ UpdateListRequestSchema,
38
+ UpdateListStatusRequestSchema
39
+ } from './api-schemas'
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Helpers
43
+ // ---------------------------------------------------------------------------
44
+
45
+ const VALID_UUID = '00000000-0000-4000-8000-000000000001'
46
+ const ISO_TS = '2026-04-27T12:34:56.000Z'
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // DealStageSchema
50
+ // ---------------------------------------------------------------------------
51
+
52
+ describe('DealStageSchema', () => {
53
+ it.each(['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing'])(
54
+ 'accepts canonical stage "%s"',
55
+ (stage) => {
56
+ expect(DealStageSchema.safeParse(stage).success).toBe(true)
57
+ }
58
+ )
59
+
60
+ it('rejects an unknown stage value', () => {
61
+ expect(DealStageSchema.safeParse('open').success).toBe(false)
62
+ expect(DealStageSchema.safeParse('').success).toBe(false)
63
+ })
64
+ })
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // TransitionItemRequestSchema
68
+ // ---------------------------------------------------------------------------
69
+
70
+ describe('TransitionItemRequestSchema', () => {
71
+ const valid = {
72
+ pipelineKey: 'default',
73
+ stageKey: 'interested',
74
+ stateKey: null
75
+ }
76
+
77
+ it('accepts a minimal valid payload', () => {
78
+ expect(TransitionItemRequestSchema.safeParse(valid).success).toBe(true)
79
+ })
80
+
81
+ it('accepts stateKey as null', () => {
82
+ const result = TransitionItemRequestSchema.safeParse({ ...valid, stateKey: null })
83
+ expect(result.success).toBe(true)
84
+ })
85
+
86
+ it('accepts stateKey as a string', () => {
87
+ const result = TransitionItemRequestSchema.safeParse({ ...valid, stateKey: 'discovery_replied' })
88
+ expect(result.success).toBe(true)
89
+ })
90
+
91
+ it('accepts stateKey as undefined (optional)', () => {
92
+ const { stateKey: _omit, ...withoutState } = valid
93
+ const result = TransitionItemRequestSchema.safeParse(withoutState)
94
+ expect(result.success).toBe(true)
95
+ })
96
+
97
+ it('accepts a valid ISO datetime for expectedUpdatedAt', () => {
98
+ const result = TransitionItemRequestSchema.safeParse({ ...valid, expectedUpdatedAt: ISO_TS })
99
+ expect(result.success).toBe(true)
100
+ })
101
+
102
+ it('rejects a non-ISO datetime for expectedUpdatedAt', () => {
103
+ const result = TransitionItemRequestSchema.safeParse({ ...valid, expectedUpdatedAt: 'not-a-date' })
104
+ expect(result.success).toBe(false)
105
+ })
106
+
107
+ it('accepts all canonical CRM deal stages', () => {
108
+ const stages = ['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing']
109
+ for (const stageKey of stages) {
110
+ expect(TransitionItemRequestSchema.safeParse({ pipelineKey: 'default', stageKey, stateKey: null }).success).toBe(
111
+ true
112
+ )
113
+ }
114
+ })
115
+
116
+ it('accepts lead-gen pipeline stage/state pairs from LEAD_GEN_PIPELINE_DEFINITIONS', () => {
117
+ for (const pipelineDefinitions of Object.values(LEAD_GEN_PIPELINE_DEFINITIONS)) {
118
+ for (const pipeline of pipelineDefinitions) {
119
+ for (const stage of pipeline.stages) {
120
+ for (const state of stage.states) {
121
+ expect(
122
+ TransitionItemRequestSchema.safeParse({
123
+ pipelineKey: pipeline.pipelineKey,
124
+ stageKey: stage.stageKey,
125
+ stateKey: state.stateKey
126
+ }).success
127
+ ).toBe(true)
128
+ }
129
+ }
130
+ }
131
+ }
132
+ })
133
+
134
+ it('rejects an empty pipelineKey', () => {
135
+ const result = TransitionItemRequestSchema.safeParse({ ...valid, pipelineKey: '' })
136
+ expect(result.success).toBe(false)
137
+ })
138
+
139
+ it('rejects an empty stageKey', () => {
140
+ const result = TransitionItemRequestSchema.safeParse({ ...valid, stageKey: '' })
141
+ expect(result.success).toBe(false)
142
+ })
143
+
144
+ it('rejects unknown top-level fields (strict mode)', () => {
145
+ const result = TransitionItemRequestSchema.safeParse({ ...valid, unknownField: 'x' })
146
+ expect(result.success).toBe(false)
147
+ })
148
+
149
+ it('rejects missing pipelineKey', () => {
150
+ const { pipelineKey: _omit, ...missing } = valid
151
+ const result = TransitionItemRequestSchema.safeParse(missing)
152
+ expect(result.success).toBe(false)
153
+ })
154
+
155
+ it('rejects missing stageKey', () => {
156
+ const { stageKey: _omit, ...missing } = valid
157
+ const result = TransitionItemRequestSchema.safeParse(missing)
158
+ expect(result.success).toBe(false)
159
+ })
160
+ })
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // ExecuteActionRequestSchema
164
+ // ---------------------------------------------------------------------------
165
+
166
+ describe('ExecuteActionRequestSchema', () => {
167
+ it('accepts an empty object (payload is optional)', () => {
168
+ expect(ExecuteActionRequestSchema.safeParse({}).success).toBe(true)
169
+ })
170
+
171
+ it('accepts a payload record with arbitrary string keys', () => {
172
+ const result = ExecuteActionRequestSchema.safeParse({
173
+ payload: { channel: 'email', count: 5, nested: { ok: true } }
174
+ })
175
+ expect(result.success).toBe(true)
176
+ })
177
+
178
+ it('accepts an empty payload record', () => {
179
+ expect(ExecuteActionRequestSchema.safeParse({ payload: {} }).success).toBe(true)
180
+ })
181
+
182
+ it('rejects unknown top-level fields (strict mode)', () => {
183
+ const result = ExecuteActionRequestSchema.safeParse({ payload: {}, extra: 'bad' })
184
+ expect(result.success).toBe(false)
185
+ })
186
+ })
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // CreateDealNoteRequestSchema
190
+ // ---------------------------------------------------------------------------
191
+
192
+ describe('CreateDealNoteRequestSchema', () => {
193
+ it('accepts a valid body', () => {
194
+ expect(CreateDealNoteRequestSchema.safeParse({ body: 'Hello' }).success).toBe(true)
195
+ })
196
+
197
+ it('accepts a single character after trim', () => {
198
+ expect(CreateDealNoteRequestSchema.safeParse({ body: 'a' }).success).toBe(true)
199
+ })
200
+
201
+ it('trims whitespace and accepts content that remains non-empty', () => {
202
+ const result = CreateDealNoteRequestSchema.safeParse({ body: ' note ' })
203
+ expect(result.success).toBe(true)
204
+ if (result.success) expect(result.data.body).toBe('note')
205
+ })
206
+
207
+ it('rejects a whitespace-only string (empty after trim)', () => {
208
+ expect(CreateDealNoteRequestSchema.safeParse({ body: ' ' }).success).toBe(false)
209
+ })
210
+
211
+ it('rejects an empty string', () => {
212
+ expect(CreateDealNoteRequestSchema.safeParse({ body: '' }).success).toBe(false)
213
+ })
214
+
215
+ it('accepts a body at the max length boundary (10000 chars)', () => {
216
+ expect(CreateDealNoteRequestSchema.safeParse({ body: 'a'.repeat(10000) }).success).toBe(true)
217
+ })
218
+
219
+ it('rejects a body exceeding the max length (10001 chars)', () => {
220
+ expect(CreateDealNoteRequestSchema.safeParse({ body: 'a'.repeat(10001) }).success).toBe(false)
221
+ })
222
+
223
+ it('rejects unknown top-level fields (strict mode)', () => {
224
+ expect(CreateDealNoteRequestSchema.safeParse({ body: 'note', extra: 'x' }).success).toBe(false)
225
+ })
226
+ })
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // CreateDealTaskRequestSchema
230
+ // ---------------------------------------------------------------------------
231
+
232
+ describe('CreateDealTaskRequestSchema', () => {
233
+ const valid = { title: 'Follow up call' }
234
+
235
+ it('accepts a minimal valid payload (title only)', () => {
236
+ expect(CreateDealTaskRequestSchema.safeParse(valid).success).toBe(true)
237
+ })
238
+
239
+ it('kind is optional and accepts all valid values', () => {
240
+ for (const kind of ['call', 'email', 'meeting', 'other'] as const) {
241
+ expect(CreateDealTaskRequestSchema.safeParse({ ...valid, kind }).success).toBe(true)
242
+ }
243
+ })
244
+
245
+ it('rejects an invalid kind value', () => {
246
+ expect(CreateDealTaskRequestSchema.safeParse({ ...valid, kind: 'sms' }).success).toBe(false)
247
+ })
248
+
249
+ it('accepts a valid ISO datetime for dueAt', () => {
250
+ expect(CreateDealTaskRequestSchema.safeParse({ ...valid, dueAt: ISO_TS }).success).toBe(true)
251
+ })
252
+
253
+ it('rejects an invalid datetime for dueAt', () => {
254
+ expect(CreateDealTaskRequestSchema.safeParse({ ...valid, dueAt: 'tomorrow' }).success).toBe(false)
255
+ })
256
+
257
+ it('accepts null for dueAt', () => {
258
+ expect(CreateDealTaskRequestSchema.safeParse({ ...valid, dueAt: null }).success).toBe(true)
259
+ })
260
+
261
+ it('accepts a valid UUID for assigneeUserId', () => {
262
+ expect(CreateDealTaskRequestSchema.safeParse({ ...valid, assigneeUserId: VALID_UUID }).success).toBe(true)
263
+ })
264
+
265
+ it('rejects a non-UUID for assigneeUserId', () => {
266
+ expect(CreateDealTaskRequestSchema.safeParse({ ...valid, assigneeUserId: 'not-a-uuid' }).success).toBe(false)
267
+ })
268
+
269
+ it('accepts null for assigneeUserId', () => {
270
+ expect(CreateDealTaskRequestSchema.safeParse({ ...valid, assigneeUserId: null }).success).toBe(true)
271
+ })
272
+
273
+ it('rejects an empty title', () => {
274
+ expect(CreateDealTaskRequestSchema.safeParse({ title: '' }).success).toBe(false)
275
+ })
276
+
277
+ it('rejects a title exceeding 255 chars', () => {
278
+ expect(CreateDealTaskRequestSchema.safeParse({ title: 'a'.repeat(256) }).success).toBe(false)
279
+ })
280
+
281
+ it('rejects unknown top-level fields (strict mode)', () => {
282
+ expect(CreateDealTaskRequestSchema.safeParse({ ...valid, bogus: true }).success).toBe(false)
283
+ })
284
+ })
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // ListDealsQuerySchema
288
+ // ---------------------------------------------------------------------------
289
+
290
+ describe('ListDealsQuerySchema', () => {
291
+ it('accepts an empty query (all defaults applied)', () => {
292
+ const result = ListDealsQuerySchema.safeParse({})
293
+ expect(result.success).toBe(true)
294
+ if (result.success) {
295
+ expect(result.data.limit).toBe(50)
296
+ expect(result.data.offset).toBe(0)
297
+ }
298
+ })
299
+
300
+ it('coerces limit from string "50" to number 50', () => {
301
+ const result = ListDealsQuerySchema.safeParse({ limit: '50' })
302
+ expect(result.success).toBe(true)
303
+ if (result.success) expect(result.data.limit).toBe(50)
304
+ })
305
+
306
+ it('coerces offset from string "20" to number 20', () => {
307
+ const result = ListDealsQuerySchema.safeParse({ offset: '20' })
308
+ expect(result.success).toBe(true)
309
+ if (result.success) expect(result.data.offset).toBe(20)
310
+ })
311
+
312
+ it('rejects non-numeric string for limit', () => {
313
+ expect(ListDealsQuerySchema.safeParse({ limit: 'abc' }).success).toBe(false)
314
+ })
315
+
316
+ it('rejects non-numeric string for offset', () => {
317
+ expect(ListDealsQuerySchema.safeParse({ offset: 'abc' }).success).toBe(false)
318
+ })
319
+
320
+ it('rejects zero or negative limit (must be positive)', () => {
321
+ expect(ListDealsQuerySchema.safeParse({ limit: '0' }).success).toBe(false)
322
+ expect(ListDealsQuerySchema.safeParse({ limit: '-1' }).success).toBe(false)
323
+ })
324
+
325
+ it('rejects a negative offset', () => {
326
+ expect(ListDealsQuerySchema.safeParse({ offset: '-1' }).success).toBe(false)
327
+ })
328
+
329
+ it('accepts zero offset', () => {
330
+ expect(ListDealsQuerySchema.safeParse({ offset: '0' }).success).toBe(true)
331
+ })
332
+
333
+ it('accepts a valid stage filter with search and pagination', () => {
334
+ const result = ListDealsQuerySchema.safeParse({
335
+ stage: 'interested',
336
+ search: 'acme',
337
+ limit: '25',
338
+ offset: '10'
339
+ })
340
+ expect(result.success).toBe(true)
341
+ if (result.success) {
342
+ expect(result.data.stage).toBe('interested')
343
+ expect(result.data.search).toBe('acme')
344
+ expect(result.data.limit).toBe(25)
345
+ expect(result.data.offset).toBe(10)
346
+ }
347
+ })
348
+
349
+ it('rejects an invalid stage value', () => {
350
+ expect(ListDealsQuerySchema.safeParse({ stage: 'pipeline' }).success).toBe(false)
351
+ })
352
+
353
+ it('rejects unknown query fields (strict mode)', () => {
354
+ expect(ListDealsQuerySchema.safeParse({ unknownParam: 'x' }).success).toBe(false)
355
+ })
356
+ })
357
+
358
+ // ---------------------------------------------------------------------------
359
+ // UpdateContactRequestSchema
360
+ // ---------------------------------------------------------------------------
361
+
362
+ describe('UpdateContactRequestSchema', () => {
363
+ it('rejects an object with no fields provided', () => {
364
+ const result = UpdateContactRequestSchema.safeParse({})
365
+ expect(result.success).toBe(false)
366
+ })
367
+
368
+ it('accepts a single field: firstName', () => {
369
+ expect(UpdateContactRequestSchema.safeParse({ firstName: 'Alice' }).success).toBe(true)
370
+ })
371
+
372
+ it('accepts a single field: emailValid', () => {
373
+ for (const v of ['VALID', 'INVALID', 'RISKY', 'UNKNOWN'] as const) {
374
+ expect(UpdateContactRequestSchema.safeParse({ emailValid: v }).success).toBe(true)
375
+ }
376
+ })
377
+
378
+ it('rejects an invalid emailValid value', () => {
379
+ expect(UpdateContactRequestSchema.safeParse({ emailValid: 'maybe' }).success).toBe(false)
380
+ })
381
+
382
+ it('accepts a valid UUID for companyId', () => {
383
+ expect(UpdateContactRequestSchema.safeParse({ companyId: VALID_UUID }).success).toBe(true)
384
+ })
385
+
386
+ it('rejects a non-UUID for companyId', () => {
387
+ expect(UpdateContactRequestSchema.safeParse({ companyId: 'bad-id' }).success).toBe(false)
388
+ })
389
+
390
+ it('accepts a valid LinkedIn URL', () => {
391
+ expect(UpdateContactRequestSchema.safeParse({ linkedinUrl: 'https://linkedin.com/in/alice' }).success).toBe(true)
392
+ })
393
+
394
+ it('rejects a non-URL for linkedinUrl', () => {
395
+ expect(UpdateContactRequestSchema.safeParse({ linkedinUrl: 'not-a-url' }).success).toBe(false)
396
+ })
397
+
398
+ it('accepts multiple fields at once', () => {
399
+ expect(UpdateContactRequestSchema.safeParse({ firstName: 'Alice', lastName: 'Smith', title: 'CEO' }).success).toBe(
400
+ true
401
+ )
402
+ })
403
+
404
+ it('rejects unknown top-level fields (strict mode)', () => {
405
+ expect(UpdateContactRequestSchema.safeParse({ firstName: 'Alice', unknown: 'x' }).success).toBe(false)
406
+ })
407
+
408
+ it('rejects an empty string for firstName (min 1 after trim)', () => {
409
+ expect(UpdateContactRequestSchema.safeParse({ firstName: '' }).success).toBe(false)
410
+ })
411
+
412
+ it('rejects a firstName exceeding 255 chars', () => {
413
+ expect(UpdateContactRequestSchema.safeParse({ firstName: 'a'.repeat(256) }).success).toBe(false)
414
+ })
415
+ })
416
+
417
+ // ---------------------------------------------------------------------------
418
+ // Response schemas — smoke-level forward-compat checks
419
+ // ---------------------------------------------------------------------------
420
+
421
+ describe('DealDetailResponseSchema (forward-compat)', () => {
422
+ const baseDeal = {
423
+ id: VALID_UUID,
424
+ organization_id: VALID_UUID,
425
+ contact_id: null,
426
+ contact_email: 'test@example.com',
427
+ pipeline_key: 'default',
428
+ stage_key: null,
429
+ state_key: null,
430
+ activity_log: [],
431
+ discovery_data: null,
432
+ discovery_submitted_at: null,
433
+ discovery_submitted_by: null,
434
+ proposal_data: null,
435
+ proposal_sent_at: null,
436
+ proposal_pdf_url: null,
437
+ signature_envelope_id: null,
438
+ source_list_id: null,
439
+ source_type: null,
440
+ initial_fee: null,
441
+ monthly_fee: null,
442
+ closed_lost_at: null,
443
+ closed_lost_reason: null,
444
+ created_at: ISO_TS,
445
+ updated_at: ISO_TS,
446
+ contact: null
447
+ }
448
+
449
+ it('accepts a deal with null contact', () => {
450
+ expect(DealDetailResponseSchema.safeParse(baseDeal).success).toBe(true)
451
+ })
452
+
453
+ it('accepts a deal with nested contact that has null company', () => {
454
+ const withContact = {
455
+ ...baseDeal,
456
+ contact: {
457
+ id: VALID_UUID,
458
+ first_name: 'Alice',
459
+ last_name: 'Smith',
460
+ email: 'alice@example.com',
461
+ title: null,
462
+ headline: null,
463
+ linkedin_url: null,
464
+ pipeline_status: null,
465
+ enrichment_data: null,
466
+ company: null
467
+ }
468
+ }
469
+ expect(DealDetailResponseSchema.safeParse(withContact).success).toBe(true)
470
+ })
471
+
472
+ it('accepts extra unknown fields at top level (not strict)', () => {
473
+ expect(DealDetailResponseSchema.safeParse({ ...baseDeal, futureField: 'value' }).success).toBe(true)
474
+ })
475
+ })
476
+
477
+ describe('DealListResponseSchema', () => {
478
+ it('accepts a valid paginated list response', () => {
479
+ const result = DealListResponseSchema.safeParse({
480
+ data: [],
481
+ total: 0,
482
+ limit: 50,
483
+ offset: 0
484
+ })
485
+ expect(result.success).toBe(true)
486
+ })
487
+
488
+ it('rejects non-integer total', () => {
489
+ expect(DealListResponseSchema.safeParse({ data: [], total: 1.5, limit: 50, offset: 0 }).success).toBe(false)
490
+ })
491
+ })
492
+
493
+ describe('DealNoteResponseSchema', () => {
494
+ it('accepts a valid note response', () => {
495
+ const result = DealNoteResponseSchema.safeParse({
496
+ id: VALID_UUID,
497
+ dealId: VALID_UUID,
498
+ organizationId: VALID_UUID,
499
+ authorUserId: null,
500
+ body: 'A note',
501
+ createdAt: ISO_TS,
502
+ updatedAt: ISO_TS
503
+ })
504
+ expect(result.success).toBe(true)
505
+ })
506
+
507
+ it('accepts extra fields (not strict)', () => {
508
+ const result = DealNoteResponseSchema.safeParse({
509
+ id: VALID_UUID,
510
+ dealId: VALID_UUID,
511
+ organizationId: VALID_UUID,
512
+ authorUserId: null,
513
+ body: 'A note',
514
+ createdAt: ISO_TS,
515
+ updatedAt: ISO_TS,
516
+ futureField: 'ignored'
517
+ })
518
+ expect(result.success).toBe(true)
519
+ })
520
+ })
521
+
522
+ describe('DealTaskResponseSchema', () => {
523
+ it('accepts a valid task response', () => {
524
+ const result = DealTaskResponseSchema.safeParse({
525
+ id: VALID_UUID,
526
+ organizationId: VALID_UUID,
527
+ dealId: VALID_UUID,
528
+ title: 'Call prospect',
529
+ description: null,
530
+ kind: 'call',
531
+ dueAt: null,
532
+ assigneeUserId: null,
533
+ completedAt: null,
534
+ completedByUserId: null,
535
+ createdAt: ISO_TS,
536
+ updatedAt: ISO_TS,
537
+ createdByUserId: null
538
+ })
539
+ expect(result.success).toBe(true)
540
+ })
541
+
542
+ it('rejects an invalid kind in task response', () => {
543
+ const result = DealTaskResponseSchema.safeParse({
544
+ id: VALID_UUID,
545
+ organizationId: VALID_UUID,
546
+ dealId: VALID_UUID,
547
+ title: 'Call',
548
+ description: null,
549
+ kind: 'text_message',
550
+ dueAt: null,
551
+ assigneeUserId: null,
552
+ completedAt: null,
553
+ completedByUserId: null,
554
+ createdAt: ISO_TS,
555
+ updatedAt: ISO_TS,
556
+ createdByUserId: null
557
+ })
558
+ expect(result.success).toBe(false)
559
+ })
560
+ })
561
+
562
+ // ---------------------------------------------------------------------------
563
+ // ListStatusSchema
564
+ // ---------------------------------------------------------------------------
565
+
566
+ describe('ListStatusSchema', () => {
567
+ it.each(['draft', 'enriching', 'launched', 'closing', 'archived'])('accepts "%s"', (status) => {
568
+ expect(ListStatusSchema.safeParse(status).success).toBe(true)
569
+ })
570
+
571
+ it('rejects unknown status', () => {
572
+ expect(ListStatusSchema.safeParse('active').success).toBe(false)
573
+ })
574
+ })
575
+
576
+ // ---------------------------------------------------------------------------
577
+ // PipelineStageSchema
578
+ // ---------------------------------------------------------------------------
579
+
580
+ describe('PipelineStageSchema', () => {
581
+ it('accepts a key present in LEAD_GEN_STAGE_CATALOG', () => {
582
+ for (const key of Object.keys(LEAD_GEN_STAGE_CATALOG)) {
583
+ expect(PipelineStageSchema.safeParse({ key }).success).toBe(true)
584
+ }
585
+ })
586
+
587
+ it('rejects a key not in LEAD_GEN_STAGE_CATALOG', () => {
588
+ const result = PipelineStageSchema.safeParse({ key: 'not_a_real_stage' })
589
+ expect(result.success).toBe(false)
590
+ if (!result.success) {
591
+ expect(result.error.issues[0]?.message).toMatch(/LEAD_GEN_STAGE_CATALOG/)
592
+ }
593
+ })
594
+
595
+ it('accepts optional label, enabled, and order fields', () => {
596
+ expect(PipelineStageSchema.safeParse({ key: 'scraped', label: 'Scraped', enabled: true, order: 1 }).success).toBe(
597
+ true
598
+ )
599
+ })
600
+ })
601
+
602
+ // ---------------------------------------------------------------------------
603
+ // ScrapingConfigSchema
604
+ // ---------------------------------------------------------------------------
605
+
606
+ describe('ScrapingConfigSchema', () => {
607
+ it('accepts an empty object (all fields optional)', () => {
608
+ expect(ScrapingConfigSchema.safeParse({}).success).toBe(true)
609
+ })
610
+
611
+ it('accepts a fully populated config', () => {
612
+ expect(
613
+ ScrapingConfigSchema.safeParse({
614
+ vertical: 'SaaS',
615
+ geography: 'USA',
616
+ size: '10-50',
617
+ apifyInput: { actorId: 'test' }
618
+ }).success
619
+ ).toBe(true)
620
+ })
621
+
622
+ it('rejects a vertical exceeding 255 chars', () => {
623
+ expect(ScrapingConfigSchema.safeParse({ vertical: 'a'.repeat(256) }).success).toBe(false)
624
+ })
625
+ })
626
+
627
+ // ---------------------------------------------------------------------------
628
+ // IcpRubricSchema
629
+ // ---------------------------------------------------------------------------
630
+
631
+ describe('IcpRubricSchema', () => {
632
+ it('accepts an empty object', () => {
633
+ expect(IcpRubricSchema.safeParse({}).success).toBe(true)
634
+ })
635
+
636
+ it('accepts valid minRating boundary values 0 and 5', () => {
637
+ expect(IcpRubricSchema.safeParse({ minRating: 0 }).success).toBe(true)
638
+ expect(IcpRubricSchema.safeParse({ minRating: 5 }).success).toBe(true)
639
+ })
640
+
641
+ it('rejects minRating above 5', () => {
642
+ expect(IcpRubricSchema.safeParse({ minRating: 5.1 }).success).toBe(false)
643
+ })
644
+
645
+ it('rejects negative minRating', () => {
646
+ expect(IcpRubricSchema.safeParse({ minRating: -1 }).success).toBe(false)
647
+ })
648
+ })
649
+
650
+ // ---------------------------------------------------------------------------
651
+ // CreateListRequestSchema
652
+ // ---------------------------------------------------------------------------
653
+
654
+ describe('CreateListRequestSchema', () => {
655
+ it('accepts a minimal valid payload (name only)', () => {
656
+ expect(CreateListRequestSchema.safeParse({ name: 'My List' }).success).toBe(true)
657
+ })
658
+
659
+ it('rejects an empty name', () => {
660
+ expect(CreateListRequestSchema.safeParse({ name: '' }).success).toBe(false)
661
+ })
662
+
663
+ it('rejects a name exceeding 255 chars', () => {
664
+ expect(CreateListRequestSchema.safeParse({ name: 'a'.repeat(256) }).success).toBe(false)
665
+ })
666
+
667
+ it('accepts an optional status from ListStatusSchema', () => {
668
+ expect(CreateListRequestSchema.safeParse({ name: 'X', status: 'draft' }).success).toBe(true)
669
+ })
670
+
671
+ it('rejects an invalid status', () => {
672
+ expect(CreateListRequestSchema.safeParse({ name: 'X', status: 'active' }).success).toBe(false)
673
+ })
674
+
675
+ it('rejects unknown fields (strict mode)', () => {
676
+ expect(CreateListRequestSchema.safeParse({ name: 'X', bogus: true }).success).toBe(false)
677
+ })
678
+
679
+ it('accepts nested scrapingConfig, icp, and pipelineConfig', () => {
680
+ expect(
681
+ CreateListRequestSchema.safeParse({
682
+ name: 'SaaS List',
683
+ scrapingConfig: { vertical: 'SaaS' },
684
+ icp: { minReviewCount: 5 },
685
+ pipelineConfig: { stages: [] }
686
+ }).success
687
+ ).toBe(true)
688
+ })
689
+ })
690
+
691
+ // ---------------------------------------------------------------------------
692
+ // UpdateListRequestSchema
693
+ // ---------------------------------------------------------------------------
694
+
695
+ describe('UpdateListRequestSchema', () => {
696
+ it('rejects an object with none of the required fields', () => {
697
+ const result = UpdateListRequestSchema.safeParse({})
698
+ expect(result.success).toBe(false)
699
+ })
700
+
701
+ it('accepts providing only name', () => {
702
+ expect(UpdateListRequestSchema.safeParse({ name: 'New Name' }).success).toBe(true)
703
+ })
704
+
705
+ it('accepts providing only description', () => {
706
+ expect(UpdateListRequestSchema.safeParse({ description: 'Desc' }).success).toBe(true)
707
+ })
708
+
709
+ it('accepts providing only batchIds', () => {
710
+ expect(UpdateListRequestSchema.safeParse({ batchIds: ['batch-1'] }).success).toBe(true)
711
+ })
712
+
713
+ it('rejects unknown fields (strict mode)', () => {
714
+ expect(UpdateListRequestSchema.safeParse({ name: 'X', extra: 'bad' }).success).toBe(false)
715
+ })
716
+ })
717
+
718
+ // ---------------------------------------------------------------------------
719
+ // UpdateListStatusRequestSchema
720
+ // ---------------------------------------------------------------------------
721
+
722
+ describe('UpdateListStatusRequestSchema', () => {
723
+ it('accepts a valid status', () => {
724
+ expect(UpdateListStatusRequestSchema.safeParse({ status: 'launched' }).success).toBe(true)
725
+ })
726
+
727
+ it('rejects an invalid status', () => {
728
+ expect(UpdateListStatusRequestSchema.safeParse({ status: 'active' }).success).toBe(false)
729
+ })
730
+
731
+ it('rejects missing status field', () => {
732
+ expect(UpdateListStatusRequestSchema.safeParse({}).success).toBe(false)
733
+ })
734
+
735
+ it('rejects unknown fields (strict mode)', () => {
736
+ expect(UpdateListStatusRequestSchema.safeParse({ status: 'draft', extra: 'x' }).success).toBe(false)
737
+ })
738
+ })
739
+
740
+ // ---------------------------------------------------------------------------
741
+ // UpdateListConfigRequestSchema
742
+ // ---------------------------------------------------------------------------
743
+
744
+ describe('UpdateListConfigRequestSchema', () => {
745
+ it('rejects an object with none of the config fields', () => {
746
+ expect(UpdateListConfigRequestSchema.safeParse({}).success).toBe(false)
747
+ })
748
+
749
+ it('accepts providing only scrapingConfig', () => {
750
+ expect(UpdateListConfigRequestSchema.safeParse({ scrapingConfig: { vertical: 'SaaS' } }).success).toBe(true)
751
+ })
752
+
753
+ it('accepts providing only icp', () => {
754
+ expect(UpdateListConfigRequestSchema.safeParse({ icp: { minReviewCount: 3 } }).success).toBe(true)
755
+ })
756
+
757
+ it('accepts providing only pipelineConfig', () => {
758
+ expect(UpdateListConfigRequestSchema.safeParse({ pipelineConfig: { stages: [] } }).success).toBe(true)
759
+ })
760
+
761
+ it('rejects unknown fields (strict mode)', () => {
762
+ expect(UpdateListConfigRequestSchema.safeParse({ scrapingConfig: {}, bogus: true }).success).toBe(false)
763
+ })
764
+ })
765
+
766
+ // ---------------------------------------------------------------------------
767
+ // AddCompaniesToListRequestSchema / AddContactsToListRequestSchema
768
+ // ---------------------------------------------------------------------------
769
+
770
+ describe('AddCompaniesToListRequestSchema', () => {
771
+ it('accepts a valid list with one UUID', () => {
772
+ expect(AddCompaniesToListRequestSchema.safeParse({ companyIds: [VALID_UUID] }).success).toBe(true)
773
+ })
774
+
775
+ it('rejects an empty array (min 1)', () => {
776
+ expect(AddCompaniesToListRequestSchema.safeParse({ companyIds: [] }).success).toBe(false)
777
+ })
778
+
779
+ it('rejects more than 1000 IDs (max 1000)', () => {
780
+ const ids = Array.from({ length: 1001 }, (_, i) => `00000000-0000-0000-0000-${String(i).padStart(12, '0')}`)
781
+ expect(AddCompaniesToListRequestSchema.safeParse({ companyIds: ids }).success).toBe(false)
782
+ })
783
+
784
+ it('rejects non-UUID entries', () => {
785
+ expect(AddCompaniesToListRequestSchema.safeParse({ companyIds: ['not-a-uuid'] }).success).toBe(false)
786
+ })
787
+ })
788
+
789
+ describe('AddContactsToListRequestSchema', () => {
790
+ it('accepts a valid list with one UUID', () => {
791
+ expect(AddContactsToListRequestSchema.safeParse({ contactIds: [VALID_UUID] }).success).toBe(true)
792
+ })
793
+
794
+ it('rejects an empty array (min 1)', () => {
795
+ expect(AddContactsToListRequestSchema.safeParse({ contactIds: [] }).success).toBe(false)
796
+ })
797
+ })
798
+
799
+ // ---------------------------------------------------------------------------
800
+ // ListCompaniesQuerySchema
801
+ // ---------------------------------------------------------------------------
802
+
803
+ describe('ListCompaniesQuerySchema', () => {
804
+ it('accepts an empty query (defaults applied)', () => {
805
+ const result = ListCompaniesQuerySchema.safeParse({})
806
+ expect(result.success).toBe(true)
807
+ if (result.success) {
808
+ expect(result.data.limit).toBe(50)
809
+ expect(result.data.offset).toBe(0)
810
+ }
811
+ })
812
+
813
+ it('coerces limit from string "100" to number 100', () => {
814
+ const result = ListCompaniesQuerySchema.safeParse({ limit: '100' })
815
+ expect(result.success).toBe(true)
816
+ if (result.success) expect(result.data.limit).toBe(100)
817
+ })
818
+
819
+ it('rejects limit exceeding 5000', () => {
820
+ expect(ListCompaniesQuerySchema.safeParse({ limit: '5001' }).success).toBe(false)
821
+ })
822
+
823
+ it('rejects limit less than 1', () => {
824
+ expect(ListCompaniesQuerySchema.safeParse({ limit: '0' }).success).toBe(false)
825
+ })
826
+
827
+ it('accepts a valid status filter', () => {
828
+ expect(ListCompaniesQuerySchema.safeParse({ status: 'active' }).success).toBe(true)
829
+ expect(ListCompaniesQuerySchema.safeParse({ status: 'invalid' }).success).toBe(true)
830
+ })
831
+
832
+ it('rejects an invalid status', () => {
833
+ expect(ListCompaniesQuerySchema.safeParse({ status: 'pending' }).success).toBe(false)
834
+ })
835
+
836
+ it('accepts includeAll as boolean-like strings', () => {
837
+ const r1 = ListCompaniesQuerySchema.safeParse({ includeAll: 'true' })
838
+ expect(r1.success).toBe(true)
839
+ if (r1.success) expect(r1.data.includeAll).toBe(true)
840
+
841
+ const r2 = ListCompaniesQuerySchema.safeParse({ includeAll: 'false' })
842
+ expect(r2.success).toBe(true)
843
+ if (r2.success) expect(r2.data.includeAll).toBe(false)
844
+ })
845
+
846
+ it('rejects unknown fields (strict mode)', () => {
847
+ expect(ListCompaniesQuerySchema.safeParse({ unknownField: 'x' }).success).toBe(false)
848
+ })
849
+ })
850
+
851
+ // ---------------------------------------------------------------------------
852
+ // ListContactsQuerySchema
853
+ // ---------------------------------------------------------------------------
854
+
855
+ describe('ListContactsQuerySchema', () => {
856
+ it('accepts an empty query with defaults', () => {
857
+ const result = ListContactsQuerySchema.safeParse({})
858
+ expect(result.success).toBe(true)
859
+ if (result.success) {
860
+ expect(result.data.limit).toBe(5000)
861
+ expect(result.data.offset).toBe(0)
862
+ }
863
+ })
864
+
865
+ it('accepts openingLineIsNull as "true"', () => {
866
+ const result = ListContactsQuerySchema.safeParse({ openingLineIsNull: 'true' })
867
+ expect(result.success).toBe(true)
868
+ if (result.success) expect(result.data.openingLineIsNull).toBe(true)
869
+ })
870
+ })
871
+
872
+ // ---------------------------------------------------------------------------
873
+ // ListDealTasksDueQuerySchema
874
+ // ---------------------------------------------------------------------------
875
+
876
+ describe('ListDealTasksDueQuerySchema', () => {
877
+ it('accepts an empty query', () => {
878
+ expect(ListDealTasksDueQuerySchema.safeParse({}).success).toBe(true)
879
+ })
880
+
881
+ it.each(['overdue', 'today', 'today_and_overdue', 'upcoming'])('accepts window "%s"', (window) => {
882
+ expect(ListDealTasksDueQuerySchema.safeParse({ window }).success).toBe(true)
883
+ })
884
+
885
+ it('rejects an invalid window value', () => {
886
+ expect(ListDealTasksDueQuerySchema.safeParse({ window: 'this_week' }).success).toBe(false)
887
+ })
888
+
889
+ it('accepts a valid UUID for assigneeUserId', () => {
890
+ expect(ListDealTasksDueQuerySchema.safeParse({ assigneeUserId: VALID_UUID }).success).toBe(true)
891
+ })
892
+
893
+ it('rejects unknown fields (strict mode)', () => {
894
+ expect(ListDealTasksDueQuerySchema.safeParse({ window: 'today', extra: 'x' }).success).toBe(false)
895
+ })
896
+ })
897
+
898
+ // ---------------------------------------------------------------------------
899
+ // CreateCompanyRequestSchema
900
+ // ---------------------------------------------------------------------------
901
+
902
+ describe('CreateCompanyRequestSchema', () => {
903
+ it('accepts a minimal payload (name only)', () => {
904
+ expect(CreateCompanyRequestSchema.safeParse({ name: 'Acme' }).success).toBe(true)
905
+ })
906
+
907
+ it('rejects an empty name', () => {
908
+ expect(CreateCompanyRequestSchema.safeParse({ name: '' }).success).toBe(false)
909
+ })
910
+
911
+ it('rejects a non-URL for linkedinUrl', () => {
912
+ expect(CreateCompanyRequestSchema.safeParse({ name: 'Acme', linkedinUrl: 'linkedin.com/co/acme' }).success).toBe(
913
+ false
914
+ )
915
+ })
916
+
917
+ it('accepts a valid URL for linkedinUrl', () => {
918
+ expect(
919
+ CreateCompanyRequestSchema.safeParse({ name: 'Acme', linkedinUrl: 'https://linkedin.com/company/acme' }).success
920
+ ).toBe(true)
921
+ })
922
+
923
+ it('rejects unknown fields (strict mode)', () => {
924
+ expect(CreateCompanyRequestSchema.safeParse({ name: 'Acme', bogus: true }).success).toBe(false)
925
+ })
926
+ })
927
+
928
+ // ---------------------------------------------------------------------------
929
+ // UpdateCompanyRequestSchema
930
+ // ---------------------------------------------------------------------------
931
+
932
+ describe('UpdateCompanyRequestSchema', () => {
933
+ it('rejects an object with no fields (at-least-one refine)', () => {
934
+ expect(UpdateCompanyRequestSchema.safeParse({}).success).toBe(false)
935
+ })
936
+
937
+ it('accepts providing only name', () => {
938
+ expect(UpdateCompanyRequestSchema.safeParse({ name: 'Acme Corp' }).success).toBe(true)
939
+ })
940
+
941
+ it('accepts providing only status', () => {
942
+ expect(UpdateCompanyRequestSchema.safeParse({ status: 'active' }).success).toBe(true)
943
+ expect(UpdateCompanyRequestSchema.safeParse({ status: 'invalid' }).success).toBe(true)
944
+ })
945
+
946
+ it('accepts numEmployees of 0', () => {
947
+ expect(UpdateCompanyRequestSchema.safeParse({ numEmployees: 0 }).success).toBe(true)
948
+ })
949
+
950
+ it('rejects negative numEmployees', () => {
951
+ expect(UpdateCompanyRequestSchema.safeParse({ numEmployees: -1 }).success).toBe(false)
952
+ })
953
+
954
+ it('rejects unknown fields (strict mode)', () => {
955
+ expect(UpdateCompanyRequestSchema.safeParse({ name: 'X', extra: true }).success).toBe(false)
956
+ })
957
+ })
958
+
959
+ // ---------------------------------------------------------------------------
960
+ // CreateContactRequestSchema
961
+ // ---------------------------------------------------------------------------
962
+
963
+ describe('CreateContactRequestSchema', () => {
964
+ it('accepts a minimal payload (email only)', () => {
965
+ expect(CreateContactRequestSchema.safeParse({ email: 'test@example.com' }).success).toBe(true)
966
+ })
967
+
968
+ it('rejects an invalid email', () => {
969
+ expect(CreateContactRequestSchema.safeParse({ email: 'not-an-email' }).success).toBe(false)
970
+ })
971
+
972
+ it('rejects an empty email', () => {
973
+ expect(CreateContactRequestSchema.safeParse({ email: '' }).success).toBe(false)
974
+ })
975
+
976
+ it('accepts an optional companyId as UUID', () => {
977
+ expect(CreateContactRequestSchema.safeParse({ email: 'a@b.com', companyId: VALID_UUID }).success).toBe(true)
978
+ })
979
+
980
+ it('rejects unknown fields (strict mode)', () => {
981
+ expect(CreateContactRequestSchema.safeParse({ email: 'a@b.com', unknown: 'x' }).success).toBe(false)
982
+ })
983
+ })
984
+
985
+ // ---------------------------------------------------------------------------
986
+ // AcqEmailValidSchema / AcqContactStatusSchema / AcqCompanyStatusSchema
987
+ // ---------------------------------------------------------------------------
988
+
989
+ describe('AcqEmailValidSchema', () => {
990
+ it.each(['VALID', 'INVALID', 'RISKY', 'UNKNOWN'])('accepts "%s"', (v) => {
991
+ expect(AcqEmailValidSchema.safeParse(v).success).toBe(true)
992
+ })
993
+
994
+ it('rejects lowercase or unknown value', () => {
995
+ expect(AcqEmailValidSchema.safeParse('valid').success).toBe(false)
996
+ expect(AcqEmailValidSchema.safeParse('pending').success).toBe(false)
997
+ })
998
+ })
999
+
1000
+ describe('AcqContactStatusSchema', () => {
1001
+ it.each(['active', 'invalid'])('accepts "%s"', (s) => {
1002
+ expect(AcqContactStatusSchema.safeParse(s).success).toBe(true)
1003
+ })
1004
+
1005
+ it('rejects unknown status', () => {
1006
+ expect(AcqContactStatusSchema.safeParse('deleted').success).toBe(false)
1007
+ })
1008
+ })
1009
+
1010
+ // ---------------------------------------------------------------------------
1011
+ // AcqArtifactOwnerKindSchema
1012
+ // ---------------------------------------------------------------------------
1013
+
1014
+ describe('AcqArtifactOwnerKindSchema', () => {
1015
+ it.each(['company', 'contact', 'deal', 'list', 'list_member'])('accepts "%s"', (kind) => {
1016
+ expect(AcqArtifactOwnerKindSchema.safeParse(kind).success).toBe(true)
1017
+ })
1018
+
1019
+ it('rejects unknown owner kind', () => {
1020
+ expect(AcqArtifactOwnerKindSchema.safeParse('organization').success).toBe(false)
1021
+ })
1022
+ })
1023
+
1024
+ // ---------------------------------------------------------------------------
1025
+ // ListArtifactsQuerySchema
1026
+ // ---------------------------------------------------------------------------
1027
+
1028
+ describe('ListArtifactsQuerySchema', () => {
1029
+ it('accepts a valid query', () => {
1030
+ expect(ListArtifactsQuerySchema.safeParse({ ownerKind: 'deal', ownerId: VALID_UUID }).success).toBe(true)
1031
+ })
1032
+
1033
+ it('rejects an invalid ownerKind', () => {
1034
+ expect(ListArtifactsQuerySchema.safeParse({ ownerKind: 'org', ownerId: VALID_UUID }).success).toBe(false)
1035
+ })
1036
+
1037
+ it('rejects a non-UUID ownerId', () => {
1038
+ expect(ListArtifactsQuerySchema.safeParse({ ownerKind: 'deal', ownerId: 'not-a-uuid' }).success).toBe(false)
1039
+ })
1040
+
1041
+ it('rejects unknown fields (strict mode)', () => {
1042
+ expect(ListArtifactsQuerySchema.safeParse({ ownerKind: 'deal', ownerId: VALID_UUID, extra: 'x' }).success).toBe(
1043
+ false
1044
+ )
1045
+ })
1046
+ })
1047
+
1048
+ // ---------------------------------------------------------------------------
1049
+ // CreateArtifactRequestSchema
1050
+ // ---------------------------------------------------------------------------
1051
+
1052
+ describe('CreateArtifactRequestSchema', () => {
1053
+ const valid = {
1054
+ ownerKind: 'deal' as const,
1055
+ ownerId: VALID_UUID,
1056
+ kind: 'proposal',
1057
+ content: { url: 'https://example.com' }
1058
+ }
1059
+
1060
+ it('accepts a minimal valid payload', () => {
1061
+ expect(CreateArtifactRequestSchema.safeParse(valid).success).toBe(true)
1062
+ })
1063
+
1064
+ it('accepts an optional sourceExecutionId', () => {
1065
+ expect(CreateArtifactRequestSchema.safeParse({ ...valid, sourceExecutionId: VALID_UUID }).success).toBe(true)
1066
+ })
1067
+
1068
+ it('rejects a non-UUID sourceExecutionId', () => {
1069
+ expect(CreateArtifactRequestSchema.safeParse({ ...valid, sourceExecutionId: 'not-a-uuid' }).success).toBe(false)
1070
+ })
1071
+
1072
+ it('rejects an empty kind', () => {
1073
+ expect(CreateArtifactRequestSchema.safeParse({ ...valid, kind: '' }).success).toBe(false)
1074
+ })
1075
+
1076
+ it('rejects unknown fields (strict mode)', () => {
1077
+ expect(CreateArtifactRequestSchema.safeParse({ ...valid, bogus: true }).success).toBe(false)
1078
+ })
1079
+ })
1080
+
1081
+ // ---------------------------------------------------------------------------
1082
+ // ListMembersQuerySchema
1083
+ // ---------------------------------------------------------------------------
1084
+
1085
+ describe('ListMembersQuerySchema', () => {
1086
+ it('accepts an empty query with defaults', () => {
1087
+ const result = ListMembersQuerySchema.safeParse({})
1088
+ expect(result.success).toBe(true)
1089
+ if (result.success) {
1090
+ expect(result.data.limit).toBe(50)
1091
+ expect(result.data.offset).toBe(0)
1092
+ }
1093
+ })
1094
+
1095
+ it('rejects limit exceeding 500', () => {
1096
+ expect(ListMembersQuerySchema.safeParse({ limit: '501' }).success).toBe(false)
1097
+ })
1098
+
1099
+ it('rejects limit less than 1', () => {
1100
+ expect(ListMembersQuerySchema.safeParse({ limit: '0' }).success).toBe(false)
1101
+ })
1102
+
1103
+ it('rejects unknown fields (strict mode)', () => {
1104
+ expect(ListMembersQuerySchema.safeParse({ extra: 'x' }).success).toBe(false)
1105
+ })
1106
+ })
1107
+
1108
+ // ---------------------------------------------------------------------------
1109
+ // AcqListResponseSchema (forward-compat)
1110
+ // ---------------------------------------------------------------------------
1111
+
1112
+ describe('AcqListResponseSchema (forward-compat)', () => {
1113
+ const baseList = {
1114
+ id: VALID_UUID,
1115
+ organizationId: VALID_UUID,
1116
+ name: 'Test List',
1117
+ description: null,
1118
+ type: 'standard',
1119
+ batchIds: [],
1120
+ instantlyCampaignId: null,
1121
+ status: 'draft' as const,
1122
+ metadata: {},
1123
+ launchedAt: null,
1124
+ completedAt: null,
1125
+ createdAt: ISO_TS,
1126
+ scrapingConfig: {},
1127
+ icp: {},
1128
+ pipelineConfig: {}
1129
+ }
1130
+
1131
+ it('accepts a valid list response', () => {
1132
+ expect(AcqListResponseSchema.safeParse(baseList).success).toBe(true)
1133
+ })
1134
+
1135
+ it('accepts extra fields at top level (not strict)', () => {
1136
+ expect(AcqListResponseSchema.safeParse({ ...baseList, futureField: 'extra' }).success).toBe(true)
1137
+ })
1138
+ })
1139
+
1140
+ // ---------------------------------------------------------------------------
1141
+ // AcqContactResponseSchema (forward-compat)
1142
+ // ---------------------------------------------------------------------------
1143
+
1144
+ describe('AcqContactResponseSchema (forward-compat)', () => {
1145
+ const baseContact = {
1146
+ id: VALID_UUID,
1147
+ organizationId: VALID_UUID,
1148
+ companyId: null,
1149
+ email: 'alice@example.com',
1150
+ emailValid: null,
1151
+ firstName: null,
1152
+ lastName: null,
1153
+ linkedinUrl: null,
1154
+ title: null,
1155
+ headline: null,
1156
+ filterReason: null,
1157
+ openingLine: null,
1158
+ source: null,
1159
+ sourceId: null,
1160
+ pipelineStatus: null,
1161
+ enrichmentData: null,
1162
+ attioPersonId: null,
1163
+ batchId: null,
1164
+ status: 'active' as const,
1165
+ createdAt: ISO_TS,
1166
+ updatedAt: ISO_TS
1167
+ }
1168
+
1169
+ it('accepts a valid contact response', () => {
1170
+ expect(AcqContactResponseSchema.safeParse(baseContact).success).toBe(true)
1171
+ })
1172
+
1173
+ it('accepts extra fields (not strict)', () => {
1174
+ expect(AcqContactResponseSchema.safeParse({ ...baseContact, newField: 'x' }).success).toBe(true)
1175
+ })
1176
+
1177
+ it('rejects an invalid emailValid value', () => {
1178
+ expect(AcqContactResponseSchema.safeParse({ ...baseContact, emailValid: 'BAD' }).success).toBe(false)
1179
+ })
1180
+ })