@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,470 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { getProviders } from '../src/registry.js'
3
+
4
+ const providers = () => getProviders('find_signals')
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 find_signals`)
8
+ return p
9
+ }
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // signalbase-funding
13
+ // ---------------------------------------------------------------------------
14
+
15
+ describe('signalbase-funding', () => {
16
+ const p = () => get('signalbase-funding')
17
+
18
+ it('isApplicable: true for funding signal_type', () => {
19
+ expect(p().isApplicable!({ signal_type: 'funding' })).toBe(true)
20
+ })
21
+
22
+ it('isApplicable: false for other signal types', () => {
23
+ expect(p().isApplicable!({ signal_type: 'acquisition' })).toBe(false)
24
+ expect(p().isApplicable!({ signal_type: 'hiring' })).toBe(false)
25
+ expect(p().isApplicable!({ signal_type: 'intent' })).toBe(false)
26
+ })
27
+
28
+ it('mapParams builds correct query params', () => {
29
+ const result = p().mapParams({
30
+ signal_type: 'funding',
31
+ companies: ['ColdIQ'],
32
+ since: '2026-01-01',
33
+ industries: ['Software', 'SaaS'],
34
+ countries: ['US', 'GB'],
35
+ limit: 20,
36
+ })
37
+ expect(result).toEqual({
38
+ queryParams: {
39
+ company_name: 'ColdIQ',
40
+ dateFrom: '2026-01-01',
41
+ industry: 'Software,SaaS',
42
+ countries: 'US,GB',
43
+ limit: '20',
44
+ },
45
+ })
46
+ })
47
+
48
+ it('mapParams omits missing optional fields', () => {
49
+ const result = p().mapParams({ signal_type: 'funding', limit: 10 })
50
+ const qp = result.queryParams as Record<string, string>
51
+ expect(qp.company_name).toBeUndefined()
52
+ expect(qp.dateFrom).toBeUndefined()
53
+ expect(qp.industry).toBeUndefined()
54
+ expect(qp.countries).toBeUndefined()
55
+ expect(qp.limit).toBe('10')
56
+ })
57
+
58
+ it('mapParams caps limit at 100', () => {
59
+ const result = p().mapParams({ signal_type: 'funding', limit: 999 })
60
+ expect((result.queryParams as Record<string, string>).limit).toBe('100')
61
+ })
62
+
63
+ it('hasResult: true when data array non-empty', () => {
64
+ expect(p().hasResult({ data: [{ company: 'ColdIQ', amount: 1000000 }] })).toBe(true)
65
+ })
66
+
67
+ it('hasResult: false when data is empty', () => {
68
+ expect(p().hasResult({ data: [] })).toBe(false)
69
+ })
70
+
71
+ it('hasResult: false on empty object', () => {
72
+ expect(p().hasResult({})).toBe(false)
73
+ })
74
+
75
+ it('priority is 1', () => {
76
+ expect(p().priority).toBe(1)
77
+ })
78
+
79
+ // Signalbase data quality bug: some records populate company* fields with investor data.
80
+ // postFilter detects when companyName matches an investor name and fixes it from the slug.
81
+ describe('postFilter: investor-as-company fix', () => {
82
+ const buggedSignal = {
83
+ companyName: 'Newfund',
84
+ companySlug: 'fermeate',
85
+ companyWebsite: 'newfundcap.com',
86
+ companyLinkedin: 'linkedin.com/company/newfund-management',
87
+ companyLogo: 'https://images.trysignalbase.com/Newfund.png',
88
+ companyDescription: 'Newfund Capital is an early-stage VC firm...',
89
+ investors: [
90
+ { id: 'abc', name: 'Newfund', type: null },
91
+ { id: 'def', name: 'SOSV', type: 'VC firm' },
92
+ ],
93
+ amount: 2000000,
94
+ }
95
+
96
+ it('fixes companyName from slug when companyName matches an investor', () => {
97
+ const result = p().postFilter!({ data: [buggedSignal] }, {}) as { data: Array<Record<string, unknown>> }
98
+ expect(result.data[0].companyName).toBe('Fermeate')
99
+ })
100
+
101
+ it('nulls investor-tainted company fields', () => {
102
+ const result = p().postFilter!({ data: [buggedSignal] }, {}) as { data: Array<Record<string, unknown>> }
103
+ const fixed = result.data[0]
104
+ expect(fixed.companyWebsite).toBeNull()
105
+ expect(fixed.companyLinkedin).toBeNull()
106
+ expect(fixed.companyLogo).toBeNull()
107
+ expect(fixed.companyDescription).toBeNull()
108
+ })
109
+
110
+ it('preserves all other signal fields', () => {
111
+ const result = p().postFilter!({ data: [buggedSignal] }, {}) as { data: Array<Record<string, unknown>> }
112
+ expect(result.data[0].amount).toBe(2000000)
113
+ expect(result.data[0].investors).toEqual(buggedSignal.investors)
114
+ })
115
+
116
+ it('leaves clean records untouched', () => {
117
+ const cleanSignal = {
118
+ companyName: 'Crush Security',
119
+ companySlug: 'crush-security',
120
+ companyWebsite: 'crushsecurity.com',
121
+ companyLinkedin: 'linkedin.com/company/crush-security',
122
+ companyLogo: 'https://images.trysignalbase.com/Crush Security.png',
123
+ companyDescription: 'Cybersecurity compliance remediation.',
124
+ investors: [{ id: 'xyz', name: 'SomeVC', type: 'VC' }],
125
+ amount: 3000000,
126
+ }
127
+ const result = p().postFilter!({ data: [cleanSignal] }, {}) as { data: Array<Record<string, unknown>> }
128
+ expect(result.data[0].companyName).toBe('Crush Security')
129
+ expect(result.data[0].companyWebsite).toBe('crushsecurity.com')
130
+ })
131
+
132
+ it('handles empty investors array without error', () => {
133
+ const signal = { companyName: 'Fermeate', companySlug: 'fermeate', investors: [] }
134
+ expect(() => p().postFilter!({ data: [signal] }, {})).not.toThrow()
135
+ })
136
+
137
+ it('handles missing data array without error', () => {
138
+ expect(() => p().postFilter!({}, {})).not.toThrow()
139
+ })
140
+ })
141
+
142
+ // Signalbase stamps all sources[].publishedAt with occurredAt, making date comparison
143
+ // useless. We detect stale signals by extracting years embedded in news article URLs.
144
+ describe('postFilter: stale signal filter (Bug 2)', () => {
145
+ const staleSignal = {
146
+ companyName: 'TRIGO',
147
+ companySlug: 'trigo-3',
148
+ occurredAt: '2026-04-23T08:08:14.975Z',
149
+ investors: [],
150
+ sources: [
151
+ { url: 'https://linkedin.com/posts/abc-activity-xyz', sourceType: 'social_media', publishedAt: '2026-04-23T08:08:14.975Z' },
152
+ { url: 'https://techcrunch.com/2019/09/16/trigo-raises-22m/', sourceType: 'news_article', publishedAt: '2026-04-23T08:08:14.975Z' },
153
+ { url: 'https://fintech.global/2019/09/16/trigo-raises-22m/', sourceType: 'news_article', publishedAt: '2026-04-23T08:08:14.975Z' },
154
+ ],
155
+ }
156
+ const freshSignal = {
157
+ companyName: 'Fermeate',
158
+ companySlug: 'fermeate',
159
+ occurredAt: '2026-04-27T13:28:00.056Z',
160
+ investors: [],
161
+ sources: [
162
+ { url: 'https://agfundernews.com/fermeate-raises-2m-step-change', sourceType: 'news_article', publishedAt: '2026-04-27T13:28:00.056Z' },
163
+ ],
164
+ }
165
+
166
+ it('drops signals where a news article URL embeds a year 5+ before occurredAt', () => {
167
+ const result = p().postFilter!({ data: [staleSignal] }, {}) as { data: unknown[] }
168
+ expect(result.data).toHaveLength(0)
169
+ })
170
+
171
+ it('keeps signals where news article URLs are recent', () => {
172
+ const result = p().postFilter!({ data: [freshSignal] }, {}) as { data: unknown[] }
173
+ expect(result.data).toHaveLength(1)
174
+ })
175
+
176
+ it('ignores year-like patterns in social_media URLs', () => {
177
+ const socialWithOldYear = {
178
+ ...freshSignal,
179
+ sources: [{ url: 'https://linkedin.com/posts/2019/09/old-post', sourceType: 'social_media' }],
180
+ }
181
+ const result = p().postFilter!({ data: [socialWithOldYear] }, {}) as { data: unknown[] }
182
+ expect(result.data).toHaveLength(1)
183
+ })
184
+
185
+ it('keeps signals where news URLs have no embedded year', () => {
186
+ const noYearUrl = {
187
+ ...freshSignal,
188
+ sources: [{ url: 'https://techcrunch.com/trigo-raises-22m', sourceType: 'news_article' }],
189
+ }
190
+ const result = p().postFilter!({ data: [noYearUrl] }, {}) as { data: unknown[] }
191
+ expect(result.data).toHaveLength(1)
192
+ })
193
+
194
+ it('filters stale signals and keeps fresh ones from a mixed batch', () => {
195
+ const result = p().postFilter!({ data: [staleSignal, freshSignal] }, {}) as { data: Array<Record<string, unknown>> }
196
+ expect(result.data).toHaveLength(1)
197
+ expect(result.data[0].companyName).toBe('Fermeate')
198
+ })
199
+ })
200
+ })
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // signalbase-acquisition
204
+ // ---------------------------------------------------------------------------
205
+
206
+ describe('signalbase-acquisition', () => {
207
+ const p = () => get('signalbase-acquisition')
208
+
209
+ it('isApplicable: true for acquisition signal_type', () => {
210
+ expect(p().isApplicable!({ signal_type: 'acquisition' })).toBe(true)
211
+ })
212
+
213
+ it('isApplicable: false for other signal types', () => {
214
+ expect(p().isApplicable!({ signal_type: 'funding' })).toBe(false)
215
+ })
216
+
217
+ it('mapParams builds correct query params', () => {
218
+ const result = p().mapParams({
219
+ signal_type: 'acquisition',
220
+ companies: ['Microsoft'],
221
+ since: '2026-01-01',
222
+ limit: 15,
223
+ })
224
+ expect((result.queryParams as Record<string, string>).company_name).toBe('Microsoft')
225
+ expect((result.queryParams as Record<string, string>).dateFrom).toBe('2026-01-01')
226
+ expect((result.queryParams as Record<string, string>).limit).toBe('15')
227
+ })
228
+
229
+ it('hasResult: true when data non-empty', () => {
230
+ expect(p().hasResult({ data: [{ company: 'Startup' }] })).toBe(true)
231
+ })
232
+
233
+ it('hasResult: false on empty data', () => {
234
+ expect(p().hasResult({ data: [] })).toBe(false)
235
+ })
236
+
237
+ it('priority is 2', () => {
238
+ expect(p().priority).toBe(2)
239
+ })
240
+ })
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // signalbase-hiring
244
+ // ---------------------------------------------------------------------------
245
+
246
+ describe('signalbase-hiring', () => {
247
+ const p = () => get('signalbase-hiring')
248
+
249
+ it('isApplicable: true for hiring signal_type', () => {
250
+ expect(p().isApplicable!({ signal_type: 'hiring' })).toBe(true)
251
+ })
252
+
253
+ it('isApplicable: false for other signal types', () => {
254
+ expect(p().isApplicable!({ signal_type: 'funding' })).toBe(false)
255
+ })
256
+
257
+ it('mapParams uses search field for companies', () => {
258
+ const result = p().mapParams({ signal_type: 'hiring', companies: ['ColdIQ'], limit: 10 })
259
+ expect((result.queryParams as Record<string, string>).search).toBe('ColdIQ')
260
+ })
261
+
262
+ it('hasResult: true when data non-empty', () => {
263
+ expect(p().hasResult({ data: [{ title: 'VP Sales', company: 'ColdIQ' }] })).toBe(true)
264
+ })
265
+
266
+ it('priority is 3', () => {
267
+ expect(p().priority).toBe(3)
268
+ })
269
+ })
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // signalbase-job-change
273
+ // ---------------------------------------------------------------------------
274
+
275
+ describe('signalbase-job-change', () => {
276
+ const p = () => get('signalbase-job-change')
277
+
278
+ it('isApplicable: true for job_change signal_type', () => {
279
+ expect(p().isApplicable!({ signal_type: 'job_change' })).toBe(true)
280
+ })
281
+
282
+ it('isApplicable: false for other signal types', () => {
283
+ expect(p().isApplicable!({ signal_type: 'hiring' })).toBe(false)
284
+ })
285
+
286
+ it('mapParams uses company_name field', () => {
287
+ const result = p().mapParams({ signal_type: 'job_change', companies: ['ColdIQ'], limit: 10 })
288
+ expect((result.queryParams as Record<string, string>).company_name).toBe('ColdIQ')
289
+ })
290
+
291
+ it('hasResult: true when data non-empty', () => {
292
+ expect(p().hasResult({ data: [{ person: 'Michel Lieben', new_role: 'CEO' }] })).toBe(true)
293
+ })
294
+
295
+ it('priority is 4', () => {
296
+ expect(p().priority).toBe(4)
297
+ })
298
+ })
299
+
300
+ // ---------------------------------------------------------------------------
301
+ // theirstack-buying-intents
302
+ // ---------------------------------------------------------------------------
303
+
304
+ describe('theirstack-buying-intents', () => {
305
+ const p = () => get('theirstack-buying-intents')
306
+
307
+ it('isApplicable: true for intent with companies', () => {
308
+ expect(p().isApplicable!({ signal_type: 'intent', companies: ['ColdIQ'] })).toBe(true)
309
+ })
310
+
311
+ it('isApplicable: true for intent with domains', () => {
312
+ expect(p().isApplicable!({ signal_type: 'intent', domains: ['coldiq.com'] })).toBe(true)
313
+ })
314
+
315
+ it('isApplicable: false for intent with no companies or domains', () => {
316
+ expect(p().isApplicable!({ signal_type: 'intent' })).toBe(false)
317
+ })
318
+
319
+ it('isApplicable: false for other signal types', () => {
320
+ expect(p().isApplicable!({ signal_type: 'funding' })).toBe(false)
321
+ })
322
+
323
+ it('mapParams includes company_name_or when companies present', () => {
324
+ const result = p().mapParams({ signal_type: 'intent', companies: ['ColdIQ', 'HubSpot'] })
325
+ expect((result.body as Record<string, unknown>).company_name_or).toEqual(['ColdIQ', 'HubSpot'])
326
+ })
327
+
328
+ it('mapParams includes company_domain when domains present', () => {
329
+ const result = p().mapParams({ signal_type: 'intent', domains: ['coldiq.com'] })
330
+ expect((result.body as Record<string, unknown>).company_domain).toBe('coldiq.com')
331
+ })
332
+
333
+ it('mapParams omits absent fields', () => {
334
+ const result = p().mapParams({ signal_type: 'intent' })
335
+ const body = result.body as Record<string, unknown>
336
+ expect(body.company_name_or).toBeUndefined()
337
+ expect(body.company_domain).toBeUndefined()
338
+ })
339
+
340
+ it('hasResult: true when data non-empty', () => {
341
+ expect(p().hasResult({ data: [{ company: 'ColdIQ', intents: ['crm'] }] })).toBe(true)
342
+ })
343
+
344
+ it('hasResult: false on empty data', () => {
345
+ expect(p().hasResult({ data: [] })).toBe(false)
346
+ })
347
+
348
+ it('priority is 5', () => {
349
+ expect(p().priority).toBe(5)
350
+ })
351
+ })
352
+
353
+ // ---------------------------------------------------------------------------
354
+ // predictleads-financing
355
+ // ---------------------------------------------------------------------------
356
+
357
+ describe('predictleads-financing', () => {
358
+ const p = () => get('predictleads-financing')
359
+
360
+ it('isApplicable: true for funding (fallback)', () => {
361
+ expect(p().isApplicable!({ signal_type: 'funding' })).toBe(true)
362
+ })
363
+
364
+ it('isApplicable: false for other signal types', () => {
365
+ expect(p().isApplicable!({ signal_type: 'news' })).toBe(false)
366
+ })
367
+
368
+ it('mapParams passes company_location and limit', () => {
369
+ const result = p().mapParams({ signal_type: 'funding', countries: ['United States'], limit: 30 })
370
+ expect(result).toEqual({ queryParams: { company_location: 'United States', limit: '30' } })
371
+ })
372
+
373
+ it('mapParams omits company_location when no countries', () => {
374
+ const result = p().mapParams({ signal_type: 'funding', limit: 10 })
375
+ expect((result.queryParams as Record<string, string>).company_location).toBeUndefined()
376
+ })
377
+
378
+ it('hasResult: true when data non-empty', () => {
379
+ expect(p().hasResult({ data: [{ company: 'ColdIQ', amount: 500000 }] })).toBe(true)
380
+ })
381
+
382
+ it('priority is 6 (fallback after signalbase-funding)', () => {
383
+ expect(p().priority).toBe(6)
384
+ })
385
+ })
386
+
387
+ // ---------------------------------------------------------------------------
388
+ // predictleads-news
389
+ // ---------------------------------------------------------------------------
390
+
391
+ describe('predictleads-news', () => {
392
+ const p = () => get('predictleads-news')
393
+
394
+ it('isApplicable: true for news signal_type', () => {
395
+ expect(p().isApplicable!({ signal_type: 'news' })).toBe(true)
396
+ })
397
+
398
+ it('isApplicable: false for other signal types', () => {
399
+ expect(p().isApplicable!({ signal_type: 'funding' })).toBe(false)
400
+ })
401
+
402
+ it('mapParams passes company_location and limit', () => {
403
+ const result = p().mapParams({ signal_type: 'news', countries: ['Belgium'], limit: 20 })
404
+ expect(result).toEqual({ queryParams: { company_location: 'Belgium', limit: '20' } })
405
+ })
406
+
407
+ it('hasResult: true when data non-empty', () => {
408
+ expect(p().hasResult({ data: [{ title: 'ColdIQ raises $1M' }] })).toBe(true)
409
+ })
410
+
411
+ it('priority is 7', () => {
412
+ expect(p().priority).toBe(7)
413
+ })
414
+ })
415
+
416
+ // ---------------------------------------------------------------------------
417
+ // predictleads-startup-posts
418
+ // ---------------------------------------------------------------------------
419
+
420
+ describe('predictleads-startup-posts', () => {
421
+ const p = () => get('predictleads-startup-posts')
422
+
423
+ it('isApplicable: true for startup_post signal_type', () => {
424
+ expect(p().isApplicable!({ signal_type: 'startup_post' })).toBe(true)
425
+ })
426
+
427
+ it('isApplicable: false for other signal types', () => {
428
+ expect(p().isApplicable!({ signal_type: 'news' })).toBe(false)
429
+ })
430
+
431
+ it('mapParams passes published_at_from and limit', () => {
432
+ const result = p().mapParams({ signal_type: 'startup_post', since: '2026-01-01', limit: 15 })
433
+ expect(result).toEqual({ queryParams: { published_at_from: '2026-01-01', limit: '15' } })
434
+ })
435
+
436
+ it('mapParams omits published_at_from when no since', () => {
437
+ const result = p().mapParams({ signal_type: 'startup_post', limit: 5 })
438
+ expect((result.queryParams as Record<string, string>).published_at_from).toBeUndefined()
439
+ })
440
+
441
+ it('hasResult: true when data non-empty', () => {
442
+ expect(p().hasResult({ data: [{ title: 'ColdIQ on Product Hunt' }] })).toBe(true)
443
+ })
444
+
445
+ it('priority is 8', () => {
446
+ expect(p().priority).toBe(8)
447
+ })
448
+ })
449
+
450
+ // ---------------------------------------------------------------------------
451
+ // Ordering
452
+ // ---------------------------------------------------------------------------
453
+
454
+ describe('find_signals priority ordering', () => {
455
+ it('all providers are sorted by priority', () => {
456
+ const ps = providers()
457
+ for (let i = 1; i < ps.length; i++) {
458
+ expect(ps[i].priority).toBeGreaterThanOrEqual(ps[i - 1].priority)
459
+ }
460
+ })
461
+
462
+ it('signalbase-funding is first (priority 1)', () => {
463
+ expect(providers()[0].id).toBe('signalbase-funding')
464
+ })
465
+
466
+ it('predictleads-startup-posts is last (priority 8)', () => {
467
+ const ps = providers()
468
+ expect(ps[ps.length - 1].id).toBe('predictleads-startup-posts')
469
+ })
470
+ })