Skip to content

Commit 09ec7ca

Browse files
committed
fix(search): handle replies missing during Gmail verification
1 parent a1e7e8e commit 09ec7ca

2 files changed

Lines changed: 77 additions & 75 deletions

File tree

‎apps/sim/lib/sim-search/live/google.ts‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -435,17 +435,17 @@ export async function readGmail(
435435
for (const candidate of latest) {
436436
options.signal.throwIfAborted()
437437
const candidateId = string(candidate.id)
438-
if (
439-
!(await options.verify({
440-
id: candidateId,
441-
accessMetadata: { id: candidateId, labelIds: candidate.labelIds },
442-
}))
443-
) {
444-
warnings.add('messages outside the source search scope were omitted')
445-
continue
446-
}
447438
let row: Record<string, unknown>
448439
try {
440+
if (
441+
!(await options.verify({
442+
id: candidateId,
443+
accessMetadata: { id: candidateId, labelIds: candidate.labelIds },
444+
}))
445+
) {
446+
warnings.add('messages outside the source search scope were omitted')
447+
continue
448+
}
449449
row = object(
450450
await client.json(`/gmail/v1/users/me/messages/${segment(candidateId)}`, {
451451
query: { format: 'full' },

‎apps/sim/scripts/test-search-google-content-e2e.ts‎

Lines changed: 68 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const mailbox: GmailMessage[] = Array.from({ length: 12 }, (_, index) => ({
7979
body: { data: Buffer.from(`Synthetic evidence ${index}.`).toString('base64url') },
8080
},
8181
}))
82-
const messageFailures = new Map<string, number>()
82+
const messageFailures = new Map<string, { status: number; format: string }>()
8383
const mediaStarted = createDeferred<void>()
8484
const mediaClosed = createDeferred<void>()
8585
const server = http.createServer((request, response) => {
@@ -93,13 +93,12 @@ const server = http.createServer((request, response) => {
9393
const row = mailbox.find((message) => message.id === id)
9494
const isThread = url.pathname.startsWith('/gmail/v1/users/me/threads/')
9595
const isAttachment = url.pathname.includes('/attachments/')
96-
const failure =
97-
!isThread && url.searchParams.get('format') === 'full' ? messageFailures.get(id) : undefined
98-
if (failure) {
99-
requests.push({ id, resource: 'message', status: failure })
96+
const failure = !isThread ? messageFailures.get(id) : undefined
97+
if (failure && url.searchParams.get('format') === failure.format) {
98+
requests.push({ id, resource: 'message', status: failure.status })
10099
response
101-
.writeHead(failure, { 'Content-Type': 'application/json' })
102-
.end(JSON.stringify({ error: { code: failure } }))
100+
.writeHead(failure.status, { 'Content-Type': 'application/json' })
101+
.end(JSON.stringify({ error: { code: failure.status } }))
103102
return
104103
}
105104
const threadMessages = isThread ? mailbox.filter((message) => message.threadId === id) : []
@@ -303,68 +302,71 @@ try {
303302
{ name: 'rate limit', failedId: 'reply', status: 429, expectedStatus: 'rate_limited' },
304303
{ name: 'server failure', failedId: 'reply', status: 500, expectedStatus: 'unavailable' },
305304
]) {
306-
await check(
307-
`Gmail conversation handles ${name} without hiding unrelated failures`,
308-
async () => {
309-
const before = mailbox.length
310-
mailbox.push(
311-
...['anchor', 'retained', 'reply'].map((id, index) => ({
312-
id,
313-
threadId: 'failure-conversation',
314-
labelIds: ['INBOX'],
315-
internalDate: String(Date.UTC(2026, 0, 1, 12, index)),
316-
payload: {
317-
mimeType: 'text/plain',
318-
body: { data: Buffer.from(`Synthetic ${id} evidence.`).toString('base64url') },
319-
},
320-
}))
321-
)
322-
if (failedId) messageFailures.set(failedId, status)
323-
try {
324-
const signal = AbortSignal.timeout(5000)
325-
const api = createNativeClient({ origin, accessToken: TOKEN, signal })
326-
const policy = {
327-
...defaultLiveSearchPolicy('gmail'),
328-
mode: 'selected' as const,
329-
included: ['INBOX'],
330-
}
331-
const verify = createPolicyVerifier('gmail', policy, api, origin)
332-
assert.ok(await verify({ id: 'anchor' }))
333-
const reading = readGmail(api, 'anchor', {
334-
policy,
335-
signal,
336-
verify: (reference) => verify(reference, reference.accessMetadata),
337-
})
338-
if (expectedStatus) {
339-
await assert.rejects(
340-
reading,
341-
(error: unknown) =>
342-
error instanceof NativeSearchError && error.status === expectedStatus
305+
for (const format of failedId === 'reply' ? ['metadata', 'full'] : ['full']) {
306+
await check(
307+
`Gmail conversation handles ${name} during ${format} reads without hiding unrelated failures`,
308+
async () => {
309+
const before = mailbox.length
310+
mailbox.push(
311+
...['anchor', 'retained', 'reply'].map((id, index) => ({
312+
id,
313+
threadId: 'failure-conversation',
314+
labelIds: ['INBOX'],
315+
internalDate: String(Date.UTC(2026, 0, 1, 12, index)),
316+
payload: {
317+
mimeType: 'text/plain',
318+
body: { data: Buffer.from(`Synthetic ${id} evidence.`).toString('base64url') },
319+
},
320+
}))
321+
)
322+
if (failedId) messageFailures.set(failedId, { status, format })
323+
try {
324+
const signal = AbortSignal.timeout(5000)
325+
const api = createNativeClient({ origin, accessToken: TOKEN, signal })
326+
const policy = {
327+
...defaultLiveSearchPolicy('gmail'),
328+
mode: 'selected' as const,
329+
included: ['INBOX'],
330+
}
331+
const verify = createPolicyVerifier('gmail', policy, api, origin)
332+
assert.ok(await verify({ id: 'anchor' }))
333+
const reading = readGmail(api, 'anchor', {
334+
policy,
335+
signal,
336+
verify: (reference) =>
337+
verify(reference, format === 'full' ? reference.accessMetadata : undefined),
338+
})
339+
if (expectedStatus) {
340+
await assert.rejects(
341+
reading,
342+
(error: unknown) =>
343+
error instanceof NativeSearchError && error.status === expectedStatus
344+
)
345+
return
346+
}
347+
const document = await reading
348+
assert.equal(document.id, 'anchor')
349+
assert.ok(document.content.includes('Synthetic anchor evidence.'))
350+
assert.ok(document.content.includes('Synthetic retained evidence.'))
351+
assert.equal(document.content.includes('Synthetic reply evidence.'), status === 200)
352+
assert.equal(document.content.includes('Coverage incomplete:'), status === 404)
353+
assert.deepEqual(
354+
document.accessDependencies,
355+
status === 404 ? [{ id: 'retained' }] : [{ id: 'retained' }, { id: 'reply' }]
343356
)
344-
return
357+
const current = createPolicyVerifier('gmail', policy, api, origin, undefined, {
358+
fresh: true,
359+
})
360+
assert.ok(await current({ id: document.id }))
361+
for (const dependency of document.accessDependencies ?? [])
362+
assert.ok(await current(dependency))
363+
} finally {
364+
mailbox.splice(before)
365+
messageFailures.clear()
345366
}
346-
const document = await reading
347-
assert.equal(document.id, 'anchor')
348-
assert.ok(document.content.includes('Synthetic anchor evidence.'))
349-
assert.ok(document.content.includes('Synthetic retained evidence.'))
350-
assert.equal(document.content.includes('Synthetic reply evidence.'), status === 200)
351-
assert.equal(document.content.includes('Coverage incomplete:'), status === 404)
352-
assert.deepEqual(
353-
document.accessDependencies,
354-
status === 404 ? [{ id: 'retained' }] : [{ id: 'retained' }, { id: 'reply' }]
355-
)
356-
const current = createPolicyVerifier('gmail', policy, api, origin, undefined, {
357-
fresh: true,
358-
})
359-
assert.ok(await current({ id: document.id }))
360-
for (const dependency of document.accessDependencies ?? [])
361-
assert.ok(await current(dependency))
362-
} finally {
363-
mailbox.splice(before)
364-
messageFailures.clear()
365367
}
366-
}
367-
)
368+
)
369+
}
368370
}
369371
await check(
370372
'Gmail conversation and fresh selected-label checks fit one native request budget',

0 commit comments

Comments
 (0)