Skip to content

Commit ad96c6e

Browse files
fix(integrations): harden Wiza reveal polling, soften enrichment getCost guards
Address Greptile + Cursor Bugbot review on #4777: return explicit failures from the Wiza individual_reveal poller instead of throwing (thrown errors were swallowed into a false queued success), short-circuit when the initial reveal is already terminal, tolerate transient 5xx/429 during polling, and return 0 (not throw) from Findymail getCost when the contacts/employees array is absent.
1 parent 19dde97 commit ad96c6e

4 files changed

Lines changed: 125 additions & 7 deletions

File tree

apps/sim/tools/enrichment-hosting.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,85 @@ describe('Wiza hosted key pricing', () => {
186186
expect((result.output as any).email).toBe('a@b.com')
187187
expect((result.output as any).status).toBe('finished')
188188
})
189+
190+
it('returns immediately without polling when the initial reveal is already terminal', async () => {
191+
const fetchMock = vi.fn()
192+
vi.stubGlobal('fetch', fetchMock)
193+
194+
const initial = {
195+
success: true as const,
196+
output: {
197+
id: 123,
198+
status: 'finished',
199+
is_complete: true,
200+
email: 'a@b.com',
201+
email_status: 'valid',
202+
emails: [],
203+
phones: [],
204+
} as any,
205+
}
206+
const result = await wizaIndividualRevealTool.postProcess!(
207+
initial as any,
208+
{ apiKey: 'k', enrichment_level: 'full' } as any,
209+
vi.fn()
210+
)
211+
212+
expect(fetchMock).not.toHaveBeenCalled()
213+
expect(result.success).toBe(true)
214+
expect((result.output as any).email).toBe('a@b.com')
215+
})
216+
217+
it('retries transient poll errors and still resolves on a later finished response', async () => {
218+
vi.useFakeTimers()
219+
const fetchMock = vi
220+
.fn()
221+
.mockResolvedValueOnce(new Response('busy', { status: 503 }))
222+
.mockResolvedValueOnce(new Response('rate limited', { status: 429 }))
223+
.mockResolvedValueOnce(
224+
new Response(
225+
JSON.stringify({
226+
data: { id: 1, status: 'finished', email: 'a@b.com', email_status: 'valid', emails: [], phones: [] },
227+
}),
228+
{ status: 200, headers: { 'Content-Type': 'application/json' } }
229+
)
230+
)
231+
vi.stubGlobal('fetch', fetchMock)
232+
233+
const initial = {
234+
success: true as const,
235+
output: { id: 1, status: 'queued', is_complete: false } as any,
236+
}
237+
const promise = wizaIndividualRevealTool.postProcess!(
238+
initial as any,
239+
{ apiKey: 'k', enrichment_level: 'full' } as any,
240+
vi.fn()
241+
)
242+
await vi.advanceTimersByTimeAsync(6000)
243+
const result = await promise
244+
245+
expect(fetchMock).toHaveBeenCalledTimes(3)
246+
expect(result.success).toBe(true)
247+
expect((result.output as any).email).toBe('a@b.com')
248+
})
249+
250+
it('returns an explicit failure (not a queued success) after repeated poll errors', async () => {
251+
vi.useFakeTimers()
252+
const fetchMock = vi.fn().mockResolvedValue(new Response('error', { status: 500 }))
253+
vi.stubGlobal('fetch', fetchMock)
254+
255+
const initial = {
256+
success: true as const,
257+
output: { id: 1, status: 'queued', is_complete: false } as any,
258+
}
259+
const promise = wizaIndividualRevealTool.postProcess!(
260+
initial as any,
261+
{ apiKey: 'k', enrichment_level: 'full' } as any,
262+
vi.fn()
263+
)
264+
await vi.advanceTimersByTimeAsync(6000)
265+
const result = await promise
266+
267+
expect(result.success).toBe(false)
268+
expect((result.output as any).status).toBe('queued')
269+
})
189270
})

apps/sim/tools/findymail/find_emails_by_domain.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ export const findEmailsByDomainTool: ToolConfig<
1717
version: '1.0.0',
1818

1919
hosting: findymailHosting<FindymailFindEmailsByDomainParams>((_params, output) => {
20+
// No contacts array means no verified contacts returned — no charge.
2021
if (!Array.isArray(output.contacts)) {
21-
throw new Error('Findymail find emails by domain response missing contacts array')
22+
return 0
2223
}
2324
// 1 finder credit per verified contact returned.
2425
return output.contacts.length

apps/sim/tools/findymail/find_employees.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ export const findEmployeesTool: ToolConfig<
1717
version: '1.0.0',
1818

1919
hosting: findymailHosting<FindymailFindEmployeesParams>((_params, output) => {
20+
// No employees array means no contacts found — no charge.
2021
if (!Array.isArray(output.employees)) {
21-
throw new Error('Findymail find employees response missing employees array')
22+
return 0
2223
}
2324
// 1 finder credit per contact found.
2425
return output.employees.length

apps/sim/tools/wiza/individual_reveal.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ import type {
99

1010
const POLL_INTERVAL_MS = 2000
1111
const MAX_POLL_TIME_MS = 120000
12+
/** Tolerate brief Wiza outages while polling before giving up on an already-started reveal. */
13+
const MAX_CONSECUTIVE_POLL_ERRORS = 3
14+
15+
/** Whether a reveal payload has reached a terminal state and no longer needs polling. */
16+
function isTerminalReveal(d: Record<string, unknown>): boolean {
17+
return d.status === 'finished' || d.status === 'failed' || d.is_complete === true
18+
}
1219

1320
/** Map a Wiza individual-reveal payload (`data` object) to the tool output shape. */
1421
function mapRevealData(d: Record<string, unknown>): WizaIndividualRevealData {
@@ -192,12 +199,25 @@ export const wizaIndividualRevealTool: ToolConfig<
192199
postProcess: async (result, params) => {
193200
if (!result.success) return result
194201

202+
// Wiza can resolve synchronously (e.g. a cache hit) — the initial POST payload is
203+
// already mapped, so skip polling when it is terminal.
204+
if (isTerminalReveal(result.output)) {
205+
return { success: result.output.status !== 'failed', output: result.output }
206+
}
207+
195208
const revealId = result.output.id
196209
if (revealId == null) {
197-
throw new Error('Wiza individual reveal did not return an id')
210+
// Return an explicit failure rather than throwing: a thrown error here is swallowed
211+
// by the executor and masked as the queued (incomplete) success result.
212+
return {
213+
success: false,
214+
error: 'Wiza individual reveal did not return an id',
215+
output: result.output,
216+
}
198217
}
199218

200219
let elapsedTime = 0
220+
let consecutiveErrors = 0
201221
while (elapsedTime < MAX_POLL_TIME_MS) {
202222
await sleep(POLL_INTERVAL_MS)
203223
elapsedTime += POLL_INTERVAL_MS
@@ -213,22 +233,37 @@ export const wizaIndividualRevealTool: ToolConfig<
213233
)
214234

215235
if (!statusResponse.ok) {
216-
const errorText = await statusResponse.text()
217-
throw new Error(`Wiza API error: ${statusResponse.status} - ${errorText}`)
236+
// The reveal is already started (and billed by Wiza), so tolerate brief outages and
237+
// retry rather than aborting the whole window on a single transient 5xx/429.
238+
consecutiveErrors += 1
239+
if (consecutiveErrors >= MAX_CONSECUTIVE_POLL_ERRORS) {
240+
const errorText = await statusResponse.text().catch(() => '')
241+
return {
242+
success: false,
243+
error: `Wiza API error: ${statusResponse.status} - ${errorText}`,
244+
output: result.output,
245+
}
246+
}
247+
continue
218248
}
249+
consecutiveErrors = 0
219250

220251
const json = await statusResponse.json()
221252
const data = json.data ?? {}
222253

223-
if (data.status === 'finished' || data.status === 'failed' || data.is_complete === true) {
254+
if (isTerminalReveal(data)) {
224255
return {
225256
success: data.status !== 'failed',
226257
output: mapRevealData(data),
227258
}
228259
}
229260
}
230261

231-
throw new Error('Wiza individual reveal did not complete within the polling window')
262+
return {
263+
success: false,
264+
error: 'Wiza individual reveal did not complete within the polling window',
265+
output: result.output,
266+
}
232267
},
233268

234269
outputs: {

0 commit comments

Comments
 (0)