@coldiq/mcp 0.1.18 → 0.2.4
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 +2 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +7 -1
- package/dist/client.js.map +1 -1
- package/dist/executor.d.ts +11 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +72 -11
- package/dist/executor.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/registry.d.ts +1 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +57 -8
- package/dist/registry.js.map +1 -1
- package/dist/tools/find-emails.d.ts +2 -7
- package/dist/tools/find-emails.d.ts.map +1 -1
- package/dist/tools/find-emails.js +193 -67
- package/dist/tools/find-emails.js.map +1 -1
- package/dist/tools/find-people.d.ts +3 -2
- package/dist/tools/find-people.d.ts.map +1 -1
- package/dist/tools/find-people.js +65 -7
- package/dist/tools/find-people.js.map +1 -1
- package/dist/tools/get-credit-balance.d.ts +17 -0
- package/dist/tools/get-credit-balance.d.ts.map +1 -0
- package/dist/tools/get-credit-balance.js +20 -0
- package/dist/tools/get-credit-balance.js.map +1 -0
- package/dist/utils/compact-people.d.ts +24 -0
- package/dist/utils/compact-people.d.ts.map +1 -0
- package/dist/utils/compact-people.js +306 -0
- package/dist/utils/compact-people.js.map +1 -0
- package/dist/utils/provider-resolver.d.ts.map +1 -1
- package/dist/utils/provider-resolver.js +15 -1
- package/dist/utils/provider-resolver.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +9 -1
- package/src/executor.ts +89 -17
- package/src/index.ts +8 -0
- package/src/registry.ts +67 -8
- package/src/tools/find-emails.ts +251 -80
- package/src/tools/find-people.ts +70 -7
- package/src/tools/get-credit-balance.ts +24 -0
- package/src/utils/compact-people.ts +318 -0
- package/src/utils/provider-resolver.ts +15 -1
- package/tests/executor.test.ts +165 -0
- package/tests/live/fullenrich-upstream-probe.ts +55 -0
- package/tests/live/pdl-upstream-probe.ts +83 -0
- package/tests/registry-find-people.test.ts +198 -7
- package/tests/registry-search-companies.test.ts +46 -7
- package/tests/tools/find-emails.test.ts +267 -1
- package/tests/tools/find-people.test.ts +269 -5
- package/tests/tools/get-credit-balance.test.ts +56 -0
- package/tests/utils/compact-people.test.ts +462 -0
|
@@ -108,9 +108,59 @@ describe('find_people handler', () => {
|
|
|
108
108
|
limit: 5,
|
|
109
109
|
})
|
|
110
110
|
|
|
111
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
112
|
+
expect(parsed._meta.provider).toBe('leadsfactory')
|
|
113
|
+
// Compact-by-default: raw `companies_personas` is normalized into `people[]`.
|
|
114
|
+
expect(parsed.data.people).toHaveLength(1)
|
|
115
|
+
expect(parsed.data.people[0]).toMatchObject({
|
|
116
|
+
full_name: 'Michel Lieben',
|
|
117
|
+
first_name: 'Michel',
|
|
118
|
+
last_name: 'Lieben',
|
|
119
|
+
title: 'CEO',
|
|
120
|
+
})
|
|
121
|
+
expect(parsed.data.companies_personas).toBeUndefined()
|
|
122
|
+
} finally {
|
|
123
|
+
lf.async!.timeoutMs = originalTimeout
|
|
124
|
+
lf.async!.pollIntervalMs = originalInterval
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('fields=verbose returns the raw LeadsFactory passthrough (companies_personas) unchanged', async () => {
|
|
129
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
130
|
+
const urlStr = url.toString()
|
|
131
|
+
if (urlStr.includes('/leadsfactory/contact-finder/searches') && !urlStr.includes('abc123')) {
|
|
132
|
+
return new Response(JSON.stringify({ id: 'abc123', nb_jobs_total: 1, progress_percentage: 0 }), { status: 201 })
|
|
133
|
+
}
|
|
134
|
+
if (urlStr.includes('/leadsfactory/contact-finder/searches/abc123')) {
|
|
135
|
+
return new Response(JSON.stringify({
|
|
136
|
+
id: 'abc123', status: 'SUCCESSFUL', nb_jobs_total: 1, nb_jobs_complete: 1,
|
|
137
|
+
companies_personas: [
|
|
138
|
+
{ company: 'ColdIQ', personas: [[{ contact: { first_name: 'Michel', last_name: 'Lieben', title: 'CEO' } }]] },
|
|
139
|
+
],
|
|
140
|
+
}), { status: 200 })
|
|
141
|
+
}
|
|
142
|
+
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
|
|
143
|
+
}) as typeof fetch
|
|
144
|
+
|
|
145
|
+
const { getProviders } = await import('../../src/registry.js')
|
|
146
|
+
const lf = getProviders('find_people').find((p) => p.id === 'leadsfactory')!
|
|
147
|
+
const originalTimeout = lf.async!.timeoutMs
|
|
148
|
+
const originalInterval = lf.async!.pollIntervalMs
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
lf.async!.timeoutMs = 500
|
|
152
|
+
lf.async!.pollIntervalMs = 20
|
|
153
|
+
const result = await findPeopleHandler({
|
|
154
|
+
company_domains: ['coldiq.com'],
|
|
155
|
+
job_titles: ['CEO'],
|
|
156
|
+
limit: 5,
|
|
157
|
+
fields: 'verbose',
|
|
158
|
+
})
|
|
159
|
+
|
|
111
160
|
const parsed = JSON.parse(result.content[0].text)
|
|
112
161
|
expect(parsed._meta.provider).toBe('leadsfactory')
|
|
113
162
|
expect(parsed.data.companies_personas).toHaveLength(1)
|
|
163
|
+
expect(parsed.data.people).toBeUndefined()
|
|
114
164
|
} finally {
|
|
115
165
|
lf.async!.timeoutMs = originalTimeout
|
|
116
166
|
lf.async!.pollIntervalMs = originalInterval
|
|
@@ -169,10 +219,13 @@ describe('find_people handler', () => {
|
|
|
169
219
|
|
|
170
220
|
const parsed = JSON.parse(result.content[0].text)
|
|
171
221
|
expect(parsed._meta.provider).toBe('leadsfactory')
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
expect(parsed.data.
|
|
175
|
-
expect(parsed.data.
|
|
222
|
+
// Compact-by-default: LF + Apollo gap-fill records are merged into one people[]
|
|
223
|
+
// and the gap-fill source is surfaced via gap_fill_provider.
|
|
224
|
+
expect(parsed.data.people).toHaveLength(2)
|
|
225
|
+
expect(parsed.data.people.map((p: { full_name: string }) => p.full_name)).toEqual(['Michel Lieben', 'Thibaud Elziere'])
|
|
226
|
+
expect(parsed.data.gap_fill_provider).toBe('apollo')
|
|
227
|
+
expect(parsed.data.companies_personas).toBeUndefined()
|
|
228
|
+
expect(parsed.data.gap_fill).toBeUndefined()
|
|
176
229
|
} finally {
|
|
177
230
|
lf.async!.timeoutMs = originalTimeout
|
|
178
231
|
lf.async!.pollIntervalMs = originalInterval
|
|
@@ -224,7 +277,7 @@ describe('find_people handler', () => {
|
|
|
224
277
|
|
|
225
278
|
const parsed = JSON.parse(result.content[0].text)
|
|
226
279
|
expect(parsed._meta.provider).toBe('leadsfactory')
|
|
227
|
-
expect(parsed.data.
|
|
280
|
+
expect(parsed.data.gap_fill_provider).toBeUndefined()
|
|
228
281
|
expect(apolloCalled).toBe(false)
|
|
229
282
|
} finally {
|
|
230
283
|
lf.async!.timeoutMs = originalTimeout
|
|
@@ -353,5 +406,216 @@ describe('find_people handler', () => {
|
|
|
353
406
|
const parsed = JSON.parse(result.content[0].text)
|
|
354
407
|
expect(parsed._meta.provider).toBe('apollo')
|
|
355
408
|
expect(parsed.data.people).toHaveLength(1)
|
|
409
|
+
expect(parsed.data.people[0]).toMatchObject({
|
|
410
|
+
full_name: 'Michel Lieben',
|
|
411
|
+
company_domain: 'coldiq.com',
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
it('reveal=true follows obfuscated Apollo search with bulk-match in chunks of 10', async () => {
|
|
416
|
+
const bulkMatchCalls: Array<{ url: string; body: Record<string, unknown> }> = []
|
|
417
|
+
const obfuscated = (id: string, n: number) => ({
|
|
418
|
+
id,
|
|
419
|
+
first_name: `Person${n}`,
|
|
420
|
+
last_name: `Ar***t${n}`,
|
|
421
|
+
organization: { website_url: 'coldiq.com' },
|
|
422
|
+
})
|
|
423
|
+
const revealed = (id: string, n: number) => ({
|
|
424
|
+
id,
|
|
425
|
+
first_name: `Person${n}`,
|
|
426
|
+
last_name: `Argent${n}`,
|
|
427
|
+
email: `person${n}@coldiq.com`,
|
|
428
|
+
linkedin_url: `https://linkedin.com/in/person${n}`,
|
|
429
|
+
organization: { website_url: 'coldiq.com' },
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
|
|
433
|
+
const urlStr = url.toString()
|
|
434
|
+
|
|
435
|
+
if (urlStr.includes('/apollo/people/search')) {
|
|
436
|
+
return new Response(
|
|
437
|
+
JSON.stringify({ people: Array.from({ length: 12 }, (_, i) => obfuscated(`id_${i}`, i)) }),
|
|
438
|
+
{ status: 200 },
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (urlStr.includes('/apollo/people/bulk-match')) {
|
|
443
|
+
const body = JSON.parse((opts?.body as string) ?? '{}') as { details: Array<{ id: string }> }
|
|
444
|
+
bulkMatchCalls.push({ url: urlStr, body })
|
|
445
|
+
return new Response(
|
|
446
|
+
JSON.stringify({
|
|
447
|
+
matches: body.details.map((d, idx) => revealed(d.id, parseInt(d.id.replace('id_', ''), 10) || idx)),
|
|
448
|
+
}),
|
|
449
|
+
{ status: 200 },
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Skip LeadsFactory entirely by pinning apollo via use_providers below.
|
|
454
|
+
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
|
|
455
|
+
}) as typeof fetch
|
|
456
|
+
|
|
457
|
+
const result = await findPeopleHandler({
|
|
458
|
+
company_domains: ['coldiq.com'],
|
|
459
|
+
job_titles: ['CEO'],
|
|
460
|
+
limit: 12,
|
|
461
|
+
reveal: true,
|
|
462
|
+
use_providers: ['apollo'],
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
466
|
+
expect(parsed._meta.provider).toBe('apollo')
|
|
467
|
+
expect(parsed.data.revealed).toBe(true)
|
|
468
|
+
expect(parsed.data.people).toHaveLength(12)
|
|
469
|
+
// Chunked at 10 per bulk-match call: 12 IDs → 2 calls (10 + 2)
|
|
470
|
+
expect(bulkMatchCalls).toHaveLength(2)
|
|
471
|
+
expect(bulkMatchCalls[0].body.details).toHaveLength(10)
|
|
472
|
+
expect(bulkMatchCalls[1].body.details).toHaveLength(2)
|
|
473
|
+
// Revealed payload (compacted) carries email + full last name
|
|
474
|
+
expect(parsed.data.people[0].email).toBe('person0@coldiq.com')
|
|
475
|
+
expect(parsed.data.people[0].last_name).toBe('Argent0')
|
|
476
|
+
// reveal_personal_emails flag forwarded
|
|
477
|
+
expect(bulkMatchCalls[0].body.reveal_personal_emails).toBe(true)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('reveal=true does NOT set revealed:true when bulk-match returns only some IDs (partial reveal)', async () => {
|
|
481
|
+
// Apollo's bulk-match can return matches for a subset of submitted IDs (e.g. some
|
|
482
|
+
// people aren't in their database). Setting `revealed: true` in that case would
|
|
483
|
+
// mislead callers — entries still obfuscated would look revealed at the top level.
|
|
484
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
|
|
485
|
+
const urlStr = url.toString()
|
|
486
|
+
|
|
487
|
+
if (urlStr.includes('/apollo/people/search')) {
|
|
488
|
+
return new Response(
|
|
489
|
+
JSON.stringify({
|
|
490
|
+
people: [
|
|
491
|
+
{ id: 'id_0', first_name: 'A', last_name: 'A***A', organization: { website_url: 'coldiq.com' } },
|
|
492
|
+
{ id: 'id_1', first_name: 'B', last_name: 'B***B', organization: { website_url: 'coldiq.com' } },
|
|
493
|
+
{ id: 'id_2', first_name: 'C', last_name: 'C***C', organization: { website_url: 'coldiq.com' } },
|
|
494
|
+
],
|
|
495
|
+
}),
|
|
496
|
+
{ status: 200 },
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (urlStr.includes('/apollo/people/bulk-match')) {
|
|
501
|
+
const body = JSON.parse((opts?.body as string) ?? '{}') as { details: Array<{ id: string }> }
|
|
502
|
+
// Only return a match for id_0 — id_1 and id_2 stay obfuscated.
|
|
503
|
+
const onlyFirst = body.details
|
|
504
|
+
.filter((d) => d.id === 'id_0')
|
|
505
|
+
.map((d) => ({ id: d.id, first_name: 'A', last_name: 'AlphaReal', email: 'a@coldiq.com', linkedin_url: 'https://linkedin.com/in/a', organization: { website_url: 'coldiq.com' } }))
|
|
506
|
+
return new Response(JSON.stringify({ matches: onlyFirst }), { status: 200 })
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
|
|
510
|
+
}) as typeof fetch
|
|
511
|
+
|
|
512
|
+
const result = await findPeopleHandler({
|
|
513
|
+
company_domains: ['coldiq.com'],
|
|
514
|
+
job_titles: ['CEO'],
|
|
515
|
+
limit: 3,
|
|
516
|
+
reveal: true,
|
|
517
|
+
use_providers: ['apollo'],
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
521
|
+
expect(parsed._meta.provider).toBe('apollo')
|
|
522
|
+
// Partial reveal: flag must be omitted (compactPayload only includes revealed when === true).
|
|
523
|
+
expect(parsed.data.revealed).toBeUndefined()
|
|
524
|
+
expect(parsed.data.people).toHaveLength(3)
|
|
525
|
+
// id_0 came back revealed; id_1/id_2 stayed obfuscated.
|
|
526
|
+
expect(parsed.data.people[0].last_name).toBe('AlphaReal')
|
|
527
|
+
expect(parsed.data.people[0].email).toBe('a@coldiq.com')
|
|
528
|
+
expect(parsed.data.people[1].last_name).toBe('B***B')
|
|
529
|
+
expect(parsed.data.people[2].last_name).toBe('C***C')
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
it('reveal=false (default) skips bulk-match and returns obfuscated Apollo data unchanged', async () => {
|
|
533
|
+
let bulkMatchCalled = false
|
|
534
|
+
|
|
535
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
536
|
+
const urlStr = url.toString()
|
|
537
|
+
|
|
538
|
+
if (urlStr.includes('/apollo/people/search')) {
|
|
539
|
+
return new Response(
|
|
540
|
+
JSON.stringify({
|
|
541
|
+
people: [{ id: 'id_0', first_name: 'Michel', last_name: 'Li***n', organization: { website_url: 'coldiq.com' } }],
|
|
542
|
+
}),
|
|
543
|
+
{ status: 200 },
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (urlStr.includes('/apollo/people/bulk-match')) {
|
|
548
|
+
bulkMatchCalled = true
|
|
549
|
+
return new Response(JSON.stringify({ matches: [] }), { status: 200 })
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
|
|
553
|
+
}) as typeof fetch
|
|
554
|
+
|
|
555
|
+
const result = await findPeopleHandler({
|
|
556
|
+
company_domains: ['coldiq.com'],
|
|
557
|
+
job_titles: ['CEO'],
|
|
558
|
+
limit: 5,
|
|
559
|
+
use_providers: ['apollo'],
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
563
|
+
expect(parsed._meta.provider).toBe('apollo')
|
|
564
|
+
expect(parsed.data.revealed).toBeUndefined()
|
|
565
|
+
// Obfuscated last name from /apollo/people/search is preserved through compaction.
|
|
566
|
+
expect(parsed.data.people[0].last_name).toBe('Li***n')
|
|
567
|
+
expect(bulkMatchCalled).toBe(false)
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
it('reveal=true is a no-op when the matched provider is not Apollo', async () => {
|
|
571
|
+
let bulkMatchCalled = false
|
|
572
|
+
|
|
573
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
574
|
+
const urlStr = url.toString()
|
|
575
|
+
|
|
576
|
+
if (urlStr.includes('/leadsfactory/contact-finder/searches') && !urlStr.includes('abc123')) {
|
|
577
|
+
return new Response(JSON.stringify({ id: 'abc123', nb_jobs_total: 1, progress_percentage: 0 }), { status: 201 })
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (urlStr.includes('/leadsfactory/contact-finder/searches/abc123')) {
|
|
581
|
+
return new Response(JSON.stringify({
|
|
582
|
+
id: 'abc123', status: 'SUCCESSFUL', nb_jobs_total: 1, nb_jobs_complete: 1,
|
|
583
|
+
companies_personas: [
|
|
584
|
+
{ company: 'ColdIQ', personas: [[{ contact: { first_name: 'Michel', last_name: 'Lieben' } }]] },
|
|
585
|
+
],
|
|
586
|
+
}), { status: 200 })
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (urlStr.includes('/apollo/people/bulk-match')) {
|
|
590
|
+
bulkMatchCalled = true
|
|
591
|
+
return new Response(JSON.stringify({ matches: [] }), { status: 200 })
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
|
|
595
|
+
}) as typeof fetch
|
|
596
|
+
|
|
597
|
+
const { getProviders } = await import('../../src/registry.js')
|
|
598
|
+
const lf = getProviders('find_people').find((p) => p.id === 'leadsfactory')!
|
|
599
|
+
const originalTimeout = lf.async!.timeoutMs
|
|
600
|
+
const originalInterval = lf.async!.pollIntervalMs
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
lf.async!.timeoutMs = 500
|
|
604
|
+
lf.async!.pollIntervalMs = 20
|
|
605
|
+
|
|
606
|
+
const result = await findPeopleHandler({
|
|
607
|
+
company_domains: ['coldiq.com'],
|
|
608
|
+
job_titles: ['CEO'],
|
|
609
|
+
limit: 5,
|
|
610
|
+
reveal: true,
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
614
|
+
expect(parsed._meta.provider).toBe('leadsfactory')
|
|
615
|
+
expect(bulkMatchCalled).toBe(false)
|
|
616
|
+
} finally {
|
|
617
|
+
lf.async!.timeoutMs = originalTimeout
|
|
618
|
+
lf.async!.pollIntervalMs = originalInterval
|
|
619
|
+
}
|
|
356
620
|
})
|
|
357
621
|
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { initClient } from '../../src/client.js'
|
|
3
|
+
import { getCreditBalanceHandler } from '../../src/tools/get-credit-balance.js'
|
|
4
|
+
|
|
5
|
+
describe('get_credit_balance 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 the balance payload on success', async () => {
|
|
17
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
18
|
+
const u = url.toString()
|
|
19
|
+
if (u.endsWith('/v1/me/credits')) {
|
|
20
|
+
return new Response(JSON.stringify({ balance: 250, usedThisMonth: 47 }), { status: 200 })
|
|
21
|
+
}
|
|
22
|
+
return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
|
|
23
|
+
}) as typeof fetch
|
|
24
|
+
|
|
25
|
+
const result = await getCreditBalanceHandler({})
|
|
26
|
+
|
|
27
|
+
expect(result.isError).toBeFalsy()
|
|
28
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
29
|
+
expect(parsed.data).toEqual({ balance: 250, usedThisMonth: 47 })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('sends the API key as a bearer token', async () => {
|
|
33
|
+
let capturedAuth: string | null = null
|
|
34
|
+
globalThis.fetch = vi.fn(async (_url: string | URL | Request, opts?: RequestInit) => {
|
|
35
|
+
const headers = (opts?.headers ?? {}) as Record<string, string>
|
|
36
|
+
capturedAuth = headers.Authorization ?? null
|
|
37
|
+
return new Response(JSON.stringify({ balance: 0, usedThisMonth: 0 }), { status: 200 })
|
|
38
|
+
}) as typeof fetch
|
|
39
|
+
|
|
40
|
+
await getCreditBalanceHandler({})
|
|
41
|
+
expect(capturedAuth).toBe('Bearer test-key')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('returns isError when the API responds with non-2xx', async () => {
|
|
45
|
+
globalThis.fetch = vi.fn(async () => {
|
|
46
|
+
return new Response(JSON.stringify({ error: 'Invalid API key' }), { status: 401 })
|
|
47
|
+
}) as typeof fetch
|
|
48
|
+
|
|
49
|
+
const result = await getCreditBalanceHandler({})
|
|
50
|
+
|
|
51
|
+
expect(result.isError).toBe(true)
|
|
52
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
53
|
+
expect(parsed.status).toBe(401)
|
|
54
|
+
expect(parsed.error).toBe('Failed to fetch credit balance')
|
|
55
|
+
})
|
|
56
|
+
})
|