@coldiq/mcp 0.3.12 → 0.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/tools/enrich-company.d.ts +3 -9
  2. package/dist/tools/enrich-company.d.ts.map +1 -1
  3. package/dist/tools/enrich-company.js +3 -16
  4. package/dist/tools/enrich-company.js.map +1 -1
  5. package/dist/tools/enrich-person.d.ts +3 -9
  6. package/dist/tools/enrich-person.d.ts.map +1 -1
  7. package/dist/tools/enrich-person.js +3 -12
  8. package/dist/tools/enrich-person.js.map +1 -1
  9. package/dist/tools/fetch-page-content.d.ts +3 -9
  10. package/dist/tools/fetch-page-content.d.ts.map +1 -1
  11. package/dist/tools/fetch-page-content.js +3 -12
  12. package/dist/tools/fetch-page-content.js.map +1 -1
  13. package/dist/tools/find-email.d.ts +3 -9
  14. package/dist/tools/find-email.d.ts.map +1 -1
  15. package/dist/tools/find-email.js +3 -40
  16. package/dist/tools/find-email.js.map +1 -1
  17. package/dist/tools/find-emails.d.ts +3 -4
  18. package/dist/tools/find-emails.d.ts.map +1 -1
  19. package/dist/tools/find-emails.js +8 -423
  20. package/dist/tools/find-emails.js.map +1 -1
  21. package/dist/tools/find-influencers.d.ts +3 -9
  22. package/dist/tools/find-influencers.d.ts.map +1 -1
  23. package/dist/tools/find-influencers.js +3 -12
  24. package/dist/tools/find-influencers.js.map +1 -1
  25. package/dist/tools/find-people.d.ts +3 -8
  26. package/dist/tools/find-people.d.ts.map +1 -1
  27. package/dist/tools/find-people.js +3 -98
  28. package/dist/tools/find-people.js.map +1 -1
  29. package/dist/tools/find-phone.d.ts +3 -9
  30. package/dist/tools/find-phone.d.ts.map +1 -1
  31. package/dist/tools/find-phone.js +4 -41
  32. package/dist/tools/find-phone.js.map +1 -1
  33. package/dist/tools/find-signals.d.ts +3 -9
  34. package/dist/tools/find-signals.d.ts.map +1 -1
  35. package/dist/tools/find-signals.js +3 -83
  36. package/dist/tools/find-signals.js.map +1 -1
  37. package/dist/tools/get-place-reviews.d.ts +3 -9
  38. package/dist/tools/get-place-reviews.d.ts.map +1 -1
  39. package/dist/tools/get-place-reviews.js +3 -12
  40. package/dist/tools/get-place-reviews.js.map +1 -1
  41. package/dist/tools/search-ads.d.ts +3 -9
  42. package/dist/tools/search-ads.d.ts.map +1 -1
  43. package/dist/tools/search-ads.js +3 -12
  44. package/dist/tools/search-ads.js.map +1 -1
  45. package/dist/tools/search-companies.d.ts +3 -9
  46. package/dist/tools/search-companies.d.ts.map +1 -1
  47. package/dist/tools/search-companies.js +3 -24
  48. package/dist/tools/search-companies.js.map +1 -1
  49. package/dist/tools/search-jobs.d.ts +3 -9
  50. package/dist/tools/search-jobs.d.ts.map +1 -1
  51. package/dist/tools/search-jobs.js +3 -27
  52. package/dist/tools/search-jobs.js.map +1 -1
  53. package/dist/tools/search-places.d.ts +3 -15
  54. package/dist/tools/search-places.d.ts.map +1 -1
  55. package/dist/tools/search-places.js +5 -76
  56. package/dist/tools/search-places.js.map +1 -1
  57. package/dist/tools/search-reddit.d.ts +3 -9
  58. package/dist/tools/search-reddit.d.ts.map +1 -1
  59. package/dist/tools/search-reddit.js +3 -12
  60. package/dist/tools/search-reddit.js.map +1 -1
  61. package/dist/tools/search-seo.d.ts +3 -9
  62. package/dist/tools/search-seo.d.ts.map +1 -1
  63. package/dist/tools/search-seo.js +3 -12
  64. package/dist/tools/search-seo.js.map +1 -1
  65. package/dist/tools/search-web.d.ts +3 -9
  66. package/dist/tools/search-web.d.ts.map +1 -1
  67. package/dist/tools/search-web.js +3 -12
  68. package/dist/tools/search-web.js.map +1 -1
  69. package/dist/tools/verify-email.d.ts +3 -10
  70. package/dist/tools/verify-email.d.ts.map +1 -1
  71. package/dist/tools/verify-email.js +3 -68
  72. package/dist/tools/verify-email.js.map +1 -1
  73. package/dist/verb-client.d.ts +35 -0
  74. package/dist/verb-client.d.ts.map +1 -0
  75. package/dist/verb-client.js +57 -0
  76. package/dist/verb-client.js.map +1 -0
  77. package/package.json +1 -1
  78. package/src/tools/enrich-company.ts +3 -16
  79. package/src/tools/enrich-person.ts +3 -12
  80. package/src/tools/fetch-page-content.ts +3 -12
  81. package/src/tools/find-email.ts +3 -42
  82. package/src/tools/find-emails.ts +8 -506
  83. package/src/tools/find-influencers.ts +3 -12
  84. package/src/tools/find-people.ts +3 -107
  85. package/src/tools/find-phone.ts +4 -42
  86. package/src/tools/find-signals.ts +3 -88
  87. package/src/tools/get-place-reviews.ts +3 -12
  88. package/src/tools/search-ads.ts +3 -12
  89. package/src/tools/search-companies.ts +3 -23
  90. package/src/tools/search-jobs.ts +3 -30
  91. package/src/tools/search-places.ts +5 -72
  92. package/src/tools/search-reddit.ts +3 -12
  93. package/src/tools/search-seo.ts +3 -12
  94. package/src/tools/search-web.ts +3 -12
  95. package/src/tools/verify-email.ts +3 -76
  96. package/src/verb-client.ts +93 -0
  97. package/tests/verb-client.test.ts +152 -0
  98. package/tests/tools/enrich-company.test.ts +0 -92
  99. package/tests/tools/enrich-person.test.ts +0 -140
  100. package/tests/tools/fetch-page-content.test.ts +0 -108
  101. package/tests/tools/find-email.test.ts +0 -137
  102. package/tests/tools/find-emails.test.ts +0 -818
  103. package/tests/tools/find-influencers.test.ts +0 -91
  104. package/tests/tools/find-people.test.ts +0 -621
  105. package/tests/tools/find-phone.test.ts +0 -100
  106. package/tests/tools/find-signals.test.ts +0 -175
  107. package/tests/tools/get-place-reviews.test.ts +0 -73
  108. package/tests/tools/search-ads.test.ts +0 -182
  109. package/tests/tools/search-companies.test.ts +0 -192
  110. package/tests/tools/search-jobs.test.ts +0 -210
  111. package/tests/tools/search-places.test.ts +0 -210
  112. package/tests/tools/search-reddit.test.ts +0 -194
  113. package/tests/tools/search-seo.test.ts +0 -183
  114. package/tests/tools/search-web.test.ts +0 -79
  115. package/tests/tools/verify-email.test.ts +0 -142
@@ -1,100 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { initClient } from '../../src/client.js'
3
- import { findPhoneHandler } from '../../src/tools/find-phone.js'
4
-
5
- describe('find_phone handler', () => {
6
- const originalFetch = globalThis.fetch
7
-
8
- beforeEach(() => {
9
- initClient('http://test-api.local', 'test-key')
10
- })
11
-
12
- afterEach(() => {
13
- globalThis.fetch = originalFetch
14
- })
15
-
16
- it('returns phone from FindyMail', async () => {
17
- globalThis.fetch = vi.fn(async () =>
18
- new Response(JSON.stringify({ phone: '+33612345678' }), { status: 200 })
19
- ) as typeof fetch
20
-
21
- const result = await findPhoneHandler({ linkedin_url: 'https://linkedin.com/in/michel-lieben' })
22
-
23
- const parsed = JSON.parse(result.content[0].text)
24
- expect(parsed._meta.provider).toBe('findymail')
25
- expect(parsed.data.phone).toBe('+33612345678')
26
- })
27
-
28
- it('falls through to LimaData when Findymail misses', async () => {
29
- let callCount = 0
30
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
31
- callCount++
32
- const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.href : url.url
33
- if (urlStr.includes('findymail')) return new Response(JSON.stringify({}), { status: 200 })
34
- if (urlStr.includes('coldiq/find/phone')) return new Response(JSON.stringify({ phone: '+33698765432' }), { status: 200 })
35
- return new Response(JSON.stringify({}), { status: 200 })
36
- }) as typeof fetch
37
-
38
- const result = await findPhoneHandler({ linkedin_url: 'https://linkedin.com/in/michel-lieben' })
39
-
40
- const parsed = JSON.parse(result.content[0].text)
41
- expect(result.isError).toBeFalsy()
42
- expect(parsed._meta.provider).toBe('limadata')
43
- expect(parsed.data.phone).toBe('+33698765432')
44
- })
45
-
46
- it('uses AI Ark when Findymail and LimaData both miss', async () => {
47
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
48
- const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.href : url.url
49
- if (urlStr.includes('findymail')) return new Response(JSON.stringify({}), { status: 200 })
50
- if (urlStr.includes('coldiq/find/phone')) return new Response(JSON.stringify({}), { status: 200 })
51
- if (urlStr.includes('mobile-phone-finder')) return new Response(JSON.stringify({ data: [['+33611223344']] }), { status: 200 })
52
- return new Response(JSON.stringify({}), { status: 200 })
53
- }) as typeof fetch
54
-
55
- const result = await findPhoneHandler({ linkedin_url: 'https://linkedin.com/in/michel-lieben' })
56
-
57
- const parsed = JSON.parse(result.content[0].text)
58
- expect(result.isError).toBeFalsy()
59
- expect(parsed._meta.provider).toBe('ai-ark')
60
- expect(parsed.data.data).toEqual([['+33611223344']])
61
- })
62
-
63
- it('uses LimaData and AI Ark when linkedin_url absent (name+domain path)', async () => {
64
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
65
- const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.href : url.url
66
- if (urlStr.includes('coldiq/find/phone')) return new Response(JSON.stringify({}), { status: 200 })
67
- if (urlStr.includes('mobile-phone-finder')) return new Response(JSON.stringify({ data: [['+33611223344']] }), { status: 200 })
68
- return new Response(JSON.stringify({}), { status: 200 })
69
- }) as typeof fetch
70
-
71
- const result = await findPhoneHandler({
72
- first_name: 'Michel',
73
- last_name: 'Lieben',
74
- company_domain: 'coldiq.com',
75
- })
76
-
77
- const parsed = JSON.parse(result.content[0].text)
78
- expect(result.isError).toBeFalsy()
79
- expect(parsed._meta.provider).toBe('ai-ark')
80
- })
81
-
82
- it('returns error with clear message when no valid input combo provided', async () => {
83
- const result = await findPhoneHandler({ company_name: 'ColdIQ' })
84
-
85
- expect(result.isError).toBe(true)
86
- const parsed = JSON.parse(result.content[0].text)
87
- expect(parsed.error).toContain('linkedin_url')
88
- expect(parsed.error).toContain('first_name')
89
- })
90
-
91
- it('returns error when no provider finds a phone', async () => {
92
- globalThis.fetch = vi.fn(async () =>
93
- new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
94
- ) as typeof fetch
95
-
96
- const result = await findPhoneHandler({ linkedin_url: 'https://linkedin.com/in/unknown' })
97
-
98
- expect(result.isError).toBe(true)
99
- })
100
- })
@@ -1,175 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest'
2
-
3
- // Mock executeWithFallback so handler tests never hit real providers
4
- vi.mock('../../src/executor.js', () => ({
5
- executeWithFallback: vi.fn().mockResolvedValue({ data: [], _meta: { provider: 'mock' } }),
6
- isExecutionError: vi.fn().mockReturnValue(false),
7
- }))
8
-
9
- import { findSignalsHandler } from '../../src/tools/find-signals.js'
10
-
11
- beforeEach(() => {
12
- vi.clearAllMocks()
13
- })
14
-
15
- // ---------------------------------------------------------------------------
16
- // intent pre-check: requires companies or domains
17
- // ---------------------------------------------------------------------------
18
-
19
- describe('find_signals handler — intent guard', () => {
20
- it('rejects intent with no companies or domains', async () => {
21
- const result = await findSignalsHandler({ signal_type: 'intent', limit: 10 })
22
- expect(result.isError).toBe(true)
23
- const body = JSON.parse(result.content[0].text)
24
- expect(body.error).toMatch(/intent signal_type requires/)
25
- })
26
-
27
- it('rejects intent with empty companies array', async () => {
28
- const result = await findSignalsHandler({ signal_type: 'intent', companies: [], limit: 10 })
29
- expect(result.isError).toBe(true)
30
- })
31
-
32
- it('rejects intent with empty domains array', async () => {
33
- const result = await findSignalsHandler({ signal_type: 'intent', domains: [], limit: 10 })
34
- expect(result.isError).toBe(true)
35
- })
36
-
37
- it('allows intent with companies present', async () => {
38
- const result = await findSignalsHandler({ signal_type: 'intent', companies: ['ColdIQ'], limit: 10 })
39
- expect(result.isError).toBeUndefined()
40
- })
41
-
42
- it('allows intent with domains present', async () => {
43
- const result = await findSignalsHandler({ signal_type: 'intent', domains: ['coldiq.com'], limit: 10 })
44
- expect(result.isError).toBeUndefined()
45
- })
46
- })
47
-
48
- // ---------------------------------------------------------------------------
49
- // feed-style pre-check: news and startup_post reject company filters
50
- // ---------------------------------------------------------------------------
51
-
52
- describe('find_signals handler — feed-style guard (news)', () => {
53
- it('rejects news with companies filter', async () => {
54
- const result = await findSignalsHandler({ signal_type: 'news', companies: ['ColdIQ'] })
55
- expect(result.isError).toBe(true)
56
- const body = JSON.parse(result.content[0].text)
57
- expect(body.error).toMatch(/news and startup_post/)
58
- })
59
-
60
- it('rejects news with domains filter', async () => {
61
- const result = await findSignalsHandler({ signal_type: 'news', domains: ['coldiq.com'] })
62
- expect(result.isError).toBe(true)
63
- })
64
-
65
- it('allows news with countries only', async () => {
66
- const result = await findSignalsHandler({ signal_type: 'news', countries: ['US'], limit: 10 })
67
- expect(result.isError).toBeUndefined()
68
- })
69
-
70
- it('allows news with since and limit only', async () => {
71
- const result = await findSignalsHandler({ signal_type: 'news', since: '2026-01-01', limit: 5 })
72
- expect(result.isError).toBeUndefined()
73
- })
74
- })
75
-
76
- describe('find_signals handler — feed-style guard (startup_post)', () => {
77
- it('rejects startup_post with companies filter', async () => {
78
- const result = await findSignalsHandler({ signal_type: 'startup_post', companies: ['ColdIQ'] })
79
- expect(result.isError).toBe(true)
80
- const body = JSON.parse(result.content[0].text)
81
- expect(body.error).toMatch(/news and startup_post/)
82
- })
83
-
84
- it('rejects startup_post with domains filter', async () => {
85
- const result = await findSignalsHandler({ signal_type: 'startup_post', domains: ['coldiq.com'] })
86
- expect(result.isError).toBe(true)
87
- })
88
-
89
- it('allows startup_post with since filter only', async () => {
90
- const result = await findSignalsHandler({ signal_type: 'startup_post', since: '2026-01-01', limit: 25 })
91
- expect(result.isError).toBeUndefined()
92
- })
93
- })
94
-
95
- // ---------------------------------------------------------------------------
96
- // company-targeted types: no pre-check rejection
97
- // ---------------------------------------------------------------------------
98
-
99
- describe('find_signals handler — company-targeted types pass through', () => {
100
- const types = ['funding', 'acquisition', 'hiring', 'job_change'] as const
101
-
102
- for (const signal_type of types) {
103
- it(`allows ${signal_type} with or without companies`, async () => {
104
- const withCompanies = await findSignalsHandler({ signal_type, companies: ['ColdIQ'] })
105
- const withoutCompanies = await findSignalsHandler({ signal_type, limit: 10 })
106
- expect(withCompanies.isError).toBeUndefined()
107
- expect(withoutCompanies.isError).toBeUndefined()
108
- })
109
- }
110
- })
111
-
112
- // ---------------------------------------------------------------------------
113
- // hiring industries client-side post-filter
114
- //
115
- // Signalbase /hiring-signals has no upstream `industry` query param, so the
116
- // handler filters the result rows by industries instead of silently dropping
117
- // the filter.
118
- // ---------------------------------------------------------------------------
119
-
120
- import { executeWithFallback as mockedExecute } from '../../src/executor.js'
121
-
122
- describe('find_signals handler — hiring industries post-filter', () => {
123
- const hiringRows = [
124
- { id: 'a', title: 'Sales Lead', companyName: 'CreatePay', industries: 'Financial Services' },
125
- { id: 'b', title: 'Sales Assistant', companyName: 'AstraZeneca', industries: 'Pharmaceutical Manufacturing' },
126
- { id: 'c', title: 'Account Executive', companyName: 'Monex Europe', industries: 'Financial Services and Foreign Exchange' },
127
- { id: 'd', title: 'Legal Counsel', companyName: 'Eversheds', industries: 'Law Practice and Legal Services' },
128
- { id: 'e', title: 'Engineer', companyName: 'NoIndustries', industries: '' },
129
- ]
130
-
131
- it('filters rows by case-insensitive industries substring match', async () => {
132
- vi.mocked(mockedExecute).mockResolvedValueOnce({
133
- data: { success: true, data: [...hiringRows] },
134
- _meta: { provider: 'signalbase-hiring', latencyMs: 1 },
135
- })
136
- const result = await findSignalsHandler({ signal_type: 'hiring', industries: ['financial services'], limit: 25 })
137
- const body = JSON.parse(result.content[0].text)
138
- expect(body.data.data).toHaveLength(2)
139
- expect(body.data.data.map((r: { id: string }) => r.id)).toEqual(['a', 'c'])
140
- })
141
-
142
- it('returns unfiltered rows with a note when no row matches any industry', async () => {
143
- vi.mocked(mockedExecute).mockResolvedValueOnce({
144
- data: { success: true, data: [...hiringRows] },
145
- _meta: { provider: 'signalbase-hiring', latencyMs: 1 },
146
- })
147
- const result = await findSignalsHandler({ signal_type: 'hiring', industries: ['Aerospace'], limit: 25 })
148
- const body = JSON.parse(result.content[0].text)
149
- // Non-destructive: keep all rows rather than return a misleading empty set,
150
- // and flag that the industry filter matched nothing.
151
- expect(body.data.data).toHaveLength(5)
152
- expect(body.data._industry_filter).toMatch(/UNFILTERED/)
153
- })
154
-
155
- it('leaves rows untouched when industries is absent', async () => {
156
- vi.mocked(mockedExecute).mockResolvedValueOnce({
157
- data: { success: true, data: [...hiringRows] },
158
- _meta: { provider: 'signalbase-hiring', latencyMs: 1 },
159
- })
160
- const result = await findSignalsHandler({ signal_type: 'hiring', limit: 25 })
161
- const body = JSON.parse(result.content[0].text)
162
- expect(body.data.data).toHaveLength(5)
163
- })
164
-
165
- it('does not apply filter to non-hiring signal types', async () => {
166
- vi.mocked(mockedExecute).mockResolvedValueOnce({
167
- data: { success: true, data: [...hiringRows] },
168
- _meta: { provider: 'signalbase-funding', latencyMs: 1 },
169
- })
170
- const result = await findSignalsHandler({ signal_type: 'funding', industries: ['Aerospace'], limit: 25 })
171
- const body = JSON.parse(result.content[0].text)
172
- // Untouched: industries is forwarded upstream for funding, not post-filtered here.
173
- expect(body.data.data).toHaveLength(5)
174
- })
175
- })
@@ -1,73 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { initClient } from '../../src/client.js'
3
- import { getPlaceReviewsHandler } from '../../src/tools/get-place-reviews.js'
4
- import { getProviders } from '../../src/registry.js'
5
-
6
- const PLACE_URL = 'https://www.google.com/maps/place/ColdIQ'
7
-
8
- describe('get_place_reviews handler', () => {
9
- const originalFetch = globalThis.fetch
10
-
11
- beforeEach(() => {
12
- initClient('http://test-api.local', 'test-key')
13
- })
14
-
15
- afterEach(() => {
16
- globalThis.fetch = originalFetch
17
- })
18
-
19
- function withFastPolling<T>(fn: () => Promise<T>): Promise<T> {
20
- const p = getProviders('get_place_reviews').find((x) => x.id === 'google_maps_reviews')!
21
- const orig = { t: p.async!.timeoutMs, i: p.async!.pollIntervalMs }
22
- p.async!.timeoutMs = 500
23
- p.async!.pollIntervalMs = 20
24
- return fn().finally(() => {
25
- p.async!.timeoutMs = orig.t
26
- p.async!.pollIntervalMs = orig.i
27
- })
28
- }
29
-
30
- it('submits the reviews job and returns reviews when done', async () => {
31
- const reviews = [{ text: 'Great service', stars: 5 }]
32
- globalThis.fetch = vi.fn(async (url: string | URL) => {
33
- const u = url.toString()
34
- if (u.includes('/google-maps/reviews/rev-1')) {
35
- return new Response(JSON.stringify({ jobId: 'rev-1', status: 'done', reviews }), { status: 200 })
36
- }
37
- if (u.includes('/google-maps/reviews')) {
38
- return new Response(JSON.stringify({ jobId: 'rev-1', status: 'processing' }), { status: 200 })
39
- }
40
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
41
- }) as typeof fetch
42
-
43
- const result = await withFastPolling(() =>
44
- getPlaceReviewsHandler({ place_urls: [PLACE_URL], max_reviews: 10, sort: 'lowestRanking' }),
45
- )
46
- expect(result.isError).toBeFalsy()
47
- const parsed = JSON.parse(result.content[0].text)
48
- expect(parsed._meta.provider).toBe('google_maps_reviews')
49
- expect(parsed.data.reviews).toEqual(reviews)
50
- })
51
-
52
- it('forwards place_urls, max_reviews and sort to the upstream body', async () => {
53
- let submitBody: Record<string, unknown> = {}
54
- globalThis.fetch = vi.fn(async (url: string | URL, init?: RequestInit) => {
55
- const u = url.toString()
56
- if (u.includes('/google-maps/reviews/rev-1')) {
57
- return new Response(JSON.stringify({ jobId: 'rev-1', status: 'done', reviews: [] }), { status: 200 })
58
- }
59
- if (u.includes('/google-maps/reviews')) {
60
- submitBody = JSON.parse(init!.body as string)
61
- return new Response(JSON.stringify({ jobId: 'rev-1', status: 'processing' }), { status: 200 })
62
- }
63
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
64
- }) as typeof fetch
65
-
66
- await withFastPolling(() =>
67
- getPlaceReviewsHandler({ place_urls: [PLACE_URL], max_reviews: 25, sort: 'newest' }),
68
- )
69
- expect(submitBody.startUrls).toEqual([{ url: PLACE_URL }])
70
- expect(submitBody.maxReviews).toBe(25)
71
- expect(submitBody.reviewsSort).toBe('newest')
72
- })
73
- })
@@ -1,182 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { initClient } from '../../src/client.js'
3
- import { searchAdsHandler } from '../../src/tools/search-ads.js'
4
-
5
- describe('search_ads handler', () => {
6
- const originalFetch = globalThis.fetch
7
-
8
- beforeEach(() => {
9
- initClient('http://test-api.local', 'test-key')
10
- })
11
-
12
- afterEach(() => {
13
- globalThis.fetch = originalFetch
14
- })
15
-
16
- it('domains filter — routes to google_ads only', async () => {
17
- let metaCalled = false
18
- let linkedinCalled = false
19
-
20
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
21
- const u = url.toString()
22
- if (u.includes('/meta-ads/')) metaCalled = true
23
- if (u.includes('/linkedin-ad-library/')) linkedinCalled = true
24
- if (u.includes('/google-ads/search') && !u.includes('/search/')) {
25
- return new Response(JSON.stringify({ jobId: 'ads-1' }), { status: 200 })
26
- }
27
- if (u.includes('/google-ads/search/ads-1')) {
28
- return new Response(JSON.stringify({ jobId: 'ads-1', status: 'done', ads: [{ advertiser: 'Salesforce', headline: 'CRM Leader' }] }), { status: 200 })
29
- }
30
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
31
- }) as typeof fetch
32
-
33
- const { getProviders } = await import('../../src/registry.js')
34
- const providers = getProviders('search_ads')
35
- const ga = providers.find((p) => p.id === 'google_ads')!
36
- const orig = { t: ga.async!.timeoutMs, i: ga.async!.pollIntervalMs }
37
- ga.async!.timeoutMs = 500
38
- ga.async!.pollIntervalMs = 20
39
-
40
- try {
41
- const result = await searchAdsHandler({ domains: ['salesforce.com'] })
42
-
43
- expect(result.isError).toBeFalsy()
44
- const parsed = JSON.parse(result.content[0].text)
45
- expect(parsed._meta.provider).toBe('google_ads')
46
- expect(metaCalled).toBe(false)
47
- expect(linkedinCalled).toBe(false)
48
- } finally {
49
- ga.async!.timeoutMs = orig.t
50
- ga.async!.pollIntervalMs = orig.i
51
- }
52
- })
53
-
54
- it('search_urls filter — routes to linkedin_ad_library only', async () => {
55
- let googleCalled = false
56
-
57
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
58
- const u = url.toString()
59
- if (u.includes('/google-ads/')) googleCalled = true
60
- if (u.includes('/linkedin-ad-library/search') && !u.includes('/search/')) {
61
- return new Response(JSON.stringify({ jobId: 'li-1' }), { status: 200 })
62
- }
63
- if (u.includes('/linkedin-ad-library/search/li-1')) {
64
- return new Response(JSON.stringify({ jobId: 'li-1', status: 'done', ads: [{ company: 'ColdIQ', creative: 'B2B ad' }] }), { status: 200 })
65
- }
66
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
67
- }) as typeof fetch
68
-
69
- const { getProviders } = await import('../../src/registry.js')
70
- const providers = getProviders('search_ads')
71
- const li = providers.find((p) => p.id === 'linkedin_ad_library')!
72
- const orig = { t: li.async!.timeoutMs, i: li.async!.pollIntervalMs }
73
- li.async!.timeoutMs = 500
74
- li.async!.pollIntervalMs = 20
75
-
76
- try {
77
- const result = await searchAdsHandler({
78
- search_urls: ['https://linkedin.com/ad-library/search?companyIds=123'],
79
- })
80
-
81
- expect(result.isError).toBeFalsy()
82
- const parsed = JSON.parse(result.content[0].text)
83
- expect(parsed._meta.provider).toBe('linkedin_ad_library')
84
- expect(googleCalled).toBe(false)
85
- } finally {
86
- li.async!.timeoutMs = orig.t
87
- li.async!.pollIntervalMs = orig.i
88
- }
89
- })
90
-
91
- it('query only — falls through google_ads (no results) → meta_ads wins', async () => {
92
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
93
- const u = url.toString()
94
- // google_ads: create returns jobId, poll returns no ads
95
- if (u.includes('/google-ads/search') && !u.includes('/search/')) {
96
- return new Response(JSON.stringify({ jobId: 'ga-2' }), { status: 200 })
97
- }
98
- if (u.includes('/google-ads/search/ga-2')) {
99
- return new Response(JSON.stringify({ jobId: 'ga-2', status: 'done', ads: [] }), { status: 200 })
100
- }
101
- // meta_ads: returns results
102
- if (u.includes('/meta-ads/search') && !u.includes('/search/')) {
103
- return new Response(JSON.stringify({ jobId: 'meta-1' }), { status: 200 })
104
- }
105
- if (u.includes('/meta-ads/search/meta-1')) {
106
- return new Response(JSON.stringify({ jobId: 'meta-1', status: 'done', ads: [{ page_name: 'ColdIQ', body: 'Outbound faster' }] }), { status: 200 })
107
- }
108
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
109
- }) as typeof fetch
110
-
111
- const { getProviders } = await import('../../src/registry.js')
112
- const providers = getProviders('search_ads')
113
- const ga = providers.find((p) => p.id === 'google_ads')!
114
- const ma = providers.find((p) => p.id === 'meta_ads')!
115
- const origGa = { t: ga.async!.timeoutMs, i: ga.async!.pollIntervalMs }
116
- const origMa = { t: ma.async!.timeoutMs, i: ma.async!.pollIntervalMs }
117
- ga.async!.timeoutMs = 500
118
- ga.async!.pollIntervalMs = 20
119
- ma.async!.timeoutMs = 500
120
- ma.async!.pollIntervalMs = 20
121
-
122
- try {
123
- const result = await searchAdsHandler({ query: 'ColdIQ' })
124
-
125
- expect(result.isError).toBeFalsy()
126
- const parsed = JSON.parse(result.content[0].text)
127
- expect(parsed._meta.provider).toBe('meta_ads')
128
- } finally {
129
- ga.async!.timeoutMs = origGa.t
130
- ga.async!.pollIntervalMs = origGa.i
131
- ma.async!.timeoutMs = origMa.t
132
- ma.async!.pollIntervalMs = origMa.i
133
- }
134
- })
135
-
136
- it('platform=meta pinned — only meta_ads fires', async () => {
137
- let googleCalled = false
138
-
139
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
140
- const u = url.toString()
141
- if (u.includes('/google-ads/')) googleCalled = true
142
- if (u.includes('/meta-ads/search') && !u.includes('/search/')) {
143
- return new Response(JSON.stringify({ jobId: 'meta-2' }), { status: 200 })
144
- }
145
- if (u.includes('/meta-ads/search/meta-2')) {
146
- return new Response(JSON.stringify({ jobId: 'meta-2', status: 'done', ads: [{ page_name: 'ColdIQ' }] }), { status: 200 })
147
- }
148
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
149
- }) as typeof fetch
150
-
151
- const { getProviders } = await import('../../src/registry.js')
152
- const providers = getProviders('search_ads')
153
- const ma = providers.find((p) => p.id === 'meta_ads')!
154
- const orig = { t: ma.async!.timeoutMs, i: ma.async!.pollIntervalMs }
155
- ma.async!.timeoutMs = 500
156
- ma.async!.pollIntervalMs = 20
157
-
158
- try {
159
- const result = await searchAdsHandler({ platform: 'meta', query: 'ColdIQ' })
160
-
161
- expect(result.isError).toBeFalsy()
162
- const parsed = JSON.parse(result.content[0].text)
163
- expect(parsed._meta.provider).toBe('meta_ads')
164
- expect(googleCalled).toBe(false)
165
- } finally {
166
- ma.async!.timeoutMs = orig.t
167
- ma.async!.pollIntervalMs = orig.i
168
- }
169
- })
170
-
171
- it('all providers fail — returns isError', async () => {
172
- globalThis.fetch = vi.fn(async () => {
173
- return new Response(JSON.stringify({ error: 'down' }), { status: 503 })
174
- }) as typeof fetch
175
-
176
- const result = await searchAdsHandler({ query: 'ColdIQ' })
177
-
178
- expect(result.isError).toBe(true)
179
- const parsed = JSON.parse(result.content[0].text)
180
- expect(parsed.error).toBeTruthy()
181
- })
182
- })