@coldiq/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +8 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +47 -0
- package/dist/client.js.map +1 -0
- package/dist/executor.d.ts +21 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +130 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +49 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +3104 -0
- package/dist/registry.js.map +1 -0
- package/dist/tools/enrich-company.d.ts +22 -0
- package/dist/tools/enrich-company.d.ts.map +1 -0
- package/dist/tools/enrich-company.js +21 -0
- package/dist/tools/enrich-company.js.map +1 -0
- package/dist/tools/enrich-email.d.ts +24 -0
- package/dist/tools/enrich-email.d.ts.map +1 -0
- package/dist/tools/enrich-email.js +19 -0
- package/dist/tools/enrich-email.js.map +1 -0
- package/dist/tools/enrich-emails.d.ts +31 -0
- package/dist/tools/enrich-emails.d.ts.map +1 -0
- package/dist/tools/enrich-emails.js +146 -0
- package/dist/tools/enrich-emails.js.map +1 -0
- package/dist/tools/enrich-person.d.ts +26 -0
- package/dist/tools/enrich-person.d.ts.map +1 -0
- package/dist/tools/enrich-person.js +23 -0
- package/dist/tools/enrich-person.js.map +1 -0
- package/dist/tools/fetch-page-content.d.ts +22 -0
- package/dist/tools/fetch-page-content.d.ts.map +1 -0
- package/dist/tools/fetch-page-content.js +32 -0
- package/dist/tools/fetch-page-content.js.map +1 -0
- package/dist/tools/find-email.d.ts +24 -0
- package/dist/tools/find-email.d.ts.map +1 -0
- package/dist/tools/find-email.js +19 -0
- package/dist/tools/find-email.js.map +1 -0
- package/dist/tools/find-emails.d.ts +31 -0
- package/dist/tools/find-emails.d.ts.map +1 -0
- package/dist/tools/find-emails.js +146 -0
- package/dist/tools/find-emails.js.map +1 -0
- package/dist/tools/find-influencers.d.ts +29 -0
- package/dist/tools/find-influencers.d.ts.map +1 -0
- package/dist/tools/find-influencers.js +30 -0
- package/dist/tools/find-influencers.js.map +1 -0
- package/dist/tools/find-people.d.ts +26 -0
- package/dist/tools/find-people.d.ts.map +1 -0
- package/dist/tools/find-people.js +61 -0
- package/dist/tools/find-people.js.map +1 -0
- package/dist/tools/find-phone.d.ts +24 -0
- package/dist/tools/find-phone.d.ts.map +1 -0
- package/dist/tools/find-phone.js +48 -0
- package/dist/tools/find-phone.js.map +1 -0
- package/dist/tools/find-signals.d.ts +26 -0
- package/dist/tools/find-signals.d.ts.map +1 -0
- package/dist/tools/find-signals.js +82 -0
- package/dist/tools/find-signals.js.map +1 -0
- package/dist/tools/search-ads.d.ts +33 -0
- package/dist/tools/search-ads.d.ts.map +1 -0
- package/dist/tools/search-ads.js +33 -0
- package/dist/tools/search-ads.js.map +1 -0
- package/dist/tools/search-companies.d.ts +42 -0
- package/dist/tools/search-companies.d.ts.map +1 -0
- package/dist/tools/search-companies.js +37 -0
- package/dist/tools/search-companies.js.map +1 -0
- package/dist/tools/search-jobs.d.ts +51 -0
- package/dist/tools/search-jobs.d.ts.map +1 -0
- package/dist/tools/search-jobs.js +64 -0
- package/dist/tools/search-jobs.js.map +1 -0
- package/dist/tools/search-places.d.ts +47 -0
- package/dist/tools/search-places.d.ts.map +1 -0
- package/dist/tools/search-places.js +42 -0
- package/dist/tools/search-places.js.map +1 -0
- package/dist/tools/search-reddit.d.ts +27 -0
- package/dist/tools/search-reddit.d.ts.map +1 -0
- package/dist/tools/search-reddit.js +30 -0
- package/dist/tools/search-reddit.js.map +1 -0
- package/dist/tools/search-seo.d.ts +37 -0
- package/dist/tools/search-seo.d.ts.map +1 -0
- package/dist/tools/search-seo.js +49 -0
- package/dist/tools/search-seo.js.map +1 -0
- package/dist/tools/search-web.d.ts +23 -0
- package/dist/tools/search-web.d.ts.map +1 -0
- package/dist/tools/search-web.js +20 -0
- package/dist/tools/search-web.js.map +1 -0
- package/dist/tools/verify-email.d.ts +20 -0
- package/dist/tools/verify-email.d.ts.map +1 -0
- package/dist/tools/verify-email.js +15 -0
- package/dist/tools/verify-email.js.map +1 -0
- package/package.json +28 -0
- package/src/client.ts +60 -0
- package/src/executor.ts +182 -0
- package/src/index.ts +155 -0
- package/src/registry.ts +3159 -0
- package/src/tools/enrich-company.ts +25 -0
- package/src/tools/enrich-person.ts +27 -0
- package/src/tools/fetch-page-content.ts +36 -0
- package/src/tools/find-email.ts +23 -0
- package/src/tools/find-emails.ts +190 -0
- package/src/tools/find-influencers.ts +34 -0
- package/src/tools/find-people.ts +69 -0
- package/src/tools/find-phone.ts +53 -0
- package/src/tools/find-signals.ts +93 -0
- package/src/tools/search-ads.ts +44 -0
- package/src/tools/search-companies.ts +41 -0
- package/src/tools/search-jobs.ts +73 -0
- package/src/tools/search-places.ts +52 -0
- package/src/tools/search-reddit.ts +34 -0
- package/src/tools/search-seo.ts +59 -0
- package/src/tools/search-web.ts +24 -0
- package/src/tools/verify-email.ts +19 -0
- package/test-ads-live.ts +77 -0
- package/test-company-live.ts +91 -0
- package/test-email-live.ts +171 -0
- package/test-influencers-live.ts +66 -0
- package/test-jobs-live.ts +69 -0
- package/test-linkupapi-live.ts +137 -0
- package/test-phone-live.ts +41 -0
- package/test-places-live.ts +89 -0
- package/test-reddit-live.ts +66 -0
- package/test-search-live.ts +79 -0
- package/test-seo-live.ts +68 -0
- package/test-web-live.ts +67 -0
- package/tests/client.test.ts +90 -0
- package/tests/executor.test.ts +83 -0
- package/tests/gtm/01-icp-to-emails.test.ts +43 -0
- package/tests/gtm/02-icp-bulk-emails.test.ts +38 -0
- package/tests/gtm/03-icp-to-phones.test.ts +39 -0
- package/tests/gtm/04-funding-signal-outreach.test.ts +42 -0
- package/tests/gtm/05-hiring-signal-decisionmakers.test.ts +41 -0
- package/tests/gtm/06-intent-signal-outreach.test.ts +44 -0
- package/tests/gtm/07-places-to-content.test.ts +50 -0
- package/tests/gtm/08-domain-to-account.test.ts +44 -0
- package/tests/gtm/09-linkedin-to-everything.test.ts +41 -0
- package/tests/gtm/10-jobs-vs-signals-routing.test.ts +38 -0
- package/tests/gtm/11-find-vs-enrich-routing.test.ts +39 -0
- package/tests/gtm/12-bogus-domain-graceful.test.ts +42 -0
- package/tests/gtm/13-private-linkedin-graceful.test.ts +44 -0
- package/tests/gtm/14-empty-handoff.test.ts +43 -0
- package/tests/gtm/15-seo-reddit-research.test.ts +38 -0
- package/tests/gtm/README.md +59 -0
- package/tests/gtm/harness.ts +217 -0
- package/tests/gtm/tools-bridge.ts +232 -0
- package/tests/gtm-scenarios.md +32 -0
- package/tests/live/smoke-report.ts +255 -0
- package/tests/live/smoke.test.ts +134 -0
- package/tests/registry-enrich-person.test.ts +447 -0
- package/tests/registry-fetch-page-content.test.ts +90 -0
- package/tests/registry-find-people.test.ts +467 -0
- package/tests/registry-find-signals.test.ts +470 -0
- package/tests/registry-linkupapi.test.ts +331 -0
- package/tests/registry-search-companies.test.ts +188 -0
- package/tests/registry-search-jobs.test.ts +116 -0
- package/tests/registry.test.ts +2210 -0
- package/tests/tools/enrich-company.test.ts +92 -0
- package/tests/tools/enrich-email.test.ts +94 -0
- package/tests/tools/enrich-emails.test.ts +271 -0
- package/tests/tools/enrich-person.test.ts +140 -0
- package/tests/tools/fetch-page-content.test.ts +108 -0
- package/tests/tools/find-influencers.test.ts +91 -0
- package/tests/tools/find-people.test.ts +344 -0
- package/tests/tools/find-phone.test.ts +100 -0
- package/tests/tools/find-signals.test.ts +110 -0
- package/tests/tools/search-ads.test.ts +182 -0
- package/tests/tools/search-companies.test.ts +58 -0
- package/tests/tools/search-jobs.test.ts +210 -0
- package/tests/tools/search-places.test.ts +114 -0
- package/tests/tools/search-reddit.test.ts +125 -0
- package/tests/tools/search-seo.test.ts +183 -0
- package/tests/tools/search-web.test.ts +79 -0
- package/tests/tools/verify-email.test.ts +68 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { initClient } from '../../src/client.js'
|
|
3
|
+
import { searchCompaniesHandler } from '../../src/tools/search-companies.js'
|
|
4
|
+
|
|
5
|
+
describe('search_companies 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 results from first successful provider', async () => {
|
|
17
|
+
globalThis.fetch = vi.fn(async () =>
|
|
18
|
+
new Response(JSON.stringify({ data: [{ name: 'ColdIQ', domain: 'coldiq.com' }] }), { status: 200 })
|
|
19
|
+
) as typeof fetch
|
|
20
|
+
|
|
21
|
+
const result = await searchCompaniesHandler({ countries: ['FR'], limit: 5 })
|
|
22
|
+
|
|
23
|
+
expect(result.isError).toBeUndefined()
|
|
24
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
25
|
+
expect(parsed._meta.provider).toBe('companyenrich')
|
|
26
|
+
expect(parsed.data.data).toHaveLength(1)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('falls back to FullEnrich (priority 2) on CompanyEnrich failure', async () => {
|
|
30
|
+
let callCount = 0
|
|
31
|
+
globalThis.fetch = vi.fn(async () => {
|
|
32
|
+
callCount++
|
|
33
|
+
if (callCount === 1) {
|
|
34
|
+
return new Response(JSON.stringify({ error: 'Provider error' }), { status: 502 })
|
|
35
|
+
}
|
|
36
|
+
return new Response(JSON.stringify({ companies: [{ name: 'ColdIQ' }] }), { status: 200 })
|
|
37
|
+
}) as typeof fetch
|
|
38
|
+
|
|
39
|
+
const result = await searchCompaniesHandler({ countries: ['FR'], limit: 5 })
|
|
40
|
+
|
|
41
|
+
expect(result.isError).toBeUndefined()
|
|
42
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
43
|
+
expect(parsed._meta.provider).toBe('fullenrich')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('returns error when all providers fail', async () => {
|
|
47
|
+
globalThis.fetch = vi.fn(async () =>
|
|
48
|
+
new Response(JSON.stringify({ error: 'Service down' }), { status: 500 })
|
|
49
|
+
) as typeof fetch
|
|
50
|
+
|
|
51
|
+
const result = await searchCompaniesHandler({ countries: ['FR'], limit: 5 })
|
|
52
|
+
|
|
53
|
+
expect(result.isError).toBe(true)
|
|
54
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
55
|
+
expect(parsed.error).toContain('All')
|
|
56
|
+
expect(parsed.providers_tried.length).toBeGreaterThan(0)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { initClient } from '../../src/client.js'
|
|
3
|
+
import { searchJobsHandler } from '../../src/tools/search-jobs.js'
|
|
4
|
+
|
|
5
|
+
describe('search_jobs 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('contradictory filters — rejected at handler level before any provider is called', async () => {
|
|
17
|
+
let fetchCalled = false
|
|
18
|
+
globalThis.fetch = vi.fn(async () => {
|
|
19
|
+
fetchCalled = true
|
|
20
|
+
return new Response(JSON.stringify({}), { status: 200 })
|
|
21
|
+
}) as typeof fetch
|
|
22
|
+
|
|
23
|
+
const result = await searchJobsHandler({
|
|
24
|
+
ats_slugs: ['greenhouse'],
|
|
25
|
+
seniority_levels: ['Director'],
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
expect(result.isError).toBe(true)
|
|
29
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
30
|
+
expect(parsed.error).toMatch(/contradictory/i)
|
|
31
|
+
expect(fetchCalled).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('ats_slugs filter — routes to career_site_jobs only (LinkedIn skipped)', async () => {
|
|
35
|
+
let linkedinCalled = false
|
|
36
|
+
|
|
37
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
38
|
+
const u = url.toString()
|
|
39
|
+
if (u.includes('/linkedin-jobs-api/search')) {
|
|
40
|
+
linkedinCalled = true
|
|
41
|
+
throw new Error('linkedin_jobs_api should not fire when ats_slugs is set')
|
|
42
|
+
}
|
|
43
|
+
if (u.includes('/career-site-jobs/search') && !u.includes('/search/')) {
|
|
44
|
+
// Async create — returns a jobId
|
|
45
|
+
return new Response(JSON.stringify({ jobId: 'job-1' }), { status: 200 })
|
|
46
|
+
}
|
|
47
|
+
if (u.includes('/career-site-jobs/search/job-1')) {
|
|
48
|
+
// Poll — done with results
|
|
49
|
+
return new Response(JSON.stringify({ jobId: 'job-1', status: 'done', jobs: [{ title: 'Software Engineer', company: 'Stripe' }] }), { status: 200 })
|
|
50
|
+
}
|
|
51
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
52
|
+
}) as typeof fetch
|
|
53
|
+
|
|
54
|
+
const { getProviders } = await import('../../src/registry.js')
|
|
55
|
+
const providers = getProviders('search_jobs')
|
|
56
|
+
const cs = providers.find((p) => p.id === 'career_site_jobs')!
|
|
57
|
+
const orig = { t: cs.async!.timeoutMs, i: cs.async!.pollIntervalMs }
|
|
58
|
+
cs.async!.timeoutMs = 500
|
|
59
|
+
cs.async!.pollIntervalMs = 20
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const result = await searchJobsHandler({
|
|
63
|
+
ats_slugs: ['greenhouse'],
|
|
64
|
+
title_keywords: ['Engineer'],
|
|
65
|
+
limit: 10,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
expect(result.isError).toBeFalsy()
|
|
69
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
70
|
+
expect(parsed._meta.provider).toBe('career_site_jobs')
|
|
71
|
+
expect(linkedinCalled).toBe(false)
|
|
72
|
+
} finally {
|
|
73
|
+
cs.async!.timeoutMs = orig.t
|
|
74
|
+
cs.async!.pollIntervalMs = orig.i
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('seniority_levels filter — routes to linkedin_jobs_api only (career_site_jobs skipped)', async () => {
|
|
79
|
+
let careerSiteCalled = false
|
|
80
|
+
|
|
81
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
82
|
+
const u = url.toString()
|
|
83
|
+
if (u.includes('/career-site-jobs/search') && !u.includes('/search/')) {
|
|
84
|
+
careerSiteCalled = true
|
|
85
|
+
throw new Error('career_site_jobs should not fire when seniority_levels is set')
|
|
86
|
+
}
|
|
87
|
+
if (u.includes('/linkedin-jobs-api/search') && !u.includes('/search/')) {
|
|
88
|
+
return new Response(JSON.stringify({ jobId: 'job-2' }), { status: 200 })
|
|
89
|
+
}
|
|
90
|
+
if (u.includes('/linkedin-jobs-api/search/job-2')) {
|
|
91
|
+
return new Response(JSON.stringify({ jobId: 'job-2', status: 'done', jobs: [{ title: 'VP of Sales', company: 'HubSpot' }] }), { status: 200 })
|
|
92
|
+
}
|
|
93
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
94
|
+
}) as typeof fetch
|
|
95
|
+
|
|
96
|
+
const { getProviders } = await import('../../src/registry.js')
|
|
97
|
+
const providers = getProviders('search_jobs')
|
|
98
|
+
const li = providers.find((p) => p.id === 'linkedin_jobs_api')!
|
|
99
|
+
const orig = { t: li.async!.timeoutMs, i: li.async!.pollIntervalMs }
|
|
100
|
+
li.async!.timeoutMs = 500
|
|
101
|
+
li.async!.pollIntervalMs = 20
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const result = await searchJobsHandler({
|
|
105
|
+
seniority_levels: ['Director'],
|
|
106
|
+
title_keywords: ['VP'],
|
|
107
|
+
limit: 10,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
expect(result.isError).toBeFalsy()
|
|
111
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
112
|
+
expect(parsed._meta.provider).toBe('linkedin_jobs_api')
|
|
113
|
+
expect(careerSiteCalled).toBe(false)
|
|
114
|
+
} finally {
|
|
115
|
+
li.async!.timeoutMs = orig.t
|
|
116
|
+
li.async!.pollIntervalMs = orig.i
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('async timeout — career_site_jobs times out, falls back to theirstack-jobs', async () => {
|
|
121
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
122
|
+
const u = url.toString()
|
|
123
|
+
if (u.includes('/career-site-jobs/search') && !u.includes('/search/')) {
|
|
124
|
+
return new Response(JSON.stringify({ jobId: 'job-3' }), { status: 200 })
|
|
125
|
+
}
|
|
126
|
+
if (u.includes('/career-site-jobs/search/job-3')) {
|
|
127
|
+
// Always running — will cause timeout
|
|
128
|
+
return new Response(JSON.stringify({ jobId: 'job-3', status: 'running' }), { status: 200 })
|
|
129
|
+
}
|
|
130
|
+
if (u.includes('/linkedin-jobs-api/search') && !u.includes('/search/')) {
|
|
131
|
+
return new Response(JSON.stringify({ jobId: 'job-4' }), { status: 200 })
|
|
132
|
+
}
|
|
133
|
+
if (u.includes('/linkedin-jobs-api/search/job-4')) {
|
|
134
|
+
return new Response(JSON.stringify({ jobId: 'job-4', status: 'running' }), { status: 200 })
|
|
135
|
+
}
|
|
136
|
+
if (u.includes('/theirstack/jobs/search')) {
|
|
137
|
+
return new Response(JSON.stringify({ data: [{ title: 'SDR', company_name: 'ColdIQ' }] }), { status: 200 })
|
|
138
|
+
}
|
|
139
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
140
|
+
}) as typeof fetch
|
|
141
|
+
|
|
142
|
+
const { getProviders } = await import('../../src/registry.js')
|
|
143
|
+
const providers = getProviders('search_jobs')
|
|
144
|
+
const cs = providers.find((p) => p.id === 'career_site_jobs')!
|
|
145
|
+
const li = providers.find((p) => p.id === 'linkedin_jobs_api')!
|
|
146
|
+
const origCs = { t: cs.async!.timeoutMs, i: cs.async!.pollIntervalMs }
|
|
147
|
+
const origLi = { t: li.async!.timeoutMs, i: li.async!.pollIntervalMs }
|
|
148
|
+
cs.async!.timeoutMs = 100
|
|
149
|
+
cs.async!.pollIntervalMs = 20
|
|
150
|
+
li.async!.timeoutMs = 100
|
|
151
|
+
li.async!.pollIntervalMs = 20
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const result = await searchJobsHandler({
|
|
155
|
+
title_keywords: ['SDR'],
|
|
156
|
+
limit: 10,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
expect(result.isError).toBeFalsy()
|
|
160
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
161
|
+
expect(parsed._meta.provider).toBe('theirstack-jobs')
|
|
162
|
+
expect(parsed.data.data).toHaveLength(1)
|
|
163
|
+
} finally {
|
|
164
|
+
cs.async!.timeoutMs = origCs.t
|
|
165
|
+
cs.async!.pollIntervalMs = origCs.i
|
|
166
|
+
li.async!.timeoutMs = origLi.t
|
|
167
|
+
li.async!.pollIntervalMs = origLi.i
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('shared-only filters — both career_site_jobs and linkedin_jobs_api are applicable', async () => {
|
|
172
|
+
// With only title_keywords and locations (shared filters), career_site_jobs fires first
|
|
173
|
+
let careerSiteCreate = 0
|
|
174
|
+
|
|
175
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
176
|
+
const u = url.toString()
|
|
177
|
+
if (u.includes('/career-site-jobs/search') && !u.includes('/search/')) {
|
|
178
|
+
careerSiteCreate++
|
|
179
|
+
return new Response(JSON.stringify({ jobId: 'job-5' }), { status: 200 })
|
|
180
|
+
}
|
|
181
|
+
if (u.includes('/career-site-jobs/search/job-5')) {
|
|
182
|
+
return new Response(JSON.stringify({ jobId: 'job-5', status: 'done', jobs: [{ title: 'SDR', company: 'ColdIQ' }] }), { status: 200 })
|
|
183
|
+
}
|
|
184
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
185
|
+
}) as typeof fetch
|
|
186
|
+
|
|
187
|
+
const { getProviders } = await import('../../src/registry.js')
|
|
188
|
+
const providers = getProviders('search_jobs')
|
|
189
|
+
const cs = providers.find((p) => p.id === 'career_site_jobs')!
|
|
190
|
+
const orig = { t: cs.async!.timeoutMs, i: cs.async!.pollIntervalMs }
|
|
191
|
+
cs.async!.timeoutMs = 500
|
|
192
|
+
cs.async!.pollIntervalMs = 20
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const result = await searchJobsHandler({
|
|
196
|
+
title_keywords: ['SDR'],
|
|
197
|
+
locations: ['Paris, France'],
|
|
198
|
+
limit: 10,
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
expect(result.isError).toBeFalsy()
|
|
202
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
203
|
+
expect(parsed._meta.provider).toBe('career_site_jobs')
|
|
204
|
+
expect(careerSiteCreate).toBe(1)
|
|
205
|
+
} finally {
|
|
206
|
+
cs.async!.timeoutMs = orig.t
|
|
207
|
+
cs.async!.pollIntervalMs = orig.i
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { initClient } from '../../src/client.js'
|
|
3
|
+
import { searchPlacesHandler } from '../../src/tools/search-places.js'
|
|
4
|
+
|
|
5
|
+
describe('search_places 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('US query — openmart wins (sync, highest priority for supported countries)', async () => {
|
|
17
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
18
|
+
const u = url.toString()
|
|
19
|
+
if (u.includes('/openmart/search')) {
|
|
20
|
+
return new Response(JSON.stringify({ data: [{ name: 'Blue Bottle Coffee', city: 'San Francisco' }] }), { status: 200 })
|
|
21
|
+
}
|
|
22
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
23
|
+
}) as typeof fetch
|
|
24
|
+
|
|
25
|
+
const result = await searchPlacesHandler({ query: 'coffee shop', country: 'US', limit: 5 })
|
|
26
|
+
|
|
27
|
+
expect(result.isError).toBeFalsy()
|
|
28
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
29
|
+
expect(parsed._meta.provider).toBe('openmart')
|
|
30
|
+
expect(parsed.data.data).toHaveLength(1)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('non-openmart country — routes to google_maps (openmart skipped)', async () => {
|
|
34
|
+
let openmartCalled = false
|
|
35
|
+
|
|
36
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
37
|
+
const u = url.toString()
|
|
38
|
+
if (u.includes('/openmart/search')) { openmartCalled = true }
|
|
39
|
+
if (u.includes('/google-maps/scraper') && !u.includes('/scraper/')) {
|
|
40
|
+
return new Response(JSON.stringify({ jobId: 'gm-1' }), { status: 200 })
|
|
41
|
+
}
|
|
42
|
+
if (u.includes('/google-maps/scraper/gm-1')) {
|
|
43
|
+
return new Response(JSON.stringify({ jobId: 'gm-1', status: 'done', places: [{ title: 'Café de Flore', address: 'Paris' }] }), { status: 200 })
|
|
44
|
+
}
|
|
45
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
46
|
+
}) as typeof fetch
|
|
47
|
+
|
|
48
|
+
const { getProviders } = await import('../../src/registry.js')
|
|
49
|
+
const providers = getProviders('search_places')
|
|
50
|
+
const gm = providers.find((p) => p.id === 'google_maps')!
|
|
51
|
+
const orig = { t: gm.async!.timeoutMs, i: gm.async!.pollIntervalMs }
|
|
52
|
+
gm.async!.timeoutMs = 500
|
|
53
|
+
gm.async!.pollIntervalMs = 20
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const result = await searchPlacesHandler({ query: 'café', country: 'FR', limit: 5 })
|
|
57
|
+
|
|
58
|
+
expect(result.isError).toBeFalsy()
|
|
59
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
60
|
+
expect(parsed._meta.provider).toBe('google_maps')
|
|
61
|
+
expect(openmartCalled).toBe(false)
|
|
62
|
+
} finally {
|
|
63
|
+
gm.async!.timeoutMs = orig.t
|
|
64
|
+
gm.async!.pollIntervalMs = orig.i
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('provider=google_maps pinned — openmart skipped even for US', async () => {
|
|
69
|
+
let openmartCalled = false
|
|
70
|
+
|
|
71
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
72
|
+
const u = url.toString()
|
|
73
|
+
if (u.includes('/openmart/search')) { openmartCalled = true }
|
|
74
|
+
if (u.includes('/google-maps/scraper') && !u.includes('/scraper/')) {
|
|
75
|
+
return new Response(JSON.stringify({ jobId: 'gm-2' }), { status: 200 })
|
|
76
|
+
}
|
|
77
|
+
if (u.includes('/google-maps/scraper/gm-2')) {
|
|
78
|
+
return new Response(JSON.stringify({ jobId: 'gm-2', status: 'done', places: [{ title: 'Starbucks' }] }), { status: 200 })
|
|
79
|
+
}
|
|
80
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
81
|
+
}) as typeof fetch
|
|
82
|
+
|
|
83
|
+
const { getProviders } = await import('../../src/registry.js')
|
|
84
|
+
const providers = getProviders('search_places')
|
|
85
|
+
const gm = providers.find((p) => p.id === 'google_maps')!
|
|
86
|
+
const orig = { t: gm.async!.timeoutMs, i: gm.async!.pollIntervalMs }
|
|
87
|
+
gm.async!.timeoutMs = 500
|
|
88
|
+
gm.async!.pollIntervalMs = 20
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const result = await searchPlacesHandler({ query: 'coffee', country: 'US', provider: 'google_maps', limit: 5 })
|
|
92
|
+
|
|
93
|
+
expect(result.isError).toBeFalsy()
|
|
94
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
95
|
+
expect(parsed._meta.provider).toBe('google_maps')
|
|
96
|
+
expect(openmartCalled).toBe(false)
|
|
97
|
+
} finally {
|
|
98
|
+
gm.async!.timeoutMs = orig.t
|
|
99
|
+
gm.async!.pollIntervalMs = orig.i
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('all providers fail — returns isError', async () => {
|
|
104
|
+
globalThis.fetch = vi.fn(async () => {
|
|
105
|
+
return new Response(JSON.stringify({ error: 'down' }), { status: 503 })
|
|
106
|
+
}) as typeof fetch
|
|
107
|
+
|
|
108
|
+
const result = await searchPlacesHandler({ query: 'coffee', country: 'US' })
|
|
109
|
+
|
|
110
|
+
expect(result.isError).toBe(true)
|
|
111
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
112
|
+
expect(parsed.error).toBeTruthy()
|
|
113
|
+
})
|
|
114
|
+
})
|