@coldiq/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +8 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +47 -0
- package/dist/client.js.map +1 -0
- package/dist/executor.d.ts +21 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +130 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +49 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +3104 -0
- package/dist/registry.js.map +1 -0
- package/dist/tools/enrich-company.d.ts +22 -0
- package/dist/tools/enrich-company.d.ts.map +1 -0
- package/dist/tools/enrich-company.js +21 -0
- package/dist/tools/enrich-company.js.map +1 -0
- package/dist/tools/enrich-email.d.ts +24 -0
- package/dist/tools/enrich-email.d.ts.map +1 -0
- package/dist/tools/enrich-email.js +19 -0
- package/dist/tools/enrich-email.js.map +1 -0
- package/dist/tools/enrich-emails.d.ts +31 -0
- package/dist/tools/enrich-emails.d.ts.map +1 -0
- package/dist/tools/enrich-emails.js +146 -0
- package/dist/tools/enrich-emails.js.map +1 -0
- package/dist/tools/enrich-person.d.ts +26 -0
- package/dist/tools/enrich-person.d.ts.map +1 -0
- package/dist/tools/enrich-person.js +23 -0
- package/dist/tools/enrich-person.js.map +1 -0
- package/dist/tools/fetch-page-content.d.ts +22 -0
- package/dist/tools/fetch-page-content.d.ts.map +1 -0
- package/dist/tools/fetch-page-content.js +32 -0
- package/dist/tools/fetch-page-content.js.map +1 -0
- package/dist/tools/find-email.d.ts +24 -0
- package/dist/tools/find-email.d.ts.map +1 -0
- package/dist/tools/find-email.js +19 -0
- package/dist/tools/find-email.js.map +1 -0
- package/dist/tools/find-emails.d.ts +31 -0
- package/dist/tools/find-emails.d.ts.map +1 -0
- package/dist/tools/find-emails.js +146 -0
- package/dist/tools/find-emails.js.map +1 -0
- package/dist/tools/find-influencers.d.ts +29 -0
- package/dist/tools/find-influencers.d.ts.map +1 -0
- package/dist/tools/find-influencers.js +30 -0
- package/dist/tools/find-influencers.js.map +1 -0
- package/dist/tools/find-people.d.ts +26 -0
- package/dist/tools/find-people.d.ts.map +1 -0
- package/dist/tools/find-people.js +61 -0
- package/dist/tools/find-people.js.map +1 -0
- package/dist/tools/find-phone.d.ts +24 -0
- package/dist/tools/find-phone.d.ts.map +1 -0
- package/dist/tools/find-phone.js +48 -0
- package/dist/tools/find-phone.js.map +1 -0
- package/dist/tools/find-signals.d.ts +26 -0
- package/dist/tools/find-signals.d.ts.map +1 -0
- package/dist/tools/find-signals.js +82 -0
- package/dist/tools/find-signals.js.map +1 -0
- package/dist/tools/search-ads.d.ts +33 -0
- package/dist/tools/search-ads.d.ts.map +1 -0
- package/dist/tools/search-ads.js +33 -0
- package/dist/tools/search-ads.js.map +1 -0
- package/dist/tools/search-companies.d.ts +42 -0
- package/dist/tools/search-companies.d.ts.map +1 -0
- package/dist/tools/search-companies.js +37 -0
- package/dist/tools/search-companies.js.map +1 -0
- package/dist/tools/search-jobs.d.ts +51 -0
- package/dist/tools/search-jobs.d.ts.map +1 -0
- package/dist/tools/search-jobs.js +64 -0
- package/dist/tools/search-jobs.js.map +1 -0
- package/dist/tools/search-places.d.ts +47 -0
- package/dist/tools/search-places.d.ts.map +1 -0
- package/dist/tools/search-places.js +42 -0
- package/dist/tools/search-places.js.map +1 -0
- package/dist/tools/search-reddit.d.ts +27 -0
- package/dist/tools/search-reddit.d.ts.map +1 -0
- package/dist/tools/search-reddit.js +30 -0
- package/dist/tools/search-reddit.js.map +1 -0
- package/dist/tools/search-seo.d.ts +37 -0
- package/dist/tools/search-seo.d.ts.map +1 -0
- package/dist/tools/search-seo.js +49 -0
- package/dist/tools/search-seo.js.map +1 -0
- package/dist/tools/search-web.d.ts +23 -0
- package/dist/tools/search-web.d.ts.map +1 -0
- package/dist/tools/search-web.js +20 -0
- package/dist/tools/search-web.js.map +1 -0
- package/dist/tools/verify-email.d.ts +20 -0
- package/dist/tools/verify-email.d.ts.map +1 -0
- package/dist/tools/verify-email.js +15 -0
- package/dist/tools/verify-email.js.map +1 -0
- package/package.json +28 -0
- package/src/client.ts +60 -0
- package/src/executor.ts +182 -0
- package/src/index.ts +155 -0
- package/src/registry.ts +3159 -0
- package/src/tools/enrich-company.ts +25 -0
- package/src/tools/enrich-person.ts +27 -0
- package/src/tools/fetch-page-content.ts +36 -0
- package/src/tools/find-email.ts +23 -0
- package/src/tools/find-emails.ts +190 -0
- package/src/tools/find-influencers.ts +34 -0
- package/src/tools/find-people.ts +69 -0
- package/src/tools/find-phone.ts +53 -0
- package/src/tools/find-signals.ts +93 -0
- package/src/tools/search-ads.ts +44 -0
- package/src/tools/search-companies.ts +41 -0
- package/src/tools/search-jobs.ts +73 -0
- package/src/tools/search-places.ts +52 -0
- package/src/tools/search-reddit.ts +34 -0
- package/src/tools/search-seo.ts +59 -0
- package/src/tools/search-web.ts +24 -0
- package/src/tools/verify-email.ts +19 -0
- package/test-ads-live.ts +77 -0
- package/test-company-live.ts +91 -0
- package/test-email-live.ts +171 -0
- package/test-influencers-live.ts +66 -0
- package/test-jobs-live.ts +69 -0
- package/test-linkupapi-live.ts +137 -0
- package/test-phone-live.ts +41 -0
- package/test-places-live.ts +89 -0
- package/test-reddit-live.ts +66 -0
- package/test-search-live.ts +79 -0
- package/test-seo-live.ts +68 -0
- package/test-web-live.ts +67 -0
- package/tests/client.test.ts +90 -0
- package/tests/executor.test.ts +83 -0
- package/tests/gtm/01-icp-to-emails.test.ts +43 -0
- package/tests/gtm/02-icp-bulk-emails.test.ts +38 -0
- package/tests/gtm/03-icp-to-phones.test.ts +39 -0
- package/tests/gtm/04-funding-signal-outreach.test.ts +42 -0
- package/tests/gtm/05-hiring-signal-decisionmakers.test.ts +41 -0
- package/tests/gtm/06-intent-signal-outreach.test.ts +44 -0
- package/tests/gtm/07-places-to-content.test.ts +50 -0
- package/tests/gtm/08-domain-to-account.test.ts +44 -0
- package/tests/gtm/09-linkedin-to-everything.test.ts +41 -0
- package/tests/gtm/10-jobs-vs-signals-routing.test.ts +38 -0
- package/tests/gtm/11-find-vs-enrich-routing.test.ts +39 -0
- package/tests/gtm/12-bogus-domain-graceful.test.ts +42 -0
- package/tests/gtm/13-private-linkedin-graceful.test.ts +44 -0
- package/tests/gtm/14-empty-handoff.test.ts +43 -0
- package/tests/gtm/15-seo-reddit-research.test.ts +38 -0
- package/tests/gtm/README.md +59 -0
- package/tests/gtm/harness.ts +217 -0
- package/tests/gtm/tools-bridge.ts +232 -0
- package/tests/gtm-scenarios.md +32 -0
- package/tests/live/smoke-report.ts +255 -0
- package/tests/live/smoke.test.ts +134 -0
- package/tests/registry-enrich-person.test.ts +447 -0
- package/tests/registry-fetch-page-content.test.ts +90 -0
- package/tests/registry-find-people.test.ts +467 -0
- package/tests/registry-find-signals.test.ts +470 -0
- package/tests/registry-linkupapi.test.ts +331 -0
- package/tests/registry-search-companies.test.ts +188 -0
- package/tests/registry-search-jobs.test.ts +116 -0
- package/tests/registry.test.ts +2210 -0
- package/tests/tools/enrich-company.test.ts +92 -0
- package/tests/tools/enrich-email.test.ts +94 -0
- package/tests/tools/enrich-emails.test.ts +271 -0
- package/tests/tools/enrich-person.test.ts +140 -0
- package/tests/tools/fetch-page-content.test.ts +108 -0
- package/tests/tools/find-influencers.test.ts +91 -0
- package/tests/tools/find-people.test.ts +344 -0
- package/tests/tools/find-phone.test.ts +100 -0
- package/tests/tools/find-signals.test.ts +110 -0
- package/tests/tools/search-ads.test.ts +182 -0
- package/tests/tools/search-companies.test.ts +58 -0
- package/tests/tools/search-jobs.test.ts +210 -0
- package/tests/tools/search-places.test.ts +114 -0
- package/tests/tools/search-reddit.test.ts +125 -0
- package/tests/tools/search-seo.test.ts +183 -0
- package/tests/tools/search-web.test.ts +79 -0
- package/tests/tools/verify-email.test.ts +68 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { initClient } from '../../src/client.js'
|
|
3
|
+
import { enrichCompanyHandler } from '../../src/tools/enrich-company.js'
|
|
4
|
+
|
|
5
|
+
describe('enrich_company 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('returns error when no identifier provided', async () => {
|
|
17
|
+
const result = await enrichCompanyHandler({})
|
|
18
|
+
expect(result.isError).toBe(true)
|
|
19
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
20
|
+
expect(parsed.error).toMatch(/domain.*linkedin_url.*name/i)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('returns enriched company from CompanyEnrich (GET)', async () => {
|
|
24
|
+
let capturedUrl = ''
|
|
25
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
26
|
+
capturedUrl = url.toString()
|
|
27
|
+
return new Response(JSON.stringify({ name: 'Stripe', domain: 'stripe.com', employees: 8000 }), { status: 200 })
|
|
28
|
+
}) as typeof fetch
|
|
29
|
+
|
|
30
|
+
const result = await enrichCompanyHandler({ domain: 'stripe.com' })
|
|
31
|
+
|
|
32
|
+
expect(capturedUrl).toContain('domain=stripe.com')
|
|
33
|
+
expect(capturedUrl).toContain('/companyenrich/companies/enrich')
|
|
34
|
+
|
|
35
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
36
|
+
expect(parsed._meta.provider).toBe('companyenrich')
|
|
37
|
+
expect(parsed.data.name).toBe('Stripe')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('falls back to Apollo on CompanyEnrich failure', async () => {
|
|
41
|
+
let callCount = 0
|
|
42
|
+
globalThis.fetch = vi.fn(async () => {
|
|
43
|
+
callCount++
|
|
44
|
+
if (callCount === 1) {
|
|
45
|
+
return new Response(JSON.stringify({ error: 'Provider error' }), { status: 502 })
|
|
46
|
+
}
|
|
47
|
+
return new Response(JSON.stringify({ organization: { name: 'Stripe' } }), { status: 200 })
|
|
48
|
+
}) as typeof fetch
|
|
49
|
+
|
|
50
|
+
const result = await enrichCompanyHandler({ domain: 'stripe.com' })
|
|
51
|
+
|
|
52
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
53
|
+
expect(parsed._meta.provider).toBe('apollo')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('skips CompanyEnrich GET and Apollo when only linkedin_url is provided', async () => {
|
|
57
|
+
const calledEndpoints: string[] = []
|
|
58
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
59
|
+
const endpoint = url.toString()
|
|
60
|
+
calledEndpoints.push(endpoint)
|
|
61
|
+
// PDL (3rd provider) succeeds
|
|
62
|
+
if (endpoint.includes('/pdl/company/enrich')) {
|
|
63
|
+
return new Response(JSON.stringify({ name: 'Stripe', website: 'stripe.com' }), { status: 200 })
|
|
64
|
+
}
|
|
65
|
+
return new Response(JSON.stringify({ error: 'not found' }), { status: 404 })
|
|
66
|
+
}) as typeof fetch
|
|
67
|
+
|
|
68
|
+
const result = await enrichCompanyHandler({ linkedin_url: 'https://www.linkedin.com/company/stripe' })
|
|
69
|
+
|
|
70
|
+
expect(calledEndpoints.every((u) => !u.includes('/companyenrich/companies/enrich?'))).toBe(true)
|
|
71
|
+
expect(calledEndpoints.every((u) => !u.includes('/apollo/'))).toBe(true)
|
|
72
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
73
|
+
expect(parsed._meta.provider).toBe('pdl')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('skips all domain-only providers when only name is provided', async () => {
|
|
77
|
+
const calledEndpoints: string[] = []
|
|
78
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
79
|
+
const endpoint = url.toString()
|
|
80
|
+
calledEndpoints.push(endpoint)
|
|
81
|
+
if (endpoint.includes('/pdl/company/enrich')) {
|
|
82
|
+
return new Response(JSON.stringify({ name: 'Stripe', website: 'stripe.com' }), { status: 200 })
|
|
83
|
+
}
|
|
84
|
+
return new Response(JSON.stringify({ error: 'not found' }), { status: 404 })
|
|
85
|
+
}) as typeof fetch
|
|
86
|
+
|
|
87
|
+
await enrichCompanyHandler({ name: 'Stripe' })
|
|
88
|
+
|
|
89
|
+
expect(calledEndpoints.every((u) => !u.includes('/companyenrich/companies/enrich?'))).toBe(true)
|
|
90
|
+
expect(calledEndpoints.every((u) => !u.includes('/apollo/'))).toBe(true)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { initClient } from '../../src/client.js'
|
|
3
|
+
import { findEmailHandler as enrichEmailHandler } from '../../src/tools/find-email.js'
|
|
4
|
+
|
|
5
|
+
describe('find_email handler (waterfall)', () => {
|
|
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('returns email from first provider that finds one', async () => {
|
|
17
|
+
globalThis.fetch = vi.fn(async () =>
|
|
18
|
+
new Response(JSON.stringify({ email: 'michel@coldiq.com' }), { status: 200 })
|
|
19
|
+
) as typeof fetch
|
|
20
|
+
|
|
21
|
+
const result = await enrichEmailHandler({
|
|
22
|
+
first_name: 'Michel',
|
|
23
|
+
last_name: 'Lieben',
|
|
24
|
+
domain: 'coldiq.com',
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
28
|
+
expect(parsed._meta.provider).toBe('findymail')
|
|
29
|
+
expect(parsed.data.email).toBe('michel@coldiq.com')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('falls through to IcyPeas when FindyMail has no result', async () => {
|
|
33
|
+
let callCount = 0
|
|
34
|
+
globalThis.fetch = vi.fn(async () => {
|
|
35
|
+
callCount++
|
|
36
|
+
if (callCount === 1) {
|
|
37
|
+
// FindyMail — no email found
|
|
38
|
+
return new Response(JSON.stringify({ email: null }), { status: 200 })
|
|
39
|
+
}
|
|
40
|
+
// IcyPeas — found
|
|
41
|
+
return new Response(JSON.stringify({ email: 'michel@coldiq.com' }), { status: 200 })
|
|
42
|
+
}) as typeof fetch
|
|
43
|
+
|
|
44
|
+
const result = await enrichEmailHandler({
|
|
45
|
+
first_name: 'Michel',
|
|
46
|
+
last_name: 'Lieben',
|
|
47
|
+
domain: 'coldiq.com',
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
51
|
+
expect(parsed._meta.provider).toBe('icypeas')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('falls through to Prospeo when FindyMail and IcyPeas both fail', async () => {
|
|
55
|
+
let callCount = 0
|
|
56
|
+
globalThis.fetch = vi.fn(async () => {
|
|
57
|
+
callCount++
|
|
58
|
+
if (callCount <= 2) {
|
|
59
|
+
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
|
|
60
|
+
}
|
|
61
|
+
// Prospeo success response — email is nested under person.email.email
|
|
62
|
+
return new Response(
|
|
63
|
+
JSON.stringify({ error: false, person: { email: { email: 'michel@coldiq.com', status: 'VERIFIED' } } }),
|
|
64
|
+
{ status: 200 },
|
|
65
|
+
)
|
|
66
|
+
}) as typeof fetch
|
|
67
|
+
|
|
68
|
+
const result = await enrichEmailHandler({
|
|
69
|
+
first_name: 'Michel',
|
|
70
|
+
last_name: 'Lieben',
|
|
71
|
+
domain: 'coldiq.com',
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
75
|
+
expect(parsed._meta.provider).toBe('prospeo')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('returns error when all providers fail', async () => {
|
|
79
|
+
globalThis.fetch = vi.fn(async () =>
|
|
80
|
+
new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
|
|
81
|
+
) as typeof fetch
|
|
82
|
+
|
|
83
|
+
const result = await enrichEmailHandler({
|
|
84
|
+
first_name: 'Unknown',
|
|
85
|
+
last_name: 'Person',
|
|
86
|
+
domain: 'unknown.xyz',
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
expect(result.isError).toBe(true)
|
|
90
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
91
|
+
// No linkedin_url → blitzapi and limadata-work-email-linkedin are skipped via isApplicable
|
|
92
|
+
expect(parsed.providers_tried.length).toBe(6) // findymail, icypeas, limadata-work-email, prospeo, fullenrich, linkupapi
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { initClient } from '../../src/client.js'
|
|
3
|
+
import { findEmailsHandler as enrichEmailsHandler } 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 enrichEmailsHandler({
|
|
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' },
|
|
47
|
+
{ id: 'p2', email: 'bob@example.com', provider: '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 enrichEmailsHandler({
|
|
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' })
|
|
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 enrichEmailsHandler({
|
|
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' })
|
|
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 enrichEmailsHandler({
|
|
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 })
|
|
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 enrichEmailsHandler({
|
|
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 enrichEmailsHandler({
|
|
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 index', async () => {
|
|
200
|
+
vi.useFakeTimers()
|
|
201
|
+
|
|
202
|
+
let feCreateCalled = false
|
|
203
|
+
let fePollCalled = false
|
|
204
|
+
|
|
205
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
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: [{ emails: ['alice@example.com'] }],
|
|
221
|
+
}), { status: 200 })
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (u.includes('/fullenrich/contact/enrich/bulk')) {
|
|
225
|
+
feCreateCalled = true
|
|
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 = enrichEmailsHandler({
|
|
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(feCreateCalled).toBe(true)
|
|
241
|
+
expect(fePollCalled).toBe(true)
|
|
242
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
243
|
+
expect(parsed.data.results[0]).toEqual({ id: 'p1', email: 'alice@example.com', provider: 'fullenrich' })
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('gracefully handles prospeo bulk failure and still tries fallbacks', async () => {
|
|
247
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
248
|
+
const u = url.toString()
|
|
249
|
+
if (u.includes('/prospeo/bulk-enrich-person')) {
|
|
250
|
+
return new Response(JSON.stringify({ error: 'service unavailable' }), { status: 502 })
|
|
251
|
+
}
|
|
252
|
+
if (u.includes('/findymail/search/name')) {
|
|
253
|
+
return new Response(JSON.stringify({ email: 'alice@example.com' }), { status: 200 })
|
|
254
|
+
}
|
|
255
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
256
|
+
}) as typeof fetch
|
|
257
|
+
|
|
258
|
+
const result = await enrichEmailsHandler({
|
|
259
|
+
people: [{ id: 'p1', first_name: 'Alice', last_name: 'Smith', domain: 'example.com' }],
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
263
|
+
expect(parsed.data.results[0]).toMatchObject({ email: 'alice@example.com', provider: 'findymail' })
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
interface EmailResult {
|
|
268
|
+
id: string
|
|
269
|
+
email: string | null
|
|
270
|
+
provider: string | null
|
|
271
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { initClient } from '../../src/client.js'
|
|
3
|
+
import { enrichPersonHandler } from '../../src/tools/enrich-person.js'
|
|
4
|
+
|
|
5
|
+
describe('enrich_person 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('email input — linkupapi-email-reverse wins on first try', async () => {
|
|
17
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
18
|
+
const u = url.toString()
|
|
19
|
+
if (u.includes('/linkupapi/data/profil/enrich')) {
|
|
20
|
+
// profile-enrich not applicable for email-only, should not be called
|
|
21
|
+
throw new Error('profile-enrich should not fire for email-only input')
|
|
22
|
+
}
|
|
23
|
+
if (u.includes('/linkupapi/data/mail/reverse')) {
|
|
24
|
+
return new Response(JSON.stringify({ status: 'success', data: { linkedin_url: 'https://linkedin.com/in/michel-lieben', name: 'Michel Lieben' } }), { status: 200 })
|
|
25
|
+
}
|
|
26
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
27
|
+
}) as typeof fetch
|
|
28
|
+
|
|
29
|
+
const result = await enrichPersonHandler({ email: 'michel@coldiq.com' })
|
|
30
|
+
|
|
31
|
+
expect(result.isError).toBeFalsy()
|
|
32
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
33
|
+
expect(parsed._meta.provider).toBe('linkupapi-email-reverse')
|
|
34
|
+
// linkupapi wraps: { status, data: { ... } }
|
|
35
|
+
expect(parsed.data.data.linkedin_url).toContain('linkedin.com')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('email input — falls back to pdl-person-enrich when linkupapi-email-reverse returns empty', async () => {
|
|
39
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
40
|
+
const u = url.toString()
|
|
41
|
+
if (u.includes('/linkupapi/data/mail/reverse')) {
|
|
42
|
+
// hasResult fails: status not 'success'
|
|
43
|
+
return new Response(JSON.stringify({ status: 'not_found', data: null }), { status: 200 })
|
|
44
|
+
}
|
|
45
|
+
if (u.includes('/pdl/person/enrich')) {
|
|
46
|
+
return new Response(JSON.stringify({ status: 200, data: { full_name: 'Michel Lieben', linkedin_url: 'https://linkedin.com/in/michel-lieben' } }), { status: 200 })
|
|
47
|
+
}
|
|
48
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
49
|
+
}) as typeof fetch
|
|
50
|
+
|
|
51
|
+
const result = await enrichPersonHandler({ email: 'michel@coldiq.com' })
|
|
52
|
+
|
|
53
|
+
expect(result.isError).toBeFalsy()
|
|
54
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
55
|
+
expect(parsed._meta.provider).toBe('pdl-person-enrich')
|
|
56
|
+
// PDL wraps: { status: 200, data: { ... } }
|
|
57
|
+
expect(parsed.data.data.full_name).toBe('Michel Lieben')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('name+company input — linkupapi-profile-enrich wins (highest priority for this input type)', async () => {
|
|
61
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
62
|
+
const u = url.toString()
|
|
63
|
+
if (u.includes('/linkupapi/data/profil/enrich')) {
|
|
64
|
+
return new Response(JSON.stringify({ status: 'success', data: { linkedin_url: 'https://linkedin.com/in/michel-lieben', headline: 'CEO at ColdIQ' } }), { status: 200 })
|
|
65
|
+
}
|
|
66
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
67
|
+
}) as typeof fetch
|
|
68
|
+
|
|
69
|
+
const result = await enrichPersonHandler({ first_name: 'Michel', last_name: 'Lieben', company_name: 'ColdIQ' })
|
|
70
|
+
|
|
71
|
+
expect(result.isError).toBeFalsy()
|
|
72
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
73
|
+
expect(parsed._meta.provider).toBe('linkupapi-profile-enrich')
|
|
74
|
+
// linkupapi wraps: { status, data: { ... } }
|
|
75
|
+
expect(parsed.data.data.headline).toBe('CEO at ColdIQ')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('linkedin_url input — findymail-business-profile wins after non-applicable providers are skipped', async () => {
|
|
79
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
80
|
+
const u = url.toString()
|
|
81
|
+
// linkupapi-profile-enrich not applicable (no first_name)
|
|
82
|
+
// linkupapi-email-reverse not applicable (no email)
|
|
83
|
+
// pdl-person-enrich fires but returns no match
|
|
84
|
+
if (u.includes('/pdl/person/enrich')) {
|
|
85
|
+
return new Response(JSON.stringify({ status: 404, data: null }), { status: 200 })
|
|
86
|
+
}
|
|
87
|
+
// apollo-people-match fires but returns no person
|
|
88
|
+
if (u.includes('/apollo/people/match')) {
|
|
89
|
+
return new Response(JSON.stringify({ person: null }), { status: 200 })
|
|
90
|
+
}
|
|
91
|
+
// blitzapi-reverse-email not applicable (no email)
|
|
92
|
+
// findymail-business-profile: applicable (linkedin_url present)
|
|
93
|
+
if (u.includes('/findymail/search/business-profile')) {
|
|
94
|
+
return new Response(JSON.stringify({ name: 'Michel Lieben', email: 'michel@coldiq.com', linkedin_url: 'https://linkedin.com/in/michel-lieben' }), { status: 200 })
|
|
95
|
+
}
|
|
96
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
97
|
+
}) as typeof fetch
|
|
98
|
+
|
|
99
|
+
const result = await enrichPersonHandler({ linkedin_url: 'https://www.linkedin.com/in/michel-lieben' })
|
|
100
|
+
|
|
101
|
+
expect(result.isError).toBeFalsy()
|
|
102
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
103
|
+
expect(parsed._meta.provider).toBe('findymail-business-profile')
|
|
104
|
+
expect(parsed.data.name).toBe('Michel Lieben')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('phone input — ai-ark-reverse-lookup wins (gated on phone present)', async () => {
|
|
108
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
109
|
+
const u = url.toString()
|
|
110
|
+
if (u.includes('/pdl/person/enrich')) {
|
|
111
|
+
return new Response(JSON.stringify({ status: 404, data: null }), { status: 200 })
|
|
112
|
+
}
|
|
113
|
+
if (u.includes('/apollo/people/match')) {
|
|
114
|
+
return new Response(JSON.stringify({ person: null }), { status: 200 })
|
|
115
|
+
}
|
|
116
|
+
if (u.includes('/ai-ark/people/reverse-lookup')) {
|
|
117
|
+
return new Response(JSON.stringify({ name: 'Michel Lieben', email: 'michel@coldiq.com' }), { status: 200 })
|
|
118
|
+
}
|
|
119
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
120
|
+
}) as typeof fetch
|
|
121
|
+
|
|
122
|
+
const result = await enrichPersonHandler({ phone: '+32123456789' })
|
|
123
|
+
|
|
124
|
+
expect(result.isError).toBeFalsy()
|
|
125
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
126
|
+
expect(parsed._meta.provider).toBe('ai-ark-reverse-lookup')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('all providers fail — returns isError with error object', async () => {
|
|
130
|
+
globalThis.fetch = vi.fn(async () => {
|
|
131
|
+
return new Response(JSON.stringify({ error: 'Provider unavailable' }), { status: 503 })
|
|
132
|
+
}) as typeof fetch
|
|
133
|
+
|
|
134
|
+
const result = await enrichPersonHandler({ email: 'michel@coldiq.com' })
|
|
135
|
+
|
|
136
|
+
expect(result.isError).toBe(true)
|
|
137
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
138
|
+
expect(parsed.error).toBeTruthy()
|
|
139
|
+
})
|
|
140
|
+
})
|