From 88c3dd93700fe1727e74cf0bf62b8d9c2a01cb67 Mon Sep 17 00:00:00 2001 From: "@mrubens" <2600+mrubens@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:21:38 +0000 Subject: [PATCH] fix(sdk): notify Slack promptly on task failure --- .../task-runs/__tests__/finish-run.test.ts | 46 +++++++++++++++++++ .../src/server/lib/task-runs/finish-run.ts | 29 ++++++------ 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/packages/sdk/src/server/lib/task-runs/__tests__/finish-run.test.ts b/packages/sdk/src/server/lib/task-runs/__tests__/finish-run.test.ts index b3c0a6e5f..1f8fd0baa 100644 --- a/packages/sdk/src/server/lib/task-runs/__tests__/finish-run.test.ts +++ b/packages/sdk/src/server/lib/task-runs/__tests__/finish-run.test.ts @@ -1256,6 +1256,52 @@ describe('finishRun', () => { }); describe('Slack failure notification', () => { + it('notifies Slack before slower post-settlement work completes', async () => { + const job = makeRun( + { + payloadKind: TaskPayloadKind.SlackAppMention, + payload: { + repo: 'owner/repo', + channel: 'C123', + user: 'U456', + text: 'test', + ts: '111.222', + }, + }, + { slackChannelId: 'C123', slackThreadTs: '111.222' }, + ); + mockFindFirstRun.mockResolvedValue(job); + mockFindFirstTask.mockResolvedValue(job.task); + mockGetSlackStartedMessageTs.mockResolvedValue('111.333'); + mockFindFirstSlackInstallation.mockResolvedValue({ + id: 'slack-inst-1', + botAccessToken: 'xoxb-test', + isActive: true, + }); + + let releasePostSettlementWork: (() => void) | undefined; + mockNotifySourceRunOnSettle.mockImplementationOnce( + () => + new Promise((resolve) => { + releasePostSettlementWork = resolve; + }), + ); + + const finishing = finishRun({ + id: 1, + status: RunStatus.Failed, + error: 'The operation was aborted due to timeout', + }); + + await vi.waitFor(() => { + expect(mockUpdateMessage).toHaveBeenCalled(); + }); + expect(mockNotifySourceRunOnSettle).toHaveBeenCalled(); + + releasePostSettlementWork?.(); + await finishing; + }); + it('posts a retryable generic thread reply when a non-setup Slack job fails', async () => { const job = makeRun( { diff --git a/packages/sdk/src/server/lib/task-runs/finish-run.ts b/packages/sdk/src/server/lib/task-runs/finish-run.ts index 2fc03047b..e66f2e39c 100644 --- a/packages/sdk/src/server/lib/task-runs/finish-run.ts +++ b/packages/sdk/src/server/lib/task-runs/finish-run.ts @@ -243,6 +243,20 @@ export const finishRun = async ({ }); }); + // Start notifying the originating Slack thread as soon as the terminal state + // is durable. Run it alongside the parent-task callback so neither remote + // notification delays the other. + const slackFailureNotification = + status === RunStatus.Failed && task.slackThreadTs + ? sendSlackFailureNotification(run, channelProviderError).catch((err) => { + console.error( + `[finishRun] Failed to send Slack failure notification for run ${id}: ${ + err instanceof Error ? err.message : String(err) + }`, + ); + }) + : Promise.resolve(); + // Deterministic spawned-task feedback: when this run was launched by // another task's run with notify-on-settle requested, deliver the outcome // into that launching run's session (waking it if idle) so the parent @@ -253,6 +267,7 @@ export const finishRun = async ({ status, run.task.title, ); + await slackFailureNotification; // Anonymous analytics (no-op unless enabled): terminal task outcome with // non-identifying routing facts only. @@ -321,20 +336,6 @@ export const finishRun = async ({ await cleanupGithubPrReviewArtifacts(run, status); } - // Slack failure notification: post a thread reply when the run failed and - // was triggered from Slack (the task carries a Slack thread binding). - if (status === RunStatus.Failed && task.slackThreadTs) { - try { - await sendSlackFailureNotification(run, channelProviderError); - } catch (err) { - console.error( - `[finishRun] Failed to send Slack failure notification for run ${id}: ${ - err instanceof Error ? err.message : String(err) - }`, - ); - } - } - // Teams failure notification: post a thread reply when the run failed and // was triggered from Teams (payload carries Teams communication metadata). if (