@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,108 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { initClient } from '../../src/client.js'
|
|
3
|
+
import { findInfluencersHandler } from '../../src/tools/find-influencers.js'
|
|
4
|
+
|
|
5
|
+
describe('find_influencers 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('handle set — influencers_similar fires first (lookalike route)', async () => {
|
|
17
|
+
let discoveryCalled = false
|
|
18
|
+
|
|
19
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
20
|
+
const u = url.toString()
|
|
21
|
+
if (u.includes('/influencers-club/discovery') && !u.includes('/creators/similar')) {
|
|
22
|
+
discoveryCalled = true
|
|
23
|
+
}
|
|
24
|
+
if (u.includes('/influencers-club/discovery/creators/similar')) {
|
|
25
|
+
return new Response(JSON.stringify({
|
|
26
|
+
creators: [{ handle: 'garyvee', platform: 'instagram', followers: 8_500_000 }],
|
|
27
|
+
}), { status: 200 })
|
|
28
|
+
}
|
|
29
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
30
|
+
}) as typeof fetch
|
|
31
|
+
|
|
32
|
+
const result = await findInfluencersHandler({ platform: 'instagram', handle: 'hubspot', limit: 5 })
|
|
33
|
+
|
|
34
|
+
expect(result.isError).toBeFalsy()
|
|
35
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
36
|
+
expect(parsed._meta.provider).toBe('influencers_similar')
|
|
37
|
+
expect(parsed.data.creators).toHaveLength(1)
|
|
38
|
+
expect(discoveryCalled).toBe(false)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('no handle — influencers_similar skipped, influencers_discovery fires', async () => {
|
|
42
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
43
|
+
const u = url.toString()
|
|
44
|
+
if (u.includes('/influencers-club/discovery') && !u.includes('/creators/similar')) {
|
|
45
|
+
return new Response(JSON.stringify({
|
|
46
|
+
accounts: [{ handle: 'salesforce', platform: 'linkedin', followers: 1_200_000 }],
|
|
47
|
+
}), { status: 200 })
|
|
48
|
+
}
|
|
49
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
50
|
+
}) as typeof fetch
|
|
51
|
+
|
|
52
|
+
const result = await findInfluencersHandler({ platform: 'linkedin', ai_search: 'B2B SaaS founders', limit: 5 })
|
|
53
|
+
|
|
54
|
+
expect(result.isError).toBeFalsy()
|
|
55
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
56
|
+
expect(parsed._meta.provider).toBe('influencers_discovery')
|
|
57
|
+
expect(parsed.data.accounts).toHaveLength(1)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('handle set but similar returns empty — falls back to influencers_discovery', async () => {
|
|
61
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
62
|
+
const u = url.toString()
|
|
63
|
+
if (u.includes('/influencers-club/discovery/creators/similar')) {
|
|
64
|
+
// hasResult fails: no creators/accounts array
|
|
65
|
+
return new Response(JSON.stringify({ creators: [] }), { status: 200 })
|
|
66
|
+
}
|
|
67
|
+
if (u.includes('/influencers-club/discovery') && !u.includes('/creators/similar')) {
|
|
68
|
+
return new Response(JSON.stringify({ accounts: [{ handle: 'techinfluencer' }] }), { status: 200 })
|
|
69
|
+
}
|
|
70
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
71
|
+
}) as typeof fetch
|
|
72
|
+
|
|
73
|
+
const result = await findInfluencersHandler({ platform: 'instagram', handle: 'unknown_handle', limit: 5 })
|
|
74
|
+
|
|
75
|
+
expect(result.isError).toBeFalsy()
|
|
76
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
77
|
+
expect(parsed._meta.provider).toBe('influencers_discovery')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('all providers fail — returns isError', async () => {
|
|
81
|
+
globalThis.fetch = vi.fn(async () => {
|
|
82
|
+
return new Response(JSON.stringify({ error: 'down' }), { status: 503 })
|
|
83
|
+
}) as typeof fetch
|
|
84
|
+
|
|
85
|
+
const result = await findInfluencersHandler({ platform: 'instagram', limit: 5 })
|
|
86
|
+
|
|
87
|
+
expect(result.isError).toBe(true)
|
|
88
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
89
|
+
expect(parsed.error).toBeTruthy()
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { initClient } from '../../src/client.js'
|
|
3
|
+
import { findPeopleHandler } from '../../src/tools/find-people.js'
|
|
4
|
+
|
|
5
|
+
describe('find_people handler', () => {
|
|
6
|
+
const originalFetch = globalThis.fetch
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
initClient('http://test-api.local', 'test-key')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
globalThis.fetch = originalFetch
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('falls back to Apollo when LeadsFactory async times out', async () => {
|
|
17
|
+
let callCount = 0
|
|
18
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
19
|
+
callCount++
|
|
20
|
+
const urlStr = url.toString()
|
|
21
|
+
|
|
22
|
+
// LeadsFactory create search — returns an ID
|
|
23
|
+
if (urlStr.includes('/leadsfactory/contact-finder/searches') && callCount === 1) {
|
|
24
|
+
return new Response(JSON.stringify({ _id: 'abc123', search_id: 'abc', nb_jobs_total: 1, progress_percentage: 0 }), { status: 201 })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// LeadsFactory poll — always RUNNING (will timeout)
|
|
28
|
+
if (urlStr.includes('/leadsfactory/contact-finder/searches/abc123')) {
|
|
29
|
+
return new Response(JSON.stringify({ id: 'abc123', search_id: 'abc', status: 'RUNNING', nb_jobs_total: 1, progress_percentage: 50 }), { status: 200 })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Apollo — succeeds
|
|
33
|
+
if (urlStr.includes('/apollo/people/search')) {
|
|
34
|
+
return new Response(JSON.stringify({ people: [{ name: 'Michel Lieben', title: 'CEO' }] }), { status: 200 })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
|
|
38
|
+
}) as typeof fetch
|
|
39
|
+
|
|
40
|
+
// Override timeout to 100ms so test runs fast
|
|
41
|
+
const { getProviders } = await import('../../src/registry.js')
|
|
42
|
+
const providers = getProviders('find_people')
|
|
43
|
+
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
44
|
+
const originalTimeout = lf.async!.timeoutMs
|
|
45
|
+
const originalInterval = lf.async!.pollIntervalMs
|
|
46
|
+
lf.async!.timeoutMs = 100
|
|
47
|
+
lf.async!.pollIntervalMs = 20
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const result = await findPeopleHandler({
|
|
51
|
+
company_domains: ['coldiq.com'],
|
|
52
|
+
job_titles: ['CEO'],
|
|
53
|
+
limit: 5,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
57
|
+
// Should fall back to Apollo since LeadsFactory times out
|
|
58
|
+
expect(parsed._meta.provider).toBe('apollo')
|
|
59
|
+
} finally {
|
|
60
|
+
lf.async!.timeoutMs = originalTimeout
|
|
61
|
+
lf.async!.pollIntervalMs = originalInterval
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('returns LeadsFactory results when poll completes with companies_personas', async () => {
|
|
66
|
+
let callCount = 0
|
|
67
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
68
|
+
callCount++
|
|
69
|
+
const urlStr = url.toString()
|
|
70
|
+
|
|
71
|
+
// LeadsFactory create search
|
|
72
|
+
if (urlStr.includes('/leadsfactory/contact-finder/searches') && !urlStr.includes('abc123')) {
|
|
73
|
+
return new Response(JSON.stringify({ _id: 'abc123', search_id: 'abc', nb_jobs_total: 1, progress_percentage: 0 }), { status: 201 })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// LeadsFactory poll — first RUNNING, then SUCCESSFUL with contacts
|
|
77
|
+
if (urlStr.includes('/leadsfactory/contact-finder/searches/abc123')) {
|
|
78
|
+
if (callCount <= 3) {
|
|
79
|
+
return new Response(JSON.stringify({ id: 'abc123', search_id: 'abc', status: 'RUNNING', nb_jobs_total: 1, progress_percentage: 50 }), { status: 200 })
|
|
80
|
+
}
|
|
81
|
+
return new Response(JSON.stringify({
|
|
82
|
+
id: 'abc123',
|
|
83
|
+
search_id: 'abc',
|
|
84
|
+
status: 'SUCCESSFUL',
|
|
85
|
+
nb_jobs_total: 1,
|
|
86
|
+
nb_jobs_complete: 1,
|
|
87
|
+
companies_personas: [{ company: 'ColdIQ', contacts: [{ first_name: 'Michel', last_name: 'Lieben', title: 'CEO' }] }],
|
|
88
|
+
}), { status: 200 })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
|
|
92
|
+
}) as typeof fetch
|
|
93
|
+
|
|
94
|
+
const { getProviders } = await import('../../src/registry.js')
|
|
95
|
+
const providers = getProviders('find_people')
|
|
96
|
+
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
97
|
+
const originalTimeout = lf.async!.timeoutMs
|
|
98
|
+
const originalInterval = lf.async!.pollIntervalMs
|
|
99
|
+
lf.async!.timeoutMs = 500
|
|
100
|
+
lf.async!.pollIntervalMs = 20
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const result = await findPeopleHandler({
|
|
104
|
+
company_domains: ['coldiq.com'],
|
|
105
|
+
job_titles: ['CEO'],
|
|
106
|
+
limit: 5,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
110
|
+
expect(parsed._meta.provider).toBe('leadsfactory')
|
|
111
|
+
expect(parsed.data.companies_personas).toHaveLength(1)
|
|
112
|
+
} finally {
|
|
113
|
+
lf.async!.timeoutMs = originalTimeout
|
|
114
|
+
lf.async!.pollIntervalMs = originalInterval
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('gap-fills with Apollo for domains LeadsFactory missed (no_results_domains)', async () => {
|
|
119
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
|
|
120
|
+
const urlStr = url.toString()
|
|
121
|
+
|
|
122
|
+
// LeadsFactory create
|
|
123
|
+
if (urlStr.includes('/leadsfactory/contact-finder/searches') && !urlStr.includes('abc123')) {
|
|
124
|
+
return new Response(JSON.stringify({ _id: 'abc123', search_id: 'abc', nb_jobs_total: 2, progress_percentage: 0 }), { status: 201 })
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// LeadsFactory poll — SUCCESSFUL with one miss
|
|
128
|
+
if (urlStr.includes('/leadsfactory/contact-finder/searches/abc123')) {
|
|
129
|
+
return new Response(JSON.stringify({
|
|
130
|
+
id: 'abc123', search_id: 'abc', status: 'SUCCESSFUL',
|
|
131
|
+
nb_jobs_total: 2, nb_jobs_complete: 2,
|
|
132
|
+
companies_personas: [{ company: 'ColdIQ', contacts: [{ first_name: 'Michel', last_name: 'Lieben' }] }],
|
|
133
|
+
no_results_domains: ['folk.app'],
|
|
134
|
+
}), { status: 200 })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Apollo gap-fill for folk.app
|
|
138
|
+
if (urlStr.includes('/apollo/people/search')) {
|
|
139
|
+
const body = JSON.parse((opts?.body as string) ?? '{}')
|
|
140
|
+
if (body.q_organization_domains?.includes('folk.app')) {
|
|
141
|
+
return new Response(JSON.stringify({ people: [{ name: 'Thibaud Elziere', title: 'CEO' }] }), { status: 200 })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
|
|
146
|
+
}) as typeof fetch
|
|
147
|
+
|
|
148
|
+
const { getProviders } = await import('../../src/registry.js')
|
|
149
|
+
const providers = getProviders('find_people')
|
|
150
|
+
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
151
|
+
const originalTimeout = lf.async!.timeoutMs
|
|
152
|
+
const originalInterval = lf.async!.pollIntervalMs
|
|
153
|
+
lf.async!.timeoutMs = 500
|
|
154
|
+
lf.async!.pollIntervalMs = 20
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const result = await findPeopleHandler({
|
|
158
|
+
company_domains: ['coldiq.com', 'folk.app'],
|
|
159
|
+
job_titles: ['CEO'],
|
|
160
|
+
limit: 2,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
164
|
+
expect(parsed._meta.provider).toBe('leadsfactory')
|
|
165
|
+
expect(parsed.data.companies_personas).toHaveLength(1)
|
|
166
|
+
expect(parsed.data.gap_fill.provider).toBe('apollo')
|
|
167
|
+
expect(parsed.data.gap_fill.domains).toEqual(['folk.app'])
|
|
168
|
+
expect(parsed.data.gap_fill.people).toHaveLength(1)
|
|
169
|
+
} finally {
|
|
170
|
+
lf.async!.timeoutMs = originalTimeout
|
|
171
|
+
lf.async!.pollIntervalMs = originalInterval
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('skips gap-fill when input used LinkedIn URLs (no trusted domains to pass Apollo)', async () => {
|
|
176
|
+
let apolloCalled = false
|
|
177
|
+
|
|
178
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
179
|
+
const urlStr = url.toString()
|
|
180
|
+
|
|
181
|
+
if (urlStr.includes('/leadsfactory/contact-finder/searches') && !urlStr.includes('abc123')) {
|
|
182
|
+
return new Response(JSON.stringify({ _id: 'abc123', nb_jobs_total: 2, progress_percentage: 0 }), { status: 201 })
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (urlStr.includes('/leadsfactory/contact-finder/searches/abc123')) {
|
|
186
|
+
return new Response(JSON.stringify({
|
|
187
|
+
id: 'abc123', status: 'SUCCESSFUL', nb_jobs_total: 2, nb_jobs_complete: 2,
|
|
188
|
+
companies_personas: [{ company: 'ColdIQ', contacts: [{ first_name: 'Michel' }] }],
|
|
189
|
+
no_results_domains: ['folk.app'],
|
|
190
|
+
}), { status: 200 })
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (urlStr.includes('/apollo/people/search')) {
|
|
194
|
+
apolloCalled = true
|
|
195
|
+
return new Response(JSON.stringify({ people: [{ name: 'Thibaud Elziere' }] }), { status: 200 })
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
|
|
199
|
+
}) as typeof fetch
|
|
200
|
+
|
|
201
|
+
const { getProviders } = await import('../../src/registry.js')
|
|
202
|
+
const providers = getProviders('find_people')
|
|
203
|
+
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
204
|
+
const originalTimeout = lf.async!.timeoutMs
|
|
205
|
+
const originalInterval = lf.async!.pollIntervalMs
|
|
206
|
+
lf.async!.timeoutMs = 500
|
|
207
|
+
lf.async!.pollIntervalMs = 20
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const result = await findPeopleHandler({
|
|
211
|
+
company_linkedin_urls: ['https://linkedin.com/company/coldiq', 'https://linkedin.com/company/folk'],
|
|
212
|
+
job_titles: ['CEO'],
|
|
213
|
+
limit: 2,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
217
|
+
expect(parsed._meta.provider).toBe('leadsfactory')
|
|
218
|
+
expect(parsed.data.gap_fill).toBeUndefined()
|
|
219
|
+
expect(apolloCalled).toBe(false)
|
|
220
|
+
} finally {
|
|
221
|
+
lf.async!.timeoutMs = originalTimeout
|
|
222
|
+
lf.async!.pollIntervalMs = originalInterval
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('does not pass company_domains to LeadsFactory when LinkedIn URLs are provided', async () => {
|
|
227
|
+
let capturedBody: Record<string, unknown> | null = null
|
|
228
|
+
|
|
229
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
|
|
230
|
+
const urlStr = url.toString()
|
|
231
|
+
|
|
232
|
+
if (urlStr.includes('/leadsfactory/contact-finder/searches') && !urlStr.includes('abc123')) {
|
|
233
|
+
capturedBody = JSON.parse((opts?.body as string) ?? '{}')
|
|
234
|
+
return new Response(JSON.stringify({ _id: 'abc123', nb_jobs_total: 2, progress_percentage: 0 }), { status: 201 })
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (urlStr.includes('/leadsfactory/contact-finder/searches/abc123')) {
|
|
238
|
+
return new Response(JSON.stringify({
|
|
239
|
+
id: 'abc123', status: 'SUCCESSFUL', nb_jobs_total: 2, nb_jobs_complete: 2,
|
|
240
|
+
companies_personas: [{ company: 'ColdIQ', contacts: [{ first_name: 'Michel' }] }],
|
|
241
|
+
}), { status: 200 })
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
|
|
245
|
+
}) as typeof fetch
|
|
246
|
+
|
|
247
|
+
const { getProviders } = await import('../../src/registry.js')
|
|
248
|
+
const providers = getProviders('find_people')
|
|
249
|
+
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
250
|
+
const originalTimeout = lf.async!.timeoutMs
|
|
251
|
+
const originalInterval = lf.async!.pollIntervalMs
|
|
252
|
+
lf.async!.timeoutMs = 500
|
|
253
|
+
lf.async!.pollIntervalMs = 20
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
await findPeopleHandler({
|
|
257
|
+
company_linkedin_urls: ['https://linkedin.com/company/coldiq', 'https://linkedin.com/company/folk'],
|
|
258
|
+
company_domains: ['coldiq.com', 'folk.app'],
|
|
259
|
+
job_titles: ['CEO'],
|
|
260
|
+
limit: 2,
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
expect(capturedBody).not.toBeNull()
|
|
264
|
+
expect(capturedBody!.company_linkedin_urls).toBeDefined()
|
|
265
|
+
expect(capturedBody!.company_domains).toBeUndefined()
|
|
266
|
+
} finally {
|
|
267
|
+
lf.async!.timeoutMs = originalTimeout
|
|
268
|
+
lf.async!.pollIntervalMs = originalInterval
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('sends each job title as a separate persona to LeadsFactory', async () => {
|
|
273
|
+
let capturedBody: Record<string, unknown> | null = null
|
|
274
|
+
|
|
275
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
|
|
276
|
+
const urlStr = url.toString()
|
|
277
|
+
|
|
278
|
+
if (urlStr.includes('/leadsfactory/contact-finder/searches') && !urlStr.includes('abc123')) {
|
|
279
|
+
capturedBody = JSON.parse((opts?.body as string) ?? '{}')
|
|
280
|
+
return new Response(JSON.stringify({ _id: 'abc123', nb_jobs_total: 1, progress_percentage: 0 }), { status: 201 })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (urlStr.includes('/leadsfactory/contact-finder/searches/abc123')) {
|
|
284
|
+
return new Response(JSON.stringify({
|
|
285
|
+
id: 'abc123', status: 'SUCCESSFUL', nb_jobs_total: 1, nb_jobs_complete: 1,
|
|
286
|
+
companies_personas: [{ company: 'ColdIQ', contacts: [{ first_name: 'Michel' }] }],
|
|
287
|
+
}), { status: 200 })
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
|
|
291
|
+
}) as typeof fetch
|
|
292
|
+
|
|
293
|
+
const { getProviders } = await import('../../src/registry.js')
|
|
294
|
+
const providers = getProviders('find_people')
|
|
295
|
+
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
296
|
+
const originalTimeout = lf.async!.timeoutMs
|
|
297
|
+
const originalInterval = lf.async!.pollIntervalMs
|
|
298
|
+
lf.async!.timeoutMs = 500
|
|
299
|
+
lf.async!.pollIntervalMs = 20
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
await findPeopleHandler({
|
|
303
|
+
company_domains: ['coldiq.com'],
|
|
304
|
+
job_titles: ['CEO', 'VP of Sales'],
|
|
305
|
+
limit: 10,
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
expect(capturedBody).not.toBeNull()
|
|
309
|
+
expect(capturedBody!.search).toMatchObject({
|
|
310
|
+
max_persona_results: 10,
|
|
311
|
+
personas: [{ job_title: 'CEO' }, { job_title: 'VP of Sales' }],
|
|
312
|
+
})
|
|
313
|
+
} finally {
|
|
314
|
+
lf.async!.timeoutMs = originalTimeout
|
|
315
|
+
lf.async!.pollIntervalMs = originalInterval
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('falls back to Apollo when LeadsFactory fails', async () => {
|
|
320
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
321
|
+
const urlStr = url.toString()
|
|
322
|
+
|
|
323
|
+
if (urlStr.includes('/leadsfactory/')) {
|
|
324
|
+
return new Response(JSON.stringify({ error: 'Provider error' }), { status: 502 })
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (urlStr.includes('/apollo/people/search')) {
|
|
328
|
+
return new Response(JSON.stringify({ people: [{ name: 'Michel Lieben' }] }), { status: 200 })
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
|
|
332
|
+
}) as typeof fetch
|
|
333
|
+
|
|
334
|
+
const result = await findPeopleHandler({
|
|
335
|
+
company_domains: ['coldiq.com'],
|
|
336
|
+
job_titles: ['CEO'],
|
|
337
|
+
limit: 5,
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
341
|
+
expect(parsed._meta.provider).toBe('apollo')
|
|
342
|
+
expect(parsed.data.people).toHaveLength(1)
|
|
343
|
+
})
|
|
344
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { initClient } from '../../src/client.js'
|
|
3
|
+
import { findPhoneHandler } from '../../src/tools/find-phone.js'
|
|
4
|
+
|
|
5
|
+
describe('find_phone 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 phone from FindyMail', async () => {
|
|
17
|
+
globalThis.fetch = vi.fn(async () =>
|
|
18
|
+
new Response(JSON.stringify({ phone: '+33612345678' }), { status: 200 })
|
|
19
|
+
) as typeof fetch
|
|
20
|
+
|
|
21
|
+
const result = await findPhoneHandler({ linkedin_url: 'https://linkedin.com/in/michel-lieben' })
|
|
22
|
+
|
|
23
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
24
|
+
expect(parsed._meta.provider).toBe('findymail')
|
|
25
|
+
expect(parsed.data.phone).toBe('+33612345678')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('falls through to LimaData when Findymail misses', async () => {
|
|
29
|
+
let callCount = 0
|
|
30
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
31
|
+
callCount++
|
|
32
|
+
const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.href : url.url
|
|
33
|
+
if (urlStr.includes('findymail')) return new Response(JSON.stringify({}), { status: 200 })
|
|
34
|
+
if (urlStr.includes('coldiq/find/phone')) return new Response(JSON.stringify({ phone: '+33698765432' }), { status: 200 })
|
|
35
|
+
return new Response(JSON.stringify({}), { status: 200 })
|
|
36
|
+
}) as typeof fetch
|
|
37
|
+
|
|
38
|
+
const result = await findPhoneHandler({ linkedin_url: 'https://linkedin.com/in/michel-lieben' })
|
|
39
|
+
|
|
40
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
41
|
+
expect(result.isError).toBeFalsy()
|
|
42
|
+
expect(parsed._meta.provider).toBe('limadata')
|
|
43
|
+
expect(parsed.data.phone).toBe('+33698765432')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('uses AI Ark when Findymail and LimaData both miss', async () => {
|
|
47
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
48
|
+
const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.href : url.url
|
|
49
|
+
if (urlStr.includes('findymail')) return new Response(JSON.stringify({}), { status: 200 })
|
|
50
|
+
if (urlStr.includes('coldiq/find/phone')) return new Response(JSON.stringify({}), { status: 200 })
|
|
51
|
+
if (urlStr.includes('mobile-phone-finder')) return new Response(JSON.stringify({ data: [['+33611223344']] }), { status: 200 })
|
|
52
|
+
return new Response(JSON.stringify({}), { status: 200 })
|
|
53
|
+
}) as typeof fetch
|
|
54
|
+
|
|
55
|
+
const result = await findPhoneHandler({ linkedin_url: 'https://linkedin.com/in/michel-lieben' })
|
|
56
|
+
|
|
57
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
58
|
+
expect(result.isError).toBeFalsy()
|
|
59
|
+
expect(parsed._meta.provider).toBe('ai-ark')
|
|
60
|
+
expect(parsed.data.data).toEqual([['+33611223344']])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('uses LimaData and AI Ark when linkedin_url absent (name+domain path)', async () => {
|
|
64
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
65
|
+
const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.href : url.url
|
|
66
|
+
if (urlStr.includes('coldiq/find/phone')) return new Response(JSON.stringify({}), { status: 200 })
|
|
67
|
+
if (urlStr.includes('mobile-phone-finder')) return new Response(JSON.stringify({ data: [['+33611223344']] }), { status: 200 })
|
|
68
|
+
return new Response(JSON.stringify({}), { status: 200 })
|
|
69
|
+
}) as typeof fetch
|
|
70
|
+
|
|
71
|
+
const result = await findPhoneHandler({
|
|
72
|
+
first_name: 'Michel',
|
|
73
|
+
last_name: 'Lieben',
|
|
74
|
+
company_domain: 'coldiq.com',
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
78
|
+
expect(result.isError).toBeFalsy()
|
|
79
|
+
expect(parsed._meta.provider).toBe('ai-ark')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('returns error with clear message when no valid input combo provided', async () => {
|
|
83
|
+
const result = await findPhoneHandler({ company_name: 'ColdIQ' })
|
|
84
|
+
|
|
85
|
+
expect(result.isError).toBe(true)
|
|
86
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
87
|
+
expect(parsed.error).toContain('linkedin_url')
|
|
88
|
+
expect(parsed.error).toContain('first_name')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('returns error when no provider finds a phone', async () => {
|
|
92
|
+
globalThis.fetch = vi.fn(async () =>
|
|
93
|
+
new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
|
|
94
|
+
) as typeof fetch
|
|
95
|
+
|
|
96
|
+
const result = await findPhoneHandler({ linkedin_url: 'https://linkedin.com/in/unknown' })
|
|
97
|
+
|
|
98
|
+
expect(result.isError).toBe(true)
|
|
99
|
+
})
|
|
100
|
+
})
|