@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
package/tests/executor.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
2
|
import { initClient } from '../src/client.js'
|
|
3
3
|
import { executeWithFallback } from '../src/executor.js'
|
|
4
|
+
import { getSessionUsage, resetSessionUsage } from '../src/session-usage.js'
|
|
4
5
|
import * as registry from '../src/registry.js'
|
|
5
6
|
import type { ProviderEntry } from '../src/registry.js'
|
|
6
7
|
|
|
@@ -225,6 +226,74 @@ describe('executor exception safety', () => {
|
|
|
225
226
|
})
|
|
226
227
|
})
|
|
227
228
|
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Session-usage integration — the async create/poll path must record a charge
|
|
231
|
+
// exactly once. Locks in the "no double count" guarantee against future
|
|
232
|
+
// refactors of executeAsync's header merging.
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
describe('executor records session credit usage exactly once on the async path', () => {
|
|
236
|
+
const originalFetch = globalThis.fetch
|
|
237
|
+
|
|
238
|
+
beforeEach(() => {
|
|
239
|
+
initClient('http://test-api.local', 'test-key-123')
|
|
240
|
+
resetSessionUsage(0)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
afterEach(() => {
|
|
244
|
+
globalThis.fetch = originalFetch
|
|
245
|
+
vi.restoreAllMocks()
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('charge header on the create response is counted once; polls add nothing', async () => {
|
|
249
|
+
stubProviders([
|
|
250
|
+
makeProvider({
|
|
251
|
+
id: 'async-charged',
|
|
252
|
+
endpoint: '/async-charged',
|
|
253
|
+
hasResult: (data) => (data as Record<string, unknown>).done === true,
|
|
254
|
+
async: {
|
|
255
|
+
extractId: () => 'job-1',
|
|
256
|
+
pollEndpoint: (id) => `/poll/${id}`,
|
|
257
|
+
pollIntervalMs: 5,
|
|
258
|
+
timeoutMs: 500,
|
|
259
|
+
// Complete only once the poll body says so, forcing ≥1 poll round.
|
|
260
|
+
isComplete: (data) => (data as Record<string, unknown>).done === true,
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
])
|
|
264
|
+
|
|
265
|
+
let pollCount = 0
|
|
266
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
267
|
+
const u = url.toString()
|
|
268
|
+
if (u.includes('/poll/')) {
|
|
269
|
+
pollCount++
|
|
270
|
+
// First poll still processing (no charge header), second completes.
|
|
271
|
+
const body = pollCount >= 2 ? { done: true } : { status: 'processing' }
|
|
272
|
+
return new Response(JSON.stringify(body), { status: 200 })
|
|
273
|
+
}
|
|
274
|
+
// Create response — carries the billing headers, like the real API.
|
|
275
|
+
return new Response(JSON.stringify({ job_id: 'job-1' }), {
|
|
276
|
+
status: 200,
|
|
277
|
+
headers: {
|
|
278
|
+
'X-ColdIQ-Credits-Charged': '5',
|
|
279
|
+
'X-ColdIQ-Credits-Remaining': '120',
|
|
280
|
+
},
|
|
281
|
+
})
|
|
282
|
+
}) as typeof fetch
|
|
283
|
+
|
|
284
|
+
const result = await executeWithFallback('find_people', { domain: 'coldiq.com' })
|
|
285
|
+
|
|
286
|
+
expect('data' in result).toBe(true)
|
|
287
|
+
expect(pollCount).toBeGreaterThanOrEqual(2)
|
|
288
|
+
|
|
289
|
+
const usage = getSessionUsage()
|
|
290
|
+
expect(usage.billedCalls).toBe(1)
|
|
291
|
+
expect(usage.totalCreditsCharged).toBe(5)
|
|
292
|
+
expect(usage.balanceAfterLastCharge).toBe(120)
|
|
293
|
+
expect(usage.byEndpoint).toEqual([{ endpoint: '/async-charged', calls: 1, credits: 5 }])
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
228
297
|
// ---------------------------------------------------------------------------
|
|
229
298
|
// postFilter integration (existing tests)
|
|
230
299
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
recordCharge,
|
|
4
|
+
recordChargeFromHeaders,
|
|
5
|
+
getSessionUsage,
|
|
6
|
+
resetSessionUsage,
|
|
7
|
+
} from '../src/session-usage.js'
|
|
8
|
+
import { initClient, callApi } from '../src/client.js'
|
|
9
|
+
|
|
10
|
+
describe('session-usage accumulator', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
resetSessionUsage(1_000)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('starts empty', () => {
|
|
16
|
+
const usage = getSessionUsage(1_000)
|
|
17
|
+
expect(usage.billedCalls).toBe(0)
|
|
18
|
+
expect(usage.totalCreditsCharged).toBe(0)
|
|
19
|
+
expect(usage.byEndpoint).toEqual([])
|
|
20
|
+
expect(usage.balanceAfterLastCharge).toBeUndefined()
|
|
21
|
+
expect(usage.startedAt).toBe(new Date(1_000).toISOString())
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('accumulates charges and tracks the last reported balance', () => {
|
|
25
|
+
recordCharge('/prospeo/find-email', 1, 99)
|
|
26
|
+
recordCharge('/prospeo/find-email', 1, 98)
|
|
27
|
+
recordCharge('/companyenrich/companies/search', 3, 95)
|
|
28
|
+
|
|
29
|
+
const usage = getSessionUsage(2_000)
|
|
30
|
+
expect(usage.billedCalls).toBe(3)
|
|
31
|
+
expect(usage.totalCreditsCharged).toBe(5)
|
|
32
|
+
expect(usage.balanceAfterLastCharge).toBe(95)
|
|
33
|
+
expect(usage.durationMs).toBe(1_000)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('breaks down per endpoint sorted by credits descending', () => {
|
|
37
|
+
recordCharge('/cheap', 1)
|
|
38
|
+
recordCharge('/expensive', 10)
|
|
39
|
+
recordCharge('/cheap', 1)
|
|
40
|
+
|
|
41
|
+
const { byEndpoint } = getSessionUsage()
|
|
42
|
+
expect(byEndpoint).toEqual([
|
|
43
|
+
{ endpoint: '/expensive', calls: 1, credits: 10 },
|
|
44
|
+
{ endpoint: '/cheap', calls: 2, credits: 2 },
|
|
45
|
+
])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('rounds fractional credit sums to avoid float noise', () => {
|
|
49
|
+
recordCharge('/x', 0.1)
|
|
50
|
+
recordCharge('/x', 0.2)
|
|
51
|
+
expect(getSessionUsage().totalCreditsCharged).toBe(0.3)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('ignores malformed and negative charges', () => {
|
|
55
|
+
recordCharge('/x', Number.NaN)
|
|
56
|
+
recordCharge('/x', -5)
|
|
57
|
+
recordCharge('/x', Infinity)
|
|
58
|
+
const usage = getSessionUsage()
|
|
59
|
+
expect(usage.billedCalls).toBe(0)
|
|
60
|
+
expect(usage.totalCreditsCharged).toBe(0)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('recordChargeFromHeaders', () => {
|
|
64
|
+
it('records when the charge header is present', () => {
|
|
65
|
+
recordChargeFromHeaders('/find-email', {
|
|
66
|
+
'x-coldiq-credits-charged': '2',
|
|
67
|
+
'x-coldiq-credits-remaining': '198',
|
|
68
|
+
})
|
|
69
|
+
const usage = getSessionUsage()
|
|
70
|
+
expect(usage.billedCalls).toBe(1)
|
|
71
|
+
expect(usage.totalCreditsCharged).toBe(2)
|
|
72
|
+
expect(usage.balanceAfterLastCharge).toBe(198)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('is a no-op when no charge header is present (e.g. a poll response)', () => {
|
|
76
|
+
recordChargeFromHeaders('/poll', { 'content-type': 'application/json' })
|
|
77
|
+
expect(getSessionUsage().billedCalls).toBe(0)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('records the charge even when the remaining header is absent', () => {
|
|
81
|
+
recordChargeFromHeaders('/x', { 'x-coldiq-credits-charged': '4' })
|
|
82
|
+
const usage = getSessionUsage()
|
|
83
|
+
expect(usage.totalCreditsCharged).toBe(4)
|
|
84
|
+
expect(usage.balanceAfterLastCharge).toBeUndefined()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('callApi records credit charges into session usage', () => {
|
|
90
|
+
const originalFetch = globalThis.fetch
|
|
91
|
+
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
initClient('http://test-api.local', 'test-key')
|
|
94
|
+
resetSessionUsage(0)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
globalThis.fetch = originalFetch
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('tallies a charge surfaced in response headers', async () => {
|
|
102
|
+
globalThis.fetch = vi.fn(async () =>
|
|
103
|
+
new Response(JSON.stringify({ email: 'michel@coldiq.com' }), {
|
|
104
|
+
status: 200,
|
|
105
|
+
headers: {
|
|
106
|
+
'X-ColdIQ-Credits-Charged': '1',
|
|
107
|
+
'X-ColdIQ-Credits-Remaining': '249',
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
) as typeof fetch
|
|
111
|
+
|
|
112
|
+
await callApi('POST', '/prospeo/find-email', { domain: 'coldiq.com' })
|
|
113
|
+
|
|
114
|
+
const usage = getSessionUsage()
|
|
115
|
+
expect(usage.billedCalls).toBe(1)
|
|
116
|
+
expect(usage.totalCreditsCharged).toBe(1)
|
|
117
|
+
expect(usage.balanceAfterLastCharge).toBe(249)
|
|
118
|
+
expect(usage.byEndpoint).toEqual([{ endpoint: '/prospeo/find-email', calls: 1, credits: 1 }])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('does not tally calls that return no charge header', async () => {
|
|
122
|
+
globalThis.fetch = vi.fn(async () =>
|
|
123
|
+
new Response(JSON.stringify({ status: 'processing' }), { status: 200 }),
|
|
124
|
+
) as typeof fetch
|
|
125
|
+
|
|
126
|
+
await callApi('GET', '/jobs/abc123')
|
|
127
|
+
|
|
128
|
+
expect(getSessionUsage().billedCalls).toBe(0)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
getSessionUsageHandler,
|
|
4
|
+
getSessionUsageName,
|
|
5
|
+
getSessionUsageSchema,
|
|
6
|
+
} from '../../src/tools/get-session-usage.js'
|
|
7
|
+
import { recordCharge, resetSessionUsage } from '../../src/session-usage.js'
|
|
8
|
+
|
|
9
|
+
describe('get_session_usage handler', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
resetSessionUsage()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('is registered as a zero-arg tool', () => {
|
|
15
|
+
expect(getSessionUsageName).toBe('get_session_usage')
|
|
16
|
+
expect(getSessionUsageSchema).toEqual({})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('returns an empty snapshot before any billed call', async () => {
|
|
20
|
+
const result = await getSessionUsageHandler({})
|
|
21
|
+
expect(result.isError).toBeFalsy()
|
|
22
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
23
|
+
expect(parsed.data.billedCalls).toBe(0)
|
|
24
|
+
expect(parsed.data.totalCreditsCharged).toBe(0)
|
|
25
|
+
expect(parsed.data.byEndpoint).toEqual([])
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('reflects recorded charges with a per-endpoint breakdown', async () => {
|
|
29
|
+
recordCharge('/prospeo/find-email', 1, 99)
|
|
30
|
+
recordCharge('/companyenrich/companies/search', 3, 96)
|
|
31
|
+
|
|
32
|
+
const result = await getSessionUsageHandler({})
|
|
33
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
34
|
+
|
|
35
|
+
expect(parsed.data.billedCalls).toBe(2)
|
|
36
|
+
expect(parsed.data.totalCreditsCharged).toBe(4)
|
|
37
|
+
expect(parsed.data.balanceAfterLastCharge).toBe(96)
|
|
38
|
+
expect(parsed.data.byEndpoint[0]).toEqual({
|
|
39
|
+
endpoint: '/companyenrich/companies/search',
|
|
40
|
+
calls: 1,
|
|
41
|
+
credits: 3,
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// Mock the HTTP client so callVerb's envelope-building can be asserted without
|
|
4
|
+
// touching the network. The factory returns a vi.fn() we capture below.
|
|
5
|
+
const callApiMock = vi.fn()
|
|
6
|
+
vi.mock('../src/client.js', () => ({
|
|
7
|
+
callApi: (...args: unknown[]) => callApiMock(...args),
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
import { callVerb } from '../src/verb-client.js'
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
callApiMock.mockReset()
|
|
14
|
+
callApiMock.mockResolvedValue({ ok: true, status: 200, data: { data: { ok: true }, _meta: {} }, headers: {} })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('callVerb', () => {
|
|
18
|
+
it('maps single input into envelope.input and forwards the verb path', async () => {
|
|
19
|
+
await callVerb('/email/find', { first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' })
|
|
20
|
+
|
|
21
|
+
expect(callApiMock).toHaveBeenCalledTimes(1)
|
|
22
|
+
const [method, path, envelope] = callApiMock.mock.calls[0]
|
|
23
|
+
expect(method).toBe('POST')
|
|
24
|
+
expect(path).toBe('/email/find')
|
|
25
|
+
expect(envelope).toEqual({
|
|
26
|
+
input: { first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' },
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('maps use_providers (non-empty array) to envelope.provider and keeps it out of input', async () => {
|
|
31
|
+
await callVerb('/email/find', {
|
|
32
|
+
first_name: 'Michel',
|
|
33
|
+
domain: 'coldiq.com',
|
|
34
|
+
use_providers: ['prospeo', 'fullenrich'],
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
38
|
+
expect(envelope.provider).toEqual(['prospeo', 'fullenrich'])
|
|
39
|
+
expect(envelope.input).toEqual({ first_name: 'Michel', domain: 'coldiq.com' })
|
|
40
|
+
expect(envelope.input).not.toHaveProperty('use_providers')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('omits provider when use_providers is empty or absent', async () => {
|
|
44
|
+
await callVerb('/email/find', { domain: 'coldiq.com', use_providers: [] })
|
|
45
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
46
|
+
expect(envelope).not.toHaveProperty('provider')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('passes fields and reveal through to the envelope', async () => {
|
|
50
|
+
await callVerb('/people/search', {
|
|
51
|
+
job_titles: ['CEO'],
|
|
52
|
+
fields: 'verbose',
|
|
53
|
+
reveal: true,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
57
|
+
expect(envelope.fields).toBe('verbose')
|
|
58
|
+
expect(envelope.reveal).toBe(true)
|
|
59
|
+
expect(envelope.input).toEqual({ job_titles: ['CEO'] })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('only sets fields/reveal when defined', async () => {
|
|
63
|
+
await callVerb('/people/search', { job_titles: ['CEO'] })
|
|
64
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
65
|
+
expect(envelope).not.toHaveProperty('fields')
|
|
66
|
+
expect(envelope).not.toHaveProperty('reveal')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('bulk mode maps the named list field to inputs[]', async () => {
|
|
70
|
+
const people = [
|
|
71
|
+
{ id: 'a', first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' },
|
|
72
|
+
{ id: 'b', first_name: 'Ada', last_name: 'Lovelace', domain: 'microsoft.com' },
|
|
73
|
+
]
|
|
74
|
+
await callVerb('/email/find', { people, use_providers: ['prospeo'] }, { bulkField: 'people' })
|
|
75
|
+
|
|
76
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
77
|
+
expect(envelope.inputs).toEqual(people)
|
|
78
|
+
expect(envelope).not.toHaveProperty('input')
|
|
79
|
+
expect(envelope.provider).toEqual(['prospeo'])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('bulk mode merges sibling rest fields alongside inputs', async () => {
|
|
83
|
+
await callVerb(
|
|
84
|
+
'/email/find',
|
|
85
|
+
{ people: [{ id: 'a' }], country: 'US' },
|
|
86
|
+
{ bulkField: 'people' },
|
|
87
|
+
)
|
|
88
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
89
|
+
expect(envelope.inputs).toEqual([{ id: 'a' }])
|
|
90
|
+
expect(envelope.country).toBe('US')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('forwards res.data and does not set isError on a 2xx response', async () => {
|
|
94
|
+
callApiMock.mockResolvedValue({ ok: true, status: 200, data: { data: { email: 'x@y.com' } }, headers: {} })
|
|
95
|
+
const out = await callVerb('/email/find', { domain: 'coldiq.com' })
|
|
96
|
+
|
|
97
|
+
expect(out.content[0].type).toBe('text')
|
|
98
|
+
expect(JSON.parse(out.content[0].text)).toEqual({ data: { email: 'x@y.com' } })
|
|
99
|
+
expect(out.isError).toBe(false)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('sets isError when the response is not ok', async () => {
|
|
103
|
+
callApiMock.mockResolvedValue({
|
|
104
|
+
ok: false,
|
|
105
|
+
status: 404,
|
|
106
|
+
data: { error: 'No usable result', providers_tried: ['prospeo'] },
|
|
107
|
+
headers: {},
|
|
108
|
+
})
|
|
109
|
+
const out = await callVerb('/email/find', { domain: 'coldiq.com' })
|
|
110
|
+
|
|
111
|
+
expect(out.isError).toBe(true)
|
|
112
|
+
expect(JSON.parse(out.content[0].text)).toEqual({ error: 'No usable result', providers_tried: ['prospeo'] })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// --- schema-drift mappings (find_phone / search_places / find_emails) ---
|
|
116
|
+
|
|
117
|
+
it('remap renames input keys before building the envelope (company_domain → domain)', async () => {
|
|
118
|
+
await callVerb('/phone/find', { first_name: 'Michel', company_domain: 'coldiq.com', company_name: 'ColdIQ' }, { remap: { company_domain: 'domain' } })
|
|
119
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
120
|
+
expect(envelope.input).toEqual({ first_name: 'Michel', domain: 'coldiq.com', company_name: 'ColdIQ' })
|
|
121
|
+
expect(envelope.input).not.toHaveProperty('company_domain')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('providerFromField routes a tool-specific provider field into the provider pin', async () => {
|
|
125
|
+
await callVerb('/places/search', { query: 'coffee', provider: 'openmart' }, { providerFromField: 'provider' })
|
|
126
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
127
|
+
expect(envelope.provider).toEqual(['openmart'])
|
|
128
|
+
expect(envelope.input).toEqual({ query: 'coffee' })
|
|
129
|
+
expect(envelope.input).not.toHaveProperty('provider')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('providerFromField defers to an explicit use_providers when both are present', async () => {
|
|
133
|
+
await callVerb('/places/search', { query: 'coffee', provider: 'openmart', use_providers: ['google_maps'] }, { providerFromField: 'provider' })
|
|
134
|
+
const [, , envelope] = callApiMock.mock.calls[0]
|
|
135
|
+
expect(envelope.provider).toEqual(['google_maps'])
|
|
136
|
+
expect(envelope.input).toEqual({ query: 'coffee' })
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('reattachIdField re-attaches each input id onto bulk results by index', async () => {
|
|
140
|
+
callApiMock.mockResolvedValue({
|
|
141
|
+
ok: true,
|
|
142
|
+
status: 200,
|
|
143
|
+
data: { results: [{ data: { email: 'm@coldiq.com' } }, { error: 'not found' }] },
|
|
144
|
+
headers: {},
|
|
145
|
+
})
|
|
146
|
+
const people = [{ id: 'michel', first_name: 'Michel', domain: 'coldiq.com' }, { id: 'ada', first_name: 'Ada', domain: 'microsoft.com' }]
|
|
147
|
+
const out = await callVerb('/email/find', { people }, { bulkField: 'people', reattachIdField: 'id' })
|
|
148
|
+
const results = JSON.parse(out.content[0].text).results
|
|
149
|
+
expect(results[0]).toEqual({ id: 'michel', data: { email: 'm@coldiq.com' } })
|
|
150
|
+
expect(results[1]).toEqual({ id: 'ada', error: 'not found' })
|
|
151
|
+
})
|
|
152
|
+
})
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { initClient } from '../../src/client.js'
|
|
3
|
-
import { enrichCompanyHandler } from '../../src/tools/enrich-company.js'
|
|
4
|
-
|
|
5
|
-
describe('enrich_company 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 error when no identifier provided', async () => {
|
|
17
|
-
const result = await enrichCompanyHandler({})
|
|
18
|
-
expect(result.isError).toBe(true)
|
|
19
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
20
|
-
expect(parsed.error).toMatch(/domain.*linkedin_url.*name/i)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('returns enriched company from CompanyEnrich (GET)', async () => {
|
|
24
|
-
let capturedUrl = ''
|
|
25
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
26
|
-
capturedUrl = url.toString()
|
|
27
|
-
return new Response(JSON.stringify({ name: 'Stripe', domain: 'stripe.com', employees: 8000 }), { status: 200 })
|
|
28
|
-
}) as typeof fetch
|
|
29
|
-
|
|
30
|
-
const result = await enrichCompanyHandler({ domain: 'stripe.com' })
|
|
31
|
-
|
|
32
|
-
expect(capturedUrl).toContain('domain=stripe.com')
|
|
33
|
-
expect(capturedUrl).toContain('/companyenrich/companies/enrich')
|
|
34
|
-
|
|
35
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
36
|
-
expect(parsed._meta.provider).toBe('companyenrich')
|
|
37
|
-
expect(parsed.data.name).toBe('Stripe')
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('falls back to Apollo on CompanyEnrich failure', async () => {
|
|
41
|
-
let callCount = 0
|
|
42
|
-
globalThis.fetch = vi.fn(async () => {
|
|
43
|
-
callCount++
|
|
44
|
-
if (callCount === 1) {
|
|
45
|
-
return new Response(JSON.stringify({ error: 'Provider error' }), { status: 502 })
|
|
46
|
-
}
|
|
47
|
-
return new Response(JSON.stringify({ organization: { name: 'Stripe' } }), { status: 200 })
|
|
48
|
-
}) as typeof fetch
|
|
49
|
-
|
|
50
|
-
const result = await enrichCompanyHandler({ domain: 'stripe.com' })
|
|
51
|
-
|
|
52
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
53
|
-
expect(parsed._meta.provider).toBe('apollo')
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('skips CompanyEnrich GET and Apollo when only linkedin_url is provided', async () => {
|
|
57
|
-
const calledEndpoints: string[] = []
|
|
58
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
59
|
-
const endpoint = url.toString()
|
|
60
|
-
calledEndpoints.push(endpoint)
|
|
61
|
-
// PDL (3rd provider) succeeds
|
|
62
|
-
if (endpoint.includes('/pdl/company/enrich')) {
|
|
63
|
-
return new Response(JSON.stringify({ name: 'Stripe', website: 'stripe.com' }), { status: 200 })
|
|
64
|
-
}
|
|
65
|
-
return new Response(JSON.stringify({ error: 'not found' }), { status: 404 })
|
|
66
|
-
}) as typeof fetch
|
|
67
|
-
|
|
68
|
-
const result = await enrichCompanyHandler({ linkedin_url: 'https://www.linkedin.com/company/stripe' })
|
|
69
|
-
|
|
70
|
-
expect(calledEndpoints.every((u) => !u.includes('/companyenrich/companies/enrich?'))).toBe(true)
|
|
71
|
-
expect(calledEndpoints.every((u) => !u.includes('/apollo/'))).toBe(true)
|
|
72
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
73
|
-
expect(parsed._meta.provider).toBe('pdl')
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('skips all domain-only providers when only name is provided', async () => {
|
|
77
|
-
const calledEndpoints: string[] = []
|
|
78
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
79
|
-
const endpoint = url.toString()
|
|
80
|
-
calledEndpoints.push(endpoint)
|
|
81
|
-
if (endpoint.includes('/pdl/company/enrich')) {
|
|
82
|
-
return new Response(JSON.stringify({ name: 'Stripe', website: 'stripe.com' }), { status: 200 })
|
|
83
|
-
}
|
|
84
|
-
return new Response(JSON.stringify({ error: 'not found' }), { status: 404 })
|
|
85
|
-
}) as typeof fetch
|
|
86
|
-
|
|
87
|
-
await enrichCompanyHandler({ name: 'Stripe' })
|
|
88
|
-
|
|
89
|
-
expect(calledEndpoints.every((u) => !u.includes('/companyenrich/companies/enrich?'))).toBe(true)
|
|
90
|
-
expect(calledEndpoints.every((u) => !u.includes('/apollo/'))).toBe(true)
|
|
91
|
-
})
|
|
92
|
-
})
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { initClient } from '../../src/client.js'
|
|
3
|
-
import { enrichPersonHandler } from '../../src/tools/enrich-person.js'
|
|
4
|
-
|
|
5
|
-
describe('enrich_person 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('email input — linkupapi-email-reverse wins on first try', async () => {
|
|
17
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
18
|
-
const u = url.toString()
|
|
19
|
-
if (u.includes('/linkupapi/data/profil/enrich')) {
|
|
20
|
-
// profile-enrich not applicable for email-only, should not be called
|
|
21
|
-
throw new Error('profile-enrich should not fire for email-only input')
|
|
22
|
-
}
|
|
23
|
-
if (u.includes('/linkupapi/data/mail/reverse')) {
|
|
24
|
-
return new Response(JSON.stringify({ status: 'success', data: { linkedin_url: 'https://linkedin.com/in/michel-lieben', name: 'Michel Lieben' } }), { status: 200 })
|
|
25
|
-
}
|
|
26
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
27
|
-
}) as typeof fetch
|
|
28
|
-
|
|
29
|
-
const result = await enrichPersonHandler({ email: 'michel@coldiq.com' })
|
|
30
|
-
|
|
31
|
-
expect(result.isError).toBeFalsy()
|
|
32
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
33
|
-
expect(parsed._meta.provider).toBe('linkupapi-email-reverse')
|
|
34
|
-
// linkupapi wraps: { status, data: { ... } }
|
|
35
|
-
expect(parsed.data.data.linkedin_url).toContain('linkedin.com')
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('email input — falls back to pdl-person-enrich when linkupapi-email-reverse returns empty', async () => {
|
|
39
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
40
|
-
const u = url.toString()
|
|
41
|
-
if (u.includes('/linkupapi/data/mail/reverse')) {
|
|
42
|
-
// hasResult fails: status not 'success'
|
|
43
|
-
return new Response(JSON.stringify({ status: 'not_found', data: null }), { status: 200 })
|
|
44
|
-
}
|
|
45
|
-
if (u.includes('/pdl/person/enrich')) {
|
|
46
|
-
return new Response(JSON.stringify({ status: 200, data: { full_name: 'Michel Lieben', linkedin_url: 'https://linkedin.com/in/michel-lieben' } }), { status: 200 })
|
|
47
|
-
}
|
|
48
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
49
|
-
}) as typeof fetch
|
|
50
|
-
|
|
51
|
-
const result = await enrichPersonHandler({ email: 'michel@coldiq.com' })
|
|
52
|
-
|
|
53
|
-
expect(result.isError).toBeFalsy()
|
|
54
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
55
|
-
expect(parsed._meta.provider).toBe('pdl-person-enrich')
|
|
56
|
-
// PDL wraps: { status: 200, data: { ... } }
|
|
57
|
-
expect(parsed.data.data.full_name).toBe('Michel Lieben')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('name+company input — linkupapi-profile-enrich wins (highest priority for this input type)', async () => {
|
|
61
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
62
|
-
const u = url.toString()
|
|
63
|
-
if (u.includes('/linkupapi/data/profil/enrich')) {
|
|
64
|
-
return new Response(JSON.stringify({ status: 'success', data: { linkedin_url: 'https://linkedin.com/in/michel-lieben', headline: 'CEO at ColdIQ' } }), { status: 200 })
|
|
65
|
-
}
|
|
66
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
67
|
-
}) as typeof fetch
|
|
68
|
-
|
|
69
|
-
const result = await enrichPersonHandler({ first_name: 'Michel', last_name: 'Lieben', company_name: 'ColdIQ' })
|
|
70
|
-
|
|
71
|
-
expect(result.isError).toBeFalsy()
|
|
72
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
73
|
-
expect(parsed._meta.provider).toBe('linkupapi-profile-enrich')
|
|
74
|
-
// linkupapi wraps: { status, data: { ... } }
|
|
75
|
-
expect(parsed.data.data.headline).toBe('CEO at ColdIQ')
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('linkedin_url input — findymail-business-profile wins after non-applicable providers are skipped', async () => {
|
|
79
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
80
|
-
const u = url.toString()
|
|
81
|
-
// linkupapi-profile-enrich not applicable (no first_name)
|
|
82
|
-
// linkupapi-email-reverse not applicable (no email)
|
|
83
|
-
// pdl-person-enrich fires but returns no match
|
|
84
|
-
if (u.includes('/pdl/person/enrich')) {
|
|
85
|
-
return new Response(JSON.stringify({ status: 404, data: null }), { status: 200 })
|
|
86
|
-
}
|
|
87
|
-
// apollo-people-match fires but returns no person
|
|
88
|
-
if (u.includes('/apollo/people/match')) {
|
|
89
|
-
return new Response(JSON.stringify({ person: null }), { status: 200 })
|
|
90
|
-
}
|
|
91
|
-
// blitzapi-reverse-email not applicable (no email)
|
|
92
|
-
// findymail-business-profile: applicable (linkedin_url present)
|
|
93
|
-
if (u.includes('/findymail/search/business-profile')) {
|
|
94
|
-
return new Response(JSON.stringify({ name: 'Michel Lieben', email: 'michel@coldiq.com', linkedin_url: 'https://linkedin.com/in/michel-lieben' }), { status: 200 })
|
|
95
|
-
}
|
|
96
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
97
|
-
}) as typeof fetch
|
|
98
|
-
|
|
99
|
-
const result = await enrichPersonHandler({ linkedin_url: 'https://www.linkedin.com/in/michel-lieben' })
|
|
100
|
-
|
|
101
|
-
expect(result.isError).toBeFalsy()
|
|
102
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
103
|
-
expect(parsed._meta.provider).toBe('findymail-business-profile')
|
|
104
|
-
expect(parsed.data.name).toBe('Michel Lieben')
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('phone input — ai-ark-reverse-lookup wins (gated on phone present)', async () => {
|
|
108
|
-
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
109
|
-
const u = url.toString()
|
|
110
|
-
if (u.includes('/pdl/person/enrich')) {
|
|
111
|
-
return new Response(JSON.stringify({ status: 404, data: null }), { status: 200 })
|
|
112
|
-
}
|
|
113
|
-
if (u.includes('/apollo/people/match')) {
|
|
114
|
-
return new Response(JSON.stringify({ person: null }), { status: 200 })
|
|
115
|
-
}
|
|
116
|
-
if (u.includes('/ai-ark/people/reverse-lookup')) {
|
|
117
|
-
return new Response(JSON.stringify({ name: 'Michel Lieben', email: 'michel@coldiq.com' }), { status: 200 })
|
|
118
|
-
}
|
|
119
|
-
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
120
|
-
}) as typeof fetch
|
|
121
|
-
|
|
122
|
-
const result = await enrichPersonHandler({ phone: '+32123456789' })
|
|
123
|
-
|
|
124
|
-
expect(result.isError).toBeFalsy()
|
|
125
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
126
|
-
expect(parsed._meta.provider).toBe('ai-ark-reverse-lookup')
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('all providers fail — returns isError with error object', async () => {
|
|
130
|
-
globalThis.fetch = vi.fn(async () => {
|
|
131
|
-
return new Response(JSON.stringify({ error: 'Provider unavailable' }), { status: 503 })
|
|
132
|
-
}) as typeof fetch
|
|
133
|
-
|
|
134
|
-
const result = await enrichPersonHandler({ email: 'michel@coldiq.com' })
|
|
135
|
-
|
|
136
|
-
expect(result.isError).toBe(true)
|
|
137
|
-
const parsed = JSON.parse(result.content[0].text)
|
|
138
|
-
expect(parsed.error).toBeTruthy()
|
|
139
|
-
})
|
|
140
|
-
})
|