@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,818 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { initClient } from '../../src/client.js'
|
|
3
|
-
import { findEmailsHandler, FIND_EMAILS_CHUNK_SIZE } from '../../src/tools/find-emails.js'
|
|
4
|
-
|
|
5
|
-
describe('find_emails handler (bulk)', () => {
|
|
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('resolves all people from prospeo bulk when all match', async () => {
|
|
17
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
18
|
-
const u = url.toString()
|
|
19
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
20
|
-
return new Response(
|
|
21
|
-
JSON.stringify({
|
|
22
|
-
error: false,
|
|
23
|
-
results: [
|
|
24
|
-
{ identifier: 'p1', error: false, person: { email: { email: 'alice@example.com' } } },
|
|
25
|
-
{ identifier: 'p2', error: false, person: { email: { email: 'bob@example.com' } } },
|
|
26
|
-
],
|
|
27
|
-
total_cost: 2,
|
|
28
|
-
}),
|
|
29
|
-
{ status: 200 },
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
return new Response(JSON.stringify({ error: 'unexpected call' }), { status: 500 })
|
|
33
|
-
}) as typeof fetch
|
|
34
|
-
|
|
35
|
-
const result = await findEmailsHandler({
|
|
36
|
-
people: [
|
|
37
|
-
{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'example.com' },
|
|
38
|
-
{ id: 'p2', first_name: 'Bob', last_name: 'Jones', domain: 'example.com' },
|
|
39
|
-
],
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
43
|
-
expect(parsed.data.found).toBe(2)
|
|
44
|
-
expect(parsed.data.total).toBe(2)
|
|
45
|
-
expect(parsed.data.results).toEqual([
|
|
46
|
-
{ id: 'p1', email: 'alice@example.com', provider: 'prospeo', provider_name: 'Prospeo' },
|
|
47
|
-
{ id: 'p2', email: 'bob@example.com', provider: 'prospeo', provider_name: 'Prospeo' },
|
|
48
|
-
])
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('falls back to FindyMail for prospeo misses', async () => {
|
|
52
|
-
let fmCalled = false
|
|
53
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
54
|
-
const u = url.toString()
|
|
55
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
56
|
-
return new Response(
|
|
57
|
-
JSON.stringify({
|
|
58
|
-
error: false,
|
|
59
|
-
results: [
|
|
60
|
-
{ identifier: 'p1', error: true, person: null },
|
|
61
|
-
],
|
|
62
|
-
total_cost: 0,
|
|
63
|
-
}),
|
|
64
|
-
{ status: 200 },
|
|
65
|
-
)
|
|
66
|
-
}
|
|
67
|
-
if (u.includes('/findymail/search/name')) {
|
|
68
|
-
fmCalled = true
|
|
69
|
-
return new Response(JSON.stringify({ email: 'alice@example.com' }), { status: 200 })
|
|
70
|
-
}
|
|
71
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
72
|
-
}) as typeof fetch
|
|
73
|
-
|
|
74
|
-
const result = await findEmailsHandler({
|
|
75
|
-
people: [{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'example.com' }],
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
expect(fmCalled).toBe(true)
|
|
79
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
80
|
-
expect(parsed.data.results[0]).toEqual({ id: 'p1', email: 'alice@example.com', provider: 'findymail', provider_name: 'Findymail' })
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('falls back to IcyPeas when FindyMail also misses', async () => {
|
|
84
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
85
|
-
const u = url.toString()
|
|
86
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
87
|
-
return new Response(
|
|
88
|
-
JSON.stringify({ error: false, results: [{ identifier: 'p1', error: true, person: null }], total_cost: 0 }),
|
|
89
|
-
{ status: 200 },
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
if (u.includes('/findymail/search/name')) {
|
|
93
|
-
return new Response(JSON.stringify({ email: null }), { status: 200 })
|
|
94
|
-
}
|
|
95
|
-
if (u.includes('/icypeas/email-search')) {
|
|
96
|
-
return new Response(JSON.stringify({ email: 'alice@example.com' }), { status: 200 })
|
|
97
|
-
}
|
|
98
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
99
|
-
}) as typeof fetch
|
|
100
|
-
|
|
101
|
-
const result = await findEmailsHandler({
|
|
102
|
-
people: [{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'example.com' }],
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
106
|
-
expect(parsed.data.results[0]).toEqual({ id: 'p1', email: 'alice@example.com', provider: 'icypeas', provider_name: 'Icypeas' })
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('returns email:null when all providers miss for a person', async () => {
|
|
110
|
-
globalThis.fetch = vi.fn(async () =>
|
|
111
|
-
new Response(JSON.stringify({ error: true }), { status: 200 }),
|
|
112
|
-
) as typeof fetch
|
|
113
|
-
|
|
114
|
-
const result = await findEmailsHandler({
|
|
115
|
-
people: [{ id: 'p1', first_name: 'Unknown', last_name: 'Person', domain: 'nowhere.xyz' }],
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
119
|
-
expect(parsed.data.found).toBe(0)
|
|
120
|
-
expect(parsed.data.results[0]).toEqual({ id: 'p1', email: null, provider: null, provider_name: null })
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('handles mixed results: some from prospeo, some from fallback, some missed', async () => {
|
|
124
|
-
// Track findymail call count to serve different results per person
|
|
125
|
-
let findymailCalls = 0
|
|
126
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
127
|
-
const u = url.toString()
|
|
128
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
129
|
-
return new Response(
|
|
130
|
-
JSON.stringify({
|
|
131
|
-
error: false,
|
|
132
|
-
results: [
|
|
133
|
-
{ identifier: 'p1', error: false, person: { email: { email: 'alice@a.com' } } },
|
|
134
|
-
{ identifier: 'p2', error: true, person: null },
|
|
135
|
-
{ identifier: 'p3', error: true, person: null },
|
|
136
|
-
],
|
|
137
|
-
total_cost: 1,
|
|
138
|
-
}),
|
|
139
|
-
{ status: 200 },
|
|
140
|
-
)
|
|
141
|
-
}
|
|
142
|
-
if (u.includes('/findymail/search/name')) {
|
|
143
|
-
findymailCalls++
|
|
144
|
-
// First miss (p2 or p3): one finds, one doesn't
|
|
145
|
-
return new Response(
|
|
146
|
-
JSON.stringify({ email: findymailCalls === 1 ? 'bob@b.com' : null }),
|
|
147
|
-
{ status: 200 },
|
|
148
|
-
)
|
|
149
|
-
}
|
|
150
|
-
if (u.includes('/icypeas/email-search')) {
|
|
151
|
-
return new Response(JSON.stringify({ email: null }), { status: 200 })
|
|
152
|
-
}
|
|
153
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
154
|
-
}) as typeof fetch
|
|
155
|
-
|
|
156
|
-
const result = await findEmailsHandler({
|
|
157
|
-
people: [
|
|
158
|
-
{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'a.com' },
|
|
159
|
-
{ id: 'p2', first_name: 'Bob', last_name: 'Jones', domain: 'b.com' },
|
|
160
|
-
{ id: 'p3', first_name: 'Carol', last_name: 'White', domain: 'c.com' },
|
|
161
|
-
],
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
165
|
-
expect(parsed.data.found).toBe(2)
|
|
166
|
-
expect(parsed.data.results.find((r: EmailResult) => r.id === 'p1')).toMatchObject({ email: 'alice@a.com', provider: 'prospeo' })
|
|
167
|
-
// One of p2/p3 found by findymail (order non-deterministic due to Promise.all)
|
|
168
|
-
const findymailHit = parsed.data.results.find((r: EmailResult) => r.provider === 'findymail')
|
|
169
|
-
expect(findymailHit).toBeDefined()
|
|
170
|
-
expect(findymailHit.email).toMatch(/@/)
|
|
171
|
-
const missed = parsed.data.results.find((r: EmailResult) => r.email === null)
|
|
172
|
-
expect(missed).toBeDefined()
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('uses linkedin_url in prospeo bulk when provided', async () => {
|
|
176
|
-
let capturedBulkBody: unknown
|
|
177
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
178
|
-
const u = url.toString()
|
|
179
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
180
|
-
capturedBulkBody = JSON.parse(init?.body as string)
|
|
181
|
-
return new Response(
|
|
182
|
-
JSON.stringify({ error: false, results: [], total_cost: 0 }),
|
|
183
|
-
{ status: 200 },
|
|
184
|
-
)
|
|
185
|
-
}
|
|
186
|
-
// Fallback calls (no domain/name so findymail/icypeas return nothing)
|
|
187
|
-
return new Response(JSON.stringify({ email: null }), { status: 200 })
|
|
188
|
-
}) as typeof fetch
|
|
189
|
-
|
|
190
|
-
await findEmailsHandler({
|
|
191
|
-
people: [{ id: 'p1', linkedin_url: 'https://linkedin.com/in/michel-lieben' }],
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
const body = capturedBulkBody as { data: Array<Record<string, unknown>> }
|
|
195
|
-
expect(body.data[0]).toMatchObject({ identifier: 'p1', linkedin_url: 'https://linkedin.com/in/michel-lieben' })
|
|
196
|
-
expect(body.data[0].first_name).toBeUndefined()
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
it('uses FullEnrich batch for Prospeo misses and matches by custom_id', async () => {
|
|
200
|
-
vi.useFakeTimers()
|
|
201
|
-
|
|
202
|
-
let feCreateBody: unknown
|
|
203
|
-
let fePollCalled = false
|
|
204
|
-
|
|
205
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
206
|
-
const u = url.toString()
|
|
207
|
-
|
|
208
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
209
|
-
return new Response(JSON.stringify({
|
|
210
|
-
error: false,
|
|
211
|
-
results: [{ identifier: 'p1', error: true, person: null }],
|
|
212
|
-
total_cost: 0,
|
|
213
|
-
}), { status: 200 })
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (u.includes('/fullenrich/contact/enrich/bulk/abc-fe-123')) {
|
|
217
|
-
fePollCalled = true
|
|
218
|
-
return new Response(JSON.stringify({
|
|
219
|
-
status: 'DONE',
|
|
220
|
-
data: [{ custom_id: 'p1', emails: ['alice@example.com'] }],
|
|
221
|
-
}), { status: 200 })
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (u.includes('/fullenrich/contact/enrich/bulk')) {
|
|
225
|
-
feCreateBody = JSON.parse(init?.body as string)
|
|
226
|
-
return new Response(JSON.stringify({ enrichment_id: 'abc-fe-123' }), { status: 200 })
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
230
|
-
}) as typeof fetch
|
|
231
|
-
|
|
232
|
-
const handlerPromise = findEmailsHandler({
|
|
233
|
-
people: [{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'example.com' }],
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
await vi.runAllTimersAsync()
|
|
237
|
-
const result = await handlerPromise
|
|
238
|
-
vi.useRealTimers()
|
|
239
|
-
|
|
240
|
-
expect(fePollCalled).toBe(true)
|
|
241
|
-
const body = feCreateBody as { data: Array<Record<string, unknown>> }
|
|
242
|
-
expect(body.data[0].custom_id).toBe('p1')
|
|
243
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
244
|
-
expect(parsed.data.results[0]).toEqual({ id: 'p1', email: 'alice@example.com', provider: 'fullenrich', provider_name: 'FullEnrich' })
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
it('parallel branches: faster FindyMail wins, slower FullEnrich does not overwrite', async () => {
|
|
248
|
-
// Step 2 (FullEnrich) and Step 3 (FindyMail/IcyPeas) run concurrently. If FindyMail
|
|
249
|
-
// returns an email first, the FullEnrich poll later must NOT overwrite it.
|
|
250
|
-
vi.useFakeTimers()
|
|
251
|
-
|
|
252
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
253
|
-
const u = url.toString()
|
|
254
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
255
|
-
return new Response(
|
|
256
|
-
JSON.stringify({ error: false, results: [{ identifier: 'p1', error: true, person: null }], total_cost: 0 }),
|
|
257
|
-
{ status: 200 },
|
|
258
|
-
)
|
|
259
|
-
}
|
|
260
|
-
// FindyMail wins immediately
|
|
261
|
-
if (u.includes('/findymail/search/name')) {
|
|
262
|
-
return new Response(JSON.stringify({ email: 'fm@example.com' }), { status: 200 })
|
|
263
|
-
}
|
|
264
|
-
// FullEnrich create + later poll succeed but with a different email
|
|
265
|
-
if (u.includes('/fullenrich/contact/enrich/bulk/abc-fe-456')) {
|
|
266
|
-
return new Response(
|
|
267
|
-
JSON.stringify({ status: 'DONE', data: [{ custom_id: 'p1', emails: ['fe@example.com'] }] }),
|
|
268
|
-
{ status: 200 },
|
|
269
|
-
)
|
|
270
|
-
}
|
|
271
|
-
if (u.includes('/fullenrich/contact/enrich/bulk')) {
|
|
272
|
-
return new Response(JSON.stringify({ enrichment_id: 'abc-fe-456' }), { status: 200 })
|
|
273
|
-
}
|
|
274
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
275
|
-
}) as typeof fetch
|
|
276
|
-
|
|
277
|
-
const handlerPromise = findEmailsHandler({
|
|
278
|
-
people: [{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'example.com' }],
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
await vi.runAllTimersAsync()
|
|
282
|
-
const result = await handlerPromise
|
|
283
|
-
vi.useRealTimers()
|
|
284
|
-
|
|
285
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
286
|
-
// FindyMail won the race — its email + provider must persist
|
|
287
|
-
expect(parsed.data.results[0]).toEqual({ id: 'p1', email: 'fm@example.com', provider: 'findymail', provider_name: 'Findymail' })
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
it('gracefully handles prospeo bulk failure and still tries fallbacks', async () => {
|
|
291
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
292
|
-
const u = url.toString()
|
|
293
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
294
|
-
return new Response(JSON.stringify({ error: 'service unavailable' }), { status: 502 })
|
|
295
|
-
}
|
|
296
|
-
if (u.includes('/findymail/search/name')) {
|
|
297
|
-
return new Response(JSON.stringify({ email: 'alice@example.com' }), { status: 200 })
|
|
298
|
-
}
|
|
299
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
300
|
-
}) as typeof fetch
|
|
301
|
-
|
|
302
|
-
const result = await findEmailsHandler({
|
|
303
|
-
people: [{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'example.com' }],
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
307
|
-
expect(parsed.data.results[0]).toMatchObject({ email: 'alice@example.com', provider: 'findymail' })
|
|
308
|
-
})
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
interface EmailResult {
|
|
312
|
-
id: string
|
|
313
|
-
email: string | null
|
|
314
|
-
provider: string | null
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
describe('find_emails handler — use_providers', () => {
|
|
318
|
-
const originalFetch = globalThis.fetch
|
|
319
|
-
|
|
320
|
-
beforeEach(() => {
|
|
321
|
-
initClient('http://test-api.local', 'test-key')
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
afterEach(() => {
|
|
325
|
-
globalThis.fetch = originalFetch
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
it('Scenario A — pinned to prospeo only, skips fallback providers', async () => {
|
|
329
|
-
let findymailCalled = false
|
|
330
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
331
|
-
const u = url.toString()
|
|
332
|
-
if (u.includes('/findymail')) findymailCalled = true
|
|
333
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
334
|
-
return new Response(JSON.stringify({
|
|
335
|
-
error: false,
|
|
336
|
-
results: [{ identifier: 'p1', person: { email: { email: 'alice@example.com' } } }],
|
|
337
|
-
}), { status: 200 })
|
|
338
|
-
}
|
|
339
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
340
|
-
}) as typeof fetch
|
|
341
|
-
|
|
342
|
-
const result = await findEmailsHandler({
|
|
343
|
-
people: [{ id: 'p1', first_name: 'Alice', domain: 'example.com' }],
|
|
344
|
-
use_providers: ['prospeo'],
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
expect(result.isError).toBeUndefined()
|
|
348
|
-
expect(findymailCalled).toBe(false)
|
|
349
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
350
|
-
expect(parsed.data.results[0]).toMatchObject({ provider: 'prospeo' })
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
it('Scenario H — all pinned providers find nothing → returns isError with pushback message', async () => {
|
|
354
|
-
globalThis.fetch = vi.fn(async () =>
|
|
355
|
-
new Response(JSON.stringify({ error: false, results: [] }), { status: 200 })
|
|
356
|
-
) as typeof fetch
|
|
357
|
-
|
|
358
|
-
const result = await findEmailsHandler({
|
|
359
|
-
people: [{ id: 'p1', first_name: 'Unknown', domain: 'nowhere.xyz' }],
|
|
360
|
-
use_providers: ['findymail'],
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
expect(result.isError).toBe(true)
|
|
364
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
365
|
-
expect(parsed.error).toContain('Couldn\'t fulfill')
|
|
366
|
-
expect(parsed.error).toContain('ColdIQ will automatically pick the best tool')
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
it('Scenario E — unrecognized provider name returns isError', async () => {
|
|
370
|
-
const result = await findEmailsHandler({
|
|
371
|
-
people: [{ id: 'p1', first_name: 'Alice', domain: 'example.com' }],
|
|
372
|
-
use_providers: ['wiza'],
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
expect(result.isError).toBe(true)
|
|
376
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
377
|
-
expect(parsed.error).toContain("'wiza' is not a recognized provider")
|
|
378
|
-
expect(parsed.error).toContain('ColdIQ will automatically pick the best tool')
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
it('Scenario G — ordered providers: first succeeds, second skipped', async () => {
|
|
382
|
-
let icypeasCalled = false
|
|
383
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
384
|
-
const u = url.toString()
|
|
385
|
-
if (u.includes('/icypeas')) icypeasCalled = true
|
|
386
|
-
if (u.includes('/findymail/search/name')) {
|
|
387
|
-
return new Response(JSON.stringify({ email: 'alice@example.com' }), { status: 200 })
|
|
388
|
-
}
|
|
389
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
390
|
-
}) as typeof fetch
|
|
391
|
-
|
|
392
|
-
const result = await findEmailsHandler({
|
|
393
|
-
people: [{ id: 'p1', first_name: 'Alice', domain: 'example.com' }],
|
|
394
|
-
use_providers: ['findymail', 'icypeas'],
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
expect(result.isError).toBeUndefined()
|
|
398
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
399
|
-
expect(parsed.data.results[0]).toMatchObject({ provider: 'findymail', email: 'alice@example.com' })
|
|
400
|
-
expect(icypeasCalled).toBe(false)
|
|
401
|
-
})
|
|
402
|
-
|
|
403
|
-
it('pinning a single-only provider (limadata-work-email) routes through Step 4 and succeeds', async () => {
|
|
404
|
-
// Regression: previously FIND_EMAILS_PROVIDERS only listed the 4 bulk providers,
|
|
405
|
-
// so pinning a Step 4 provider was rejected as "unrecognized". Now all 8 are exposed.
|
|
406
|
-
let prospeoCalled = false
|
|
407
|
-
let limadataCalled = false
|
|
408
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
409
|
-
const u = url.toString()
|
|
410
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
411
|
-
prospeoCalled = true
|
|
412
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
413
|
-
}
|
|
414
|
-
if (u.includes('/coldiq/find/work-email')) {
|
|
415
|
-
limadataCalled = true
|
|
416
|
-
return new Response(JSON.stringify({ email: 'alice@example.com' }), { status: 200 })
|
|
417
|
-
}
|
|
418
|
-
return new Response(JSON.stringify({ error: 'unexpected', url: u }), { status: 500 })
|
|
419
|
-
}) as typeof fetch
|
|
420
|
-
|
|
421
|
-
const result = await findEmailsHandler({
|
|
422
|
-
people: [{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'example.com' }],
|
|
423
|
-
use_providers: ['limadata-work-email'],
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
expect(result.isError).toBeUndefined()
|
|
427
|
-
expect(prospeoCalled).toBe(false)
|
|
428
|
-
expect(limadataCalled).toBe(true)
|
|
429
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
430
|
-
expect(parsed.data.results[0]).toMatchObject({ provider: 'limadata-work-email', email: 'alice@example.com' })
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
it('all-failed error after pinning bulk-only providers surfaces single-only providers as remaining options', async () => {
|
|
434
|
-
// Regression: with only 4 providers exposed, after the user pinned all 4 bulk providers
|
|
435
|
-
// and they missed, available_providers was [] — falsely implying no more options existed.
|
|
436
|
-
// Now the error must point them at the single-only providers they could retry with.
|
|
437
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
438
|
-
const u = url.toString()
|
|
439
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
440
|
-
return new Response(JSON.stringify({ error: false, results: [] }), { status: 200 })
|
|
441
|
-
}
|
|
442
|
-
if (u.includes('/fullenrich/contact/enrich/bulk')) {
|
|
443
|
-
return new Response(JSON.stringify({}), { status: 200 })
|
|
444
|
-
}
|
|
445
|
-
if (u.includes('/findymail/search/name')) {
|
|
446
|
-
return new Response(JSON.stringify({ email: null }), { status: 200 })
|
|
447
|
-
}
|
|
448
|
-
if (u.includes('/icypeas/email-search')) {
|
|
449
|
-
return new Response(JSON.stringify({ email: null }), { status: 200 })
|
|
450
|
-
}
|
|
451
|
-
return new Response(JSON.stringify({ error: 'unexpected', url: u }), { status: 500 })
|
|
452
|
-
}) as typeof fetch
|
|
453
|
-
|
|
454
|
-
const result = await findEmailsHandler({
|
|
455
|
-
people: [{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'example.com' }],
|
|
456
|
-
use_providers: ['prospeo', 'fullenrich', 'findymail', 'icypeas'],
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
expect(result.isError).toBe(true)
|
|
460
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
461
|
-
expect(parsed.available_providers).toEqual([
|
|
462
|
-
'limadata-work-email',
|
|
463
|
-
'blitzapi',
|
|
464
|
-
'limadata-work-email-linkedin',
|
|
465
|
-
'linkupapi',
|
|
466
|
-
])
|
|
467
|
-
expect(parsed.error).toContain('limadata-work-email')
|
|
468
|
-
})
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
describe('find_emails handler — resilience', () => {
|
|
472
|
-
const originalFetch = globalThis.fetch
|
|
473
|
-
|
|
474
|
-
beforeEach(() => {
|
|
475
|
-
initClient('http://test-api.local', 'test-key')
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
afterEach(() => {
|
|
479
|
-
globalThis.fetch = originalFetch
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
it('findymailIcypeasStep completing correctly even when fullEnrichStep throws', async () => {
|
|
483
|
-
// Prospeo returns no results.
|
|
484
|
-
// FullEnrich create call throws (simulates unexpected error).
|
|
485
|
-
// FindyMail finds the email — result should still be populated.
|
|
486
|
-
|
|
487
|
-
vi.useFakeTimers()
|
|
488
|
-
|
|
489
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
490
|
-
const u = url.toString()
|
|
491
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
492
|
-
return new Response(JSON.stringify({
|
|
493
|
-
error: false,
|
|
494
|
-
results: [{ identifier: 'p1', error: true, person: null }],
|
|
495
|
-
total_cost: 0,
|
|
496
|
-
}), { status: 200 })
|
|
497
|
-
}
|
|
498
|
-
if (u.includes('/fullenrich/contact/enrich/bulk') && !u.includes('/fullenrich/contact/enrich/bulk/')) {
|
|
499
|
-
// Throw so fullEnrichStep rejects
|
|
500
|
-
throw new Error('FullEnrich network failure')
|
|
501
|
-
}
|
|
502
|
-
if (u.includes('/findymail/search/name')) {
|
|
503
|
-
return new Response(JSON.stringify({ email: 'alice@example.com' }), { status: 200 })
|
|
504
|
-
}
|
|
505
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
506
|
-
}) as typeof fetch
|
|
507
|
-
|
|
508
|
-
const resultPromise = findEmailsHandler({
|
|
509
|
-
people: [{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'example.com' }],
|
|
510
|
-
})
|
|
511
|
-
|
|
512
|
-
// Advance timers past the FullEnrich poll sleep
|
|
513
|
-
await vi.runAllTimersAsync()
|
|
514
|
-
const result = await resultPromise
|
|
515
|
-
|
|
516
|
-
vi.useRealTimers()
|
|
517
|
-
|
|
518
|
-
// Despite fullEnrichStep throwing, the handler must succeed
|
|
519
|
-
expect(result.isError).toBeFalsy()
|
|
520
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
521
|
-
expect(parsed.data.found).toBe(1)
|
|
522
|
-
expect(parsed.data.results[0]).toMatchObject({ id: 'p1', email: 'alice@example.com', provider: 'findymail' })
|
|
523
|
-
})
|
|
524
|
-
|
|
525
|
-
it('FullEnrich poll bails after 3 consecutive errors instead of polling to the 45s deadline', async () => {
|
|
526
|
-
// Regression: a 502-ing FullEnrich poll used to `continue` every 2s for the full
|
|
527
|
-
// 45s deadline (~22 polls) — pure latency on the chunk's critical path with zero
|
|
528
|
-
// emails. It must now stop after COLDIQ_MAX_POLL_ERRORS (default 3) consecutive errors.
|
|
529
|
-
vi.useFakeTimers()
|
|
530
|
-
|
|
531
|
-
let fePollCount = 0
|
|
532
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
533
|
-
const u = url.toString()
|
|
534
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
535
|
-
return new Response(
|
|
536
|
-
JSON.stringify({ error: false, results: [{ identifier: 'p1', error: true, person: null }], total_cost: 0 }),
|
|
537
|
-
{ status: 200 },
|
|
538
|
-
)
|
|
539
|
-
}
|
|
540
|
-
// FullEnrich poll (has the id segment) — always 502.
|
|
541
|
-
if (u.includes('/fullenrich/contact/enrich/bulk/abc-fe-err')) {
|
|
542
|
-
fePollCount++
|
|
543
|
-
return new Response(JSON.stringify({ error: 'bad gateway' }), { status: 502 })
|
|
544
|
-
}
|
|
545
|
-
// FullEnrich create.
|
|
546
|
-
if (u.includes('/fullenrich/contact/enrich/bulk')) {
|
|
547
|
-
return new Response(JSON.stringify({ enrichment_id: 'abc-fe-err' }), { status: 200 })
|
|
548
|
-
}
|
|
549
|
-
// FindyMail / IcyPeas / single-fallback all miss cleanly (no email, no failure).
|
|
550
|
-
return new Response(JSON.stringify({ email: null }), { status: 200 })
|
|
551
|
-
}) as typeof fetch
|
|
552
|
-
|
|
553
|
-
const handlerPromise = findEmailsHandler({
|
|
554
|
-
people: [{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'example.com' }],
|
|
555
|
-
})
|
|
556
|
-
|
|
557
|
-
await vi.runAllTimersAsync()
|
|
558
|
-
const result = await handlerPromise
|
|
559
|
-
vi.useRealTimers()
|
|
560
|
-
|
|
561
|
-
// Bailed at the 3-error cap, not the ~22 polls that 45000/2000 would have produced.
|
|
562
|
-
expect(fePollCount).toBe(3)
|
|
563
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
564
|
-
expect(parsed._meta.providers.fullenrich).toEqual({ attempted: 1, failed: 1 })
|
|
565
|
-
})
|
|
566
|
-
|
|
567
|
-
it('falls back to single find_email waterfall when bulk providers all miss (covers limadata/blitzapi/linkupapi gap)', async () => {
|
|
568
|
-
// Bulk path: Prospeo + FullEnrich + Findymail + Icypeas all miss.
|
|
569
|
-
// Single-only providers (limadata-work-email, blitzapi, limadata-work-email-linkedin, linkupapi)
|
|
570
|
-
// should be tried; blitzapi finds the email.
|
|
571
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
572
|
-
const u = url.toString()
|
|
573
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
574
|
-
return new Response(
|
|
575
|
-
JSON.stringify({ error: false, results: [{ identifier: 'p1', error: true, person: null }], total_cost: 0 }),
|
|
576
|
-
{ status: 200 },
|
|
577
|
-
)
|
|
578
|
-
}
|
|
579
|
-
if (u.includes('/fullenrich/contact/enrich/bulk')) {
|
|
580
|
-
// No enrichment_id returned → fullEnrichStep exits early without polling
|
|
581
|
-
return new Response(JSON.stringify({}), { status: 200 })
|
|
582
|
-
}
|
|
583
|
-
if (u.includes('/findymail/search/name')) {
|
|
584
|
-
return new Response(JSON.stringify({ email: null }), { status: 200 })
|
|
585
|
-
}
|
|
586
|
-
if (u.includes('/icypeas/email-search')) {
|
|
587
|
-
return new Response(JSON.stringify({ email: null }), { status: 200 })
|
|
588
|
-
}
|
|
589
|
-
// Single-only providers — limadata first (per priority), then blitzapi.
|
|
590
|
-
if (u.includes('/coldiq/find/work-email-linkedin')) {
|
|
591
|
-
return new Response(JSON.stringify({ email: null }), { status: 200 })
|
|
592
|
-
}
|
|
593
|
-
if (u.includes('/coldiq/find/work-email')) {
|
|
594
|
-
return new Response(JSON.stringify({ email: null }), { status: 200 })
|
|
595
|
-
}
|
|
596
|
-
if (u.includes('/blitzapi/enrichment/find-work-email')) {
|
|
597
|
-
return new Response(JSON.stringify({ email: 'alice@example.com' }), { status: 200 })
|
|
598
|
-
}
|
|
599
|
-
if (u.includes('/linkupapi/data/mail/finder')) {
|
|
600
|
-
return new Response(JSON.stringify({ email: null }), { status: 200 })
|
|
601
|
-
}
|
|
602
|
-
return new Response(JSON.stringify({ error: 'unexpected', url: u }), { status: 500 })
|
|
603
|
-
}) as typeof fetch
|
|
604
|
-
|
|
605
|
-
const result = await findEmailsHandler({
|
|
606
|
-
people: [{
|
|
607
|
-
id: 'p1',
|
|
608
|
-
first_name: 'Alice',
|
|
609
|
-
last_name: 'Smith',
|
|
610
|
-
domain: 'example.com',
|
|
611
|
-
linkedin_url: 'https://linkedin.com/in/alice',
|
|
612
|
-
}],
|
|
613
|
-
})
|
|
614
|
-
|
|
615
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
616
|
-
expect(parsed.data.found).toBe(1)
|
|
617
|
-
expect(parsed.data.results[0]).toMatchObject({ id: 'p1', email: 'alice@example.com', provider: 'blitzapi' })
|
|
618
|
-
})
|
|
619
|
-
})
|
|
620
|
-
|
|
621
|
-
describe('find_emails handler — chunking', () => {
|
|
622
|
-
const originalFetch = globalThis.fetch
|
|
623
|
-
|
|
624
|
-
beforeEach(() => {
|
|
625
|
-
initClient('http://test-api.local', 'test-key')
|
|
626
|
-
})
|
|
627
|
-
|
|
628
|
-
afterEach(() => {
|
|
629
|
-
globalThis.fetch = originalFetch
|
|
630
|
-
})
|
|
631
|
-
|
|
632
|
-
function makePeople(n: number): Array<{ id: string; first_name: string; last_name: string; domain: string }> {
|
|
633
|
-
return Array.from({ length: n }, (_, i) => ({
|
|
634
|
-
id: `p${i + 1}`,
|
|
635
|
-
first_name: `First${i + 1}`,
|
|
636
|
-
last_name: `Last${i + 1}`,
|
|
637
|
-
domain: 'example.com',
|
|
638
|
-
}))
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
it('splits 25 people into 3 chunks of ≤10 and issues one Prospeo bulk per chunk', async () => {
|
|
642
|
-
const bulkBodies: Array<{ data: Array<{ identifier: string }> }> = []
|
|
643
|
-
|
|
644
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
645
|
-
const u = url.toString()
|
|
646
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
647
|
-
const body = JSON.parse(init!.body as string) as { data: Array<{ identifier: string }> }
|
|
648
|
-
bulkBodies.push(body)
|
|
649
|
-
return new Response(
|
|
650
|
-
JSON.stringify({
|
|
651
|
-
error: false,
|
|
652
|
-
results: body.data.map((d) => ({
|
|
653
|
-
identifier: d.identifier,
|
|
654
|
-
error: false,
|
|
655
|
-
person: { email: { email: `${d.identifier}@example.com` } },
|
|
656
|
-
})),
|
|
657
|
-
total_cost: body.data.length,
|
|
658
|
-
}),
|
|
659
|
-
{ status: 200 },
|
|
660
|
-
)
|
|
661
|
-
}
|
|
662
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
663
|
-
}) as typeof fetch
|
|
664
|
-
|
|
665
|
-
const result = await findEmailsHandler({ people: makePeople(25) })
|
|
666
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
667
|
-
|
|
668
|
-
expect(bulkBodies.length).toBe(3)
|
|
669
|
-
expect(bulkBodies[0]!.data.length).toBeLessThanOrEqual(FIND_EMAILS_CHUNK_SIZE)
|
|
670
|
-
expect(bulkBodies[1]!.data.length).toBeLessThanOrEqual(FIND_EMAILS_CHUNK_SIZE)
|
|
671
|
-
expect(bulkBodies[2]!.data.length).toBeLessThanOrEqual(FIND_EMAILS_CHUNK_SIZE)
|
|
672
|
-
expect(bulkBodies.flatMap((b) => b.data.map((d) => d.identifier)).sort()).toEqual(
|
|
673
|
-
makePeople(25).map((p) => p.id).sort(),
|
|
674
|
-
)
|
|
675
|
-
|
|
676
|
-
expect(parsed.data.found).toBe(25)
|
|
677
|
-
expect(parsed.data.total).toBe(25)
|
|
678
|
-
expect(parsed._meta.batch_status).toBe('complete')
|
|
679
|
-
expect(parsed._meta.chunk_count).toBe(3)
|
|
680
|
-
expect(parsed._meta.chunk_size).toBe(FIND_EMAILS_CHUNK_SIZE)
|
|
681
|
-
expect(parsed._meta.providers.prospeo).toEqual({ attempted: 3, failed: 0 })
|
|
682
|
-
expect(result.isError).toBeUndefined()
|
|
683
|
-
})
|
|
684
|
-
|
|
685
|
-
it('one chunk Prospeo network failure → batch_status="partial" + isError absent when others succeed', async () => {
|
|
686
|
-
// First Prospeo bulk call simulates a network failure (status 0 via a fetch throw).
|
|
687
|
-
// Remaining chunks succeed. FindyMail picks up the failed chunk's misses.
|
|
688
|
-
let bulkCallCount = 0
|
|
689
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
690
|
-
const u = url.toString()
|
|
691
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
692
|
-
bulkCallCount++
|
|
693
|
-
if (bulkCallCount === 1) {
|
|
694
|
-
// Simulate network failure → client returns { ok: false, status: 0 }
|
|
695
|
-
throw new Error('socket hang up')
|
|
696
|
-
}
|
|
697
|
-
const body = JSON.parse(init!.body as string) as { data: Array<{ identifier: string }> }
|
|
698
|
-
return new Response(
|
|
699
|
-
JSON.stringify({
|
|
700
|
-
error: false,
|
|
701
|
-
results: body.data.map((d) => ({
|
|
702
|
-
identifier: d.identifier,
|
|
703
|
-
error: false,
|
|
704
|
-
person: { email: { email: `${d.identifier}@example.com` } },
|
|
705
|
-
})),
|
|
706
|
-
total_cost: body.data.length,
|
|
707
|
-
}),
|
|
708
|
-
{ status: 200 },
|
|
709
|
-
)
|
|
710
|
-
}
|
|
711
|
-
// FullEnrich create call for the failed chunk's misses — no enrichment_id so it bails fast.
|
|
712
|
-
if (u.includes('/fullenrich/contact/enrich/bulk')) {
|
|
713
|
-
return new Response(JSON.stringify({}), { status: 200 })
|
|
714
|
-
}
|
|
715
|
-
if (u.includes('/findymail/search/name')) {
|
|
716
|
-
return new Response(JSON.stringify({ email: 'recovered@example.com' }), { status: 200 })
|
|
717
|
-
}
|
|
718
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
719
|
-
}) as typeof fetch
|
|
720
|
-
|
|
721
|
-
const result = await findEmailsHandler({ people: makePeople(25) })
|
|
722
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
723
|
-
|
|
724
|
-
expect(parsed._meta.providers.prospeo.attempted).toBe(3)
|
|
725
|
-
expect(parsed._meta.providers.prospeo.failed).toBe(1)
|
|
726
|
-
expect(parsed._meta.batch_status).toBe('partial')
|
|
727
|
-
expect(parsed.data.found).toBeGreaterThan(0)
|
|
728
|
-
expect(result.isError).toBeUndefined()
|
|
729
|
-
})
|
|
730
|
-
|
|
731
|
-
it('every chunk fails → batch_status="failed" and isError:true even unconstrained', async () => {
|
|
732
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
733
|
-
const u = url.toString()
|
|
734
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
735
|
-
// 502 → counted as a network-level failure
|
|
736
|
-
return new Response(JSON.stringify({ error: 'upstream down' }), { status: 502 })
|
|
737
|
-
}
|
|
738
|
-
if (u.includes('/fullenrich/contact/enrich/bulk')) {
|
|
739
|
-
return new Response(JSON.stringify({ error: 'down' }), { status: 502 })
|
|
740
|
-
}
|
|
741
|
-
if (u.includes('/findymail/search/name')) {
|
|
742
|
-
return new Response(JSON.stringify({ error: 'down' }), { status: 502 })
|
|
743
|
-
}
|
|
744
|
-
if (u.includes('/icypeas/email-search')) {
|
|
745
|
-
return new Response(JSON.stringify({ error: 'down' }), { status: 502 })
|
|
746
|
-
}
|
|
747
|
-
// Single-only fallbacks: all return 502 so executeWithFallback yields an error.
|
|
748
|
-
return new Response(JSON.stringify({ error: 'down' }), { status: 502 })
|
|
749
|
-
}) as typeof fetch
|
|
750
|
-
|
|
751
|
-
const result = await findEmailsHandler({ people: makePeople(15) })
|
|
752
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
753
|
-
|
|
754
|
-
expect(parsed.data.found).toBe(0)
|
|
755
|
-
expect(parsed._meta.batch_status).toBe('failed')
|
|
756
|
-
expect(parsed._meta.providers.prospeo.failed).toBeGreaterThan(0)
|
|
757
|
-
expect(result.isError).toBe(true)
|
|
758
|
-
})
|
|
759
|
-
|
|
760
|
-
it('legitimate "no coverage" (every provider 200s with no email) stays batch_status="complete" and is NOT an error', async () => {
|
|
761
|
-
// Distinguishing failure from no-coverage: providers respond cleanly but find no email.
|
|
762
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
763
|
-
const u = url.toString()
|
|
764
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
765
|
-
return new Response(
|
|
766
|
-
JSON.stringify({ error: false, results: [], total_cost: 0 }),
|
|
767
|
-
{ status: 200 },
|
|
768
|
-
)
|
|
769
|
-
}
|
|
770
|
-
if (u.includes('/fullenrich/contact/enrich/bulk')) {
|
|
771
|
-
return new Response(JSON.stringify({}), { status: 200 })
|
|
772
|
-
}
|
|
773
|
-
if (u.includes('/findymail/search/name')) {
|
|
774
|
-
return new Response(JSON.stringify({ email: null }), { status: 200 })
|
|
775
|
-
}
|
|
776
|
-
if (u.includes('/icypeas/email-search')) {
|
|
777
|
-
return new Response(JSON.stringify({ email: null }), { status: 200 })
|
|
778
|
-
}
|
|
779
|
-
return new Response(JSON.stringify({ email: null }), { status: 200 })
|
|
780
|
-
}) as typeof fetch
|
|
781
|
-
|
|
782
|
-
const result = await findEmailsHandler({ people: makePeople(12) })
|
|
783
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
784
|
-
|
|
785
|
-
expect(parsed.data.found).toBe(0)
|
|
786
|
-
expect(parsed._meta.batch_status).toBe('complete')
|
|
787
|
-
expect(parsed._meta.providers.prospeo.failed).toBe(0)
|
|
788
|
-
expect(parsed._meta.providers.findymail.failed).toBe(0)
|
|
789
|
-
expect(result.isError).toBeUndefined()
|
|
790
|
-
})
|
|
791
|
-
|
|
792
|
-
it('small input (≤10) still works and emits _meta without chunking overhead', async () => {
|
|
793
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
794
|
-
const u = url.toString()
|
|
795
|
-
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
796
|
-
return new Response(
|
|
797
|
-
JSON.stringify({
|
|
798
|
-
error: false,
|
|
799
|
-
results: [{ identifier: 'p1', error: false, person: { email: { email: 'alice@example.com' } } }],
|
|
800
|
-
total_cost: 1,
|
|
801
|
-
}),
|
|
802
|
-
{ status: 200 },
|
|
803
|
-
)
|
|
804
|
-
}
|
|
805
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
806
|
-
}) as typeof fetch
|
|
807
|
-
|
|
808
|
-
const result = await findEmailsHandler({
|
|
809
|
-
people: [{ id: 'p1', first_name: 'Alice', domain: 'example.com' }],
|
|
810
|
-
})
|
|
811
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
812
|
-
|
|
813
|
-
expect(parsed._meta.chunk_count).toBe(1)
|
|
814
|
-
expect(parsed._meta.batch_status).toBe('complete')
|
|
815
|
-
expect(parsed._meta.providers.prospeo).toEqual({ attempted: 1, failed: 0 })
|
|
816
|
-
expect(result.isError).toBeUndefined()
|
|
817
|
-
})
|
|
818
|
-
})
|