@coldiq/mcp 0.3.11 → 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/client.d.ts.map +1 -1
- package/dist/client.js +4 -0
- package/dist/client.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/session-usage.d.ts +59 -0
- package/dist/session-usage.d.ts.map +1 -0
- package/dist/session-usage.js +96 -0
- package/dist/session-usage.js.map +1 -0
- 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/get-session-usage.d.ts +10 -0
- package/dist/tools/get-session-usage.d.ts.map +1 -0
- package/dist/tools/get-session-usage.js +14 -0
- package/dist/tools/get-session-usage.js.map +1 -0
- 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/client.ts +4 -0
- package/src/index.ts +8 -0
- package/src/session-usage.ts +130 -0
- 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/get-session-usage.ts +18 -0
- 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/executor.test.ts +69 -0
- package/tests/session-usage.test.ts +130 -0
- package/tests/tools/get-session-usage.test.ts +44 -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,192 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { initClient } from '../../src/client.js'
|
|
3
|
-
import { searchCompaniesHandler, searchCompaniesSchema } 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 (compact by default)', 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
|
-
// Compact-by-default: the raw provider array is normalized into companies[].
|
|
27
|
-
expect(parsed.data.companies).toHaveLength(1)
|
|
28
|
-
expect(parsed.data.companies[0]).toMatchObject({ name: 'ColdIQ', domain: 'coldiq.com' })
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('compact (default) keeps identity + firmographic fields and drops heavy ones', async () => {
|
|
32
|
-
// Mirror the real CompanyEnrich shape: companies under data.items[], each a
|
|
33
|
-
// heavy object with funding/technologies/keywords/socials/seo.
|
|
34
|
-
globalThis.fetch = vi.fn(async () =>
|
|
35
|
-
new Response(JSON.stringify({
|
|
36
|
-
items: [{
|
|
37
|
-
id: '0192562e-7008-702b-97a2-0b2c489d339b',
|
|
38
|
-
name: 'Hugging Face',
|
|
39
|
-
domain: 'huggingface.co',
|
|
40
|
-
website: 'https://huggingface.co',
|
|
41
|
-
industry: 'Business Services',
|
|
42
|
-
categories: ['b2b', 'b2c', 'saas'],
|
|
43
|
-
employees: '201-500',
|
|
44
|
-
revenue: '10m-50m',
|
|
45
|
-
founded_year: 2016,
|
|
46
|
-
description: 'A very long company description that should be dropped in compact mode...',
|
|
47
|
-
keywords: Array.from({ length: 28 }, (_, i) => `kw${i}`),
|
|
48
|
-
technologies: ['Stripe', 'Nginx', 'Node-Js'],
|
|
49
|
-
naics_codes: ['511210', '518210'],
|
|
50
|
-
socials: { linkedin_url: 'https://www.linkedin.com/company/huggingface', twitter_url: 'https://twitter.com/huggingface' },
|
|
51
|
-
financial: { total_funding: 395200000, funding: [{ date: '2024-08-01', type: 'Venture Round' }] },
|
|
52
|
-
location: { country: { code: 'FR', name: 'France' }, city: { name: 'Paris' } },
|
|
53
|
-
}],
|
|
54
|
-
page: 1,
|
|
55
|
-
totalItems: 10000,
|
|
56
|
-
}), { status: 200 })
|
|
57
|
-
) as typeof fetch
|
|
58
|
-
|
|
59
|
-
const result = await searchCompaniesHandler({ keywords: ['SaaS'], countries: ['FR'], limit: 5 })
|
|
60
|
-
|
|
61
|
-
expect(result.isError).toBeUndefined()
|
|
62
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
63
|
-
expect(parsed.data.total).toBe(10000)
|
|
64
|
-
expect(parsed.data.companies).toHaveLength(1)
|
|
65
|
-
const c = parsed.data.companies[0]
|
|
66
|
-
// Kept: identity + firmographics + downstream-critical linkedin_url.
|
|
67
|
-
expect(c).toEqual({
|
|
68
|
-
name: 'Hugging Face',
|
|
69
|
-
domain: 'huggingface.co',
|
|
70
|
-
website: 'https://huggingface.co',
|
|
71
|
-
linkedin_url: 'https://www.linkedin.com/company/huggingface',
|
|
72
|
-
employees: '201-500',
|
|
73
|
-
revenue: '10m-50m',
|
|
74
|
-
industry: 'Business Services',
|
|
75
|
-
country: 'France',
|
|
76
|
-
city: 'Paris',
|
|
77
|
-
founded_year: 2016,
|
|
78
|
-
categories: ['b2b', 'b2c', 'saas'],
|
|
79
|
-
})
|
|
80
|
-
// Dropped heavy fields.
|
|
81
|
-
expect(c.description).toBeUndefined()
|
|
82
|
-
expect(c.keywords).toBeUndefined()
|
|
83
|
-
expect(c.technologies).toBeUndefined()
|
|
84
|
-
expect(c.naics_codes).toBeUndefined()
|
|
85
|
-
expect(c.financial).toBeUndefined()
|
|
86
|
-
expect(c.id).toBeUndefined()
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('fields=verbose returns the raw provider passthrough unchanged', async () => {
|
|
90
|
-
globalThis.fetch = vi.fn(async () =>
|
|
91
|
-
new Response(JSON.stringify({ items: [{ name: 'Hugging Face', keywords: ['ai'], technologies: ['Stripe'] }], totalItems: 10000 }), { status: 200 })
|
|
92
|
-
) as typeof fetch
|
|
93
|
-
|
|
94
|
-
const result = await searchCompaniesHandler({ keywords: ['SaaS'], limit: 5, fields: 'verbose' })
|
|
95
|
-
|
|
96
|
-
expect(result.isError).toBeUndefined()
|
|
97
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
98
|
-
// Raw shape preserved: items[] with heavy fields, no companies[] normalization.
|
|
99
|
-
expect(parsed.data.items).toHaveLength(1)
|
|
100
|
-
expect(parsed.data.items[0].keywords).toEqual(['ai'])
|
|
101
|
-
expect(parsed.data.items[0].technologies).toEqual(['Stripe'])
|
|
102
|
-
expect(parsed.data.companies).toBeUndefined()
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('falls back to FullEnrich (priority 2) on CompanyEnrich failure', async () => {
|
|
106
|
-
let callCount = 0
|
|
107
|
-
globalThis.fetch = vi.fn(async () => {
|
|
108
|
-
callCount++
|
|
109
|
-
if (callCount === 1) {
|
|
110
|
-
return new Response(JSON.stringify({ error: 'Provider error' }), { status: 502 })
|
|
111
|
-
}
|
|
112
|
-
return new Response(JSON.stringify({ companies: [{ name: 'ColdIQ' }] }), { status: 200 })
|
|
113
|
-
}) as typeof fetch
|
|
114
|
-
|
|
115
|
-
const result = await searchCompaniesHandler({ countries: ['FR'], limit: 5 })
|
|
116
|
-
|
|
117
|
-
expect(result.isError).toBeUndefined()
|
|
118
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
119
|
-
expect(parsed._meta.provider).toBe('fullenrich')
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('returns error when all providers fail', async () => {
|
|
123
|
-
globalThis.fetch = vi.fn(async () =>
|
|
124
|
-
new Response(JSON.stringify({ error: 'Service down' }), { status: 500 })
|
|
125
|
-
) as typeof fetch
|
|
126
|
-
|
|
127
|
-
const result = await searchCompaniesHandler({ countries: ['FR'], limit: 5 })
|
|
128
|
-
|
|
129
|
-
expect(result.isError).toBe(true)
|
|
130
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
131
|
-
expect(parsed.error).toContain('All')
|
|
132
|
-
expect(parsed.providers_tried.length).toBeGreaterThan(0)
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('surfaces the upstream match count as _meta.total_entries (TAM signal)', async () => {
|
|
136
|
-
globalThis.fetch = vi.fn(async () =>
|
|
137
|
-
new Response(JSON.stringify({ organizations: [{ name: 'ColdIQ' }], pagination: { total_entries: 8371 } }), { status: 200 })
|
|
138
|
-
) as typeof fetch
|
|
139
|
-
|
|
140
|
-
const result = await searchCompaniesHandler({ keywords: ['SaaS'], limit: 250, use_providers: ['apollo'] })
|
|
141
|
-
|
|
142
|
-
expect(result.isError).toBeUndefined()
|
|
143
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
144
|
-
expect(parsed._meta.provider).toBe('apollo')
|
|
145
|
-
expect(parsed._meta.total_entries).toBe(8371)
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('limit accepts up to 500 and rejects above', () => {
|
|
149
|
-
expect(searchCompaniesSchema.limit.safeParse(500).success).toBe(true)
|
|
150
|
-
expect(searchCompaniesSchema.limit.safeParse(600).success).toBe(false)
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
describe('use_providers', () => {
|
|
154
|
-
it('Scenario A — pinned provider succeeds, skips others', async () => {
|
|
155
|
-
let callCount = 0
|
|
156
|
-
globalThis.fetch = vi.fn(async () => {
|
|
157
|
-
callCount++
|
|
158
|
-
return new Response(JSON.stringify({ data: [{ name: 'ColdIQ' }] }), { status: 200 })
|
|
159
|
-
}) as typeof fetch
|
|
160
|
-
|
|
161
|
-
const result = await searchCompaniesHandler({ countries: ['FR'], limit: 5, use_providers: ['companyenrich'] })
|
|
162
|
-
|
|
163
|
-
expect(result.isError).toBeUndefined()
|
|
164
|
-
expect(callCount).toBe(1)
|
|
165
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
166
|
-
expect(parsed._meta.provider).toBe('companyenrich')
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
it('Scenario E — unrecognized provider returns isError with helpful message', async () => {
|
|
170
|
-
const result = await searchCompaniesHandler({ countries: ['FR'], limit: 5, use_providers: ['salesforce'] })
|
|
171
|
-
|
|
172
|
-
expect(result.isError).toBe(true)
|
|
173
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
174
|
-
expect(parsed.error).toContain("'salesforce' is not a recognized provider")
|
|
175
|
-
expect(parsed.error).toContain('ColdIQ will automatically pick the best tool')
|
|
176
|
-
expect(parsed.available_providers).toBeInstanceOf(Array)
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('Scenario D — fuzzy match resolves typo and surfaces matchedFrom', async () => {
|
|
180
|
-
globalThis.fetch = vi.fn(async () =>
|
|
181
|
-
new Response(JSON.stringify({ data: [{ name: 'ColdIQ' }] }), { status: 200 })
|
|
182
|
-
) as typeof fetch
|
|
183
|
-
|
|
184
|
-
const result = await searchCompaniesHandler({ countries: ['FR'], limit: 5, use_providers: ['fullenrch'] })
|
|
185
|
-
|
|
186
|
-
expect(result.isError).toBeUndefined()
|
|
187
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
188
|
-
expect(parsed._meta.provider).toBe('fullenrich')
|
|
189
|
-
expect(parsed._meta.matchedFrom).toEqual({ fullenrch: 'fullenrich' })
|
|
190
|
-
})
|
|
191
|
-
})
|
|
192
|
-
})
|
|
@@ -1,210 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { initClient } from '../../src/client.js'
|
|
3
|
-
import { searchPlacesHandler, applyPlaceFilters } from '../../src/tools/search-places.js'
|
|
4
|
-
|
|
5
|
-
describe('applyPlaceFilters', () => {
|
|
6
|
-
const sample = {
|
|
7
|
-
places: [
|
|
8
|
-
{ title: 'Low rating, many reviews', totalScore: 2.9, reviewsCount: 81 },
|
|
9
|
-
{ title: 'Mid rating, mid reviews', totalScore: 3.9, reviewsCount: 322 },
|
|
10
|
-
{ title: 'High rating, many reviews', totalScore: 4.6, reviewsCount: 1101 },
|
|
11
|
-
{ title: 'High rating, few reviews', totalScore: 4.7, reviewsCount: 12 },
|
|
12
|
-
{ title: 'Missing fields', /* no totalScore or reviewsCount */ },
|
|
13
|
-
],
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
it('passes through unchanged when no filters supplied', () => {
|
|
17
|
-
const out = applyPlaceFilters(sample, {}) as { places: unknown[] }
|
|
18
|
-
expect(out.places).toHaveLength(5)
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('does not mutate its input', () => {
|
|
22
|
-
const before = JSON.stringify(sample)
|
|
23
|
-
applyPlaceFilters(sample, { maxRating: 4 })
|
|
24
|
-
expect(JSON.stringify(sample)).toBe(before)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('max_overall_rating drops places above the cap', () => {
|
|
28
|
-
const out = applyPlaceFilters(sample, { maxRating: 4 }) as { places: Array<{ totalScore?: number }> }
|
|
29
|
-
expect(out.places.map((p) => p.totalScore)).toEqual([2.9, 3.9])
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('min_total_reviews drops places below the floor', () => {
|
|
33
|
-
const out = applyPlaceFilters(sample, { minReviews: 20 }) as { places: Array<{ reviewsCount?: number }> }
|
|
34
|
-
expect(out.places.map((p) => p.reviewsCount)).toEqual([81, 322, 1101])
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('combined filters apply as AND', () => {
|
|
38
|
-
const out = applyPlaceFilters(sample, { maxRating: 4, minReviews: 20 }) as { places: Array<{ title?: string }> }
|
|
39
|
-
expect(out.places.map((p) => p.title)).toEqual(['Low rating, many reviews', 'Mid rating, mid reviews'])
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('places with missing totalScore/reviewsCount are dropped when corresponding filter is active', () => {
|
|
43
|
-
const out = applyPlaceFilters(sample, { minRating: 0 }) as { places: Array<{ title?: string }> }
|
|
44
|
-
expect(out.places.map((p) => p.title)).not.toContain('Missing fields')
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('non-object / non-array shapes are passed through untouched', () => {
|
|
48
|
-
expect(applyPlaceFilters(null, { maxRating: 4 })).toBe(null)
|
|
49
|
-
expect(applyPlaceFilters({ places: 'not an array' }, { maxRating: 4 })).toEqual({ places: 'not an array' })
|
|
50
|
-
})
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
describe('search_places handler', () => {
|
|
54
|
-
const originalFetch = globalThis.fetch
|
|
55
|
-
|
|
56
|
-
beforeEach(() => {
|
|
57
|
-
initClient('http://test-api.local', 'test-key')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
afterEach(() => {
|
|
61
|
-
globalThis.fetch = originalFetch
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('US query — openmart wins (sync, highest priority for supported countries)', async () => {
|
|
65
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
66
|
-
const u = url.toString()
|
|
67
|
-
if (u.includes('/openmart/search')) {
|
|
68
|
-
return new Response(JSON.stringify({ data: [{ name: 'Blue Bottle Coffee', city: 'San Francisco' }] }), { status: 200 })
|
|
69
|
-
}
|
|
70
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
71
|
-
}) as typeof fetch
|
|
72
|
-
|
|
73
|
-
const result = await searchPlacesHandler({ query: 'coffee shop', country: 'US', limit: 5 })
|
|
74
|
-
|
|
75
|
-
expect(result.isError).toBeFalsy()
|
|
76
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
77
|
-
expect(parsed._meta.provider).toBe('openmart')
|
|
78
|
-
expect(parsed.data.data).toHaveLength(1)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('non-openmart country — routes to google_maps (openmart skipped)', async () => {
|
|
82
|
-
let openmartCalled = false
|
|
83
|
-
|
|
84
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
85
|
-
const u = url.toString()
|
|
86
|
-
if (u.includes('/openmart/search')) { openmartCalled = true }
|
|
87
|
-
if (u.includes('/google-maps/scraper') && !u.includes('/scraper/')) {
|
|
88
|
-
return new Response(JSON.stringify({ jobId: 'gm-1' }), { status: 200 })
|
|
89
|
-
}
|
|
90
|
-
if (u.includes('/google-maps/scraper/gm-1')) {
|
|
91
|
-
return new Response(JSON.stringify({ jobId: 'gm-1', status: 'done', places: [{ title: 'Café de Flore', address: 'Paris' }] }), { 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_places')
|
|
98
|
-
const gm = providers.find((p) => p.id === 'google_maps')!
|
|
99
|
-
const orig = { t: gm.async!.timeoutMs, i: gm.async!.pollIntervalMs }
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
gm.async!.timeoutMs = 500
|
|
103
|
-
gm.async!.pollIntervalMs = 20
|
|
104
|
-
const result = await searchPlacesHandler({ query: 'café', country: 'FR', limit: 5 })
|
|
105
|
-
|
|
106
|
-
expect(result.isError).toBeFalsy()
|
|
107
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
108
|
-
expect(parsed._meta.provider).toBe('google_maps')
|
|
109
|
-
expect(openmartCalled).toBe(false)
|
|
110
|
-
} finally {
|
|
111
|
-
gm.async!.timeoutMs = orig.t
|
|
112
|
-
gm.async!.pollIntervalMs = orig.i
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
it('provider=google_maps pinned — openmart skipped even for US', async () => {
|
|
117
|
-
let openmartCalled = false
|
|
118
|
-
|
|
119
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
120
|
-
const u = url.toString()
|
|
121
|
-
if (u.includes('/openmart/search')) { openmartCalled = true }
|
|
122
|
-
if (u.includes('/google-maps/scraper') && !u.includes('/scraper/')) {
|
|
123
|
-
return new Response(JSON.stringify({ jobId: 'gm-2' }), { status: 200 })
|
|
124
|
-
}
|
|
125
|
-
if (u.includes('/google-maps/scraper/gm-2')) {
|
|
126
|
-
return new Response(JSON.stringify({ jobId: 'gm-2', status: 'done', places: [{ title: 'Starbucks' }] }), { status: 200 })
|
|
127
|
-
}
|
|
128
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
129
|
-
}) as typeof fetch
|
|
130
|
-
|
|
131
|
-
const { getProviders } = await import('../../src/registry.js')
|
|
132
|
-
const providers = getProviders('search_places')
|
|
133
|
-
const gm = providers.find((p) => p.id === 'google_maps')!
|
|
134
|
-
const orig = { t: gm.async!.timeoutMs, i: gm.async!.pollIntervalMs }
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
gm.async!.timeoutMs = 500
|
|
138
|
-
gm.async!.pollIntervalMs = 20
|
|
139
|
-
const result = await searchPlacesHandler({ query: 'coffee', country: 'US', provider: 'google_maps', limit: 5 })
|
|
140
|
-
|
|
141
|
-
expect(result.isError).toBeFalsy()
|
|
142
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
143
|
-
expect(parsed._meta.provider).toBe('google_maps')
|
|
144
|
-
expect(openmartCalled).toBe(false)
|
|
145
|
-
} finally {
|
|
146
|
-
gm.async!.timeoutMs = orig.t
|
|
147
|
-
gm.async!.pollIntervalMs = orig.i
|
|
148
|
-
}
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('google_maps response is post-filtered by max_overall_rating + min_total_reviews', async () => {
|
|
152
|
-
// Regression guard: if the handler stops wiring applyPlaceFilters, this test fails.
|
|
153
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
154
|
-
const u = url.toString()
|
|
155
|
-
if (u.includes('/google-maps/scraper') && !u.includes('/scraper/')) {
|
|
156
|
-
return new Response(JSON.stringify({ jobId: 'gm-filter' }), { status: 200 })
|
|
157
|
-
}
|
|
158
|
-
if (u.includes('/google-maps/scraper/gm-filter')) {
|
|
159
|
-
return new Response(JSON.stringify({
|
|
160
|
-
jobId: 'gm-filter',
|
|
161
|
-
status: 'done',
|
|
162
|
-
places: [
|
|
163
|
-
{ title: 'Strict match — keep', totalScore: 3.5, reviewsCount: 120 },
|
|
164
|
-
{ title: 'Rating too high — drop', totalScore: 4.7, reviewsCount: 800 },
|
|
165
|
-
{ title: 'Too few reviews — drop', totalScore: 2.9, reviewsCount: 10 },
|
|
166
|
-
],
|
|
167
|
-
}), { status: 200 })
|
|
168
|
-
}
|
|
169
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
170
|
-
}) as typeof fetch
|
|
171
|
-
|
|
172
|
-
const { getProviders } = await import('../../src/registry.js')
|
|
173
|
-
const providers = getProviders('search_places')
|
|
174
|
-
const gm = providers.find((p) => p.id === 'google_maps')!
|
|
175
|
-
const orig = { t: gm.async!.timeoutMs, i: gm.async!.pollIntervalMs }
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
gm.async!.timeoutMs = 500
|
|
179
|
-
gm.async!.pollIntervalMs = 20
|
|
180
|
-
const result = await searchPlacesHandler({
|
|
181
|
-
query: 'dentist',
|
|
182
|
-
country: 'FR',
|
|
183
|
-
provider: 'google_maps',
|
|
184
|
-
max_overall_rating: 4,
|
|
185
|
-
min_total_reviews: 20,
|
|
186
|
-
limit: 10,
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
expect(result.isError).toBeFalsy()
|
|
190
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
191
|
-
expect(parsed._meta.provider).toBe('google_maps')
|
|
192
|
-
expect(parsed.data.places.map((p: { title: string }) => p.title)).toEqual(['Strict match — keep'])
|
|
193
|
-
} finally {
|
|
194
|
-
gm.async!.timeoutMs = orig.t
|
|
195
|
-
gm.async!.pollIntervalMs = orig.i
|
|
196
|
-
}
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
it('all providers fail — returns isError', async () => {
|
|
200
|
-
globalThis.fetch = vi.fn(async () => {
|
|
201
|
-
return new Response(JSON.stringify({ error: 'down' }), { status: 503 })
|
|
202
|
-
}) as typeof fetch
|
|
203
|
-
|
|
204
|
-
const result = await searchPlacesHandler({ query: 'coffee', country: 'US' })
|
|
205
|
-
|
|
206
|
-
expect(result.isError).toBe(true)
|
|
207
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
208
|
-
expect(parsed.error).toBeTruthy()
|
|
209
|
-
})
|
|
210
|
-
})
|