fix(slack): override wrong channel_type for D-prefix DM channels

This commit is contained in:
Marcus Castro
2026-02-24 10:51:37 -03:00
committed by Peter Steinberger
parent 8cc841766c
commit f33d0a884e
3 changed files with 183 additions and 1 deletions

View File

@@ -38,15 +38,20 @@ export function normalizeSlackChannelType(
channelId?: string | null,
): SlackMessageEvent["channel_type"] {
const normalized = channelType?.trim().toLowerCase();
const inferred = inferSlackChannelType(channelId);
if (
normalized === "im" ||
normalized === "mpim" ||
normalized === "channel" ||
normalized === "group"
) {
// D-prefix channel IDs are always DMs — override a contradicting channel_type.
if (inferred === "im" && normalized !== "im") {
return "im";
}
return normalized;
}
return inferSlackChannelType(channelId) ?? "channel";
return inferred ?? "channel";
}
export type SlackMonitorContext = {

View File

@@ -264,6 +264,164 @@ describe("slack prepareSlackMessage inbound contract", () => {
expect(untrusted).toContain("Do dangerous things");
});
it("classifies D-prefix DMs correctly even when channel_type is wrong", async () => {
const slackCtx = createSlackMonitorContext({
cfg: {
channels: { slack: { enabled: true } },
session: { dmScope: "main" },
} as OpenClawConfig,
accountId: "default",
botToken: "token",
app: { client: {} } as App,
runtime: {} as RuntimeEnv,
botUserId: "B1",
teamId: "T1",
apiAppId: "A1",
historyLimit: 0,
sessionScope: "per-sender",
mainKey: "main",
dmEnabled: true,
dmPolicy: "open",
allowFrom: [],
groupDmEnabled: true,
groupDmChannels: [],
defaultRequireMention: true,
groupPolicy: "open",
useAccessGroups: false,
reactionMode: "off",
reactionAllowlist: [],
replyToMode: "off",
threadHistoryScope: "thread",
threadInheritParent: false,
slashCommand: {
enabled: false,
name: "openclaw",
sessionPrefix: "slack:slash",
ephemeral: true,
},
textLimit: 4000,
ackReactionScope: "group-mentions",
mediaMaxBytes: 1024,
removeAckAfterReply: false,
});
// oxlint-disable-next-line typescript/no-explicit-any
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
// Simulate API returning correct type for DM channel
slackCtx.resolveChannelName = async () => ({ name: undefined, type: "im" as const });
const account: ResolvedSlackAccount = {
accountId: "default",
enabled: true,
botTokenSource: "config",
appTokenSource: "config",
config: {},
};
// Bug scenario: D-prefix channel but Slack event says channel_type: "channel"
const message: SlackMessageEvent = {
channel: "D0ACP6B1T8V",
channel_type: "channel",
user: "U1",
text: "hello from DM",
ts: "1.000",
} as SlackMessageEvent;
const prepared = await prepareSlackMessage({
ctx: slackCtx,
account,
message,
opts: { source: "message" },
});
expect(prepared).toBeTruthy();
// oxlint-disable-next-line typescript/no-explicit-any
expectInboundContextContract(prepared!.ctxPayload as any);
// Should be classified as DM, not channel
expect(prepared!.isDirectMessage).toBe(true);
// DM with dmScope: "main" should route to the main session
expect(prepared!.route.sessionKey).toBe("agent:main:main");
// ChatType should be "direct", not "channel"
expect(prepared!.ctxPayload.ChatType).toBe("direct");
// From should use user ID (DM pattern), not channel ID
expect(prepared!.ctxPayload.From).toContain("slack:U1");
});
it("classifies D-prefix DMs when channel_type is missing", async () => {
const slackCtx = createSlackMonitorContext({
cfg: {
channels: { slack: { enabled: true } },
session: { dmScope: "main" },
} as OpenClawConfig,
accountId: "default",
botToken: "token",
app: { client: {} } as App,
runtime: {} as RuntimeEnv,
botUserId: "B1",
teamId: "T1",
apiAppId: "A1",
historyLimit: 0,
sessionScope: "per-sender",
mainKey: "main",
dmEnabled: true,
dmPolicy: "open",
allowFrom: [],
groupDmEnabled: true,
groupDmChannels: [],
defaultRequireMention: true,
groupPolicy: "open",
useAccessGroups: false,
reactionMode: "off",
reactionAllowlist: [],
replyToMode: "off",
threadHistoryScope: "thread",
threadInheritParent: false,
slashCommand: {
enabled: false,
name: "openclaw",
sessionPrefix: "slack:slash",
ephemeral: true,
},
textLimit: 4000,
ackReactionScope: "group-mentions",
mediaMaxBytes: 1024,
removeAckAfterReply: false,
});
// oxlint-disable-next-line typescript/no-explicit-any
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
// Simulate API returning correct type for DM channel
slackCtx.resolveChannelName = async () => ({ name: undefined, type: "im" as const });
const account: ResolvedSlackAccount = {
accountId: "default",
enabled: true,
botTokenSource: "config",
appTokenSource: "config",
config: {},
};
// channel_type missing — should infer from D-prefix
const message: SlackMessageEvent = {
channel: "D0ACP6B1T8V",
user: "U1",
text: "hello from DM",
ts: "1.000",
} as SlackMessageEvent;
const prepared = await prepareSlackMessage({
ctx: slackCtx,
account,
message,
opts: { source: "message" },
});
expect(prepared).toBeTruthy();
// oxlint-disable-next-line typescript/no-explicit-any
expectInboundContextContract(prepared!.ctxPayload as any);
expect(prepared!.isDirectMessage).toBe(true);
expect(prepared!.route.sessionKey).toBe("agent:main:main");
expect(prepared!.ctxPayload.ChatType).toBe("direct");
});
it("sets MessageThreadId for top-level messages when replyToMode=all", async () => {
const slackCtx = createInboundSlackCtx({
cfg: {

View File

@@ -134,6 +134,25 @@ describe("normalizeSlackChannelType", () => {
it("prefers explicit channel_type values", () => {
expect(normalizeSlackChannelType("mpim", "C123")).toBe("mpim");
});
it("overrides wrong channel_type for D-prefix DM channels", () => {
// Slack DM channel IDs always start with "D" — if the event
// reports a wrong channel_type, the D-prefix should win.
expect(normalizeSlackChannelType("channel", "D123")).toBe("im");
expect(normalizeSlackChannelType("group", "D456")).toBe("im");
expect(normalizeSlackChannelType("mpim", "D789")).toBe("im");
});
it("preserves correct channel_type for D-prefix DM channels", () => {
expect(normalizeSlackChannelType("im", "D123")).toBe("im");
});
it("does not override G-prefix channel_type (ambiguous prefix)", () => {
// G-prefix can be either "group" (private channel) or "mpim" (group DM)
// — trust the provided channel_type since the prefix is ambiguous.
expect(normalizeSlackChannelType("group", "G123")).toBe("group");
expect(normalizeSlackChannelType("mpim", "G456")).toBe("mpim");
});
});
describe("resolveSlackSystemEventSessionKey", () => {