Skip to content

Commit 580ccde

Browse files
fix(icypeas): skip polling on BAD_INPUT and only treat validationErrors as a verdict
1 parent e07ed4c commit 580ccde

3 files changed

Lines changed: 85 additions & 14 deletions

File tree

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,71 @@ describe('Icypeas transformResponse validation errors', () => {
147147
} as any)
148148
).rejects.toThrow(/item _id/)
149149
})
150+
151+
it('verify-email throws on a bare success:false body without validationErrors', async () => {
152+
const response = new Response(JSON.stringify({ success: false }), {
153+
status: 200,
154+
headers: { 'Content-Type': 'application/json' },
155+
})
156+
157+
await expect(
158+
icypeasVerifyEmailTool.transformResponse!(response, {
159+
apiKey: 'test-key',
160+
email: 'jane@example.com',
161+
} as any)
162+
).rejects.toThrow(/item _id/)
163+
})
164+
})
165+
166+
describe('Icypeas postProcess on BAD_INPUT', () => {
167+
it('verify-email returns the BAD_INPUT verdict without polling or throwing', async () => {
168+
const fetchMock = vi.fn()
169+
vi.stubGlobal('fetch', fetchMock)
170+
171+
const result = await icypeasVerifyEmailTool.postProcess!(
172+
{
173+
success: true as const,
174+
output: {
175+
searchId: null,
176+
status: 'BAD_INPUT',
177+
email: 'support@stripe.com',
178+
valid: false,
179+
item: {},
180+
},
181+
} as any,
182+
{ apiKey: 'test-key', email: 'support@stripe.com' } as any,
183+
vi.fn()
184+
)
185+
186+
expect(fetchMock).not.toHaveBeenCalled()
187+
expect(result.success).toBe(true)
188+
expect((result.output as any).status).toBe('BAD_INPUT')
189+
})
190+
191+
it('find-email returns the BAD_INPUT verdict without polling or throwing', async () => {
192+
const fetchMock = vi.fn()
193+
vi.stubGlobal('fetch', fetchMock)
194+
195+
const result = await icypeasFindEmailTool.postProcess!(
196+
{
197+
success: true as const,
198+
output: {
199+
searchId: null,
200+
status: 'BAD_INPUT',
201+
email: null,
202+
firstname: null,
203+
lastname: null,
204+
item: {},
205+
},
206+
} as any,
207+
{ apiKey: 'test-key', domainOrCompany: 'stripe.com' } as any,
208+
vi.fn()
209+
)
210+
211+
expect(fetchMock).not.toHaveBeenCalled()
212+
expect(result.success).toBe(true)
213+
expect((result.output as any).status).toBe('BAD_INPUT')
214+
})
150215
})
151216

152217
describe('Icypeas find-email postProcess poll', () => {

apps/sim/tools/icypeas/find_email.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,10 @@ export const icypeasFindEmailTool: ToolConfig<IcypeasFindEmailParams, IcypeasFin
116116
// it rejects the input up front (missing/invalid name or domain). There is no item
117117
// to poll — this is a BAD_INPUT verdict, not a transport error. Surface it as a
118118
// successful run with a null email so the enrichment cascade records the verdict
119-
// instead of inflating the runner's error count.
120-
if (json.success === false || Array.isArray(json.validationErrors)) {
119+
// instead of inflating the runner's error count. Key on a non-empty validationErrors
120+
// array specifically: a bare { success: false } is an unexpected failure shape that
121+
// should fall through and throw, not be masked as BAD_INPUT.
122+
if (Array.isArray(json.validationErrors) && json.validationErrors.length > 0) {
121123
return {
122124
success: true,
123125
output: {
@@ -145,16 +147,17 @@ export const icypeasFindEmailTool: ToolConfig<IcypeasFindEmailParams, IcypeasFin
145147
postProcess: async (result, params) => {
146148
if (!result.success) return result
147149

150+
// If already terminal, return immediately — a BAD_INPUT verdict has no searchId
151+
// to poll, so this must run before the searchId guard.
152+
if (result.output.status && TERMINAL_STATUSES.has(result.output.status)) {
153+
return result
154+
}
155+
148156
const searchId = result.output.searchId
149157
if (!searchId) {
150158
throw new Error('Icypeas find-email result is missing a searchId')
151159
}
152160

153-
// If already terminal (unlikely on submit but defensive), return immediately.
154-
if (result.output.status && TERMINAL_STATUSES.has(result.output.status)) {
155-
return result
156-
}
157-
158161
let elapsed = 0
159162
while (elapsed < MAX_POLL_TIME_MS) {
160163
await sleep(POLL_INTERVAL_MS)

apps/sim/tools/icypeas/verify_email.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,10 @@ export const icypeasVerifyEmailTool: ToolConfig<
109109
// it rejects the input up front (bad format, role-based address, etc.). There is
110110
// no item to poll — this is a BAD_INPUT verdict, not a transport error. Surface it
111111
// as a successful run with valid=false so the enrichment cascade records the
112-
// verdict instead of inflating the runner's error count.
113-
if (json.success === false || Array.isArray(json.validationErrors)) {
112+
// verdict instead of inflating the runner's error count. Key on a non-empty
113+
// validationErrors array specifically: a bare { success: false } is an unexpected
114+
// failure shape that should fall through and throw, not be masked as BAD_INPUT.
115+
if (Array.isArray(json.validationErrors) && json.validationErrors.length > 0) {
114116
return {
115117
success: true,
116118
output: {
@@ -137,16 +139,17 @@ export const icypeasVerifyEmailTool: ToolConfig<
137139
postProcess: async (result, params) => {
138140
if (!result.success) return result
139141

142+
// If already terminal, return immediately — a BAD_INPUT verdict has no searchId
143+
// to poll, so this must run before the searchId guard.
144+
if (result.output.status && TERMINAL_STATUSES.has(result.output.status)) {
145+
return result
146+
}
147+
140148
const searchId = result.output.searchId
141149
if (!searchId) {
142150
throw new Error('Icypeas verify-email result is missing a searchId')
143151
}
144152

145-
// If already terminal, return immediately.
146-
if (result.output.status && TERMINAL_STATUSES.has(result.output.status)) {
147-
return result
148-
}
149-
150153
let elapsed = 0
151154
while (elapsed < MAX_POLL_TIME_MS) {
152155
await sleep(POLL_INTERVAL_MS)

0 commit comments

Comments
 (0)