@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,331 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getProviders } from '../src/registry.js'
|
|
3
|
+
|
|
4
|
+
describe('linkupapi providers — search_companies', () => {
|
|
5
|
+
const providers = getProviders('search_companies')
|
|
6
|
+
const search = providers.find((p) => p.id === 'linkupapi-search')!
|
|
7
|
+
const fundraising = providers.find((p) => p.id === 'linkupapi-fundraising')!
|
|
8
|
+
const hiring = providers.find((p) => p.id === 'linkupapi-hiring')!
|
|
9
|
+
|
|
10
|
+
it('all three providers are registered', () => {
|
|
11
|
+
expect(search).toBeDefined()
|
|
12
|
+
expect(fundraising).toBeDefined()
|
|
13
|
+
expect(hiring).toBeDefined()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('priorities are 14, 15, 16 in order', () => {
|
|
17
|
+
expect(search.priority).toBe(14)
|
|
18
|
+
expect(fundraising.priority).toBe(15)
|
|
19
|
+
expect(hiring.priority).toBe(16)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('linkupapi-search', () => {
|
|
23
|
+
it('maps keywords, industries, locations, employee range', () => {
|
|
24
|
+
const result = search.mapParams({
|
|
25
|
+
keywords: ['AI', 'SaaS'],
|
|
26
|
+
industries: ['Software'],
|
|
27
|
+
locations: ['San Francisco'],
|
|
28
|
+
min_employees: 51,
|
|
29
|
+
max_employees: 200,
|
|
30
|
+
limit: 10,
|
|
31
|
+
})
|
|
32
|
+
expect((result.body as Record<string, unknown>).keyword).toBe('AI SaaS')
|
|
33
|
+
expect((result.body as Record<string, unknown>).employee_range).toBe('51-200')
|
|
34
|
+
expect((result.body as Record<string, unknown>).total_results).toBe(10)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('caps total_results at 25', () => {
|
|
38
|
+
const result = search.mapParams({ limit: 100 })
|
|
39
|
+
expect((result.body as Record<string, unknown>).total_results).toBe(25)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('hasResult: true for non-empty companies', () => {
|
|
43
|
+
expect(search.hasResult({ data: { companies: [{ name: 'Acme' }] } })).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('hasResult: false for empty companies', () => {
|
|
47
|
+
expect(search.hasResult({ data: { companies: [] } })).toBe(false)
|
|
48
|
+
expect(search.hasResult({})).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('linkupapi-fundraising', () => {
|
|
53
|
+
it('is not applicable without funding filter', () => {
|
|
54
|
+
expect(fundraising.isApplicable!({ keywords: ['AI'] })).toBe(false)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('is not applicable with funding filter but no text', () => {
|
|
58
|
+
expect(fundraising.isApplicable!({ funding_stages: ['series_b'] })).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('is applicable with funding_stages + keywords', () => {
|
|
62
|
+
expect(fundraising.isApplicable!({ funding_stages: ['series_b'], keywords: ['SaaS'] })).toBe(true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('is applicable with min_funding_amount + industries', () => {
|
|
66
|
+
expect(fundraising.isApplicable!({ min_funding_amount: 1_000_000, industries: ['Software'] })).toBe(true)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('is applicable with min_funding_year + keywords', () => {
|
|
70
|
+
expect(fundraising.isApplicable!({ min_funding_year: 2023, keywords: ['fintech'] })).toBe(true)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('maps funding_stage correctly: series_b → Series B', () => {
|
|
74
|
+
const result = fundraising.mapParams({
|
|
75
|
+
funding_stages: ['series_b'],
|
|
76
|
+
keywords: ['SaaS'],
|
|
77
|
+
industries: ['Software'],
|
|
78
|
+
})
|
|
79
|
+
expect((result.body as Record<string, unknown>).funding_stage).toBe('Series B')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('maps funding_stage: series_d → Series D+', () => {
|
|
83
|
+
const result = fundraising.mapParams({ funding_stages: ['series_d'], keywords: ['tech'], industries: [] })
|
|
84
|
+
expect((result.body as Record<string, unknown>).funding_stage).toBe('Series D+')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('maps seed correctly', () => {
|
|
88
|
+
const result = fundraising.mapParams({ funding_stages: ['seed'], keywords: ['AI'], industries: [] })
|
|
89
|
+
expect((result.body as Record<string, unknown>).funding_stage).toBe('Seed')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('builds keyword from keywords + industries', () => {
|
|
93
|
+
const result = fundraising.mapParams({
|
|
94
|
+
funding_stages: ['series_a'],
|
|
95
|
+
keywords: ['AI'],
|
|
96
|
+
industries: ['Software'],
|
|
97
|
+
})
|
|
98
|
+
expect((result.body as Record<string, unknown>).keyword).toBe('AI Software')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('hasResult: true for non-empty companies', () => {
|
|
102
|
+
expect(fundraising.hasResult({ data: { companies: [{}] } })).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('hasResult: false for empty or missing companies', () => {
|
|
106
|
+
expect(fundraising.hasResult({ data: { companies: [] } })).toBe(false)
|
|
107
|
+
expect(fundraising.hasResult({})).toBe(false)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('linkupapi-hiring', () => {
|
|
112
|
+
it('is only applicable when is_hiring is true', () => {
|
|
113
|
+
expect(hiring.isApplicable!({ is_hiring: true })).toBe(true)
|
|
114
|
+
expect(hiring.isApplicable!({ is_hiring: false })).toBe(false)
|
|
115
|
+
expect(hiring.isApplicable!({})).toBe(false)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('maps industry, location, employee range', () => {
|
|
119
|
+
const result = hiring.mapParams({
|
|
120
|
+
is_hiring: true,
|
|
121
|
+
industries: ['SaaS'],
|
|
122
|
+
locations: ['New York'],
|
|
123
|
+
min_employees: 51,
|
|
124
|
+
max_employees: 200,
|
|
125
|
+
limit: 10,
|
|
126
|
+
})
|
|
127
|
+
const body = result.body as Record<string, unknown>
|
|
128
|
+
expect(body.industry).toBe('SaaS')
|
|
129
|
+
expect(body.location).toBe('New York')
|
|
130
|
+
expect(body.employee_range).toBe('51-200')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('hasResult: true for non-empty companies', () => {
|
|
134
|
+
expect(hiring.hasResult({ data: { companies: [{}] } })).toBe(true)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('linkupapi provider — find_people', () => {
|
|
140
|
+
const providers = getProviders('find_people')
|
|
141
|
+
const sp = providers.find((p) => p.id === 'linkupapi-search-profiles')!
|
|
142
|
+
|
|
143
|
+
it('is registered at priority 5', () => {
|
|
144
|
+
expect(sp).toBeDefined()
|
|
145
|
+
expect(sp.priority).toBe(5)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('isApplicable: false when company_domains present', () => {
|
|
149
|
+
expect(sp.isApplicable!({ company_domains: ['stripe.com'] })).toBe(false)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('isApplicable: false when company_linkedin_urls present', () => {
|
|
153
|
+
expect(sp.isApplicable!({ company_linkedin_urls: ['https://linkedin.com/company/stripe'] })).toBe(false)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('isApplicable: true when only title/keyword/location filters', () => {
|
|
157
|
+
expect(sp.isApplicable!({ job_titles: ['CEO'], locations: ['Paris'] })).toBe(true)
|
|
158
|
+
expect(sp.isApplicable!({})).toBe(true)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('maps job_titles, locations, keywords', () => {
|
|
162
|
+
const result = sp.mapParams({ job_titles: ['VP Sales'], locations: ['US'], keywords: ['fintech'], limit: 20 })
|
|
163
|
+
const body = result.body as Record<string, unknown>
|
|
164
|
+
expect(body.job_title).toEqual(['VP Sales'])
|
|
165
|
+
expect(body.location).toEqual(['US'])
|
|
166
|
+
expect(body.keyword).toBe('fintech')
|
|
167
|
+
expect(body.total_results).toBe(20)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('hasResult: true for non-empty profiles', () => {
|
|
171
|
+
expect(sp.hasResult({ data: { profiles: [{}] } })).toBe(true)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('hasResult: false for empty profiles', () => {
|
|
175
|
+
expect(sp.hasResult({ data: { profiles: [] } })).toBe(false)
|
|
176
|
+
expect(sp.hasResult({})).toBe(false)
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe('linkupapi provider — verify_email', () => {
|
|
181
|
+
const providers = getProviders('verify_email')
|
|
182
|
+
const lv = providers.find((p) => p.id === 'linkupapi-validate')!
|
|
183
|
+
|
|
184
|
+
it('is registered at priority 4', () => {
|
|
185
|
+
expect(lv).toBeDefined()
|
|
186
|
+
expect(lv.priority).toBe(4)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('maps email', () => {
|
|
190
|
+
const result = lv.mapParams({ email: 'michel@coldiq.com' })
|
|
191
|
+
expect((result.body as Record<string, unknown>).email).toBe('michel@coldiq.com')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('hasResult: true when status present', () => {
|
|
195
|
+
expect(lv.hasResult({ status: 'valid' })).toBe(true)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('hasResult: true when verified present', () => {
|
|
199
|
+
expect(lv.hasResult({ verified: true })).toBe(true)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('hasResult: false for empty object', () => {
|
|
203
|
+
expect(lv.hasResult({})).toBe(false)
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('linkupapi providers — enrich_company', () => {
|
|
208
|
+
const providers = getProviders('enrich_company')
|
|
209
|
+
const byDomain = providers.find((p) => p.id === 'linkupapi-by-domain')!
|
|
210
|
+
const byUrl = providers.find((p) => p.id === 'linkupapi-by-url')!
|
|
211
|
+
|
|
212
|
+
it('both providers registered', () => {
|
|
213
|
+
expect(byDomain).toBeDefined()
|
|
214
|
+
expect(byUrl).toBeDefined()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('priorities are 13 and 14', () => {
|
|
218
|
+
expect(byDomain.priority).toBe(13)
|
|
219
|
+
expect(byUrl.priority).toBe(14)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('linkupapi-by-domain: applicable only when domain present', () => {
|
|
223
|
+
expect(byDomain.isApplicable!({ domain: 'coldiq.com' })).toBe(true)
|
|
224
|
+
expect(byDomain.isApplicable!({})).toBe(false)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('linkupapi-by-domain: maps domain', () => {
|
|
228
|
+
const result = byDomain.mapParams({ domain: 'coldiq.com' })
|
|
229
|
+
expect((result.body as Record<string, unknown>).domain).toBe('coldiq.com')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('linkupapi-by-url: applicable only when linkedin_url present', () => {
|
|
233
|
+
expect(byUrl.isApplicable!({ linkedin_url: 'https://linkedin.com/company/coldiq' })).toBe(true)
|
|
234
|
+
expect(byUrl.isApplicable!({})).toBe(false)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('linkupapi-by-url: maps linkedin_url as company_url', () => {
|
|
238
|
+
const result = byUrl.mapParams({ linkedin_url: 'https://linkedin.com/company/coldiq' })
|
|
239
|
+
expect((result.body as Record<string, unknown>).company_url).toBe('https://linkedin.com/company/coldiq')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('hasResult: true when name present', () => {
|
|
243
|
+
expect(byDomain.hasResult({ name: 'ColdIQ' })).toBe(true)
|
|
244
|
+
expect(byUrl.hasResult({ domain: 'coldiq.com' })).toBe(true)
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
describe('linkupapi providers — enrich_person', () => {
|
|
249
|
+
const providers = getProviders('enrich_person')
|
|
250
|
+
const profileEnrich = providers.find((p) => p.id === 'linkupapi-profile-enrich')!
|
|
251
|
+
const emailReverse = providers.find((p) => p.id === 'linkupapi-email-reverse')!
|
|
252
|
+
|
|
253
|
+
it('both providers registered', () => {
|
|
254
|
+
expect(profileEnrich).toBeDefined()
|
|
255
|
+
expect(emailReverse).toBeDefined()
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('priorities are 1 and 2', () => {
|
|
259
|
+
expect(profileEnrich.priority).toBe(1)
|
|
260
|
+
expect(emailReverse.priority).toBe(2)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
describe('linkupapi-profile-enrich', () => {
|
|
264
|
+
it('is applicable with first+last+company', () => {
|
|
265
|
+
expect(profileEnrich.isApplicable!({ first_name: 'Michel', last_name: 'Lieben', company_name: 'ColdIQ' })).toBe(true)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('is applicable with first+last+domain', () => {
|
|
269
|
+
expect(profileEnrich.isApplicable!({ first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' })).toBe(true)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('is not applicable without company or domain', () => {
|
|
273
|
+
expect(profileEnrich.isApplicable!({ first_name: 'Michel', last_name: 'Lieben' })).toBe(false)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('is not applicable without last_name', () => {
|
|
277
|
+
expect(profileEnrich.isApplicable!({ first_name: 'Michel', company_name: 'ColdIQ' })).toBe(false)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('maps company_name from domain when company_name absent', () => {
|
|
281
|
+
const result = profileEnrich.mapParams({ first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' })
|
|
282
|
+
expect((result.body as Record<string, unknown>).company_name).toBe('coldiq.com')
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('prefers company_name over domain', () => {
|
|
286
|
+
const result = profileEnrich.mapParams({
|
|
287
|
+
first_name: 'Michel',
|
|
288
|
+
last_name: 'Lieben',
|
|
289
|
+
company_name: 'ColdIQ',
|
|
290
|
+
domain: 'coldiq.com',
|
|
291
|
+
})
|
|
292
|
+
expect((result.body as Record<string, unknown>).company_name).toBe('ColdIQ')
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('hasResult: true for { status: success, data: {...} }', () => {
|
|
296
|
+
expect(profileEnrich.hasResult({ status: 'success', data: { full_profile_data: { first_name: 'Michel' } } })).toBe(true)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('hasResult: false when status is not success', () => {
|
|
300
|
+
expect(profileEnrich.hasResult({ status: 'error' })).toBe(false)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('hasResult: false for empty object', () => {
|
|
304
|
+
expect(profileEnrich.hasResult({})).toBe(false)
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
describe('linkupapi-email-reverse', () => {
|
|
309
|
+
it('is applicable with valid email', () => {
|
|
310
|
+
expect(emailReverse.isApplicable!({ email: 'michel@coldiq.com' })).toBe(true)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('is not applicable without email', () => {
|
|
314
|
+
expect(emailReverse.isApplicable!({})).toBe(false)
|
|
315
|
+
expect(emailReverse.isApplicable!({ email: 'notanemail' })).toBe(false)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('maps email', () => {
|
|
319
|
+
const result = emailReverse.mapParams({ email: 'michel@coldiq.com' })
|
|
320
|
+
expect((result.body as Record<string, unknown>).email).toBe('michel@coldiq.com')
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('hasResult: true for { status: success, data: {...} }', () => {
|
|
324
|
+
expect(emailReverse.hasResult({ status: 'success', data: { person_found: { first_name: 'Michel' } } })).toBe(true)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('hasResult: false for empty object', () => {
|
|
328
|
+
expect(emailReverse.hasResult({})).toBe(false)
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
})
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getProviders } from '../src/registry.js'
|
|
3
|
+
|
|
4
|
+
const providers = () => getProviders('search_companies')
|
|
5
|
+
const get = (id: string) => {
|
|
6
|
+
const p = providers().find((p) => p.id === id)
|
|
7
|
+
if (!p) throw new Error(`Provider '${id}' not found in search_companies`)
|
|
8
|
+
return p
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// prospeo-search-company
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
describe('prospeo-search-company', () => {
|
|
16
|
+
const p = () => get('prospeo-search-company')
|
|
17
|
+
|
|
18
|
+
it('isApplicable: true with keywords', () => {
|
|
19
|
+
expect(p().isApplicable!({ keywords: ['cold email'] })).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('isApplicable: true with industries', () => {
|
|
23
|
+
expect(p().isApplicable!({ industries: ['SaaS'] })).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('isApplicable: true with countries', () => {
|
|
27
|
+
expect(p().isApplicable!({ countries: ['BE'] })).toBe(true)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('isApplicable: false with only company_domains', () => {
|
|
31
|
+
expect(p().isApplicable!({ company_domains: ['coldiq.com'] })).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('mapParams builds Prospeo filter shape', () => {
|
|
35
|
+
expect(p().mapParams({
|
|
36
|
+
keywords: ['sales engagement'],
|
|
37
|
+
industries: ['SaaS'],
|
|
38
|
+
countries: ['BE'],
|
|
39
|
+
min_employees: 10,
|
|
40
|
+
max_employees: 200,
|
|
41
|
+
})).toEqual({
|
|
42
|
+
body: {
|
|
43
|
+
filters: {
|
|
44
|
+
company_keywords: { include: ['sales engagement'] },
|
|
45
|
+
company_industry: { include: ['SaaS'] },
|
|
46
|
+
company_country: { include: ['Belgium'] },
|
|
47
|
+
company_headcount_custom: { min: 10, max: 200 },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('mapParams omits absent filter keys', () => {
|
|
54
|
+
const result = p().mapParams({ keywords: ['outbound'] })
|
|
55
|
+
const filters = (result.body as Record<string, unknown>).filters as Record<string, unknown>
|
|
56
|
+
expect(filters.company_industry).toBeUndefined()
|
|
57
|
+
expect(filters.company_country).toBeUndefined()
|
|
58
|
+
expect(filters.company_headcount_custom).toBeUndefined()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('mapParams includes headcount when only min is given', () => {
|
|
62
|
+
const result = p().mapParams({ keywords: ['crm'], min_employees: 50 })
|
|
63
|
+
const filters = (result.body as Record<string, unknown>).filters as Record<string, unknown>
|
|
64
|
+
expect(filters.company_headcount_custom).toEqual({ min: 50, max: undefined })
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('hasResult: true when data array non-empty', () => {
|
|
68
|
+
expect(p().hasResult({ data: [{ name: 'ColdIQ' }] })).toBe(true)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('hasResult: false when data is empty', () => {
|
|
72
|
+
expect(p().hasResult({ data: [] })).toBe(false)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('hasResult: false on empty object', () => {
|
|
76
|
+
expect(p().hasResult({})).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('priority is 17', () => {
|
|
80
|
+
expect(p().priority).toBe(17)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// ai-ark-companies
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
describe('ai-ark-companies', () => {
|
|
89
|
+
const p = () => get('ai-ark-companies')
|
|
90
|
+
|
|
91
|
+
it('isApplicable: true with keywords', () => {
|
|
92
|
+
expect(p().isApplicable!({ keywords: ['cold email'] })).toBe(true)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('isApplicable: true with industries', () => {
|
|
96
|
+
expect(p().isApplicable!({ industries: ['SaaS'] })).toBe(true)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('isApplicable: true with countries', () => {
|
|
100
|
+
expect(p().isApplicable!({ countries: ['BE'] })).toBe(true)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('isApplicable: false with only min_employees', () => {
|
|
104
|
+
expect(p().isApplicable!({ min_employees: 50 })).toBe(false)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('mapParams builds AI-Ark account/contact shape', () => {
|
|
108
|
+
const result = p().mapParams({
|
|
109
|
+
keywords: ['outbound sales'],
|
|
110
|
+
industries: ['SaaS'],
|
|
111
|
+
countries: ['BE'],
|
|
112
|
+
min_employees: 10,
|
|
113
|
+
max_employees: 500,
|
|
114
|
+
funding_stages: ['Series A'],
|
|
115
|
+
limit: 20,
|
|
116
|
+
})
|
|
117
|
+
expect(result).toEqual({
|
|
118
|
+
body: {
|
|
119
|
+
account: {
|
|
120
|
+
industries: { any: { include: ['SaaS'] } },
|
|
121
|
+
location: { any: { include: ['Belgium'] } },
|
|
122
|
+
employeeSize: {
|
|
123
|
+
type: 'RANGE',
|
|
124
|
+
range: [{ start: 10, end: 500 }],
|
|
125
|
+
},
|
|
126
|
+
funding: { stage: { include: ['Series A'] } },
|
|
127
|
+
},
|
|
128
|
+
contact: {
|
|
129
|
+
keywordsInProfile: { any: { include: ['outbound sales'] } },
|
|
130
|
+
},
|
|
131
|
+
size: 20,
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('mapParams omits absent account keys', () => {
|
|
137
|
+
const result = p().mapParams({ keywords: ['CRM'] })
|
|
138
|
+
const account = (result.body as Record<string, unknown>).account as Record<string, unknown>
|
|
139
|
+
expect(account.industries).toBeUndefined()
|
|
140
|
+
expect(account.location).toBeUndefined()
|
|
141
|
+
expect(account.employeeSize).toBeUndefined()
|
|
142
|
+
expect(account.funding).toBeUndefined()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('mapParams omits contact when no keywords', () => {
|
|
146
|
+
const result = p().mapParams({ industries: ['SaaS'] })
|
|
147
|
+
expect((result.body as Record<string, unknown>).contact).toBeUndefined()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('mapParams caps size at 100', () => {
|
|
151
|
+
const result = p().mapParams({ keywords: ['sales'], limit: 999 })
|
|
152
|
+
expect((result.body as Record<string, unknown>).size).toBe(100)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('hasResult: true when content array non-empty', () => {
|
|
156
|
+
expect(p().hasResult({ content: [{ name: 'ColdIQ' }] })).toBe(true)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('hasResult: false when content is empty', () => {
|
|
160
|
+
expect(p().hasResult({ content: [] })).toBe(false)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('hasResult: false on empty object', () => {
|
|
164
|
+
expect(p().hasResult({})).toBe(false)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('priority is 18', () => {
|
|
168
|
+
expect(p().priority).toBe(18)
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Ordering: search_companies providers sorted correctly
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
describe('search_companies priority ordering', () => {
|
|
177
|
+
it('all providers are sorted by priority', () => {
|
|
178
|
+
const ps = providers()
|
|
179
|
+
for (let i = 1; i < ps.length; i++) {
|
|
180
|
+
expect(ps[i].priority).toBeGreaterThanOrEqual(ps[i - 1].priority)
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('ai-ark-companies is last (priority 18)', () => {
|
|
185
|
+
const ps = providers()
|
|
186
|
+
expect(ps[ps.length - 1].id).toBe('ai-ark-companies')
|
|
187
|
+
})
|
|
188
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getProviders } from '../src/registry.js'
|
|
3
|
+
|
|
4
|
+
const providers = () => getProviders('search_jobs')
|
|
5
|
+
const get = (id: string) => {
|
|
6
|
+
const p = providers().find((p) => p.id === id)
|
|
7
|
+
if (!p) throw new Error(`Provider '${id}' not found in search_jobs`)
|
|
8
|
+
return p
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// theirstack-jobs
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
describe('theirstack-jobs', () => {
|
|
16
|
+
const p = () => get('theirstack-jobs')
|
|
17
|
+
|
|
18
|
+
it('isApplicable: true with title_keywords', () => {
|
|
19
|
+
expect(p().isApplicable!({ title_keywords: ['software engineer'] })).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('isApplicable: true with description_keywords', () => {
|
|
23
|
+
expect(p().isApplicable!({ description_keywords: ['React', 'TypeScript'] })).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('isApplicable: true with companies', () => {
|
|
27
|
+
expect(p().isApplicable!({ companies: ['ColdIQ'] })).toBe(true)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('isApplicable: true with company_domains', () => {
|
|
31
|
+
expect(p().isApplicable!({ company_domains: ['coldiq.com'] })).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('isApplicable: false with only locations', () => {
|
|
35
|
+
expect(p().isApplicable!({ locations: ['Belgium'] })).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('isApplicable: false on empty object', () => {
|
|
39
|
+
expect(p().isApplicable!({})).toBe(false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('isApplicable: false with empty arrays', () => {
|
|
43
|
+
expect(p().isApplicable!({ title_keywords: [], companies: [] })).toBe(false)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('mapParams maps all fields to TheirStack shape', () => {
|
|
47
|
+
expect(p().mapParams({
|
|
48
|
+
title_keywords: ['software engineer'],
|
|
49
|
+
description_keywords: ['TypeScript'],
|
|
50
|
+
companies: ['ColdIQ'],
|
|
51
|
+
company_domains: ['coldiq.com'],
|
|
52
|
+
posted_after: '2026-01-01',
|
|
53
|
+
limit: 50,
|
|
54
|
+
})).toEqual({
|
|
55
|
+
body: {
|
|
56
|
+
job_title_pattern_or: ['software engineer'],
|
|
57
|
+
job_description_pattern_or: ['TypeScript'],
|
|
58
|
+
company_name_or: ['ColdIQ'],
|
|
59
|
+
company_domain_or: ['coldiq.com'],
|
|
60
|
+
posted_at_gte: '2026-01-01',
|
|
61
|
+
limit: 50,
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('mapParams passes undefined for absent fields', () => {
|
|
67
|
+
const result = p().mapParams({ title_keywords: ['engineer'] })
|
|
68
|
+
const body = result.body as Record<string, unknown>
|
|
69
|
+
expect(body.job_description_pattern_or).toBeUndefined()
|
|
70
|
+
expect(body.company_name_or).toBeUndefined()
|
|
71
|
+
expect(body.company_domain_or).toBeUndefined()
|
|
72
|
+
expect(body.posted_at_gte).toBeUndefined()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('hasResult: true when data array is non-empty', () => {
|
|
76
|
+
expect(p().hasResult({ data: [{ id: 1, title: 'Software Engineer', company: 'ColdIQ' }] })).toBe(true)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('hasResult: false when data is empty array', () => {
|
|
80
|
+
expect(p().hasResult({ data: [] })).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('hasResult: false on empty object', () => {
|
|
84
|
+
expect(p().hasResult({})).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('hasResult: false when data is null', () => {
|
|
88
|
+
expect(p().hasResult({ data: null })).toBe(false)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('priority is 3', () => {
|
|
92
|
+
expect(p().priority).toBe(3)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Ordering: search_jobs providers sorted correctly
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
describe('search_jobs priority ordering', () => {
|
|
101
|
+
it('all providers are sorted by priority', () => {
|
|
102
|
+
const ps = providers()
|
|
103
|
+
for (let i = 1; i < ps.length; i++) {
|
|
104
|
+
expect(ps[i].priority).toBeGreaterThanOrEqual(ps[i - 1].priority)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('career_site_jobs is first (priority 1)', () => {
|
|
109
|
+
expect(providers()[0].id).toBe('career_site_jobs')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('theirstack-jobs is last (priority 3)', () => {
|
|
113
|
+
const ps = providers()
|
|
114
|
+
expect(ps[ps.length - 1].id).toBe('theirstack-jobs')
|
|
115
|
+
})
|
|
116
|
+
})
|