d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
278 lines
10 KiB
TypeScript
278 lines
10 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*/
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { findEmailFromNameTool } from '@/tools/findymail/find_email_from_name'
|
|
import { findEmailsByDomainTool } from '@/tools/findymail/find_emails_by_domain'
|
|
import { findPhoneTool } from '@/tools/findymail/find_phone'
|
|
import { FINDYMAIL_CREDIT_USD } from '@/tools/findymail/hosting'
|
|
import { reverseEmailLookupTool } from '@/tools/findymail/reverse_email_lookup'
|
|
import { verifyEmailTool } from '@/tools/findymail/verify_email'
|
|
import { bulkEnrichPersonTool } from '@/tools/prospeo/bulk_enrich_person'
|
|
import { enrichCompanyTool } from '@/tools/prospeo/enrich_company'
|
|
import { enrichPersonTool } from '@/tools/prospeo/enrich_person'
|
|
import { PROSPEO_CREDIT_USD } from '@/tools/prospeo/hosting'
|
|
import { searchPersonTool } from '@/tools/prospeo/search_person'
|
|
import type { ToolConfig } from '@/tools/types'
|
|
import { wizaCompanyEnrichmentTool } from '@/tools/wiza/company_enrichment'
|
|
import { WIZA_CREDIT_USD } from '@/tools/wiza/hosting'
|
|
import { wizaIndividualRevealTool } from '@/tools/wiza/individual_reveal'
|
|
import { wizaProspectSearchTool } from '@/tools/wiza/prospect_search'
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
function cost(tool: ToolConfig<any, any>, params: any, output: Record<string, unknown>) {
|
|
const pricing = tool.hosting?.pricing
|
|
if (!pricing || pricing.type !== 'custom') throw new Error('Expected custom pricing')
|
|
const result = pricing.getCost(params, output)
|
|
return typeof result === 'number' ? { cost: result } : result
|
|
}
|
|
|
|
describe('Findymail hosted key pricing', () => {
|
|
it('declares hosting with the shared env prefix and BYOK provider', () => {
|
|
expect(findEmailFromNameTool.hosting?.envKeyPrefix).toBe('FINDYMAIL_API_KEY')
|
|
expect(findEmailFromNameTool.hosting?.byokProviderId).toBe('findymail')
|
|
})
|
|
|
|
it('charges one credit only when an email is found', () => {
|
|
expect(cost(findEmailFromNameTool, {}, { contact: { email: 'a@b.com' } }).cost).toBeCloseTo(
|
|
FINDYMAIL_CREDIT_USD
|
|
)
|
|
expect(cost(findEmailFromNameTool, {}, { contact: null }).cost).toBe(0)
|
|
})
|
|
|
|
it('charges 10 credits for a found phone', () => {
|
|
expect(cost(findPhoneTool, {}, { phone: '+1555' }).cost).toBeCloseTo(10 * FINDYMAIL_CREDIT_USD)
|
|
expect(cost(findPhoneTool, {}, { phone: null }).cost).toBe(0)
|
|
})
|
|
|
|
it('charges one credit per contact returned by domain search', () => {
|
|
expect(cost(findEmailsByDomainTool, {}, { contacts: [{}, {}, {}] }).cost).toBeCloseTo(
|
|
3 * FINDYMAIL_CREDIT_USD
|
|
)
|
|
})
|
|
|
|
it('charges 2 credits for a reverse lookup with profile enrichment, 1 without', () => {
|
|
expect(
|
|
cost(reverseEmailLookupTool, { with_profile: true }, { email: 'a@b.com' }).cost
|
|
).toBeCloseTo(2 * FINDYMAIL_CREDIT_USD)
|
|
expect(
|
|
cost(reverseEmailLookupTool, { with_profile: false }, { email: 'a@b.com' }).cost
|
|
).toBeCloseTo(FINDYMAIL_CREDIT_USD)
|
|
expect(
|
|
cost(reverseEmailLookupTool, {}, { email: null, linkedin_url: null, fullName: null }).cost
|
|
).toBe(0)
|
|
})
|
|
|
|
it('charges one verifier credit per verification', () => {
|
|
expect(cost(verifyEmailTool, {}, { verified: true }).cost).toBeCloseTo(FINDYMAIL_CREDIT_USD)
|
|
})
|
|
})
|
|
|
|
describe('Prospeo hosted key pricing', () => {
|
|
it('declares hosting with the shared env prefix and BYOK provider', () => {
|
|
expect(enrichPersonTool.hosting?.envKeyPrefix).toBe('PROSPEO_API_KEY')
|
|
expect(enrichPersonTool.hosting?.byokProviderId).toBe('prospeo')
|
|
})
|
|
|
|
it('charges 1 credit for a person match and 10 when a mobile is revealed', () => {
|
|
expect(cost(enrichPersonTool, {}, { free_enrichment: false, person: {} }).cost).toBeCloseTo(
|
|
PROSPEO_CREDIT_USD
|
|
)
|
|
expect(
|
|
cost(enrichPersonTool, {}, { free_enrichment: false, person: { mobile: { revealed: true } } })
|
|
.cost
|
|
).toBeCloseTo(10 * PROSPEO_CREDIT_USD)
|
|
})
|
|
|
|
it('does not charge on a free or no-match enrichment', () => {
|
|
expect(cost(enrichPersonTool, {}, { free_enrichment: true, person: {} }).cost).toBe(0)
|
|
expect(cost(enrichPersonTool, {}, { free_enrichment: false, person: null }).cost).toBe(0)
|
|
expect(cost(enrichCompanyTool, {}, { free_enrichment: false, company: null }).cost).toBe(0)
|
|
})
|
|
|
|
it('uses the API-reported total_cost for bulk endpoints', () => {
|
|
expect(cost(bulkEnrichPersonTool, {}, { total_cost: 7 }).cost).toBeCloseTo(
|
|
7 * PROSPEO_CREDIT_USD
|
|
)
|
|
})
|
|
|
|
it('throws when bulk total_cost is missing', () => {
|
|
expect(() => cost(bulkEnrichPersonTool, {}, { matched: [] })).toThrow(/total_cost/)
|
|
})
|
|
|
|
it('charges one credit per non-free search page with results', () => {
|
|
expect(cost(searchPersonTool, {}, { free: false, results: [{}] }).cost).toBeCloseTo(
|
|
PROSPEO_CREDIT_USD
|
|
)
|
|
expect(cost(searchPersonTool, {}, { free: true, results: [{}] }).cost).toBe(0)
|
|
expect(cost(searchPersonTool, {}, { free: false, results: [] }).cost).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('Wiza hosted key pricing', () => {
|
|
it('declares hosting with the shared env prefix and BYOK provider', () => {
|
|
expect(wizaIndividualRevealTool.hosting?.envKeyPrefix).toBe('WIZA_API_KEY')
|
|
expect(wizaIndividualRevealTool.hosting?.byokProviderId).toBe('wiza')
|
|
})
|
|
|
|
it('charges 2 credits for a valid email and 5 for a phone on individual reveal', () => {
|
|
expect(
|
|
cost(wizaIndividualRevealTool, {}, { email_status: 'valid', phones: [] }).cost
|
|
).toBeCloseTo(2 * WIZA_CREDIT_USD)
|
|
expect(
|
|
cost(wizaIndividualRevealTool, {}, { email_status: 'unfound', mobile_phone: '+1555' }).cost
|
|
).toBeCloseTo(5 * WIZA_CREDIT_USD)
|
|
expect(
|
|
cost(wizaIndividualRevealTool, {}, { email_status: 'valid', phones: [{ number: '+1555' }] })
|
|
.cost
|
|
).toBeCloseTo(7 * WIZA_CREDIT_USD)
|
|
expect(cost(wizaIndividualRevealTool, {}, { email_status: 'unfound', phones: [] }).cost).toBe(0)
|
|
})
|
|
|
|
it('charges 2 credits per company enrichment match and nothing for prospect search', () => {
|
|
expect(cost(wizaCompanyEnrichmentTool, {}, { company_name: 'Wiza' }).cost).toBeCloseTo(
|
|
2 * WIZA_CREDIT_USD
|
|
)
|
|
expect(
|
|
cost(
|
|
wizaCompanyEnrichmentTool,
|
|
{},
|
|
{ company_name: null, company_domain: null, domain: null }
|
|
).cost
|
|
).toBe(0)
|
|
expect(cost(wizaProspectSearchTool, {}, { total: 100, profiles: [] }).cost).toBe(0)
|
|
})
|
|
|
|
it('polls the reveal to completion in postProcess', async () => {
|
|
vi.useFakeTimers()
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(
|
|
JSON.stringify({
|
|
data: {
|
|
id: 123,
|
|
status: 'finished',
|
|
email: 'a@b.com',
|
|
email_status: 'valid',
|
|
emails: [],
|
|
phones: [],
|
|
},
|
|
}),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
)
|
|
)
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const initial = {
|
|
success: true as const,
|
|
output: { id: 123, status: 'queued', is_complete: false } as any,
|
|
}
|
|
const promise = wizaIndividualRevealTool.postProcess!(
|
|
initial as any,
|
|
{ apiKey: 'k', enrichment_level: 'full' } as any,
|
|
vi.fn()
|
|
)
|
|
await vi.advanceTimersByTimeAsync(2000)
|
|
const result = await promise
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'https://wiza.co/api/individual_reveals/123',
|
|
expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer k' }) })
|
|
)
|
|
expect(result.success).toBe(true)
|
|
expect((result.output as any).email).toBe('a@b.com')
|
|
expect((result.output as any).status).toBe('finished')
|
|
})
|
|
|
|
it('returns immediately without polling when the initial reveal is already terminal', async () => {
|
|
const fetchMock = vi.fn()
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const initial = {
|
|
success: true as const,
|
|
output: {
|
|
id: 123,
|
|
status: 'finished',
|
|
is_complete: true,
|
|
email: 'a@b.com',
|
|
email_status: 'valid',
|
|
emails: [],
|
|
phones: [],
|
|
} as any,
|
|
}
|
|
const result = await wizaIndividualRevealTool.postProcess!(
|
|
initial as any,
|
|
{ apiKey: 'k', enrichment_level: 'full' } as any,
|
|
vi.fn()
|
|
)
|
|
|
|
expect(fetchMock).not.toHaveBeenCalled()
|
|
expect(result.success).toBe(true)
|
|
expect((result.output as any).email).toBe('a@b.com')
|
|
})
|
|
|
|
it('retries transient poll errors and still resolves on a later finished response', async () => {
|
|
vi.useFakeTimers()
|
|
const fetchMock = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(new Response('busy', { status: 503 }))
|
|
.mockResolvedValueOnce(new Response('rate limited', { status: 429 }))
|
|
.mockResolvedValueOnce(
|
|
new Response(
|
|
JSON.stringify({
|
|
data: {
|
|
id: 1,
|
|
status: 'finished',
|
|
email: 'a@b.com',
|
|
email_status: 'valid',
|
|
emails: [],
|
|
phones: [],
|
|
},
|
|
}),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
)
|
|
)
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const initial = {
|
|
success: true as const,
|
|
output: { id: 1, status: 'queued', is_complete: false } as any,
|
|
}
|
|
const promise = wizaIndividualRevealTool.postProcess!(
|
|
initial as any,
|
|
{ apiKey: 'k', enrichment_level: 'full' } as any,
|
|
vi.fn()
|
|
)
|
|
await vi.advanceTimersByTimeAsync(6000)
|
|
const result = await promise
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(3)
|
|
expect(result.success).toBe(true)
|
|
expect((result.output as any).email).toBe('a@b.com')
|
|
})
|
|
|
|
it('returns an explicit failure (not a queued success) after repeated poll errors', async () => {
|
|
vi.useFakeTimers()
|
|
const fetchMock = vi.fn().mockResolvedValue(new Response('error', { status: 500 }))
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const initial = {
|
|
success: true as const,
|
|
output: { id: 1, status: 'queued', is_complete: false } as any,
|
|
}
|
|
const promise = wizaIndividualRevealTool.postProcess!(
|
|
initial as any,
|
|
{ apiKey: 'k', enrichment_level: 'full' } as any,
|
|
vi.fn()
|
|
)
|
|
await vi.advanceTimersByTimeAsync(6000)
|
|
const result = await promise
|
|
|
|
expect(result.success).toBe(false)
|
|
expect((result.output as any).status).toBe('queued')
|
|
})
|
|
})
|