@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.
- package/dist/tools/enrich-company.d.ts +3 -9
- package/dist/tools/enrich-company.d.ts.map +1 -1
- package/dist/tools/enrich-company.js +3 -16
- package/dist/tools/enrich-company.js.map +1 -1
- package/dist/tools/enrich-person.d.ts +3 -9
- package/dist/tools/enrich-person.d.ts.map +1 -1
- package/dist/tools/enrich-person.js +3 -12
- package/dist/tools/enrich-person.js.map +1 -1
- package/dist/tools/fetch-page-content.d.ts +3 -9
- package/dist/tools/fetch-page-content.d.ts.map +1 -1
- package/dist/tools/fetch-page-content.js +3 -12
- package/dist/tools/fetch-page-content.js.map +1 -1
- package/dist/tools/find-email.d.ts +3 -9
- package/dist/tools/find-email.d.ts.map +1 -1
- package/dist/tools/find-email.js +3 -40
- package/dist/tools/find-email.js.map +1 -1
- package/dist/tools/find-emails.d.ts +3 -4
- package/dist/tools/find-emails.d.ts.map +1 -1
- package/dist/tools/find-emails.js +8 -423
- package/dist/tools/find-emails.js.map +1 -1
- package/dist/tools/find-influencers.d.ts +3 -9
- package/dist/tools/find-influencers.d.ts.map +1 -1
- package/dist/tools/find-influencers.js +3 -12
- package/dist/tools/find-influencers.js.map +1 -1
- package/dist/tools/find-people.d.ts +3 -8
- package/dist/tools/find-people.d.ts.map +1 -1
- package/dist/tools/find-people.js +3 -98
- package/dist/tools/find-people.js.map +1 -1
- package/dist/tools/find-phone.d.ts +3 -9
- package/dist/tools/find-phone.d.ts.map +1 -1
- package/dist/tools/find-phone.js +4 -41
- package/dist/tools/find-phone.js.map +1 -1
- package/dist/tools/find-signals.d.ts +3 -9
- package/dist/tools/find-signals.d.ts.map +1 -1
- package/dist/tools/find-signals.js +3 -83
- package/dist/tools/find-signals.js.map +1 -1
- package/dist/tools/get-place-reviews.d.ts +3 -9
- package/dist/tools/get-place-reviews.d.ts.map +1 -1
- package/dist/tools/get-place-reviews.js +3 -12
- package/dist/tools/get-place-reviews.js.map +1 -1
- package/dist/tools/search-ads.d.ts +3 -9
- package/dist/tools/search-ads.d.ts.map +1 -1
- package/dist/tools/search-ads.js +3 -12
- package/dist/tools/search-ads.js.map +1 -1
- package/dist/tools/search-companies.d.ts +3 -9
- package/dist/tools/search-companies.d.ts.map +1 -1
- package/dist/tools/search-companies.js +3 -24
- package/dist/tools/search-companies.js.map +1 -1
- package/dist/tools/search-jobs.d.ts +3 -9
- package/dist/tools/search-jobs.d.ts.map +1 -1
- package/dist/tools/search-jobs.js +3 -27
- package/dist/tools/search-jobs.js.map +1 -1
- package/dist/tools/search-places.d.ts +3 -15
- package/dist/tools/search-places.d.ts.map +1 -1
- package/dist/tools/search-places.js +5 -76
- package/dist/tools/search-places.js.map +1 -1
- package/dist/tools/search-reddit.d.ts +3 -9
- package/dist/tools/search-reddit.d.ts.map +1 -1
- package/dist/tools/search-reddit.js +3 -12
- package/dist/tools/search-reddit.js.map +1 -1
- package/dist/tools/search-seo.d.ts +3 -9
- package/dist/tools/search-seo.d.ts.map +1 -1
- package/dist/tools/search-seo.js +3 -12
- package/dist/tools/search-seo.js.map +1 -1
- package/dist/tools/search-web.d.ts +3 -9
- package/dist/tools/search-web.d.ts.map +1 -1
- package/dist/tools/search-web.js +3 -12
- package/dist/tools/search-web.js.map +1 -1
- package/dist/tools/verify-email.d.ts +3 -10
- package/dist/tools/verify-email.d.ts.map +1 -1
- package/dist/tools/verify-email.js +3 -68
- package/dist/tools/verify-email.js.map +1 -1
- package/dist/verb-client.d.ts +35 -0
- package/dist/verb-client.d.ts.map +1 -0
- package/dist/verb-client.js +57 -0
- package/dist/verb-client.js.map +1 -0
- package/package.json +1 -1
- package/src/tools/enrich-company.ts +3 -16
- package/src/tools/enrich-person.ts +3 -12
- package/src/tools/fetch-page-content.ts +3 -12
- package/src/tools/find-email.ts +3 -42
- package/src/tools/find-emails.ts +8 -506
- package/src/tools/find-influencers.ts +3 -12
- package/src/tools/find-people.ts +3 -107
- package/src/tools/find-phone.ts +4 -42
- package/src/tools/find-signals.ts +3 -88
- package/src/tools/get-place-reviews.ts +3 -12
- package/src/tools/search-ads.ts +3 -12
- package/src/tools/search-companies.ts +3 -23
- package/src/tools/search-jobs.ts +3 -30
- package/src/tools/search-places.ts +5 -72
- package/src/tools/search-reddit.ts +3 -12
- package/src/tools/search-seo.ts +3 -12
- package/src/tools/search-web.ts +3 -12
- package/src/tools/verify-email.ts +3 -76
- package/src/verb-client.ts +93 -0
- package/tests/verb-client.test.ts +152 -0
- package/tests/tools/enrich-company.test.ts +0 -92
- package/tests/tools/enrich-person.test.ts +0 -140
- package/tests/tools/fetch-page-content.test.ts +0 -108
- package/tests/tools/find-email.test.ts +0 -137
- package/tests/tools/find-emails.test.ts +0 -818
- package/tests/tools/find-influencers.test.ts +0 -91
- package/tests/tools/find-people.test.ts +0 -621
- package/tests/tools/find-phone.test.ts +0 -100
- package/tests/tools/find-signals.test.ts +0 -175
- package/tests/tools/get-place-reviews.test.ts +0 -73
- package/tests/tools/search-ads.test.ts +0 -182
- package/tests/tools/search-companies.test.ts +0 -192
- package/tests/tools/search-jobs.test.ts +0 -210
- package/tests/tools/search-places.test.ts +0 -210
- package/tests/tools/search-reddit.test.ts +0 -194
- package/tests/tools/search-seo.test.ts +0 -183
- package/tests/tools/search-web.test.ts +0 -79
- 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
|
-
})
|