Skip to content

Commit 10eb774

Browse files
fix(notifications): keep partial recipients when one lookup fails
1 parent 099d7ba commit 10eb774

2 files changed

Lines changed: 44 additions & 12 deletions

File tree

apps/sim/lib/workflows/schedules/disable-notifications.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @vitest-environment node
33
*/
44
import {
5+
dbChainMockFns,
56
queueTableRows,
67
resetDbChainMock,
78
resetUrlsMock,
@@ -194,14 +195,33 @@ describe('notifyScheduleAutoDisabled', () => {
194195
expect(sendEmailSpy).toHaveBeenCalledTimes(2)
195196
})
196197

197-
it('never throws when recipient resolution fails', async () => {
198+
it('still emails the creator when the admin lookup fails', async () => {
198199
queueTableRows(schemaMock.workflowSchedule, [WORKFLOW_SCHEDULE_ROW])
199200
queueTableRows(schemaMock.user, [CREATOR])
200201
getUsersWithPermissionsMock.mockRejectedValue(new Error('db down'))
201202

202203
await expect(
203204
notifyScheduleAutoDisabled({ scheduleId: 's-1', reason: 'consecutive_failures' })
204205
).resolves.toBeUndefined()
205-
expect(sendEmailSpy).not.toHaveBeenCalled()
206+
207+
expect(sendEmailSpy).toHaveBeenCalledTimes(1)
208+
expect(sendEmailSpy).toHaveBeenCalledWith(
209+
expect.objectContaining({ to: 'creator@example.com' })
210+
)
211+
})
212+
213+
it('still emails the admins when the creator lookup fails', async () => {
214+
// First .limit() is the schedule row, second is the creator lookup.
215+
dbChainMockFns.limit
216+
.mockResolvedValueOnce([WORKFLOW_SCHEDULE_ROW])
217+
.mockRejectedValueOnce(new Error('db down'))
218+
getUsersWithPermissionsMock.mockResolvedValue([admin('admin-a@example.com')])
219+
220+
await notifyScheduleAutoDisabled({ scheduleId: 's-1', reason: 'consecutive_failures' })
221+
222+
expect(sendEmailSpy).toHaveBeenCalledTimes(1)
223+
expect(sendEmailSpy).toHaveBeenCalledWith(
224+
expect.objectContaining({ to: 'admin-a@example.com' })
225+
)
206226
})
207227
})

apps/sim/lib/workflows/schedules/disable-notifications.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ export async function notifyScheduleAutoDisabled(params: {
117117
/**
118118
* Creator first, then workspace admins. Deduped on lowercased email so someone
119119
* who is both only receives one message.
120+
*
121+
* The two lookups are independent and each failure is contained: losing one
122+
* must not zero out the other, or a blip in either turns an auto-disable back
123+
* into the silent event this notification exists to prevent.
120124
*/
121125
async function resolveRecipients(
122126
ownerUserId: string | null,
@@ -134,19 +138,27 @@ async function resolveRecipients(
134138
}
135139

136140
if (ownerUserId) {
137-
const ownerRows = await db
138-
.select({ email: user.email, name: user.name })
139-
.from(user)
140-
.where(eq(user.id, ownerUserId))
141-
.limit(1)
142-
add(ownerRows[0]?.email ?? null, ownerRows[0]?.name ?? null)
141+
try {
142+
const ownerRows = await db
143+
.select({ email: user.email, name: user.name })
144+
.from(user)
145+
.where(eq(user.id, ownerUserId))
146+
.limit(1)
147+
add(ownerRows[0]?.email ?? null, ownerRows[0]?.name ?? null)
148+
} catch (error) {
149+
logger.error('Failed to resolve schedule creator for disable email', error, { ownerUserId })
150+
}
143151
}
144152

145153
if (workspaceId) {
146-
const members = await getUsersWithPermissions(workspaceId)
147-
for (const member of members) {
148-
if (member.permissionType !== 'admin') continue
149-
add(member.email, member.name)
154+
try {
155+
const members = await getUsersWithPermissions(workspaceId)
156+
for (const member of members) {
157+
if (member.permissionType !== 'admin') continue
158+
add(member.email, member.name)
159+
}
160+
} catch (error) {
161+
logger.error('Failed to resolve workspace admins for disable email', error, { workspaceId })
150162
}
151163
}
152164

0 commit comments

Comments
 (0)