@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.
- package/dist/tools/enrich-company.d.ts +3 -9
- package/dist/tools/enrich-company.d.ts.map +1 -1
- package/dist/tools/enrich-company.js +3 -16
- package/dist/tools/enrich-company.js.map +1 -1
- package/dist/tools/enrich-person.d.ts +3 -9
- package/dist/tools/enrich-person.d.ts.map +1 -1
- package/dist/tools/enrich-person.js +3 -12
- package/dist/tools/enrich-person.js.map +1 -1
- package/dist/tools/fetch-page-content.d.ts +3 -9
- package/dist/tools/fetch-page-content.d.ts.map +1 -1
- package/dist/tools/fetch-page-content.js +3 -12
- package/dist/tools/fetch-page-content.js.map +1 -1
- package/dist/tools/find-email.d.ts +3 -9
- package/dist/tools/find-email.d.ts.map +1 -1
- package/dist/tools/find-email.js +3 -40
- package/dist/tools/find-email.js.map +1 -1
- package/dist/tools/find-emails.d.ts +3 -4
- package/dist/tools/find-emails.d.ts.map +1 -1
- package/dist/tools/find-emails.js +8 -423
- package/dist/tools/find-emails.js.map +1 -1
- package/dist/tools/find-influencers.d.ts +3 -9
- package/dist/tools/find-influencers.d.ts.map +1 -1
- package/dist/tools/find-influencers.js +3 -12
- package/dist/tools/find-influencers.js.map +1 -1
- package/dist/tools/find-people.d.ts +3 -8
- package/dist/tools/find-people.d.ts.map +1 -1
- package/dist/tools/find-people.js +3 -98
- package/dist/tools/find-people.js.map +1 -1
- package/dist/tools/find-phone.d.ts +3 -9
- package/dist/tools/find-phone.d.ts.map +1 -1
- package/dist/tools/find-phone.js +4 -41
- package/dist/tools/find-phone.js.map +1 -1
- package/dist/tools/find-signals.d.ts +3 -9
- package/dist/tools/find-signals.d.ts.map +1 -1
- package/dist/tools/find-signals.js +3 -83
- package/dist/tools/find-signals.js.map +1 -1
- package/dist/tools/get-place-reviews.d.ts +3 -9
- package/dist/tools/get-place-reviews.d.ts.map +1 -1
- package/dist/tools/get-place-reviews.js +3 -12
- package/dist/tools/get-place-reviews.js.map +1 -1
- package/dist/tools/search-ads.d.ts +3 -9
- package/dist/tools/search-ads.d.ts.map +1 -1
- package/dist/tools/search-ads.js +3 -12
- package/dist/tools/search-ads.js.map +1 -1
- package/dist/tools/search-companies.d.ts +3 -9
- package/dist/tools/search-companies.d.ts.map +1 -1
- package/dist/tools/search-companies.js +3 -24
- package/dist/tools/search-companies.js.map +1 -1
- package/dist/tools/search-jobs.d.ts +3 -9
- package/dist/tools/search-jobs.d.ts.map +1 -1
- package/dist/tools/search-jobs.js +3 -27
- package/dist/tools/search-jobs.js.map +1 -1
- package/dist/tools/search-places.d.ts +3 -15
- package/dist/tools/search-places.d.ts.map +1 -1
- package/dist/tools/search-places.js +5 -76
- package/dist/tools/search-places.js.map +1 -1
- package/dist/tools/search-reddit.d.ts +3 -9
- package/dist/tools/search-reddit.d.ts.map +1 -1
- package/dist/tools/search-reddit.js +3 -12
- package/dist/tools/search-reddit.js.map +1 -1
- package/dist/tools/search-seo.d.ts +3 -9
- package/dist/tools/search-seo.d.ts.map +1 -1
- package/dist/tools/search-seo.js +3 -12
- package/dist/tools/search-seo.js.map +1 -1
- package/dist/tools/search-web.d.ts +3 -9
- package/dist/tools/search-web.d.ts.map +1 -1
- package/dist/tools/search-web.js +3 -12
- package/dist/tools/search-web.js.map +1 -1
- package/dist/tools/verify-email.d.ts +3 -10
- package/dist/tools/verify-email.d.ts.map +1 -1
- package/dist/tools/verify-email.js +3 -68
- package/dist/tools/verify-email.js.map +1 -1
- package/dist/verb-client.d.ts +35 -0
- package/dist/verb-client.d.ts.map +1 -0
- package/dist/verb-client.js +57 -0
- package/dist/verb-client.js.map +1 -0
- package/package.json +1 -1
- package/src/tools/enrich-company.ts +3 -16
- package/src/tools/enrich-person.ts +3 -12
- package/src/tools/fetch-page-content.ts +3 -12
- package/src/tools/find-email.ts +3 -42
- package/src/tools/find-emails.ts +8 -506
- package/src/tools/find-influencers.ts +3 -12
- package/src/tools/find-people.ts +3 -107
- package/src/tools/find-phone.ts +4 -42
- package/src/tools/find-signals.ts +3 -88
- package/src/tools/get-place-reviews.ts +3 -12
- package/src/tools/search-ads.ts +3 -12
- package/src/tools/search-companies.ts +3 -23
- package/src/tools/search-jobs.ts +3 -30
- package/src/tools/search-places.ts +5 -72
- package/src/tools/search-reddit.ts +3 -12
- package/src/tools/search-seo.ts +3 -12
- package/src/tools/search-web.ts +3 -12
- package/src/tools/verify-email.ts +3 -76
- package/src/verb-client.ts +93 -0
- package/tests/verb-client.test.ts +152 -0
- package/tests/tools/enrich-company.test.ts +0 -92
- package/tests/tools/enrich-person.test.ts +0 -140
- package/tests/tools/fetch-page-content.test.ts +0 -108
- package/tests/tools/find-email.test.ts +0 -137
- package/tests/tools/find-emails.test.ts +0 -818
- package/tests/tools/find-influencers.test.ts +0 -91
- package/tests/tools/find-people.test.ts +0 -621
- package/tests/tools/find-phone.test.ts +0 -100
- package/tests/tools/find-signals.test.ts +0 -175
- package/tests/tools/get-place-reviews.test.ts +0 -73
- package/tests/tools/search-ads.test.ts +0 -182
- package/tests/tools/search-companies.test.ts +0 -192
- package/tests/tools/search-jobs.test.ts +0 -210
- package/tests/tools/search-places.test.ts +0 -210
- package/tests/tools/search-reddit.test.ts +0 -194
- package/tests/tools/search-seo.test.ts +0 -183
- package/tests/tools/search-web.test.ts +0 -79
- 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
|
-
})
|