@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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// Mock the HTTP client so callVerb's envelope-building can be asserted without
|
|
4
|
+
// touching the network. The factory returns a vi.fn() we capture below.
|
|
5
|
+
const callApiMock = vi.fn()
|
|
6
|
+
vi.mock('../src/client.js', () => ({
|
|
7
|
+
callApi: (...args: unknown[]) => callApiMock(...args),
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
import { callVerb } from '../src/verb-client.js'
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
callApiMock.mockReset()
|
|
14
|
+
callApiMock.mockResolvedValue({ ok: true, status: 200, data: { data: { ok: true }, _meta: {} }, headers: {} })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('callVerb', () => {
|
|
18
|
+
it('maps single input into envelope.input and forwards the verb path', async () => {
|
|
19
|
+
await callVerb('/email/find', { first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' })
|
|
20
|
+
|
|
21
|
+
expect(callApiMock).toHaveBeenCalledTimes(1)
|
|
22
|
+
const [method, path, envelope] = callApiMock.mock.calls[0]
|
|
23
|
+
expect(method).toBe('POST')
|
|
24
|
+
expect(path).toBe('/email/find')
|
|
25
|
+
expect(envelope).toEqual({
|
|
26
|
+
input: { first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' },
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('maps use_providers (non-empty array) to envelope.provider and keeps it out of input', async () => {
|
|
31
|
+
await callVerb('/email/find', {
|
|
32
|
+
first_name: 'Michel',
|
|
33
|
+
domain: 'coldiq.com',
|
|
34
|
+
use_providers: ['prospeo', 'fullenrich'],
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
38
|
+
expect(envelope.provider).toEqual(['prospeo', 'fullenrich'])
|
|
39
|
+
expect(envelope.input).toEqual({ first_name: 'Michel', domain: 'coldiq.com' })
|
|
40
|
+
expect(envelope.input).not.toHaveProperty('use_providers')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('omits provider when use_providers is empty or absent', async () => {
|
|
44
|
+
await callVerb('/email/find', { domain: 'coldiq.com', use_providers: [] })
|
|
45
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
46
|
+
expect(envelope).not.toHaveProperty('provider')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('passes fields and reveal through to the envelope', async () => {
|
|
50
|
+
await callVerb('/people/search', {
|
|
51
|
+
job_titles: ['CEO'],
|
|
52
|
+
fields: 'verbose',
|
|
53
|
+
reveal: true,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
57
|
+
expect(envelope.fields).toBe('verbose')
|
|
58
|
+
expect(envelope.reveal).toBe(true)
|
|
59
|
+
expect(envelope.input).toEqual({ job_titles: ['CEO'] })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('only sets fields/reveal when defined', async () => {
|
|
63
|
+
await callVerb('/people/search', { job_titles: ['CEO'] })
|
|
64
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
65
|
+
expect(envelope).not.toHaveProperty('fields')
|
|
66
|
+
expect(envelope).not.toHaveProperty('reveal')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('bulk mode maps the named list field to inputs[]', async () => {
|
|
70
|
+
const people = [
|
|
71
|
+
{ id: 'a', first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' },
|
|
72
|
+
{ id: 'b', first_name: 'Ada', last_name: 'Lovelace', domain: 'microsoft.com' },
|
|
73
|
+
]
|
|
74
|
+
await callVerb('/email/find', { people, use_providers: ['prospeo'] }, { bulkField: 'people' })
|
|
75
|
+
|
|
76
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
77
|
+
expect(envelope.inputs).toEqual(people)
|
|
78
|
+
expect(envelope).not.toHaveProperty('input')
|
|
79
|
+
expect(envelope.provider).toEqual(['prospeo'])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('bulk mode merges sibling rest fields alongside inputs', async () => {
|
|
83
|
+
await callVerb(
|
|
84
|
+
'/email/find',
|
|
85
|
+
{ people: [{ id: 'a' }], country: 'US' },
|
|
86
|
+
{ bulkField: 'people' },
|
|
87
|
+
)
|
|
88
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
89
|
+
expect(envelope.inputs).toEqual([{ id: 'a' }])
|
|
90
|
+
expect(envelope.country).toBe('US')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('forwards res.data and does not set isError on a 2xx response', async () => {
|
|
94
|
+
callApiMock.mockResolvedValue({ ok: true, status: 200, data: { data: { email: 'x@y.com' } }, headers: {} })
|
|
95
|
+
const out = await callVerb('/email/find', { domain: 'coldiq.com' })
|
|
96
|
+
|
|
97
|
+
expect(out.content[0].type).toBe('text')
|
|
98
|
+
expect(JSON.parse(out.content[0].text)).toEqual({ data: { email: 'x@y.com' } })
|
|
99
|
+
expect(out.isError).toBe(false)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('sets isError when the response is not ok', async () => {
|
|
103
|
+
callApiMock.mockResolvedValue({
|
|
104
|
+
ok: false,
|
|
105
|
+
status: 404,
|
|
106
|
+
data: { error: 'No usable result', providers_tried: ['prospeo'] },
|
|
107
|
+
headers: {},
|
|
108
|
+
})
|
|
109
|
+
const out = await callVerb('/email/find', { domain: 'coldiq.com' })
|
|
110
|
+
|
|
111
|
+
expect(out.isError).toBe(true)
|
|
112
|
+
expect(JSON.parse(out.content[0].text)).toEqual({ error: 'No usable result', providers_tried: ['prospeo'] })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// --- schema-drift mappings (find_phone / search_places / find_emails) ---
|
|
116
|
+
|
|
117
|
+
it('remap renames input keys before building the envelope (company_domain → domain)', async () => {
|
|
118
|
+
await callVerb('/phone/find', { first_name: 'Michel', company_domain: 'coldiq.com', company_name: 'ColdIQ' }, { remap: { company_domain: 'domain' } })
|
|
119
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
120
|
+
expect(envelope.input).toEqual({ first_name: 'Michel', domain: 'coldiq.com', company_name: 'ColdIQ' })
|
|
121
|
+
expect(envelope.input).not.toHaveProperty('company_domain')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('providerFromField routes a tool-specific provider field into the provider pin', async () => {
|
|
125
|
+
await callVerb('/places/search', { query: 'coffee', provider: 'openmart' }, { providerFromField: 'provider' })
|
|
126
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
127
|
+
expect(envelope.provider).toEqual(['openmart'])
|
|
128
|
+
expect(envelope.input).toEqual({ query: 'coffee' })
|
|
129
|
+
expect(envelope.input).not.toHaveProperty('provider')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('providerFromField defers to an explicit use_providers when both are present', async () => {
|
|
133
|
+
await callVerb('/places/search', { query: 'coffee', provider: 'openmart', use_providers: ['google_maps'] }, { providerFromField: 'provider' })
|
|
134
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
135
|
+
expect(envelope.provider).toEqual(['google_maps'])
|
|
136
|
+
expect(envelope.input).toEqual({ query: 'coffee' })
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('reattachIdField re-attaches each input id onto bulk results by index', async () => {
|
|
140
|
+
callApiMock.mockResolvedValue({
|
|
141
|
+
ok: true,
|
|
142
|
+
status: 200,
|
|
143
|
+
data: { results: [{ data: { email: 'm@coldiq.com' } }, { error: 'not found' }] },
|
|
144
|
+
headers: {},
|
|
145
|
+
})
|
|
146
|
+
const people = [{ id: 'michel', first_name: 'Michel', domain: 'coldiq.com' }, { id: 'ada', first_name: 'Ada', domain: 'microsoft.com' }]
|
|
147
|
+
const out = await callVerb('/email/find', { people }, { bulkField: 'people', reattachIdField: 'id' })
|
|
148
|
+
const results = JSON.parse(out.content[0].text).results
|
|
149
|
+
expect(results[0]).toEqual({ id: 'michel', data: { email: 'm@coldiq.com' } })
|
|
150
|
+
expect(results[1]).toEqual({ id: 'ada', error: 'not found' })
|
|
151
|
+
})
|
|
152
|
+
})
|
|
@@ -1,92 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,140 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { initClient } from '../../src/client.js'
|
|
3
|
-
import { fetchPageContentHandler } from '../../src/tools/fetch-page-content.js'
|
|
4
|
-
|
|
5
|
-
describe('fetch_page_content 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('single URL — exa-contents returns results', async () => {
|
|
17
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
18
|
-
const u = url.toString()
|
|
19
|
-
if (u.includes('/exa/contents')) {
|
|
20
|
-
return new Response(JSON.stringify({
|
|
21
|
-
results: [{ url: 'https://coldiq.com', text: 'ColdIQ page text' }],
|
|
22
|
-
}), { status: 200 })
|
|
23
|
-
}
|
|
24
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
25
|
-
}) as typeof fetch
|
|
26
|
-
|
|
27
|
-
const result = await fetchPageContentHandler({ urls: ['https://coldiq.com'] })
|
|
28
|
-
|
|
29
|
-
expect(result.isError).toBeFalsy()
|
|
30
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
31
|
-
expect(parsed._meta.provider).toBe('exa-contents')
|
|
32
|
-
expect(parsed.data.results).toHaveLength(1)
|
|
33
|
-
expect(parsed.data.results[0].text).toBe('ColdIQ page text')
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('batch URLs — all returned in one call', async () => {
|
|
37
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
|
|
38
|
-
const u = url.toString()
|
|
39
|
-
if (u.includes('/exa/contents')) {
|
|
40
|
-
const body = JSON.parse((opts?.body as string) ?? '{}')
|
|
41
|
-
return new Response(JSON.stringify({
|
|
42
|
-
results: body.urls.map((pageUrl: string) => ({ url: pageUrl, text: `text for ${pageUrl}` })),
|
|
43
|
-
}), { status: 200 })
|
|
44
|
-
}
|
|
45
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
46
|
-
}) as typeof fetch
|
|
47
|
-
|
|
48
|
-
const urls = ['https://coldiq.com', 'https://hubspot.com', 'https://salesforce.com']
|
|
49
|
-
const result = await fetchPageContentHandler({ urls })
|
|
50
|
-
|
|
51
|
-
expect(result.isError).toBeFalsy()
|
|
52
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
53
|
-
expect(parsed._meta.provider).toBe('exa-contents')
|
|
54
|
-
expect(parsed.data.results).toHaveLength(3)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('include_summary=true — summary flag forwarded to exa', async () => {
|
|
58
|
-
let capturedBody: Record<string, unknown> | null = null
|
|
59
|
-
|
|
60
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
|
|
61
|
-
const u = url.toString()
|
|
62
|
-
if (u.includes('/exa/contents')) {
|
|
63
|
-
capturedBody = JSON.parse((opts?.body as string) ?? '{}')
|
|
64
|
-
return new Response(JSON.stringify({
|
|
65
|
-
results: [{ url: 'https://coldiq.com', text: 'text', summary: 'summary text' }],
|
|
66
|
-
}), { status: 200 })
|
|
67
|
-
}
|
|
68
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
69
|
-
}) as typeof fetch
|
|
70
|
-
|
|
71
|
-
await fetchPageContentHandler({ urls: ['https://coldiq.com'], include_summary: true })
|
|
72
|
-
|
|
73
|
-
expect(capturedBody).not.toBeNull()
|
|
74
|
-
expect(capturedBody!.summary).toEqual({})
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('include_summary=false (default) — no summary field in request', async () => {
|
|
78
|
-
let capturedBody: Record<string, unknown> | null = null
|
|
79
|
-
|
|
80
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
|
|
81
|
-
const u = url.toString()
|
|
82
|
-
if (u.includes('/exa/contents')) {
|
|
83
|
-
capturedBody = JSON.parse((opts?.body as string) ?? '{}')
|
|
84
|
-
return new Response(JSON.stringify({
|
|
85
|
-
results: [{ url: 'https://coldiq.com', text: 'text' }],
|
|
86
|
-
}), { status: 200 })
|
|
87
|
-
}
|
|
88
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
89
|
-
}) as typeof fetch
|
|
90
|
-
|
|
91
|
-
await fetchPageContentHandler({ urls: ['https://coldiq.com'] })
|
|
92
|
-
|
|
93
|
-
expect(capturedBody).not.toBeNull()
|
|
94
|
-
expect(capturedBody!.summary).toBeUndefined()
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('provider fails — returns isError', async () => {
|
|
98
|
-
globalThis.fetch = vi.fn(async () => {
|
|
99
|
-
return new Response(JSON.stringify({ error: 'upstream error' }), { status: 502 })
|
|
100
|
-
}) as typeof fetch
|
|
101
|
-
|
|
102
|
-
const result = await fetchPageContentHandler({ urls: ['https://coldiq.com'] })
|
|
103
|
-
|
|
104
|
-
expect(result.isError).toBe(true)
|
|
105
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
106
|
-
expect(parsed.error).toBeTruthy()
|
|
107
|
-
})
|
|
108
|
-
})
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { initClient } from '../../src/client.js'
|
|
3
|
-
import { findEmailHandler } 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 findEmailHandler({
|
|
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 findEmailHandler({
|
|
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 findEmailHandler({
|
|
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 findEmailHandler({
|
|
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
|
-
|
|
95
|
-
describe('use_providers', () => {
|
|
96
|
-
it('Scenario A — pinned provider succeeds', async () => {
|
|
97
|
-
globalThis.fetch = vi.fn(async () =>
|
|
98
|
-
new Response(JSON.stringify({ email: 'michel@coldiq.com' }), { status: 200 })
|
|
99
|
-
) as typeof fetch
|
|
100
|
-
|
|
101
|
-
const result = await findEmailHandler({
|
|
102
|
-
first_name: 'Michel',
|
|
103
|
-
domain: 'coldiq.com',
|
|
104
|
-
use_providers: ['findymail'],
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
expect(result.isError).toBeUndefined()
|
|
108
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
109
|
-
expect(parsed._meta.provider).toBe('findymail')
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('Scenario C — gated provider without required input returns helpful error', async () => {
|
|
113
|
-
const result = await findEmailHandler({
|
|
114
|
-
first_name: 'Michel',
|
|
115
|
-
domain: 'coldiq.com',
|
|
116
|
-
use_providers: ['blitzapi'],
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
expect(result.isError).toBe(true)
|
|
120
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
121
|
-
expect(parsed.error).toContain("'blitzapi' requires")
|
|
122
|
-
expect(parsed.error).toContain('ColdIQ will automatically pick the best tool')
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('Scenario E — unrecognized provider returns isError', async () => {
|
|
126
|
-
const result = await findEmailHandler({
|
|
127
|
-
first_name: 'Michel',
|
|
128
|
-
domain: 'coldiq.com',
|
|
129
|
-
use_providers: ['apollo'],
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
expect(result.isError).toBe(true)
|
|
133
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
134
|
-
expect(parsed.error).toContain("'apollo' is not a recognized provider")
|
|
135
|
-
})
|
|
136
|
-
})
|
|
137
|
-
})
|