From f3519d895c8dc0fc75075210af72ac756125cc87 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 15 Jan 2026 08:00:07 +0000 Subject: [PATCH] fix: normalize slack channel types for sessions --- src/slack/monitor/context.test.ts | 62 ++++++++++++++++++++ src/slack/monitor/context.ts | 36 +++++++++--- src/slack/monitor/message-handler/prepare.ts | 4 +- 3 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 src/slack/monitor/context.test.ts diff --git a/src/slack/monitor/context.test.ts b/src/slack/monitor/context.test.ts new file mode 100644 index 000000000..c52c5e919 --- /dev/null +++ b/src/slack/monitor/context.test.ts @@ -0,0 +1,62 @@ +import type { App } from "@slack/bolt"; +import { describe, expect, it } from "vitest"; + +import type { ClawdbotConfig } from "../../config/config.js"; +import type { RuntimeEnv } from "../../runtime.js"; +import { createSlackMonitorContext, normalizeSlackChannelType } from "./context.js"; + +const baseParams = () => ({ + cfg: {} as ClawdbotConfig, + accountId: "default", + botToken: "token", + app: { client: {} } as App, + runtime: {} as RuntimeEnv, + botUserId: "B1", + teamId: "T1", + apiAppId: "A1", + historyLimit: 0, + sessionScope: "per-sender" as const, + mainKey: "main", + dmEnabled: true, + dmPolicy: "open" as const, + allowFrom: [], + groupDmEnabled: true, + groupDmChannels: [], + defaultRequireMention: true, + groupPolicy: "open" as const, + useAccessGroups: false, + reactionMode: "off" as const, + reactionAllowlist: [], + replyToMode: "off" as const, + slashCommand: { + enabled: false, + name: "clawd", + sessionPrefix: "slack:slash", + ephemeral: true, + }, + textLimit: 4000, + ackReactionScope: "group-mentions", + mediaMaxBytes: 1, + removeAckAfterReply: false, +}); + +describe("normalizeSlackChannelType", () => { + it("infers channel types from ids when missing", () => { + expect(normalizeSlackChannelType(undefined, "C123")).toBe("channel"); + expect(normalizeSlackChannelType(undefined, "D123")).toBe("im"); + expect(normalizeSlackChannelType(undefined, "G123")).toBe("group"); + }); + + it("prefers explicit channel_type values", () => { + expect(normalizeSlackChannelType("mpim", "C123")).toBe("mpim"); + }); +}); + +describe("resolveSlackSystemEventSessionKey", () => { + it("defaults missing channel_type to channel sessions", () => { + const ctx = createSlackMonitorContext(baseParams()); + expect(ctx.resolveSlackSystemEventSessionKey({ channelId: "C123" })).toBe( + "agent:main:slack:channel:C123", + ); + }); +}); diff --git a/src/slack/monitor/context.ts b/src/slack/monitor/context.ts index 19ce39fc0..452ef3389 100644 --- a/src/slack/monitor/context.ts +++ b/src/slack/monitor/context.ts @@ -13,6 +13,28 @@ import { normalizeAllowList, normalizeAllowListLower, normalizeSlackSlug } from import { resolveSlackChannelConfig } from "./channel-config.js"; import { isSlackRoomAllowedByPolicy } from "./policy.js"; +export function inferSlackChannelType( + channelId?: string | null, +): SlackMessageEvent["channel_type"] | undefined { + const trimmed = channelId?.trim(); + if (!trimmed) return undefined; + if (trimmed.startsWith("D")) return "im"; + if (trimmed.startsWith("C")) return "channel"; + if (trimmed.startsWith("G")) return "group"; + return undefined; +} + +export function normalizeSlackChannelType( + channelType?: string | null, + channelId?: string | null, +): SlackMessageEvent["channel_type"] { + const normalized = channelType?.trim().toLowerCase(); + if (normalized === "im" || normalized === "mpim" || normalized === "channel" || normalized === "group") { + return normalized; + } + return inferSlackChannelType(channelId) ?? "channel"; +} + export type SlackMonitorContext = { cfg: ClawdbotConfig; accountId: string; @@ -147,15 +169,15 @@ export function createSlackMonitorContext(params: { }) => { const channelId = p.channelId?.trim() ?? ""; if (!channelId) return params.mainKey; - const channelType = p.channelType?.trim().toLowerCase() ?? ""; - const isRoom = channelType === "channel" || channelType === "group"; + const channelType = normalizeSlackChannelType(p.channelType, channelId); + const isDirectMessage = channelType === "im"; const isGroup = channelType === "mpim"; - const from = isRoom - ? `slack:channel:${channelId}` + const from = isDirectMessage + ? `slack:${channelId}` : isGroup ? `slack:group:${channelId}` - : `slack:${channelId}`; - const chatType = isRoom ? "room" : isGroup ? "group" : "direct"; + : `slack:channel:${channelId}`; + const chatType = isDirectMessage ? "direct" : isGroup ? "group" : "room"; return resolveSessionKey( params.sessionScope, { From: from, ChatType: chatType, Provider: "slack" }, @@ -249,7 +271,7 @@ export function createSlackMonitorContext(params: { channelName?: string; channelType?: SlackMessageEvent["channel_type"]; }) => { - const channelType = p.channelType; + const channelType = normalizeSlackChannelType(p.channelType, p.channelId); const isDirectMessage = channelType === "im"; const isGroupDm = channelType === "mpim"; const isRoom = channelType === "channel" || channelType === "group"; diff --git a/src/slack/monitor/message-handler/prepare.ts b/src/slack/monitor/message-handler/prepare.ts index 224426823..3253851ea 100644 --- a/src/slack/monitor/message-handler/prepare.ts +++ b/src/slack/monitor/message-handler/prepare.ts @@ -19,7 +19,7 @@ import type { SlackMessageEvent } from "../../types.js"; import { allowListMatches, resolveSlackUserAllowed } from "../allow-list.js"; import { isSlackSenderAllowListed, resolveSlackEffectiveAllowFrom } from "../auth.js"; import { resolveSlackChannelConfig } from "../channel-config.js"; -import type { SlackMonitorContext } from "../context.js"; +import { normalizeSlackChannelType, type SlackMonitorContext } from "../context.js"; import { resolveSlackMedia, resolveSlackThreadStarter } from "../media.js"; import type { PreparedSlackMessage } from "./types.js"; @@ -45,7 +45,7 @@ export async function prepareSlackMessage(params: { channelType = channelType ?? channelInfo.type; } const channelName = channelInfo?.name; - const resolvedChannelType = channelType; + const resolvedChannelType = normalizeSlackChannelType(channelType, message.channel); const isDirectMessage = resolvedChannelType === "im"; const isGroupDm = resolvedChannelType === "mpim"; const isRoom = resolvedChannelType === "channel" || resolvedChannelType === "group";