@coldiq/mcp 0.3.12 → 0.3.13

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 (115) hide show
  1. package/dist/tools/enrich-company.d.ts +3 -9
  2. package/dist/tools/enrich-company.d.ts.map +1 -1
  3. package/dist/tools/enrich-company.js +3 -16
  4. package/dist/tools/enrich-company.js.map +1 -1
  5. package/dist/tools/enrich-person.d.ts +3 -9
  6. package/dist/tools/enrich-person.d.ts.map +1 -1
  7. package/dist/tools/enrich-person.js +3 -12
  8. package/dist/tools/enrich-person.js.map +1 -1
  9. package/dist/tools/fetch-page-content.d.ts +3 -9
  10. package/dist/tools/fetch-page-content.d.ts.map +1 -1
  11. package/dist/tools/fetch-page-content.js +3 -12
  12. package/dist/tools/fetch-page-content.js.map +1 -1
  13. package/dist/tools/find-email.d.ts +3 -9
  14. package/dist/tools/find-email.d.ts.map +1 -1
  15. package/dist/tools/find-email.js +3 -40
  16. package/dist/tools/find-email.js.map +1 -1
  17. package/dist/tools/find-emails.d.ts +3 -4
  18. package/dist/tools/find-emails.d.ts.map +1 -1
  19. package/dist/tools/find-emails.js +8 -423
  20. package/dist/tools/find-emails.js.map +1 -1
  21. package/dist/tools/find-influencers.d.ts +3 -9
  22. package/dist/tools/find-influencers.d.ts.map +1 -1
  23. package/dist/tools/find-influencers.js +3 -12
  24. package/dist/tools/find-influencers.js.map +1 -1
  25. package/dist/tools/find-people.d.ts +3 -8
  26. package/dist/tools/find-people.d.ts.map +1 -1
  27. package/dist/tools/find-people.js +3 -98
  28. package/dist/tools/find-people.js.map +1 -1
  29. package/dist/tools/find-phone.d.ts +3 -9
  30. package/dist/tools/find-phone.d.ts.map +1 -1
  31. package/dist/tools/find-phone.js +4 -41
  32. package/dist/tools/find-phone.js.map +1 -1
  33. package/dist/tools/find-signals.d.ts +3 -9
  34. package/dist/tools/find-signals.d.ts.map +1 -1
  35. package/dist/tools/find-signals.js +3 -83
  36. package/dist/tools/find-signals.js.map +1 -1
  37. package/dist/tools/get-place-reviews.d.ts +3 -9
  38. package/dist/tools/get-place-reviews.d.ts.map +1 -1
  39. package/dist/tools/get-place-reviews.js +3 -12
  40. package/dist/tools/get-place-reviews.js.map +1 -1
  41. package/dist/tools/search-ads.d.ts +3 -9
  42. package/dist/tools/search-ads.d.ts.map +1 -1
  43. package/dist/tools/search-ads.js +3 -12
  44. package/dist/tools/search-ads.js.map +1 -1
  45. package/dist/tools/search-companies.d.ts +3 -9
  46. package/dist/tools/search-companies.d.ts.map +1 -1
  47. package/dist/tools/search-companies.js +3 -24
  48. package/dist/tools/search-companies.js.map +1 -1
  49. package/dist/tools/search-jobs.d.ts +3 -9
  50. package/dist/tools/search-jobs.d.ts.map +1 -1
  51. package/dist/tools/search-jobs.js +3 -27
  52. package/dist/tools/search-jobs.js.map +1 -1
  53. package/dist/tools/search-places.d.ts +3 -15
  54. package/dist/tools/search-places.d.ts.map +1 -1
  55. package/dist/tools/search-places.js +5 -76
  56. package/dist/tools/search-places.js.map +1 -1
  57. package/dist/tools/search-reddit.d.ts +3 -9
  58. package/dist/tools/search-reddit.d.ts.map +1 -1
  59. package/dist/tools/search-reddit.js +3 -12
  60. package/dist/tools/search-reddit.js.map +1 -1
  61. package/dist/tools/search-seo.d.ts +3 -9
  62. package/dist/tools/search-seo.d.ts.map +1 -1
  63. package/dist/tools/search-seo.js +3 -12
  64. package/dist/tools/search-seo.js.map +1 -1
  65. package/dist/tools/search-web.d.ts +3 -9
  66. package/dist/tools/search-web.d.ts.map +1 -1
  67. package/dist/tools/search-web.js +3 -12
  68. package/dist/tools/search-web.js.map +1 -1
  69. package/dist/tools/verify-email.d.ts +3 -10
  70. package/dist/tools/verify-email.d.ts.map +1 -1
  71. package/dist/tools/verify-email.js +3 -68
  72. package/dist/tools/verify-email.js.map +1 -1
  73. package/dist/verb-client.d.ts +35 -0
  74. package/dist/verb-client.d.ts.map +1 -0
  75. package/dist/verb-client.js +57 -0
  76. package/dist/verb-client.js.map +1 -0
  77. package/package.json +1 -1
  78. package/src/tools/enrich-company.ts +3 -16
  79. package/src/tools/enrich-person.ts +3 -12
  80. package/src/tools/fetch-page-content.ts +3 -12
  81. package/src/tools/find-email.ts +3 -42
  82. package/src/tools/find-emails.ts +8 -506
  83. package/src/tools/find-influencers.ts +3 -12
  84. package/src/tools/find-people.ts +3 -107
  85. package/src/tools/find-phone.ts +4 -42
  86. package/src/tools/find-signals.ts +3 -88
  87. package/src/tools/get-place-reviews.ts +3 -12
  88. package/src/tools/search-ads.ts +3 -12
  89. package/src/tools/search-companies.ts +3 -23
  90. package/src/tools/search-jobs.ts +3 -30
  91. package/src/tools/search-places.ts +5 -72
  92. package/src/tools/search-reddit.ts +3 -12
  93. package/src/tools/search-seo.ts +3 -12
  94. package/src/tools/search-web.ts +3 -12
  95. package/src/tools/verify-email.ts +3 -76
  96. package/src/verb-client.ts +93 -0
  97. package/tests/verb-client.test.ts +152 -0
  98. package/tests/tools/enrich-company.test.ts +0 -92
  99. package/tests/tools/enrich-person.test.ts +0 -140
  100. package/tests/tools/fetch-page-content.test.ts +0 -108
  101. package/tests/tools/find-email.test.ts +0 -137
  102. package/tests/tools/find-emails.test.ts +0 -818
  103. package/tests/tools/find-influencers.test.ts +0 -91
  104. package/tests/tools/find-people.test.ts +0 -621
  105. package/tests/tools/find-phone.test.ts +0 -100
  106. package/tests/tools/find-signals.test.ts +0 -175
  107. package/tests/tools/get-place-reviews.test.ts +0 -73
  108. package/tests/tools/search-ads.test.ts +0 -182
  109. package/tests/tools/search-companies.test.ts +0 -192
  110. package/tests/tools/search-jobs.test.ts +0 -210
  111. package/tests/tools/search-places.test.ts +0 -210
  112. package/tests/tools/search-reddit.test.ts +0 -194
  113. package/tests/tools/search-seo.test.ts +0 -183
  114. package/tests/tools/search-web.test.ts +0 -79
  115. package/tests/tools/verify-email.test.ts +0 -142
@@ -1,621 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { initClient } from '../../src/client.js'
3
- import { findPeopleHandler } from '../../src/tools/find-people.js'
4
-
5
- describe('find_people handler', () => {
6
- const originalFetch = globalThis.fetch
7
-
8
- beforeEach(() => {
9
- initClient('http://test-api.local', 'test-key')
10
- })
11
-
12
- afterEach(() => {
13
- globalThis.fetch = originalFetch
14
- })
15
-
16
- it('falls back to Apollo when LeadsFactory async times out', async () => {
17
- let callCount = 0
18
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
19
- callCount++
20
- const urlStr = url.toString()
21
-
22
- // LeadsFactory create search — returns an ID
23
- if (urlStr.includes('/leadsfactory/contact-finder/searches') && callCount === 1) {
24
- return new Response(JSON.stringify({ id: 'abc123', search_id: 'abc', nb_jobs_total: 1, progress_percentage: 0 }), { status: 201 })
25
- }
26
-
27
- // LeadsFactory poll — always RUNNING (will timeout)
28
- if (urlStr.includes('/leadsfactory/contact-finder/searches/abc123')) {
29
- return new Response(JSON.stringify({ id: 'abc123', search_id: 'abc', status: 'RUNNING', nb_jobs_total: 1, progress_percentage: 50 }), { status: 200 })
30
- }
31
-
32
- // Apollo — succeeds
33
- if (urlStr.includes('/apollo/people/search')) {
34
- return new Response(JSON.stringify({ people: [{ name: 'Michel Lieben', title: 'CEO', organization: { website_url: 'coldiq.com' } }] }), { status: 200 })
35
- }
36
-
37
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
38
- }) as typeof fetch
39
-
40
- // Override timeout to 100ms so test runs fast
41
- const { getProviders } = await import('../../src/registry.js')
42
- const providers = getProviders('find_people')
43
- const lf = providers.find((p) => p.id === 'leadsfactory')!
44
- const originalTimeout = lf.async!.timeoutMs
45
- const originalInterval = lf.async!.pollIntervalMs
46
-
47
- try {
48
- lf.async!.timeoutMs = 100
49
- lf.async!.pollIntervalMs = 20
50
- const result = await findPeopleHandler({
51
- company_domains: ['coldiq.com'],
52
- job_titles: ['CEO'],
53
- limit: 5,
54
- })
55
-
56
- const parsed = JSON.parse(result.content[0].text)
57
- // Should fall back to Apollo since LeadsFactory times out
58
- expect(parsed._meta.provider).toBe('apollo')
59
- } finally {
60
- lf.async!.timeoutMs = originalTimeout
61
- lf.async!.pollIntervalMs = originalInterval
62
- }
63
- })
64
-
65
- it('returns LeadsFactory results when poll completes with companies_personas', async () => {
66
- let callCount = 0
67
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
68
- callCount++
69
- const urlStr = url.toString()
70
-
71
- // LeadsFactory create search
72
- if (urlStr.includes('/leadsfactory/contact-finder/searches') && !urlStr.includes('abc123')) {
73
- return new Response(JSON.stringify({ id: 'abc123', search_id: 'abc', nb_jobs_total: 1, progress_percentage: 0 }), { status: 201 })
74
- }
75
-
76
- // LeadsFactory poll — first RUNNING, then SUCCESSFUL with contacts
77
- if (urlStr.includes('/leadsfactory/contact-finder/searches/abc123')) {
78
- if (callCount <= 3) {
79
- return new Response(JSON.stringify({ id: 'abc123', search_id: 'abc', status: 'RUNNING', nb_jobs_total: 1, progress_percentage: 50 }), { status: 200 })
80
- }
81
- return new Response(JSON.stringify({
82
- id: 'abc123',
83
- search_id: 'abc',
84
- status: 'SUCCESSFUL',
85
- nb_jobs_total: 1,
86
- nb_jobs_complete: 1,
87
- companies_personas: [
88
- { company: 'ColdIQ', personas: [[{ contact: { first_name: 'Michel', last_name: 'Lieben', title: 'CEO' } }]] },
89
- ],
90
- }), { status: 200 })
91
- }
92
-
93
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
94
- }) as typeof fetch
95
-
96
- const { getProviders } = await import('../../src/registry.js')
97
- const providers = getProviders('find_people')
98
- const lf = providers.find((p) => p.id === 'leadsfactory')!
99
- const originalTimeout = lf.async!.timeoutMs
100
- const originalInterval = lf.async!.pollIntervalMs
101
-
102
- try {
103
- lf.async!.timeoutMs = 500
104
- lf.async!.pollIntervalMs = 20
105
- const result = await findPeopleHandler({
106
- company_domains: ['coldiq.com'],
107
- job_titles: ['CEO'],
108
- limit: 5,
109
- })
110
-
111
- const parsed = JSON.parse(result.content[0].text)
112
- expect(parsed._meta.provider).toBe('leadsfactory')
113
- // Compact-by-default: raw `companies_personas` is normalized into `people[]`.
114
- expect(parsed.data.people).toHaveLength(1)
115
- expect(parsed.data.people[0]).toMatchObject({
116
- full_name: 'Michel Lieben',
117
- first_name: 'Michel',
118
- last_name: 'Lieben',
119
- title: 'CEO',
120
- })
121
- expect(parsed.data.companies_personas).toBeUndefined()
122
- } finally {
123
- lf.async!.timeoutMs = originalTimeout
124
- lf.async!.pollIntervalMs = originalInterval
125
- }
126
- })
127
-
128
- it('fields=verbose returns the raw LeadsFactory passthrough (companies_personas) unchanged', async () => {
129
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
130
- const urlStr = url.toString()
131
- if (urlStr.includes('/leadsfactory/contact-finder/searches') && !urlStr.includes('abc123')) {
132
- return new Response(JSON.stringify({ id: 'abc123', nb_jobs_total: 1, progress_percentage: 0 }), { status: 201 })
133
- }
134
- if (urlStr.includes('/leadsfactory/contact-finder/searches/abc123')) {
135
- return new Response(JSON.stringify({
136
- id: 'abc123', status: 'SUCCESSFUL', nb_jobs_total: 1, nb_jobs_complete: 1,
137
- companies_personas: [
138
- { company: 'ColdIQ', personas: [[{ contact: { first_name: 'Michel', last_name: 'Lieben', title: 'CEO' } }]] },
139
- ],
140
- }), { status: 200 })
141
- }
142
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
143
- }) as typeof fetch
144
-
145
- const { getProviders } = await import('../../src/registry.js')
146
- const lf = getProviders('find_people').find((p) => p.id === 'leadsfactory')!
147
- const originalTimeout = lf.async!.timeoutMs
148
- const originalInterval = lf.async!.pollIntervalMs
149
-
150
- try {
151
- lf.async!.timeoutMs = 500
152
- lf.async!.pollIntervalMs = 20
153
- const result = await findPeopleHandler({
154
- company_domains: ['coldiq.com'],
155
- job_titles: ['CEO'],
156
- limit: 5,
157
- fields: 'verbose',
158
- })
159
-
160
- const parsed = JSON.parse(result.content[0].text)
161
- expect(parsed._meta.provider).toBe('leadsfactory')
162
- expect(parsed.data.companies_personas).toHaveLength(1)
163
- expect(parsed.data.people).toBeUndefined()
164
- } finally {
165
- lf.async!.timeoutMs = originalTimeout
166
- lf.async!.pollIntervalMs = originalInterval
167
- }
168
- })
169
-
170
- it('gap-fills with Apollo for domains LeadsFactory missed (no_results_domains)', async () => {
171
- globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
172
- const urlStr = url.toString()
173
-
174
- // LeadsFactory create
175
- if (urlStr.includes('/leadsfactory/contact-finder/searches') && !urlStr.includes('abc123')) {
176
- return new Response(JSON.stringify({ id: 'abc123', search_id: 'abc', nb_jobs_total: 2, progress_percentage: 0 }), { status: 201 })
177
- }
178
-
179
- // LeadsFactory poll — SUCCESSFUL with one miss.
180
- // Real upstream shape: companies_personas[].personas is unknown[][], each
181
- // inner item wraps a contact object. The hasResult check counts contacts
182
- // across all entries, so mocks must use this nested shape to match prod.
183
- if (urlStr.includes('/leadsfactory/contact-finder/searches/abc123')) {
184
- return new Response(JSON.stringify({
185
- id: 'abc123', search_id: 'abc', status: 'SUCCESSFUL',
186
- nb_jobs_total: 2, nb_jobs_complete: 2,
187
- companies_personas: [
188
- { company: 'ColdIQ', personas: [[{ contact: { first_name: 'Michel', last_name: 'Lieben' } }]] },
189
- ],
190
- no_results_domains: ['folk.app'],
191
- }), { status: 200 })
192
- }
193
-
194
- // Apollo gap-fill for folk.app
195
- if (urlStr.includes('/apollo/people/search')) {
196
- const body = JSON.parse((opts?.body as string) ?? '{}')
197
- if (body.q_organization_domains?.includes('folk.app')) {
198
- return new Response(JSON.stringify({ people: [{ name: 'Thibaud Elziere', title: 'CEO', organization: { website_url: 'folk.app' } }] }), { status: 200 })
199
- }
200
- }
201
-
202
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
203
- }) as typeof fetch
204
-
205
- const { getProviders } = await import('../../src/registry.js')
206
- const providers = getProviders('find_people')
207
- const lf = providers.find((p) => p.id === 'leadsfactory')!
208
- const originalTimeout = lf.async!.timeoutMs
209
- const originalInterval = lf.async!.pollIntervalMs
210
-
211
- try {
212
- lf.async!.timeoutMs = 500
213
- lf.async!.pollIntervalMs = 20
214
- const result = await findPeopleHandler({
215
- company_domains: ['coldiq.com', 'folk.app'],
216
- job_titles: ['CEO'],
217
- limit: 2,
218
- })
219
-
220
- const parsed = JSON.parse(result.content[0].text)
221
- expect(parsed._meta.provider).toBe('leadsfactory')
222
- // Compact-by-default: LF + Apollo gap-fill records are merged into one people[]
223
- // and the gap-fill source is surfaced via gap_fill_provider.
224
- expect(parsed.data.people).toHaveLength(2)
225
- expect(parsed.data.people.map((p: { full_name: string }) => p.full_name)).toEqual(['Michel Lieben', 'Thibaud Elziere'])
226
- expect(parsed.data.gap_fill_provider).toBe('apollo')
227
- expect(parsed.data.companies_personas).toBeUndefined()
228
- expect(parsed.data.gap_fill).toBeUndefined()
229
- } finally {
230
- lf.async!.timeoutMs = originalTimeout
231
- lf.async!.pollIntervalMs = originalInterval
232
- }
233
- })
234
-
235
- it('skips gap-fill when input used LinkedIn URLs (no trusted domains to pass Apollo)', async () => {
236
- let apolloCalled = false
237
-
238
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
239
- const urlStr = url.toString()
240
-
241
- if (urlStr.includes('/leadsfactory/contact-finder/searches') && !urlStr.includes('abc123')) {
242
- return new Response(JSON.stringify({ id: 'abc123', nb_jobs_total: 2, progress_percentage: 0 }), { status: 201 })
243
- }
244
-
245
- if (urlStr.includes('/leadsfactory/contact-finder/searches/abc123')) {
246
- return new Response(JSON.stringify({
247
- id: 'abc123', status: 'SUCCESSFUL', nb_jobs_total: 2, nb_jobs_complete: 2,
248
- companies_personas: [
249
- { company: 'ColdIQ', personas: [[{ contact: { first_name: 'Michel' } }]] },
250
- ],
251
- no_results_domains: ['folk.app'],
252
- }), { status: 200 })
253
- }
254
-
255
- if (urlStr.includes('/apollo/people/search')) {
256
- apolloCalled = true
257
- return new Response(JSON.stringify({ people: [{ name: 'Thibaud Elziere' }] }), { status: 200 })
258
- }
259
-
260
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
261
- }) as typeof fetch
262
-
263
- const { getProviders } = await import('../../src/registry.js')
264
- const providers = getProviders('find_people')
265
- const lf = providers.find((p) => p.id === 'leadsfactory')!
266
- const originalTimeout = lf.async!.timeoutMs
267
- const originalInterval = lf.async!.pollIntervalMs
268
-
269
- try {
270
- lf.async!.timeoutMs = 500
271
- lf.async!.pollIntervalMs = 20
272
- const result = await findPeopleHandler({
273
- company_linkedin_urls: ['https://linkedin.com/company/coldiq', 'https://linkedin.com/company/folk'],
274
- job_titles: ['CEO'],
275
- limit: 2,
276
- })
277
-
278
- const parsed = JSON.parse(result.content[0].text)
279
- expect(parsed._meta.provider).toBe('leadsfactory')
280
- expect(parsed.data.gap_fill_provider).toBeUndefined()
281
- expect(apolloCalled).toBe(false)
282
- } finally {
283
- lf.async!.timeoutMs = originalTimeout
284
- lf.async!.pollIntervalMs = originalInterval
285
- }
286
- })
287
-
288
- it('does not pass company_domains to LeadsFactory when LinkedIn URLs are provided', async () => {
289
- let capturedBody: Record<string, unknown> | null = null
290
-
291
- globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
292
- const urlStr = url.toString()
293
-
294
- if (urlStr.includes('/leadsfactory/contact-finder/searches') && !urlStr.includes('abc123')) {
295
- capturedBody = JSON.parse((opts?.body as string) ?? '{}')
296
- return new Response(JSON.stringify({ id: 'abc123', nb_jobs_total: 2, progress_percentage: 0 }), { status: 201 })
297
- }
298
-
299
- if (urlStr.includes('/leadsfactory/contact-finder/searches/abc123')) {
300
- return new Response(JSON.stringify({
301
- id: 'abc123', status: 'SUCCESSFUL', nb_jobs_total: 2, nb_jobs_complete: 2,
302
- companies_personas: [
303
- { company: 'ColdIQ', personas: [[{ contact: { first_name: 'Michel' } }]] },
304
- ],
305
- }), { status: 200 })
306
- }
307
-
308
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
309
- }) as typeof fetch
310
-
311
- const { getProviders } = await import('../../src/registry.js')
312
- const providers = getProviders('find_people')
313
- const lf = providers.find((p) => p.id === 'leadsfactory')!
314
- const originalTimeout = lf.async!.timeoutMs
315
- const originalInterval = lf.async!.pollIntervalMs
316
-
317
- try {
318
- lf.async!.timeoutMs = 500
319
- lf.async!.pollIntervalMs = 20
320
- await findPeopleHandler({
321
- company_linkedin_urls: ['https://linkedin.com/company/coldiq', 'https://linkedin.com/company/folk'],
322
- company_domains: ['coldiq.com', 'folk.app'],
323
- job_titles: ['CEO'],
324
- limit: 2,
325
- })
326
-
327
- expect(capturedBody).not.toBeNull()
328
- expect(capturedBody!.company_linkedin_urls).toBeDefined()
329
- expect(capturedBody!.company_domains).toBeUndefined()
330
- } finally {
331
- lf.async!.timeoutMs = originalTimeout
332
- lf.async!.pollIntervalMs = originalInterval
333
- }
334
- })
335
-
336
- it('sends each job title as a separate persona to LeadsFactory', async () => {
337
- let capturedBody: Record<string, unknown> | null = null
338
-
339
- globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
340
- const urlStr = url.toString()
341
-
342
- if (urlStr.includes('/leadsfactory/contact-finder/searches') && !urlStr.includes('abc123')) {
343
- capturedBody = JSON.parse((opts?.body as string) ?? '{}')
344
- return new Response(JSON.stringify({ id: 'abc123', nb_jobs_total: 1, progress_percentage: 0 }), { status: 201 })
345
- }
346
-
347
- if (urlStr.includes('/leadsfactory/contact-finder/searches/abc123')) {
348
- return new Response(JSON.stringify({
349
- id: 'abc123', status: 'SUCCESSFUL', nb_jobs_total: 1, nb_jobs_complete: 1,
350
- companies_personas: [
351
- { company: 'ColdIQ', personas: [[{ contact: { first_name: 'Michel' } }]] },
352
- ],
353
- }), { status: 200 })
354
- }
355
-
356
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
357
- }) as typeof fetch
358
-
359
- const { getProviders } = await import('../../src/registry.js')
360
- const providers = getProviders('find_people')
361
- const lf = providers.find((p) => p.id === 'leadsfactory')!
362
- const originalTimeout = lf.async!.timeoutMs
363
- const originalInterval = lf.async!.pollIntervalMs
364
-
365
- try {
366
- lf.async!.timeoutMs = 500
367
- lf.async!.pollIntervalMs = 20
368
- await findPeopleHandler({
369
- company_domains: ['coldiq.com'],
370
- job_titles: ['CEO', 'VP of Sales'],
371
- limit: 10,
372
- })
373
-
374
- expect(capturedBody).not.toBeNull()
375
- expect(capturedBody!.search).toMatchObject({
376
- max_persona_results: 10,
377
- personas: [{ job_title: 'CEO' }, { job_title: 'VP of Sales' }],
378
- })
379
- } finally {
380
- lf.async!.timeoutMs = originalTimeout
381
- lf.async!.pollIntervalMs = originalInterval
382
- }
383
- })
384
-
385
- it('falls back to Apollo when LeadsFactory fails', async () => {
386
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
387
- const urlStr = url.toString()
388
-
389
- if (urlStr.includes('/leadsfactory/')) {
390
- return new Response(JSON.stringify({ error: 'Provider error' }), { status: 502 })
391
- }
392
-
393
- if (urlStr.includes('/apollo/people/search')) {
394
- return new Response(JSON.stringify({ people: [{ name: 'Michel Lieben', organization: { website_url: 'coldiq.com' } }] }), { status: 200 })
395
- }
396
-
397
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
398
- }) as typeof fetch
399
-
400
- const result = await findPeopleHandler({
401
- company_domains: ['coldiq.com'],
402
- job_titles: ['CEO'],
403
- limit: 5,
404
- })
405
-
406
- const parsed = JSON.parse(result.content[0].text)
407
- expect(parsed._meta.provider).toBe('apollo')
408
- expect(parsed.data.people).toHaveLength(1)
409
- expect(parsed.data.people[0]).toMatchObject({
410
- full_name: 'Michel Lieben',
411
- company_domain: 'coldiq.com',
412
- })
413
- })
414
-
415
- it('reveal=true follows obfuscated Apollo search with bulk-match in chunks of 10', async () => {
416
- const bulkMatchCalls: Array<{ url: string; body: Record<string, unknown> }> = []
417
- const obfuscated = (id: string, n: number) => ({
418
- id,
419
- first_name: `Person${n}`,
420
- last_name: `Ar***t${n}`,
421
- organization: { website_url: 'coldiq.com' },
422
- })
423
- const revealed = (id: string, n: number) => ({
424
- id,
425
- first_name: `Person${n}`,
426
- last_name: `Argent${n}`,
427
- email: `person${n}@coldiq.com`,
428
- linkedin_url: `https://linkedin.com/in/person${n}`,
429
- organization: { website_url: 'coldiq.com' },
430
- })
431
-
432
- globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
433
- const urlStr = url.toString()
434
-
435
- if (urlStr.includes('/apollo/people/search')) {
436
- return new Response(
437
- JSON.stringify({ people: Array.from({ length: 12 }, (_, i) => obfuscated(`id_${i}`, i)) }),
438
- { status: 200 },
439
- )
440
- }
441
-
442
- if (urlStr.includes('/apollo/people/bulk-match')) {
443
- const body = JSON.parse((opts?.body as string) ?? '{}') as { details: Array<{ id: string }> }
444
- bulkMatchCalls.push({ url: urlStr, body })
445
- return new Response(
446
- JSON.stringify({
447
- matches: body.details.map((d, idx) => revealed(d.id, parseInt(d.id.replace('id_', ''), 10) || idx)),
448
- }),
449
- { status: 200 },
450
- )
451
- }
452
-
453
- // Skip LeadsFactory entirely by pinning apollo via use_providers below.
454
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
455
- }) as typeof fetch
456
-
457
- const result = await findPeopleHandler({
458
- company_domains: ['coldiq.com'],
459
- job_titles: ['CEO'],
460
- limit: 12,
461
- reveal: true,
462
- use_providers: ['apollo'],
463
- })
464
-
465
- const parsed = JSON.parse(result.content[0].text)
466
- expect(parsed._meta.provider).toBe('apollo')
467
- expect(parsed.data.revealed).toBe(true)
468
- expect(parsed.data.people).toHaveLength(12)
469
- // Chunked at 10 per bulk-match call: 12 IDs → 2 calls (10 + 2)
470
- expect(bulkMatchCalls).toHaveLength(2)
471
- expect(bulkMatchCalls[0].body.details).toHaveLength(10)
472
- expect(bulkMatchCalls[1].body.details).toHaveLength(2)
473
- // Revealed payload (compacted) carries email + full last name
474
- expect(parsed.data.people[0].email).toBe('person0@coldiq.com')
475
- expect(parsed.data.people[0].last_name).toBe('Argent0')
476
- // reveal_personal_emails flag forwarded
477
- expect(bulkMatchCalls[0].body.reveal_personal_emails).toBe(true)
478
- })
479
-
480
- it('reveal=true does NOT set revealed:true when bulk-match returns only some IDs (partial reveal)', async () => {
481
- // Apollo's bulk-match can return matches for a subset of submitted IDs (e.g. some
482
- // people aren't in their database). Setting `revealed: true` in that case would
483
- // mislead callers — entries still obfuscated would look revealed at the top level.
484
- globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
485
- const urlStr = url.toString()
486
-
487
- if (urlStr.includes('/apollo/people/search')) {
488
- return new Response(
489
- JSON.stringify({
490
- people: [
491
- { id: 'id_0', first_name: 'A', last_name: 'A***A', organization: { website_url: 'coldiq.com' } },
492
- { id: 'id_1', first_name: 'B', last_name: 'B***B', organization: { website_url: 'coldiq.com' } },
493
- { id: 'id_2', first_name: 'C', last_name: 'C***C', organization: { website_url: 'coldiq.com' } },
494
- ],
495
- }),
496
- { status: 200 },
497
- )
498
- }
499
-
500
- if (urlStr.includes('/apollo/people/bulk-match')) {
501
- const body = JSON.parse((opts?.body as string) ?? '{}') as { details: Array<{ id: string }> }
502
- // Only return a match for id_0 — id_1 and id_2 stay obfuscated.
503
- const onlyFirst = body.details
504
- .filter((d) => d.id === 'id_0')
505
- .map((d) => ({ id: d.id, first_name: 'A', last_name: 'AlphaReal', email: 'a@coldiq.com', linkedin_url: 'https://linkedin.com/in/a', organization: { website_url: 'coldiq.com' } }))
506
- return new Response(JSON.stringify({ matches: onlyFirst }), { status: 200 })
507
- }
508
-
509
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
510
- }) as typeof fetch
511
-
512
- const result = await findPeopleHandler({
513
- company_domains: ['coldiq.com'],
514
- job_titles: ['CEO'],
515
- limit: 3,
516
- reveal: true,
517
- use_providers: ['apollo'],
518
- })
519
-
520
- const parsed = JSON.parse(result.content[0].text)
521
- expect(parsed._meta.provider).toBe('apollo')
522
- // Partial reveal: flag must be omitted (compactPayload only includes revealed when === true).
523
- expect(parsed.data.revealed).toBeUndefined()
524
- expect(parsed.data.people).toHaveLength(3)
525
- // id_0 came back revealed; id_1/id_2 stayed obfuscated.
526
- expect(parsed.data.people[0].last_name).toBe('AlphaReal')
527
- expect(parsed.data.people[0].email).toBe('a@coldiq.com')
528
- expect(parsed.data.people[1].last_name).toBe('B***B')
529
- expect(parsed.data.people[2].last_name).toBe('C***C')
530
- })
531
-
532
- it('reveal=false (default) skips bulk-match and returns obfuscated Apollo data unchanged', async () => {
533
- let bulkMatchCalled = false
534
-
535
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
536
- const urlStr = url.toString()
537
-
538
- if (urlStr.includes('/apollo/people/search')) {
539
- return new Response(
540
- JSON.stringify({
541
- people: [{ id: 'id_0', first_name: 'Michel', last_name: 'Li***n', organization: { website_url: 'coldiq.com' } }],
542
- }),
543
- { status: 200 },
544
- )
545
- }
546
-
547
- if (urlStr.includes('/apollo/people/bulk-match')) {
548
- bulkMatchCalled = true
549
- return new Response(JSON.stringify({ matches: [] }), { status: 200 })
550
- }
551
-
552
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
553
- }) as typeof fetch
554
-
555
- const result = await findPeopleHandler({
556
- company_domains: ['coldiq.com'],
557
- job_titles: ['CEO'],
558
- limit: 5,
559
- use_providers: ['apollo'],
560
- })
561
-
562
- const parsed = JSON.parse(result.content[0].text)
563
- expect(parsed._meta.provider).toBe('apollo')
564
- expect(parsed.data.revealed).toBeUndefined()
565
- // Obfuscated last name from /apollo/people/search is preserved through compaction.
566
- expect(parsed.data.people[0].last_name).toBe('Li***n')
567
- expect(bulkMatchCalled).toBe(false)
568
- })
569
-
570
- it('reveal=true is a no-op when the matched provider is not Apollo', async () => {
571
- let bulkMatchCalled = false
572
-
573
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
574
- const urlStr = url.toString()
575
-
576
- if (urlStr.includes('/leadsfactory/contact-finder/searches') && !urlStr.includes('abc123')) {
577
- return new Response(JSON.stringify({ id: 'abc123', nb_jobs_total: 1, progress_percentage: 0 }), { status: 201 })
578
- }
579
-
580
- if (urlStr.includes('/leadsfactory/contact-finder/searches/abc123')) {
581
- return new Response(JSON.stringify({
582
- id: 'abc123', status: 'SUCCESSFUL', nb_jobs_total: 1, nb_jobs_complete: 1,
583
- companies_personas: [
584
- { company: 'ColdIQ', personas: [[{ contact: { first_name: 'Michel', last_name: 'Lieben' } }]] },
585
- ],
586
- }), { status: 200 })
587
- }
588
-
589
- if (urlStr.includes('/apollo/people/bulk-match')) {
590
- bulkMatchCalled = true
591
- return new Response(JSON.stringify({ matches: [] }), { status: 200 })
592
- }
593
-
594
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
595
- }) as typeof fetch
596
-
597
- const { getProviders } = await import('../../src/registry.js')
598
- const lf = getProviders('find_people').find((p) => p.id === 'leadsfactory')!
599
- const originalTimeout = lf.async!.timeoutMs
600
- const originalInterval = lf.async!.pollIntervalMs
601
-
602
- try {
603
- lf.async!.timeoutMs = 500
604
- lf.async!.pollIntervalMs = 20
605
-
606
- const result = await findPeopleHandler({
607
- company_domains: ['coldiq.com'],
608
- job_titles: ['CEO'],
609
- limit: 5,
610
- reveal: true,
611
- })
612
-
613
- const parsed = JSON.parse(result.content[0].text)
614
- expect(parsed._meta.provider).toBe('leadsfactory')
615
- expect(bulkMatchCalled).toBe(false)
616
- } finally {
617
- lf.async!.timeoutMs = originalTimeout
618
- lf.async!.pollIntervalMs = originalInterval
619
- }
620
- })
621
- })