@coldiq/mcp 0.1.0 → 0.1.1
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/README.md +126 -0
- package/package.json +1 -1
- package/src/client.ts +4 -1
- package/src/executor.ts +46 -24
- package/src/registry.ts +7 -74
- package/src/tools/find-email.ts +32 -2
- package/src/tools/find-emails.ts +4 -3
- package/src/tools/find-signals.ts +1 -1
- package/test-ads-live.ts +3 -2
- package/test-company-live.ts +3 -2
- package/test-email-live.ts +13 -12
- package/test-influencers-live.ts +3 -2
- package/test-jobs-live.ts +3 -2
- package/test-linkupapi-live.ts +3 -2
- package/test-phone-live.ts +3 -2
- package/test-places-live.ts +3 -2
- package/test-reddit-live.ts +3 -2
- package/test-search-live.ts +3 -2
- package/test-seo-live.ts +3 -2
- package/test-web-live.ts +3 -2
- package/tests/client.test.ts +1 -1
- package/tests/executor.test.ts +227 -0
- package/tests/registry.test.ts +1 -77
- package/tests/tools/{enrich-email.test.ts → find-email.test.ts} +5 -5
- package/tests/tools/{enrich-emails.test.ts → find-emails.test.ts} +16 -15
- package/tests/tools/find-people.test.ts +12 -12
- package/tests/tools/search-places.test.ts +4 -4
- package/tests/tools/search-reddit.test.ts +6 -6
package/test-web-live.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { initClient } from './src/client.js'
|
|
2
2
|
import { searchWebHandler } from './src/tools/search-web.js'
|
|
3
3
|
|
|
4
|
-
const API_URL = 'https://api.coldiq.com'
|
|
5
|
-
const API_KEY =
|
|
4
|
+
const API_URL = process.env.COLDIQ_API_URL ?? 'https://api.coldiq.com'
|
|
5
|
+
const API_KEY = process.env.COLDIQ_API_KEY
|
|
6
|
+
if (!API_KEY) { console.error('COLDIQ_API_KEY env var is required'); process.exit(1) }
|
|
6
7
|
|
|
7
8
|
initClient(API_URL, API_KEY)
|
|
8
9
|
|
package/tests/client.test.ts
CHANGED
|
@@ -62,7 +62,7 @@ describe('client', () => {
|
|
|
62
62
|
|
|
63
63
|
expect(result.ok).toBe(false)
|
|
64
64
|
expect(result.status).toBe(0)
|
|
65
|
-
expect((result.data as Record<string, unknown>).error).toBe('Network
|
|
65
|
+
expect((result.data as Record<string, unknown>).error).toBe('Network error')
|
|
66
66
|
})
|
|
67
67
|
|
|
68
68
|
it('returns parsed JSON on success', async () => {
|
package/tests/executor.test.ts
CHANGED
|
@@ -1,6 +1,233 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
2
|
import { initClient } from '../src/client.js'
|
|
3
3
|
import { executeWithFallback } from '../src/executor.js'
|
|
4
|
+
import * as registry from '../src/registry.js'
|
|
5
|
+
import type { ProviderEntry } from '../src/registry.js'
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Helpers for synthetic provider injection
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
function makeProvider(overrides: Partial<ProviderEntry> & { id: string }): ProviderEntry {
|
|
12
|
+
return {
|
|
13
|
+
endpoint: '/test',
|
|
14
|
+
method: 'POST',
|
|
15
|
+
priority: 1,
|
|
16
|
+
mapParams: () => ({ body: {} }),
|
|
17
|
+
hasResult: (data) => (data as Record<string, unknown>).ok === true,
|
|
18
|
+
...overrides,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stubProviders(providers: ProviderEntry[]) {
|
|
23
|
+
return vi.spyOn(registry, 'getProviders').mockReturnValueOnce(providers)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Exception safety — mapParams / hasResult / postFilter / isApplicable
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
describe('executor exception safety', () => {
|
|
31
|
+
const originalFetch = globalThis.fetch
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
initClient('http://test-api.local', 'test-key-123')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
globalThis.fetch = originalFetch
|
|
39
|
+
vi.restoreAllMocks()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('mapParams throws → skips to next provider', async () => {
|
|
43
|
+
stubProviders([
|
|
44
|
+
makeProvider({
|
|
45
|
+
id: 'bad',
|
|
46
|
+
mapParams: () => { throw new Error('mapParams explosion') },
|
|
47
|
+
hasResult: () => false,
|
|
48
|
+
}),
|
|
49
|
+
makeProvider({
|
|
50
|
+
id: 'good',
|
|
51
|
+
mapParams: () => ({ body: {} }),
|
|
52
|
+
hasResult: (data) => (data as Record<string, unknown>).ok === true,
|
|
53
|
+
}),
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
globalThis.fetch = vi.fn(async () =>
|
|
57
|
+
new Response(JSON.stringify({ ok: true }), { status: 200 }),
|
|
58
|
+
) as typeof fetch
|
|
59
|
+
|
|
60
|
+
const result = await executeWithFallback('enrich_company', { domain: 'coldiq.com' })
|
|
61
|
+
|
|
62
|
+
expect('data' in result).toBe(true)
|
|
63
|
+
if ('data' in result) {
|
|
64
|
+
expect(result._meta.provider).toBe('good')
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('hasResult throws → treated as no result, next provider tried', async () => {
|
|
69
|
+
stubProviders([
|
|
70
|
+
makeProvider({
|
|
71
|
+
id: 'exploding-has-result',
|
|
72
|
+
mapParams: () => ({ body: {} }),
|
|
73
|
+
hasResult: () => { throw new Error('hasResult explosion') },
|
|
74
|
+
}),
|
|
75
|
+
makeProvider({
|
|
76
|
+
id: 'safe',
|
|
77
|
+
mapParams: () => ({ body: {} }),
|
|
78
|
+
hasResult: (data) => (data as Record<string, unknown>).ok === true,
|
|
79
|
+
}),
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
globalThis.fetch = vi.fn(async () =>
|
|
83
|
+
new Response(JSON.stringify({ ok: true }), { status: 200 }),
|
|
84
|
+
) as typeof fetch
|
|
85
|
+
|
|
86
|
+
const result = await executeWithFallback('enrich_company', { domain: 'coldiq.com' })
|
|
87
|
+
|
|
88
|
+
expect('data' in result).toBe(true)
|
|
89
|
+
if ('data' in result) {
|
|
90
|
+
expect(result._meta.provider).toBe('safe')
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('postFilter throws → falls back to raw payload, success path continues', async () => {
|
|
95
|
+
stubProviders([
|
|
96
|
+
makeProvider({
|
|
97
|
+
id: 'exploding-post-filter',
|
|
98
|
+
mapParams: () => ({ body: {} }),
|
|
99
|
+
hasResult: (data) => (data as Record<string, unknown>).ok === true,
|
|
100
|
+
postFilter: () => { throw new Error('postFilter explosion') },
|
|
101
|
+
}),
|
|
102
|
+
])
|
|
103
|
+
|
|
104
|
+
globalThis.fetch = vi.fn(async () =>
|
|
105
|
+
new Response(JSON.stringify({ ok: true }), { status: 200 }),
|
|
106
|
+
) as typeof fetch
|
|
107
|
+
|
|
108
|
+
// postFilter throws → raw payload is used → hasResult(raw) should still return true
|
|
109
|
+
const result = await executeWithFallback('enrich_company', { domain: 'coldiq.com' })
|
|
110
|
+
|
|
111
|
+
expect('data' in result).toBe(true)
|
|
112
|
+
if ('data' in result) {
|
|
113
|
+
expect(result._meta.provider).toBe('exploding-post-filter')
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('isApplicable=false → provider skipped and NOT recorded in providers_tried', async () => {
|
|
118
|
+
stubProviders([
|
|
119
|
+
makeProvider({
|
|
120
|
+
id: 'skipped',
|
|
121
|
+
isApplicable: () => false,
|
|
122
|
+
hasResult: () => false,
|
|
123
|
+
}),
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
globalThis.fetch = vi.fn(async () =>
|
|
127
|
+
new Response(JSON.stringify({ ok: false }), { status: 200 }),
|
|
128
|
+
) as typeof fetch
|
|
129
|
+
|
|
130
|
+
const result = await executeWithFallback('enrich_company', { domain: 'coldiq.com' })
|
|
131
|
+
|
|
132
|
+
expect('error' in result).toBe(true)
|
|
133
|
+
if ('error' in result) {
|
|
134
|
+
// Skipped providers do not appear in providers_tried
|
|
135
|
+
expect(result.providers_tried.length).toBe(0)
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('isApplicable throws → provider skipped gracefully', async () => {
|
|
140
|
+
stubProviders([
|
|
141
|
+
makeProvider({
|
|
142
|
+
id: 'throwing-applicable',
|
|
143
|
+
isApplicable: () => { throw new Error('isApplicable explosion') },
|
|
144
|
+
hasResult: () => false,
|
|
145
|
+
}),
|
|
146
|
+
makeProvider({
|
|
147
|
+
id: 'safe',
|
|
148
|
+
hasResult: (data) => (data as Record<string, unknown>).ok === true,
|
|
149
|
+
}),
|
|
150
|
+
])
|
|
151
|
+
|
|
152
|
+
globalThis.fetch = vi.fn(async () =>
|
|
153
|
+
new Response(JSON.stringify({ ok: true }), { status: 200 }),
|
|
154
|
+
) as typeof fetch
|
|
155
|
+
|
|
156
|
+
const result = await executeWithFallback('enrich_company', { domain: 'coldiq.com' })
|
|
157
|
+
|
|
158
|
+
expect('data' in result).toBe(true)
|
|
159
|
+
if ('data' in result) {
|
|
160
|
+
expect(result._meta.provider).toBe('safe')
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('all providers fail → providers_tried uses provider_N keys (not internal ids)', async () => {
|
|
165
|
+
stubProviders([
|
|
166
|
+
makeProvider({ id: 'alpha', hasResult: () => false }),
|
|
167
|
+
makeProvider({ id: 'beta', hasResult: () => false }),
|
|
168
|
+
])
|
|
169
|
+
|
|
170
|
+
globalThis.fetch = vi.fn(async () =>
|
|
171
|
+
new Response(JSON.stringify({ ok: false }), { status: 200 }),
|
|
172
|
+
) as typeof fetch
|
|
173
|
+
|
|
174
|
+
const result = await executeWithFallback('enrich_company', { domain: 'coldiq.com' })
|
|
175
|
+
|
|
176
|
+
expect('error' in result).toBe(true)
|
|
177
|
+
if ('error' in result) {
|
|
178
|
+
expect(result.providers_tried).toHaveLength(2)
|
|
179
|
+
expect(result.providers_tried[0].provider).toBe('provider_1')
|
|
180
|
+
expect(result.providers_tried[1].provider).toBe('provider_2')
|
|
181
|
+
expect('id' in result.providers_tried[0]).toBe(false)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('async create returns 4xx → recorded as failure, next provider tried', async () => {
|
|
186
|
+
stubProviders([
|
|
187
|
+
makeProvider({
|
|
188
|
+
id: 'async-fail',
|
|
189
|
+
hasResult: () => false,
|
|
190
|
+
async: {
|
|
191
|
+
extractId: () => 'job-1',
|
|
192
|
+
pollEndpoint: (id) => `/poll/${id}`,
|
|
193
|
+
pollIntervalMs: 10,
|
|
194
|
+
timeoutMs: 50,
|
|
195
|
+
isComplete: () => true,
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
makeProvider({
|
|
199
|
+
id: 'sync-success',
|
|
200
|
+
hasResult: (data) => (data as Record<string, unknown>).ok === true,
|
|
201
|
+
}),
|
|
202
|
+
])
|
|
203
|
+
|
|
204
|
+
let callCount = 0
|
|
205
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
206
|
+
callCount++
|
|
207
|
+
const u = url.toString()
|
|
208
|
+
if (u.includes('/test') && !u.includes('/poll/')) {
|
|
209
|
+
if (callCount === 1) {
|
|
210
|
+
// async-fail create — returns 4xx so executor skips polling
|
|
211
|
+
return new Response(JSON.stringify({ error: 'quota exceeded' }), { status: 429 })
|
|
212
|
+
}
|
|
213
|
+
// sync-success
|
|
214
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 })
|
|
215
|
+
}
|
|
216
|
+
return new Response(JSON.stringify({}), { status: 200 })
|
|
217
|
+
}) as typeof fetch
|
|
218
|
+
|
|
219
|
+
const result = await executeWithFallback('enrich_company', { domain: 'coldiq.com' })
|
|
220
|
+
|
|
221
|
+
expect('data' in result).toBe(true)
|
|
222
|
+
if ('data' in result) {
|
|
223
|
+
expect(result._meta.provider).toBe('sync-success')
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// postFilter integration (existing tests)
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
4
231
|
|
|
5
232
|
describe('executor postFilter integration', () => {
|
|
6
233
|
const originalFetch = globalThis.fetch
|
package/tests/registry.test.ts
CHANGED
|
@@ -384,53 +384,6 @@ describe('registry', () => {
|
|
|
384
384
|
expect(sumble.isApplicable!({ countries: ['FR'] })).toBe(false)
|
|
385
385
|
})
|
|
386
386
|
|
|
387
|
-
it('Wiza creates a prospect list with [{v}] filter shape, translates ISO codes, and has async config', () => {
|
|
388
|
-
const providers = getProviders('search_companies')
|
|
389
|
-
const wiza = providers.find((p) => p.id === 'wiza')!
|
|
390
|
-
const result = wiza.mapParams({
|
|
391
|
-
industries: ['SaaS'],
|
|
392
|
-
countries: ['FR'],
|
|
393
|
-
min_founded_year: 2015,
|
|
394
|
-
limit: 25,
|
|
395
|
-
})
|
|
396
|
-
expect(result.body).toEqual({
|
|
397
|
-
list: { name: 'mcp-search-companies', max_profiles: 25 },
|
|
398
|
-
filters: {
|
|
399
|
-
company_industry: [{ v: 'SaaS' }],
|
|
400
|
-
company_location: [{ v: 'France' }],
|
|
401
|
-
year_founded_start: [{ v: '2015' }],
|
|
402
|
-
},
|
|
403
|
-
enrichment_level: 'none',
|
|
404
|
-
})
|
|
405
|
-
expect(wiza.async).toBeDefined()
|
|
406
|
-
expect(wiza.async!.pollEndpoint('123')).toBe('/wiza/lists/123')
|
|
407
|
-
expect(wiza.async!.extractId({ data: { id: 123 } })).toBe('123')
|
|
408
|
-
})
|
|
409
|
-
|
|
410
|
-
it('Wiza isComplete accepts both finished and completed terminal states', () => {
|
|
411
|
-
const providers = getProviders('search_companies')
|
|
412
|
-
const wiza = providers.find((p) => p.id === 'wiza')!
|
|
413
|
-
expect(wiza.async!.isComplete({ data: { status: 'finished' } })).toBe(true)
|
|
414
|
-
expect(wiza.async!.isComplete({ data: { status: 'completed' } })).toBe(true)
|
|
415
|
-
expect(wiza.async!.isComplete({ data: { status: 'failed' } })).toBe(true)
|
|
416
|
-
expect(wiza.async!.isComplete({ data: { status: 'scraping' } })).toBe(false)
|
|
417
|
-
expect(wiza.async!.isComplete({ data: { status: 'queued' } })).toBe(false)
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
it('Wiza extractId throws when no list id is present', () => {
|
|
421
|
-
const providers = getProviders('search_companies')
|
|
422
|
-
const wiza = providers.find((p) => p.id === 'wiza')!
|
|
423
|
-
expect(() => wiza.async!.extractId({})).toThrow('Wiza response has no list id')
|
|
424
|
-
expect(() => wiza.async!.extractId({ data: {} })).toThrow('Wiza response has no list id')
|
|
425
|
-
})
|
|
426
|
-
|
|
427
|
-
it('Wiza hasResult checks data.stats.people > 0', () => {
|
|
428
|
-
const providers = getProviders('search_companies')
|
|
429
|
-
const wiza = providers.find((p) => p.id === 'wiza')!
|
|
430
|
-
expect(wiza.hasResult({ data: { status: 'finished', stats: { people: 10 } } })).toBe(true)
|
|
431
|
-
expect(wiza.hasResult({ data: { status: 'finished', stats: { people: 0 } } })).toBe(false)
|
|
432
|
-
expect(wiza.hasResult({ data: { status: 'finished' } })).toBe(false)
|
|
433
|
-
})
|
|
434
387
|
|
|
435
388
|
it('LimaData prospect filter maps employees to company_headcount filter_type', () => {
|
|
436
389
|
const providers = getProviders('search_companies')
|
|
@@ -464,14 +417,13 @@ describe('registry', () => {
|
|
|
464
417
|
expect(ld.isApplicable!({ linkedin_search_url: 'https://...' })).toBe(true)
|
|
465
418
|
})
|
|
466
419
|
|
|
467
|
-
it('search_companies has
|
|
420
|
+
it('search_companies has 17 providers in priority order', () => {
|
|
468
421
|
const providers = getProviders('search_companies')
|
|
469
422
|
expect(providers.map((p) => p.id)).toEqual([
|
|
470
423
|
'companyenrich',
|
|
471
424
|
'fullenrich',
|
|
472
425
|
'pdl',
|
|
473
426
|
'theirstack',
|
|
474
|
-
'wiza',
|
|
475
427
|
'signalbase',
|
|
476
428
|
'blitzapi',
|
|
477
429
|
'apollo',
|
|
@@ -1176,16 +1128,6 @@ describe('registry', () => {
|
|
|
1176
1128
|
expect(ts.isApplicable!({ countries: ['FR'] })).toBe(false)
|
|
1177
1129
|
})
|
|
1178
1130
|
|
|
1179
|
-
it('Wiza isApplicable: fires for min/max_founded_year and funding filters', () => {
|
|
1180
|
-
const wiza = getProviders('search_companies').find((p) => p.id === 'wiza')!
|
|
1181
|
-
expect(wiza.isApplicable!({})).toBe(false)
|
|
1182
|
-
expect(wiza.isApplicable!({ keywords: ['x'] })).toBe(false)
|
|
1183
|
-
expect(wiza.isApplicable!({ min_founded_year: 2015 })).toBe(true)
|
|
1184
|
-
expect(wiza.isApplicable!({ max_founded_year: 2020 })).toBe(true)
|
|
1185
|
-
expect(wiza.isApplicable!({ funding_stages: ['seed'] })).toBe(true)
|
|
1186
|
-
expect(wiza.isApplicable!({ min_funding_amount: 500_000 })).toBe(true)
|
|
1187
|
-
expect(wiza.isApplicable!({ max_funding_amount: 10_000_000 })).toBe(true)
|
|
1188
|
-
})
|
|
1189
1131
|
|
|
1190
1132
|
it('Apollo postFilter strips accounts key entirely', () => {
|
|
1191
1133
|
const apollo = getProviders('search_companies').find((p) => p.id === 'apollo')!
|
|
@@ -1327,24 +1269,6 @@ describe('registry', () => {
|
|
|
1327
1269
|
expect((result.body as Record<string, unknown>).exclude).toEqual({ domains: ['a.com'] })
|
|
1328
1270
|
})
|
|
1329
1271
|
|
|
1330
|
-
it('Wiza mapParams adds funding_stage, funding_min, funding_max in [{v}] shape', () => {
|
|
1331
|
-
const wiza = getProviders('search_companies').find((p) => p.id === 'wiza')!
|
|
1332
|
-
const result = wiza.mapParams({
|
|
1333
|
-
funding_stages: ['seed', 'series_a'],
|
|
1334
|
-
min_funding_amount: 500_000,
|
|
1335
|
-
max_funding_amount: 20_000_000,
|
|
1336
|
-
limit: 25,
|
|
1337
|
-
})
|
|
1338
|
-
expect(result.body).toEqual({
|
|
1339
|
-
list: { name: 'mcp-search-companies', max_profiles: 25 },
|
|
1340
|
-
filters: {
|
|
1341
|
-
funding_stage: [{ v: 'seed' }, { v: 'series_a' }],
|
|
1342
|
-
funding_min: [{ v: '500000' }],
|
|
1343
|
-
funding_max: [{ v: '20000000' }],
|
|
1344
|
-
},
|
|
1345
|
-
enrichment_level: 'none',
|
|
1346
|
-
})
|
|
1347
|
-
})
|
|
1348
1272
|
|
|
1349
1273
|
it('LimaData passes has_jobs: true when is_hiring is true', () => {
|
|
1350
1274
|
const ld = getProviders('search_companies').find((p) => p.id === 'limadata')!
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
2
|
import { initClient } from '../../src/client.js'
|
|
3
|
-
import { findEmailHandler
|
|
3
|
+
import { findEmailHandler } from '../../src/tools/find-email.js'
|
|
4
4
|
|
|
5
5
|
describe('find_email handler (waterfall)', () => {
|
|
6
6
|
const originalFetch = globalThis.fetch
|
|
@@ -18,7 +18,7 @@ describe('find_email handler (waterfall)', () => {
|
|
|
18
18
|
new Response(JSON.stringify({ email: 'michel@coldiq.com' }), { status: 200 })
|
|
19
19
|
) as typeof fetch
|
|
20
20
|
|
|
21
|
-
const result = await
|
|
21
|
+
const result = await findEmailHandler({
|
|
22
22
|
first_name: 'Michel',
|
|
23
23
|
last_name: 'Lieben',
|
|
24
24
|
domain: 'coldiq.com',
|
|
@@ -41,7 +41,7 @@ describe('find_email handler (waterfall)', () => {
|
|
|
41
41
|
return new Response(JSON.stringify({ email: 'michel@coldiq.com' }), { status: 200 })
|
|
42
42
|
}) as typeof fetch
|
|
43
43
|
|
|
44
|
-
const result = await
|
|
44
|
+
const result = await findEmailHandler({
|
|
45
45
|
first_name: 'Michel',
|
|
46
46
|
last_name: 'Lieben',
|
|
47
47
|
domain: 'coldiq.com',
|
|
@@ -65,7 +65,7 @@ describe('find_email handler (waterfall)', () => {
|
|
|
65
65
|
)
|
|
66
66
|
}) as typeof fetch
|
|
67
67
|
|
|
68
|
-
const result = await
|
|
68
|
+
const result = await findEmailHandler({
|
|
69
69
|
first_name: 'Michel',
|
|
70
70
|
last_name: 'Lieben',
|
|
71
71
|
domain: 'coldiq.com',
|
|
@@ -80,7 +80,7 @@ describe('find_email handler (waterfall)', () => {
|
|
|
80
80
|
new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
|
|
81
81
|
) as typeof fetch
|
|
82
82
|
|
|
83
|
-
const result = await
|
|
83
|
+
const result = await findEmailHandler({
|
|
84
84
|
first_name: 'Unknown',
|
|
85
85
|
last_name: 'Person',
|
|
86
86
|
domain: 'unknown.xyz',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
2
|
import { initClient } from '../../src/client.js'
|
|
3
|
-
import { findEmailsHandler
|
|
3
|
+
import { findEmailsHandler } from '../../src/tools/find-emails.js'
|
|
4
4
|
|
|
5
5
|
describe('find_emails handler (bulk)', () => {
|
|
6
6
|
const originalFetch = globalThis.fetch
|
|
@@ -32,7 +32,7 @@ describe('find_emails handler (bulk)', () => {
|
|
|
32
32
|
return new Response(JSON.stringify({ error: 'unexpected call' }), { status: 500 })
|
|
33
33
|
}) as typeof fetch
|
|
34
34
|
|
|
35
|
-
const result = await
|
|
35
|
+
const result = await findEmailsHandler({
|
|
36
36
|
people: [
|
|
37
37
|
{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'example.com' },
|
|
38
38
|
{ id: 'p2', first_name: 'Bob', last_name: 'Jones', domain: 'example.com' },
|
|
@@ -71,7 +71,7 @@ describe('find_emails handler (bulk)', () => {
|
|
|
71
71
|
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
72
72
|
}) as typeof fetch
|
|
73
73
|
|
|
74
|
-
const result = await
|
|
74
|
+
const result = await findEmailsHandler({
|
|
75
75
|
people: [{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'example.com' }],
|
|
76
76
|
})
|
|
77
77
|
|
|
@@ -98,7 +98,7 @@ describe('find_emails handler (bulk)', () => {
|
|
|
98
98
|
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
99
99
|
}) as typeof fetch
|
|
100
100
|
|
|
101
|
-
const result = await
|
|
101
|
+
const result = await findEmailsHandler({
|
|
102
102
|
people: [{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'example.com' }],
|
|
103
103
|
})
|
|
104
104
|
|
|
@@ -111,7 +111,7 @@ describe('find_emails handler (bulk)', () => {
|
|
|
111
111
|
new Response(JSON.stringify({ error: true }), { status: 200 }),
|
|
112
112
|
) as typeof fetch
|
|
113
113
|
|
|
114
|
-
const result = await
|
|
114
|
+
const result = await findEmailsHandler({
|
|
115
115
|
people: [{ id: 'p1', first_name: 'Unknown', last_name: 'Person', domain: 'nowhere.xyz' }],
|
|
116
116
|
})
|
|
117
117
|
|
|
@@ -153,7 +153,7 @@ describe('find_emails handler (bulk)', () => {
|
|
|
153
153
|
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
154
154
|
}) as typeof fetch
|
|
155
155
|
|
|
156
|
-
const result = await
|
|
156
|
+
const result = await findEmailsHandler({
|
|
157
157
|
people: [
|
|
158
158
|
{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'a.com' },
|
|
159
159
|
{ id: 'p2', first_name: 'Bob', last_name: 'Jones', domain: 'b.com' },
|
|
@@ -187,7 +187,7 @@ describe('find_emails handler (bulk)', () => {
|
|
|
187
187
|
return new Response(JSON.stringify({ email: null }), { status: 200 })
|
|
188
188
|
}) as typeof fetch
|
|
189
189
|
|
|
190
|
-
await
|
|
190
|
+
await findEmailsHandler({
|
|
191
191
|
people: [{ id: 'p1', linkedin_url: 'https://linkedin.com/in/michel-lieben' }],
|
|
192
192
|
})
|
|
193
193
|
|
|
@@ -196,13 +196,13 @@ describe('find_emails handler (bulk)', () => {
|
|
|
196
196
|
expect(body.data[0].first_name).toBeUndefined()
|
|
197
197
|
})
|
|
198
198
|
|
|
199
|
-
it('uses FullEnrich batch for Prospeo misses and matches by
|
|
199
|
+
it('uses FullEnrich batch for Prospeo misses and matches by custom_id', async () => {
|
|
200
200
|
vi.useFakeTimers()
|
|
201
201
|
|
|
202
|
-
let
|
|
202
|
+
let feCreateBody: unknown
|
|
203
203
|
let fePollCalled = false
|
|
204
204
|
|
|
205
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
205
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
206
206
|
const u = url.toString()
|
|
207
207
|
|
|
208
208
|
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
@@ -217,19 +217,19 @@ describe('find_emails handler (bulk)', () => {
|
|
|
217
217
|
fePollCalled = true
|
|
218
218
|
return new Response(JSON.stringify({
|
|
219
219
|
status: 'DONE',
|
|
220
|
-
data: [{ emails: ['alice@example.com'] }],
|
|
220
|
+
data: [{ custom_id: 'p1', emails: ['alice@example.com'] }],
|
|
221
221
|
}), { status: 200 })
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
if (u.includes('/fullenrich/contact/enrich/bulk')) {
|
|
225
|
-
|
|
225
|
+
feCreateBody = JSON.parse(init?.body as string)
|
|
226
226
|
return new Response(JSON.stringify({ enrichment_id: 'abc-fe-123' }), { status: 200 })
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
230
230
|
}) as typeof fetch
|
|
231
231
|
|
|
232
|
-
const handlerPromise =
|
|
232
|
+
const handlerPromise = findEmailsHandler({
|
|
233
233
|
people: [{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'example.com' }],
|
|
234
234
|
})
|
|
235
235
|
|
|
@@ -237,8 +237,9 @@ describe('find_emails handler (bulk)', () => {
|
|
|
237
237
|
const result = await handlerPromise
|
|
238
238
|
vi.useRealTimers()
|
|
239
239
|
|
|
240
|
-
expect(feCreateCalled).toBe(true)
|
|
241
240
|
expect(fePollCalled).toBe(true)
|
|
241
|
+
const body = feCreateBody as { data: Array<Record<string, unknown>> }
|
|
242
|
+
expect(body.data[0].custom_id).toBe('p1')
|
|
242
243
|
const parsed = JSON.parse(result.content[0].text)
|
|
243
244
|
expect(parsed.data.results[0]).toEqual({ id: 'p1', email: 'alice@example.com', provider: 'fullenrich' })
|
|
244
245
|
})
|
|
@@ -255,7 +256,7 @@ describe('find_emails handler (bulk)', () => {
|
|
|
255
256
|
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
256
257
|
}) as typeof fetch
|
|
257
258
|
|
|
258
|
-
const result = await
|
|
259
|
+
const result = await findEmailsHandler({
|
|
259
260
|
people: [{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'example.com' }],
|
|
260
261
|
})
|
|
261
262
|
|
|
@@ -43,10 +43,10 @@ describe('find_people handler', () => {
|
|
|
43
43
|
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
44
44
|
const originalTimeout = lf.async!.timeoutMs
|
|
45
45
|
const originalInterval = lf.async!.pollIntervalMs
|
|
46
|
-
lf.async!.timeoutMs = 100
|
|
47
|
-
lf.async!.pollIntervalMs = 20
|
|
48
46
|
|
|
49
47
|
try {
|
|
48
|
+
lf.async!.timeoutMs = 100
|
|
49
|
+
lf.async!.pollIntervalMs = 20
|
|
50
50
|
const result = await findPeopleHandler({
|
|
51
51
|
company_domains: ['coldiq.com'],
|
|
52
52
|
job_titles: ['CEO'],
|
|
@@ -96,10 +96,10 @@ describe('find_people handler', () => {
|
|
|
96
96
|
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
97
97
|
const originalTimeout = lf.async!.timeoutMs
|
|
98
98
|
const originalInterval = lf.async!.pollIntervalMs
|
|
99
|
-
lf.async!.timeoutMs = 500
|
|
100
|
-
lf.async!.pollIntervalMs = 20
|
|
101
99
|
|
|
102
100
|
try {
|
|
101
|
+
lf.async!.timeoutMs = 500
|
|
102
|
+
lf.async!.pollIntervalMs = 20
|
|
103
103
|
const result = await findPeopleHandler({
|
|
104
104
|
company_domains: ['coldiq.com'],
|
|
105
105
|
job_titles: ['CEO'],
|
|
@@ -150,10 +150,10 @@ describe('find_people handler', () => {
|
|
|
150
150
|
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
151
151
|
const originalTimeout = lf.async!.timeoutMs
|
|
152
152
|
const originalInterval = lf.async!.pollIntervalMs
|
|
153
|
-
lf.async!.timeoutMs = 500
|
|
154
|
-
lf.async!.pollIntervalMs = 20
|
|
155
153
|
|
|
156
154
|
try {
|
|
155
|
+
lf.async!.timeoutMs = 500
|
|
156
|
+
lf.async!.pollIntervalMs = 20
|
|
157
157
|
const result = await findPeopleHandler({
|
|
158
158
|
company_domains: ['coldiq.com', 'folk.app'],
|
|
159
159
|
job_titles: ['CEO'],
|
|
@@ -203,10 +203,10 @@ describe('find_people handler', () => {
|
|
|
203
203
|
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
204
204
|
const originalTimeout = lf.async!.timeoutMs
|
|
205
205
|
const originalInterval = lf.async!.pollIntervalMs
|
|
206
|
-
lf.async!.timeoutMs = 500
|
|
207
|
-
lf.async!.pollIntervalMs = 20
|
|
208
206
|
|
|
209
207
|
try {
|
|
208
|
+
lf.async!.timeoutMs = 500
|
|
209
|
+
lf.async!.pollIntervalMs = 20
|
|
210
210
|
const result = await findPeopleHandler({
|
|
211
211
|
company_linkedin_urls: ['https://linkedin.com/company/coldiq', 'https://linkedin.com/company/folk'],
|
|
212
212
|
job_titles: ['CEO'],
|
|
@@ -249,10 +249,10 @@ describe('find_people handler', () => {
|
|
|
249
249
|
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
250
250
|
const originalTimeout = lf.async!.timeoutMs
|
|
251
251
|
const originalInterval = lf.async!.pollIntervalMs
|
|
252
|
-
lf.async!.timeoutMs = 500
|
|
253
|
-
lf.async!.pollIntervalMs = 20
|
|
254
252
|
|
|
255
253
|
try {
|
|
254
|
+
lf.async!.timeoutMs = 500
|
|
255
|
+
lf.async!.pollIntervalMs = 20
|
|
256
256
|
await findPeopleHandler({
|
|
257
257
|
company_linkedin_urls: ['https://linkedin.com/company/coldiq', 'https://linkedin.com/company/folk'],
|
|
258
258
|
company_domains: ['coldiq.com', 'folk.app'],
|
|
@@ -295,10 +295,10 @@ describe('find_people handler', () => {
|
|
|
295
295
|
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
296
296
|
const originalTimeout = lf.async!.timeoutMs
|
|
297
297
|
const originalInterval = lf.async!.pollIntervalMs
|
|
298
|
-
lf.async!.timeoutMs = 500
|
|
299
|
-
lf.async!.pollIntervalMs = 20
|
|
300
298
|
|
|
301
299
|
try {
|
|
300
|
+
lf.async!.timeoutMs = 500
|
|
301
|
+
lf.async!.pollIntervalMs = 20
|
|
302
302
|
await findPeopleHandler({
|
|
303
303
|
company_domains: ['coldiq.com'],
|
|
304
304
|
job_titles: ['CEO', 'VP of Sales'],
|
|
@@ -49,10 +49,10 @@ describe('search_places handler', () => {
|
|
|
49
49
|
const providers = getProviders('search_places')
|
|
50
50
|
const gm = providers.find((p) => p.id === 'google_maps')!
|
|
51
51
|
const orig = { t: gm.async!.timeoutMs, i: gm.async!.pollIntervalMs }
|
|
52
|
-
gm.async!.timeoutMs = 500
|
|
53
|
-
gm.async!.pollIntervalMs = 20
|
|
54
52
|
|
|
55
53
|
try {
|
|
54
|
+
gm.async!.timeoutMs = 500
|
|
55
|
+
gm.async!.pollIntervalMs = 20
|
|
56
56
|
const result = await searchPlacesHandler({ query: 'café', country: 'FR', limit: 5 })
|
|
57
57
|
|
|
58
58
|
expect(result.isError).toBeFalsy()
|
|
@@ -84,10 +84,10 @@ describe('search_places handler', () => {
|
|
|
84
84
|
const providers = getProviders('search_places')
|
|
85
85
|
const gm = providers.find((p) => p.id === 'google_maps')!
|
|
86
86
|
const orig = { t: gm.async!.timeoutMs, i: gm.async!.pollIntervalMs }
|
|
87
|
-
gm.async!.timeoutMs = 500
|
|
88
|
-
gm.async!.pollIntervalMs = 20
|
|
89
87
|
|
|
90
88
|
try {
|
|
89
|
+
gm.async!.timeoutMs = 500
|
|
90
|
+
gm.async!.pollIntervalMs = 20
|
|
91
91
|
const result = await searchPlacesHandler({ query: 'coffee', country: 'US', provider: 'google_maps', limit: 5 })
|
|
92
92
|
|
|
93
93
|
expect(result.isError).toBeFalsy()
|
|
@@ -33,10 +33,10 @@ describe('search_reddit handler', () => {
|
|
|
33
33
|
const providers = getProviders('search_reddit')
|
|
34
34
|
const reddit = providers.find((p) => p.id === 'reddit')!
|
|
35
35
|
const orig = { t: reddit.async!.timeoutMs, i: reddit.async!.pollIntervalMs }
|
|
36
|
-
reddit.async!.timeoutMs = 500
|
|
37
|
-
reddit.async!.pollIntervalMs = 20
|
|
38
36
|
|
|
39
37
|
try {
|
|
38
|
+
reddit.async!.timeoutMs = 500
|
|
39
|
+
reddit.async!.pollIntervalMs = 20
|
|
40
40
|
const result = await searchRedditHandler({ start_urls: ['https://www.reddit.com/r/sales/'], query: 'best CRM for startups', limit: 5 })
|
|
41
41
|
|
|
42
42
|
expect(result.isError).toBeFalsy()
|
|
@@ -72,10 +72,10 @@ describe('search_reddit handler', () => {
|
|
|
72
72
|
const providers = getProviders('search_reddit')
|
|
73
73
|
const reddit = providers.find((p) => p.id === 'reddit')!
|
|
74
74
|
const orig = { t: reddit.async!.timeoutMs, i: reddit.async!.pollIntervalMs }
|
|
75
|
-
reddit.async!.timeoutMs = 500
|
|
76
|
-
reddit.async!.pollIntervalMs = 20
|
|
77
75
|
|
|
78
76
|
try {
|
|
77
|
+
reddit.async!.timeoutMs = 500
|
|
78
|
+
reddit.async!.pollIntervalMs = 20
|
|
79
79
|
await searchRedditHandler({
|
|
80
80
|
start_urls: ['https://www.reddit.com/r/entrepreneur/'],
|
|
81
81
|
limit: 5,
|
|
@@ -108,10 +108,10 @@ describe('search_reddit handler', () => {
|
|
|
108
108
|
const providers = getProviders('search_reddit')
|
|
109
109
|
const reddit = providers.find((p) => p.id === 'reddit')!
|
|
110
110
|
const orig = { t: reddit.async!.timeoutMs, i: reddit.async!.pollIntervalMs }
|
|
111
|
-
reddit.async!.timeoutMs = 100
|
|
112
|
-
reddit.async!.pollIntervalMs = 20
|
|
113
111
|
|
|
114
112
|
try {
|
|
113
|
+
reddit.async!.timeoutMs = 100
|
|
114
|
+
reddit.async!.pollIntervalMs = 20
|
|
115
115
|
const result = await searchRedditHandler({ start_urls: ['https://www.reddit.com/r/sales/'], query: 'cold email', limit: 5 })
|
|
116
116
|
|
|
117
117
|
expect(result.isError).toBe(true)
|