@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.
Files changed (176) hide show
  1. package/dist/client.d.ts +8 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +47 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/executor.d.ts +21 -0
  6. package/dist/executor.d.ts.map +1 -0
  7. package/dist/executor.js +130 -0
  8. package/dist/executor.js.map +1 -0
  9. package/dist/index.d.ts +3 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +49 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/registry.d.ts +49 -0
  14. package/dist/registry.d.ts.map +1 -0
  15. package/dist/registry.js +3104 -0
  16. package/dist/registry.js.map +1 -0
  17. package/dist/tools/enrich-company.d.ts +22 -0
  18. package/dist/tools/enrich-company.d.ts.map +1 -0
  19. package/dist/tools/enrich-company.js +21 -0
  20. package/dist/tools/enrich-company.js.map +1 -0
  21. package/dist/tools/enrich-email.d.ts +24 -0
  22. package/dist/tools/enrich-email.d.ts.map +1 -0
  23. package/dist/tools/enrich-email.js +19 -0
  24. package/dist/tools/enrich-email.js.map +1 -0
  25. package/dist/tools/enrich-emails.d.ts +31 -0
  26. package/dist/tools/enrich-emails.d.ts.map +1 -0
  27. package/dist/tools/enrich-emails.js +146 -0
  28. package/dist/tools/enrich-emails.js.map +1 -0
  29. package/dist/tools/enrich-person.d.ts +26 -0
  30. package/dist/tools/enrich-person.d.ts.map +1 -0
  31. package/dist/tools/enrich-person.js +23 -0
  32. package/dist/tools/enrich-person.js.map +1 -0
  33. package/dist/tools/fetch-page-content.d.ts +22 -0
  34. package/dist/tools/fetch-page-content.d.ts.map +1 -0
  35. package/dist/tools/fetch-page-content.js +32 -0
  36. package/dist/tools/fetch-page-content.js.map +1 -0
  37. package/dist/tools/find-email.d.ts +24 -0
  38. package/dist/tools/find-email.d.ts.map +1 -0
  39. package/dist/tools/find-email.js +19 -0
  40. package/dist/tools/find-email.js.map +1 -0
  41. package/dist/tools/find-emails.d.ts +31 -0
  42. package/dist/tools/find-emails.d.ts.map +1 -0
  43. package/dist/tools/find-emails.js +146 -0
  44. package/dist/tools/find-emails.js.map +1 -0
  45. package/dist/tools/find-influencers.d.ts +29 -0
  46. package/dist/tools/find-influencers.d.ts.map +1 -0
  47. package/dist/tools/find-influencers.js +30 -0
  48. package/dist/tools/find-influencers.js.map +1 -0
  49. package/dist/tools/find-people.d.ts +26 -0
  50. package/dist/tools/find-people.d.ts.map +1 -0
  51. package/dist/tools/find-people.js +61 -0
  52. package/dist/tools/find-people.js.map +1 -0
  53. package/dist/tools/find-phone.d.ts +24 -0
  54. package/dist/tools/find-phone.d.ts.map +1 -0
  55. package/dist/tools/find-phone.js +48 -0
  56. package/dist/tools/find-phone.js.map +1 -0
  57. package/dist/tools/find-signals.d.ts +26 -0
  58. package/dist/tools/find-signals.d.ts.map +1 -0
  59. package/dist/tools/find-signals.js +82 -0
  60. package/dist/tools/find-signals.js.map +1 -0
  61. package/dist/tools/search-ads.d.ts +33 -0
  62. package/dist/tools/search-ads.d.ts.map +1 -0
  63. package/dist/tools/search-ads.js +33 -0
  64. package/dist/tools/search-ads.js.map +1 -0
  65. package/dist/tools/search-companies.d.ts +42 -0
  66. package/dist/tools/search-companies.d.ts.map +1 -0
  67. package/dist/tools/search-companies.js +37 -0
  68. package/dist/tools/search-companies.js.map +1 -0
  69. package/dist/tools/search-jobs.d.ts +51 -0
  70. package/dist/tools/search-jobs.d.ts.map +1 -0
  71. package/dist/tools/search-jobs.js +64 -0
  72. package/dist/tools/search-jobs.js.map +1 -0
  73. package/dist/tools/search-places.d.ts +47 -0
  74. package/dist/tools/search-places.d.ts.map +1 -0
  75. package/dist/tools/search-places.js +42 -0
  76. package/dist/tools/search-places.js.map +1 -0
  77. package/dist/tools/search-reddit.d.ts +27 -0
  78. package/dist/tools/search-reddit.d.ts.map +1 -0
  79. package/dist/tools/search-reddit.js +30 -0
  80. package/dist/tools/search-reddit.js.map +1 -0
  81. package/dist/tools/search-seo.d.ts +37 -0
  82. package/dist/tools/search-seo.d.ts.map +1 -0
  83. package/dist/tools/search-seo.js +49 -0
  84. package/dist/tools/search-seo.js.map +1 -0
  85. package/dist/tools/search-web.d.ts +23 -0
  86. package/dist/tools/search-web.d.ts.map +1 -0
  87. package/dist/tools/search-web.js +20 -0
  88. package/dist/tools/search-web.js.map +1 -0
  89. package/dist/tools/verify-email.d.ts +20 -0
  90. package/dist/tools/verify-email.d.ts.map +1 -0
  91. package/dist/tools/verify-email.js +15 -0
  92. package/dist/tools/verify-email.js.map +1 -0
  93. package/package.json +28 -0
  94. package/src/client.ts +60 -0
  95. package/src/executor.ts +182 -0
  96. package/src/index.ts +155 -0
  97. package/src/registry.ts +3159 -0
  98. package/src/tools/enrich-company.ts +25 -0
  99. package/src/tools/enrich-person.ts +27 -0
  100. package/src/tools/fetch-page-content.ts +36 -0
  101. package/src/tools/find-email.ts +23 -0
  102. package/src/tools/find-emails.ts +190 -0
  103. package/src/tools/find-influencers.ts +34 -0
  104. package/src/tools/find-people.ts +69 -0
  105. package/src/tools/find-phone.ts +53 -0
  106. package/src/tools/find-signals.ts +93 -0
  107. package/src/tools/search-ads.ts +44 -0
  108. package/src/tools/search-companies.ts +41 -0
  109. package/src/tools/search-jobs.ts +73 -0
  110. package/src/tools/search-places.ts +52 -0
  111. package/src/tools/search-reddit.ts +34 -0
  112. package/src/tools/search-seo.ts +59 -0
  113. package/src/tools/search-web.ts +24 -0
  114. package/src/tools/verify-email.ts +19 -0
  115. package/test-ads-live.ts +77 -0
  116. package/test-company-live.ts +91 -0
  117. package/test-email-live.ts +171 -0
  118. package/test-influencers-live.ts +66 -0
  119. package/test-jobs-live.ts +69 -0
  120. package/test-linkupapi-live.ts +137 -0
  121. package/test-phone-live.ts +41 -0
  122. package/test-places-live.ts +89 -0
  123. package/test-reddit-live.ts +66 -0
  124. package/test-search-live.ts +79 -0
  125. package/test-seo-live.ts +68 -0
  126. package/test-web-live.ts +67 -0
  127. package/tests/client.test.ts +90 -0
  128. package/tests/executor.test.ts +83 -0
  129. package/tests/gtm/01-icp-to-emails.test.ts +43 -0
  130. package/tests/gtm/02-icp-bulk-emails.test.ts +38 -0
  131. package/tests/gtm/03-icp-to-phones.test.ts +39 -0
  132. package/tests/gtm/04-funding-signal-outreach.test.ts +42 -0
  133. package/tests/gtm/05-hiring-signal-decisionmakers.test.ts +41 -0
  134. package/tests/gtm/06-intent-signal-outreach.test.ts +44 -0
  135. package/tests/gtm/07-places-to-content.test.ts +50 -0
  136. package/tests/gtm/08-domain-to-account.test.ts +44 -0
  137. package/tests/gtm/09-linkedin-to-everything.test.ts +41 -0
  138. package/tests/gtm/10-jobs-vs-signals-routing.test.ts +38 -0
  139. package/tests/gtm/11-find-vs-enrich-routing.test.ts +39 -0
  140. package/tests/gtm/12-bogus-domain-graceful.test.ts +42 -0
  141. package/tests/gtm/13-private-linkedin-graceful.test.ts +44 -0
  142. package/tests/gtm/14-empty-handoff.test.ts +43 -0
  143. package/tests/gtm/15-seo-reddit-research.test.ts +38 -0
  144. package/tests/gtm/README.md +59 -0
  145. package/tests/gtm/harness.ts +217 -0
  146. package/tests/gtm/tools-bridge.ts +232 -0
  147. package/tests/gtm-scenarios.md +32 -0
  148. package/tests/live/smoke-report.ts +255 -0
  149. package/tests/live/smoke.test.ts +134 -0
  150. package/tests/registry-enrich-person.test.ts +447 -0
  151. package/tests/registry-fetch-page-content.test.ts +90 -0
  152. package/tests/registry-find-people.test.ts +467 -0
  153. package/tests/registry-find-signals.test.ts +470 -0
  154. package/tests/registry-linkupapi.test.ts +331 -0
  155. package/tests/registry-search-companies.test.ts +188 -0
  156. package/tests/registry-search-jobs.test.ts +116 -0
  157. package/tests/registry.test.ts +2210 -0
  158. package/tests/tools/enrich-company.test.ts +92 -0
  159. package/tests/tools/enrich-email.test.ts +94 -0
  160. package/tests/tools/enrich-emails.test.ts +271 -0
  161. package/tests/tools/enrich-person.test.ts +140 -0
  162. package/tests/tools/fetch-page-content.test.ts +108 -0
  163. package/tests/tools/find-influencers.test.ts +91 -0
  164. package/tests/tools/find-people.test.ts +344 -0
  165. package/tests/tools/find-phone.test.ts +100 -0
  166. package/tests/tools/find-signals.test.ts +110 -0
  167. package/tests/tools/search-ads.test.ts +182 -0
  168. package/tests/tools/search-companies.test.ts +58 -0
  169. package/tests/tools/search-jobs.test.ts +210 -0
  170. package/tests/tools/search-places.test.ts +114 -0
  171. package/tests/tools/search-reddit.test.ts +125 -0
  172. package/tests/tools/search-seo.test.ts +183 -0
  173. package/tests/tools/search-web.test.ts +79 -0
  174. package/tests/tools/verify-email.test.ts +68 -0
  175. package/tsconfig.json +17 -0
  176. package/vitest.config.ts +7 -0
@@ -0,0 +1,447 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { getProviders } from '../src/registry.js'
3
+
4
+ const providers = () => getProviders('enrich_person')
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 enrich_person`)
8
+ return p
9
+ }
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // pdl-person-enrich
13
+ // ---------------------------------------------------------------------------
14
+
15
+ describe('pdl-person-enrich', () => {
16
+ const p = () => get('pdl-person-enrich')
17
+
18
+ it('has no isApplicable gate (always runs)', () => {
19
+ expect(p().isApplicable).toBeUndefined()
20
+ })
21
+
22
+ it('maps all inputs to PDL fields', () => {
23
+ expect(p().mapParams({
24
+ email: 'michel@coldiq.com',
25
+ linkedin_url: 'https://www.linkedin.com/in/michel-lieben',
26
+ phone: '+1234567890',
27
+ first_name: 'Michel',
28
+ last_name: 'Lieben',
29
+ company_name: 'ColdIQ',
30
+ })).toEqual({
31
+ body: {
32
+ email: 'michel@coldiq.com',
33
+ profile: 'https://www.linkedin.com/in/michel-lieben',
34
+ phone: '+1234567890',
35
+ first_name: 'Michel',
36
+ last_name: 'Lieben',
37
+ company: 'ColdIQ',
38
+ },
39
+ })
40
+ })
41
+
42
+ it('falls back to domain when company_name absent', () => {
43
+ const result = p().mapParams({ domain: 'coldiq.com' })
44
+ expect((result.body as Record<string, unknown>).company).toBe('coldiq.com')
45
+ })
46
+
47
+ it('hasResult: true on 200 + data', () => {
48
+ expect(p().hasResult({ status: 200, data: { name: 'Michel Lieben' } })).toBe(true)
49
+ })
50
+
51
+ it('hasResult: false when PDL 404 (no match)', () => {
52
+ expect(p().hasResult({ status: 404, data: null })).toBe(false)
53
+ })
54
+
55
+ it('hasResult: false on empty object', () => {
56
+ expect(p().hasResult({})).toBe(false)
57
+ })
58
+
59
+ it('priority is 3', () => {
60
+ expect(p().priority).toBe(3)
61
+ })
62
+ })
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // apollo-people-match
66
+ // ---------------------------------------------------------------------------
67
+
68
+ describe('apollo-people-match', () => {
69
+ const p = () => get('apollo-people-match')
70
+
71
+ it('has no isApplicable gate', () => {
72
+ expect(p().isApplicable).toBeUndefined()
73
+ })
74
+
75
+ it('maps fields to Apollo shape', () => {
76
+ expect(p().mapParams({
77
+ first_name: 'Michel',
78
+ last_name: 'Lieben',
79
+ email: 'michel@coldiq.com',
80
+ company_name: 'ColdIQ',
81
+ domain: 'coldiq.com',
82
+ linkedin_url: 'https://www.linkedin.com/in/michel-lieben',
83
+ })).toEqual({
84
+ body: {
85
+ first_name: 'Michel',
86
+ last_name: 'Lieben',
87
+ email: 'michel@coldiq.com',
88
+ organization_name: 'ColdIQ',
89
+ domain: 'coldiq.com',
90
+ linkedin_url: 'https://www.linkedin.com/in/michel-lieben',
91
+ },
92
+ })
93
+ })
94
+
95
+ it('hasResult: true when person object present', () => {
96
+ expect(p().hasResult({ person: { name: 'Michel Lieben', id: 'abc' } })).toBe(true)
97
+ })
98
+
99
+ it('hasResult: false when person is null', () => {
100
+ expect(p().hasResult({ person: null })).toBe(false)
101
+ })
102
+
103
+ it('hasResult: false on empty object', () => {
104
+ expect(p().hasResult({})).toBe(false)
105
+ })
106
+
107
+ it('priority is 4', () => {
108
+ expect(p().priority).toBe(4)
109
+ })
110
+ })
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // blitzapi-reverse-email
114
+ // ---------------------------------------------------------------------------
115
+
116
+ describe('blitzapi-reverse-email', () => {
117
+ const p = () => get('blitzapi-reverse-email')
118
+
119
+ it('isApplicable: true for valid email', () => {
120
+ expect(p().isApplicable!({ email: 'michel@coldiq.com' })).toBe(true)
121
+ })
122
+
123
+ it('isApplicable: false when email absent', () => {
124
+ expect(p().isApplicable!({ first_name: 'Michel' })).toBe(false)
125
+ })
126
+
127
+ it('isApplicable: false when no @ in string', () => {
128
+ expect(p().isApplicable!({ email: 'notanemail' })).toBe(false)
129
+ })
130
+
131
+ it('mapParams wraps email in body', () => {
132
+ expect(p().mapParams({ email: 'michel@coldiq.com' })).toEqual({
133
+ body: { email: 'michel@coldiq.com' },
134
+ })
135
+ })
136
+
137
+ it('hasResult: true when person key present', () => {
138
+ expect(p().hasResult({ person: { name: 'Michel Lieben' } })).toBe(true)
139
+ })
140
+
141
+ it('hasResult: true when name key present', () => {
142
+ expect(p().hasResult({ name: 'Michel Lieben' } )).toBe(true)
143
+ })
144
+
145
+ it('hasResult: false on empty object', () => {
146
+ expect(p().hasResult({})).toBe(false)
147
+ })
148
+ })
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // findymail-business-profile
152
+ // ---------------------------------------------------------------------------
153
+
154
+ describe('findymail-business-profile', () => {
155
+ const p = () => get('findymail-business-profile')
156
+
157
+ it('isApplicable: true for linkedin.com URL', () => {
158
+ expect(p().isApplicable!({ linkedin_url: 'https://www.linkedin.com/in/michel-lieben' })).toBe(true)
159
+ })
160
+
161
+ it('isApplicable: false when linkedin_url absent', () => {
162
+ expect(p().isApplicable!({ email: 'michel@coldiq.com' })).toBe(false)
163
+ })
164
+
165
+ it('isApplicable: false for non-linkedin URL', () => {
166
+ expect(p().isApplicable!({ linkedin_url: 'https://twitter.com/michel' })).toBe(false)
167
+ })
168
+
169
+ it('mapParams sends linkedin_url in body', () => {
170
+ expect(p().mapParams({ linkedin_url: 'https://www.linkedin.com/in/michel-lieben' })).toEqual({
171
+ body: { linkedin_url: 'https://www.linkedin.com/in/michel-lieben' },
172
+ })
173
+ })
174
+
175
+ it('hasResult: true when email present', () => {
176
+ expect(p().hasResult({ email: 'michel@coldiq.com', name: 'Michel Lieben' })).toBe(true)
177
+ })
178
+
179
+ it('hasResult: false when error key present', () => {
180
+ expect(p().hasResult({ error: 'Not found' })).toBe(false)
181
+ })
182
+
183
+ it('hasResult: false on empty object', () => {
184
+ expect(p().hasResult({})).toBe(false)
185
+ })
186
+ })
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // findymail-reverse-email
190
+ // ---------------------------------------------------------------------------
191
+
192
+ describe('findymail-reverse-email', () => {
193
+ const p = () => get('findymail-reverse-email')
194
+
195
+ it('isApplicable: true for email', () => {
196
+ expect(p().isApplicable!({ email: 'michel@coldiq.com' })).toBe(true)
197
+ })
198
+
199
+ it('isApplicable: false without email', () => {
200
+ expect(p().isApplicable!({ first_name: 'Michel' })).toBe(false)
201
+ })
202
+
203
+ it('mapParams sends email + with_profile:true', () => {
204
+ expect(p().mapParams({ email: 'michel@coldiq.com' })).toEqual({
205
+ body: { email: 'michel@coldiq.com', with_profile: true },
206
+ })
207
+ })
208
+
209
+ it('hasResult: true when name present', () => {
210
+ expect(p().hasResult({ name: 'Michel Lieben', email: 'michel@coldiq.com' })).toBe(true)
211
+ })
212
+
213
+ it('hasResult: false when error present', () => {
214
+ expect(p().hasResult({ error: 'Not found' })).toBe(false)
215
+ })
216
+ })
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // icypeas-scrape-profile
220
+ // ---------------------------------------------------------------------------
221
+
222
+ describe('icypeas-scrape-profile', () => {
223
+ const p = () => get('icypeas-scrape-profile')
224
+
225
+ it('isApplicable: true for linkedin.com URL', () => {
226
+ expect(p().isApplicable!({ linkedin_url: 'https://www.linkedin.com/in/michel-lieben' })).toBe(true)
227
+ })
228
+
229
+ it('isApplicable: false when not a linkedin URL', () => {
230
+ expect(p().isApplicable!({ linkedin_url: 'https://example.com/michel' })).toBe(false)
231
+ })
232
+
233
+ it('isApplicable: false when linkedin_url absent', () => {
234
+ expect(p().isApplicable!({ email: 'michel@coldiq.com' })).toBe(false)
235
+ })
236
+
237
+ it('mapParams maps linkedin_url to url field', () => {
238
+ expect(p().mapParams({ linkedin_url: 'https://www.linkedin.com/in/michel-lieben' })).toEqual({
239
+ body: { url: 'https://www.linkedin.com/in/michel-lieben' },
240
+ })
241
+ })
242
+
243
+ it('hasResult: true when success + profile', () => {
244
+ expect(p().hasResult({ success: true, profile: { name: 'Michel Lieben' } })).toBe(true)
245
+ })
246
+
247
+ it('hasResult: false when success:false', () => {
248
+ expect(p().hasResult({ success: false, profile: null })).toBe(false)
249
+ })
250
+
251
+ it('hasResult: false on empty', () => {
252
+ expect(p().hasResult({})).toBe(false)
253
+ })
254
+ })
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // icypeas-url-search-profile
258
+ // ---------------------------------------------------------------------------
259
+
260
+ describe('icypeas-url-search-profile', () => {
261
+ const p = () => get('icypeas-url-search-profile')
262
+
263
+ it('isApplicable: true with name + company_name', () => {
264
+ expect(p().isApplicable!({ first_name: 'Michel', last_name: 'Lieben', company_name: 'ColdIQ' })).toBe(true)
265
+ })
266
+
267
+ it('isApplicable: true with name + domain', () => {
268
+ expect(p().isApplicable!({ first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' })).toBe(true)
269
+ })
270
+
271
+ it('isApplicable: false with only name (no company)', () => {
272
+ expect(p().isApplicable!({ first_name: 'Michel', last_name: 'Lieben' })).toBe(false)
273
+ })
274
+
275
+ it('isApplicable: false with only email', () => {
276
+ expect(p().isApplicable!({ email: 'michel@coldiq.com' })).toBe(false)
277
+ })
278
+
279
+ it('mapParams converts to Icypeas camelCase fields', () => {
280
+ expect(p().mapParams({ first_name: 'Michel', last_name: 'Lieben', company_name: 'ColdIQ' })).toEqual({
281
+ body: { firstname: 'Michel', lastname: 'Lieben', companyOrDomain: 'ColdIQ' },
282
+ })
283
+ })
284
+
285
+ it('mapParams prefers company_name over domain', () => {
286
+ const r = p().mapParams({ first_name: 'Michel', last_name: 'Lieben', company_name: 'ColdIQ', domain: 'coldiq.com' })
287
+ expect((r.body as Record<string, unknown>).companyOrDomain).toBe('ColdIQ')
288
+ })
289
+
290
+ it('hasResult: true when profileUrl contains linkedin.com', () => {
291
+ expect(p().hasResult({ profileUrl: 'https://www.linkedin.com/in/michel-lieben' })).toBe(true)
292
+ })
293
+
294
+ it('hasResult: false when profileUrl absent', () => {
295
+ expect(p().hasResult({})).toBe(false)
296
+ })
297
+
298
+ it('hasResult: false when profileUrl is not linkedin', () => {
299
+ expect(p().hasResult({ profileUrl: 'https://example.com' })).toBe(false)
300
+ })
301
+ })
302
+
303
+ // ---------------------------------------------------------------------------
304
+ // ai-ark-reverse-lookup
305
+ // ---------------------------------------------------------------------------
306
+
307
+ describe('ai-ark-reverse-lookup', () => {
308
+ const p = () => get('ai-ark-reverse-lookup')
309
+
310
+ it('isApplicable: true for email', () => {
311
+ expect(p().isApplicable!({ email: 'michel@coldiq.com' })).toBe(true)
312
+ })
313
+
314
+ it('isApplicable: true for phone', () => {
315
+ expect(p().isApplicable!({ phone: '+32477123456' })).toBe(true)
316
+ })
317
+
318
+ it('isApplicable: false without email or phone', () => {
319
+ expect(p().isApplicable!({ first_name: 'Michel' })).toBe(false)
320
+ })
321
+
322
+ it('mapParams prefers email over phone', () => {
323
+ expect(p().mapParams({ email: 'michel@coldiq.com', phone: '+32477123456' })).toEqual({
324
+ body: { search: 'michel@coldiq.com' },
325
+ })
326
+ })
327
+
328
+ it('mapParams uses phone when no email', () => {
329
+ expect(p().mapParams({ phone: '+32477123456' })).toEqual({
330
+ body: { search: '+32477123456' },
331
+ })
332
+ })
333
+
334
+ it('hasResult: true when name present', () => {
335
+ expect(p().hasResult({ name: 'Michel Lieben', email: 'michel@coldiq.com' })).toBe(true)
336
+ })
337
+
338
+ it('hasResult: false on empty', () => {
339
+ expect(p().hasResult({})).toBe(false)
340
+ })
341
+ })
342
+
343
+ // ---------------------------------------------------------------------------
344
+ // icypeas-reverse-email-lookup
345
+ // ---------------------------------------------------------------------------
346
+
347
+ describe('icypeas-reverse-email-lookup', () => {
348
+ const p = () => get('icypeas-reverse-email-lookup')
349
+
350
+ it('isApplicable: true for email', () => {
351
+ expect(p().isApplicable!({ email: 'michel@coldiq.com' })).toBe(true)
352
+ })
353
+
354
+ it('isApplicable: false without email', () => {
355
+ expect(p().isApplicable!({ first_name: 'Michel' })).toBe(false)
356
+ })
357
+
358
+ it('mapParams sends email in body', () => {
359
+ expect(p().mapParams({ email: 'michel@coldiq.com' })).toEqual({
360
+ body: { email: 'michel@coldiq.com' },
361
+ })
362
+ })
363
+
364
+ it('hasResult: true when non-empty profiles array', () => {
365
+ expect(p().hasResult({ profiles: [{ name: 'Michel Lieben' }] })).toBe(true)
366
+ })
367
+
368
+ it('hasResult: true when success + data', () => {
369
+ expect(p().hasResult({ success: true, data: { name: 'Michel Lieben' } })).toBe(true)
370
+ })
371
+
372
+ it('hasResult: false when profiles is empty array', () => {
373
+ expect(p().hasResult({ profiles: [] })).toBe(false)
374
+ })
375
+
376
+ it('hasResult: false on empty object', () => {
377
+ expect(p().hasResult({})).toBe(false)
378
+ })
379
+ })
380
+
381
+ // ---------------------------------------------------------------------------
382
+ // pdl-person-identify
383
+ // ---------------------------------------------------------------------------
384
+
385
+ describe('pdl-person-identify', () => {
386
+ const p = () => get('pdl-person-identify')
387
+
388
+ it('has no isApplicable gate', () => {
389
+ expect(p().isApplicable).toBeUndefined()
390
+ })
391
+
392
+ it('maps inputs to PDL fields', () => {
393
+ expect(p().mapParams({
394
+ email: 'michel@coldiq.com',
395
+ first_name: 'Michel',
396
+ last_name: 'Lieben',
397
+ company_name: 'ColdIQ',
398
+ })).toEqual({
399
+ body: {
400
+ email: 'michel@coldiq.com',
401
+ profile: undefined,
402
+ phone: undefined,
403
+ first_name: 'Michel',
404
+ last_name: 'Lieben',
405
+ company: 'ColdIQ',
406
+ },
407
+ })
408
+ })
409
+
410
+ it('hasResult: true when matches array is non-empty', () => {
411
+ expect(p().hasResult({ status: 200, matches: [{ data: { name: 'Michel Lieben' }, match_score: 80 }], total: 1 })).toBe(true)
412
+ })
413
+
414
+ it('hasResult: false when matches is empty', () => {
415
+ expect(p().hasResult({ status: 200, matches: [], total: 0 })).toBe(false)
416
+ })
417
+
418
+ it('hasResult: false when status != 200', () => {
419
+ expect(p().hasResult({ status: 404, matches: [] })).toBe(false)
420
+ })
421
+
422
+ it('priority is 12 (last fallback)', () => {
423
+ expect(p().priority).toBe(12)
424
+ })
425
+ })
426
+
427
+ // ---------------------------------------------------------------------------
428
+ // Ordering: enrich_person providers sorted correctly
429
+ // ---------------------------------------------------------------------------
430
+
431
+ describe('enrich_person priority ordering', () => {
432
+ it('all providers are sorted by priority', () => {
433
+ const ps = providers()
434
+ for (let i = 1; i < ps.length; i++) {
435
+ expect(ps[i].priority).toBeGreaterThanOrEqual(ps[i - 1].priority)
436
+ }
437
+ })
438
+
439
+ it('linkupapi-profile-enrich is first (priority 1)', () => {
440
+ expect(providers()[0].id).toBe('linkupapi-profile-enrich')
441
+ })
442
+
443
+ it('pdl-person-identify is last (priority 12)', () => {
444
+ const ps = providers()
445
+ expect(ps[ps.length - 1].id).toBe('pdl-person-identify')
446
+ })
447
+ })
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { getProviders } from '../src/registry.js'
3
+
4
+ const providers = () => getProviders('fetch_page_content')
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 fetch_page_content`)
8
+ return p
9
+ }
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // exa-contents
13
+ // ---------------------------------------------------------------------------
14
+
15
+ describe('exa-contents', () => {
16
+ const p = () => get('exa-contents')
17
+
18
+ it('has no isApplicable gate (always runs)', () => {
19
+ expect(p().isApplicable).toBeUndefined()
20
+ })
21
+
22
+ it('mapParams with defaults: text=true, no summary', () => {
23
+ expect(p().mapParams({
24
+ urls: ['https://coldiq.com', 'https://microsoft.com'],
25
+ include_text: true,
26
+ include_summary: false,
27
+ })).toEqual({
28
+ body: {
29
+ urls: ['https://coldiq.com', 'https://microsoft.com'],
30
+ text: true,
31
+ summary: undefined,
32
+ },
33
+ })
34
+ })
35
+
36
+ it('mapParams with include_summary=true adds summary object', () => {
37
+ const result = p().mapParams({
38
+ urls: ['https://coldiq.com'],
39
+ include_text: true,
40
+ include_summary: true,
41
+ })
42
+ expect((result.body as Record<string, unknown>).summary).toEqual({})
43
+ })
44
+
45
+ it('mapParams with include_text=false omits text', () => {
46
+ const result = p().mapParams({
47
+ urls: ['https://coldiq.com'],
48
+ include_text: false,
49
+ include_summary: false,
50
+ })
51
+ expect((result.body as Record<string, unknown>).text).toBeUndefined()
52
+ })
53
+
54
+ it('hasResult: true when results array non-empty', () => {
55
+ expect(p().hasResult({
56
+ results: [{ url: 'https://coldiq.com', text: 'ColdIQ is a sales engagement platform.' }],
57
+ })).toBe(true)
58
+ })
59
+
60
+ it('hasResult: false when results is empty', () => {
61
+ expect(p().hasResult({ results: [] })).toBe(false)
62
+ })
63
+
64
+ it('hasResult: false on empty object', () => {
65
+ expect(p().hasResult({})).toBe(false)
66
+ })
67
+
68
+ it('priority is 1', () => {
69
+ expect(p().priority).toBe(1)
70
+ })
71
+
72
+ it('endpoint is /exa/contents', () => {
73
+ expect(p().endpoint).toBe('/exa/contents')
74
+ expect(p().method).toBe('POST')
75
+ })
76
+ })
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Ordering
80
+ // ---------------------------------------------------------------------------
81
+
82
+ describe('fetch_page_content provider list', () => {
83
+ it('has exactly one provider', () => {
84
+ expect(providers()).toHaveLength(1)
85
+ })
86
+
87
+ it('exa-contents is the only provider', () => {
88
+ expect(providers()[0].id).toBe('exa-contents')
89
+ })
90
+ })