From 474ba45a2f733d7b40cf96a3a6382dc3600c6e07 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 21 Feb 2026 22:02:43 +0000 Subject: [PATCH] refactor(slack): dedupe modal lifecycle interaction handlers --- src/slack/monitor/events/interactions.test.ts | 30 +++++++ src/slack/monitor/events/interactions.ts | 86 ++++++++++--------- 2 files changed, 77 insertions(+), 39 deletions(-) diff --git a/src/slack/monitor/events/interactions.test.ts b/src/slack/monitor/events/interactions.test.ts index 1321c05be06..244a86bb0a6 100644 --- a/src/slack/monitor/events/interactions.test.ts +++ b/src/slack/monitor/events/interactions.test.ts @@ -1115,5 +1115,35 @@ describe("registerSlackInteractionEvents", () => { ); expect(options.sessionKey).toBe("agent:main:slack:channel:C99"); }); + + it("defaults modal close isCleared to false when Slack omits the flag", async () => { + enqueueSystemEventMock.mockReset(); + const { ctx, getViewClosedHandler } = createContext(); + registerSlackInteractionEvents({ ctx: ctx as never }); + const viewClosedHandler = getViewClosedHandler(); + expect(viewClosedHandler).toBeTruthy(); + + const ack = vi.fn().mockResolvedValue(undefined); + await viewClosedHandler!({ + ack, + body: { + user: { id: "U901" }, + view: { + id: "V901", + callback_id: "openclaw:deploy_form", + }, + }, + }); + + expect(ack).toHaveBeenCalled(); + expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1); + const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string]; + const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as { + interactionType: string; + isCleared?: boolean; + }; + expect(payload.interactionType).toBe("view_closed"); + expect(payload.isCleared).toBe(false); + }); }); const selectedDateTimeEpoch = 1_771_632_300; diff --git a/src/slack/monitor/events/interactions.ts b/src/slack/monitor/events/interactions.ts index 06af384be70..094c57a9b09 100644 --- a/src/slack/monitor/events/interactions.ts +++ b/src/slack/monitor/events/interactions.ts @@ -98,6 +98,8 @@ type SlackModalEventBase = { }; }; +type SlackModalInteractionKind = "view_submission" | "view_closed"; + function readOptionValues(options: unknown): string[] | undefined { if (!Array.isArray(options)) { return undefined; @@ -442,6 +444,45 @@ function resolveSlackModalEventBase(params: { }; } +function emitSlackModalLifecycleEvent(params: { + ctx: SlackMonitorContext; + body: SlackModalBody; + interactionType: SlackModalInteractionKind; + contextPrefix: "slack:interaction:view" | "slack:interaction:view-closed"; +}): void { + const { callbackId, userId, viewId, sessionRouting, payload } = resolveSlackModalEventBase({ + ctx: params.ctx, + body: params.body, + }); + const isViewClosed = params.interactionType === "view_closed"; + const isCleared = params.body.is_cleared === true; + const eventPayload = isViewClosed + ? { + interactionType: params.interactionType, + ...payload, + isCleared, + } + : { + interactionType: params.interactionType, + ...payload, + }; + + if (isViewClosed) { + params.ctx.runtime.log?.( + `slack:interaction view_closed callback=${callbackId} user=${userId} cleared=${isCleared}`, + ); + } else { + params.ctx.runtime.log?.( + `slack:interaction view_submission callback=${callbackId} user=${userId} inputs=${payload.inputs.length}`, + ); + } + + enqueueSystemEvent(`Slack interaction: ${JSON.stringify(eventPayload)}`, { + sessionKey: sessionRouting.sessionKey, + contextKey: [params.contextPrefix, callbackId, viewId, userId].filter(Boolean).join(":"), + }); +} + export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContext }) { const { ctx } = params; if (typeof ctx.app.action !== "function") { @@ -611,26 +652,11 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex new RegExp(`^${OPENCLAW_ACTION_PREFIX}`), async ({ ack, body }: { ack: () => Promise; body: unknown }) => { await ack(); - - const modalBody = body as SlackModalBody; - const { callbackId, userId, viewId, sessionRouting, payload } = resolveSlackModalEventBase({ + emitSlackModalLifecycleEvent({ ctx, - body: modalBody, - }); - const eventPayload = { + body: body as SlackModalBody, interactionType: "view_submission", - ...payload, - }; - - ctx.runtime.log?.( - `slack:interaction view_submission callback=${callbackId} user=${userId} inputs=${payload.inputs.length}`, - ); - - enqueueSystemEvent(`Slack interaction: ${JSON.stringify(eventPayload)}`, { - sessionKey: sessionRouting.sessionKey, - contextKey: ["slack:interaction:view", callbackId, viewId, userId] - .filter(Boolean) - .join(":"), + contextPrefix: "slack:interaction:view", }); }, ); @@ -652,29 +678,11 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex new RegExp(`^${OPENCLAW_ACTION_PREFIX}`), async ({ ack, body }: { ack: () => Promise; body: unknown }) => { await ack(); - - const modalBody = body as SlackModalBody; - const { callbackId, userId, viewId, sessionRouting, payload } = resolveSlackModalEventBase({ + emitSlackModalLifecycleEvent({ ctx, - body: modalBody, - }); - const eventPayload = { + body: body as SlackModalBody, interactionType: "view_closed", - ...payload, - isCleared: modalBody.is_cleared === true, - }; - - ctx.runtime.log?.( - `slack:interaction view_closed callback=${callbackId} user=${userId} cleared=${ - modalBody.is_cleared === true - }`, - ); - - enqueueSystemEvent(`Slack interaction: ${JSON.stringify(eventPayload)}`, { - sessionKey: sessionRouting.sessionKey, - contextKey: ["slack:interaction:view-closed", callbackId, viewId, userId] - .filter(Boolean) - .join(":"), + contextPrefix: "slack:interaction:view-closed", }); }, );