test(integration): dedupe messaging, secrets, and plugin test suites

This commit is contained in:
Peter Steinberger
2026-03-02 06:41:31 +00:00
parent d3e0c0b29c
commit 45888276a3
21 changed files with 1840 additions and 2416 deletions

View File

@@ -21,6 +21,45 @@ function createClient() {
};
}
function makeSlackFileInfo(overrides?: Record<string, unknown>) {
return {
id: "F123",
name: "image.png",
mimetype: "image/png",
url_private_download: "https://files.slack.com/files-pri/T1-F123/image.png",
...overrides,
};
}
function makeResolvedSlackMedia() {
return {
path: "/tmp/image.png",
contentType: "image/png",
placeholder: "[Slack file: image.png]",
};
}
function expectNoMediaDownload(result: Awaited<ReturnType<typeof downloadSlackFile>>) {
expect(result).toBeNull();
expect(resolveSlackMedia).not.toHaveBeenCalled();
}
function expectResolveSlackMediaCalledWithDefaults() {
expect(resolveSlackMedia).toHaveBeenCalledWith({
files: [
{
id: "F123",
name: "image.png",
mimetype: "image/png",
url_private: undefined,
url_private_download: "https://files.slack.com/files-pri/T1-F123/image.png",
},
],
token: "xoxb-test",
maxBytes: 1024,
});
}
describe("downloadSlackFile", () => {
beforeEach(() => {
resolveSlackMedia.mockReset();
@@ -48,20 +87,9 @@ describe("downloadSlackFile", () => {
it("downloads via resolveSlackMedia using fresh files.info metadata", async () => {
const client = createClient();
client.files.info.mockResolvedValueOnce({
file: {
id: "F123",
name: "image.png",
mimetype: "image/png",
url_private_download: "https://files.slack.com/files-pri/T1-F123/image.png",
},
file: makeSlackFileInfo(),
});
resolveSlackMedia.mockResolvedValueOnce([
{
path: "/tmp/image.png",
contentType: "image/png",
placeholder: "[Slack file: image.png]",
},
]);
resolveSlackMedia.mockResolvedValueOnce([makeResolvedSlackMedia()]);
const result = await downloadSlackFile("F123", {
client,
@@ -70,36 +98,14 @@ describe("downloadSlackFile", () => {
});
expect(client.files.info).toHaveBeenCalledWith({ file: "F123" });
expect(resolveSlackMedia).toHaveBeenCalledWith({
files: [
{
id: "F123",
name: "image.png",
mimetype: "image/png",
url_private: undefined,
url_private_download: "https://files.slack.com/files-pri/T1-F123/image.png",
},
],
token: "xoxb-test",
maxBytes: 1024,
});
expect(result).toEqual({
path: "/tmp/image.png",
contentType: "image/png",
placeholder: "[Slack file: image.png]",
});
expectResolveSlackMediaCalledWithDefaults();
expect(result).toEqual(makeResolvedSlackMedia());
});
it("returns null when channel scope definitely mismatches file shares", async () => {
const client = createClient();
client.files.info.mockResolvedValueOnce({
file: {
id: "F123",
name: "image.png",
mimetype: "image/png",
url_private_download: "https://files.slack.com/files-pri/T1-F123/image.png",
channels: ["C999"],
},
file: makeSlackFileInfo({ channels: ["C999"] }),
});
const result = await downloadSlackFile("F123", {
@@ -109,24 +115,19 @@ describe("downloadSlackFile", () => {
channelId: "C123",
});
expect(result).toBeNull();
expect(resolveSlackMedia).not.toHaveBeenCalled();
expectNoMediaDownload(result);
});
it("returns null when thread scope definitely mismatches file share thread", async () => {
const client = createClient();
client.files.info.mockResolvedValueOnce({
file: {
id: "F123",
name: "image.png",
mimetype: "image/png",
url_private_download: "https://files.slack.com/files-pri/T1-F123/image.png",
file: makeSlackFileInfo({
shares: {
private: {
C123: [{ ts: "111.111", thread_ts: "111.111" }],
},
},
},
}),
});
const result = await downloadSlackFile("F123", {
@@ -137,27 +138,15 @@ describe("downloadSlackFile", () => {
threadId: "222.222",
});
expect(result).toBeNull();
expect(resolveSlackMedia).not.toHaveBeenCalled();
expectNoMediaDownload(result);
});
it("keeps legacy behavior when file metadata does not expose channel/thread shares", async () => {
const client = createClient();
client.files.info.mockResolvedValueOnce({
file: {
id: "F123",
name: "image.png",
mimetype: "image/png",
url_private_download: "https://files.slack.com/files-pri/T1-F123/image.png",
},
file: makeSlackFileInfo(),
});
resolveSlackMedia.mockResolvedValueOnce([
{
path: "/tmp/image.png",
contentType: "image/png",
placeholder: "[Slack file: image.png]",
},
]);
resolveSlackMedia.mockResolvedValueOnce([makeResolvedSlackMedia()]);
const result = await downloadSlackFile("F123", {
client,
@@ -167,11 +156,8 @@ describe("downloadSlackFile", () => {
threadId: "222.222",
});
expect(result).toEqual({
path: "/tmp/image.png",
contentType: "image/png",
placeholder: "[Slack file: image.png]",
});
expect(result).toEqual(makeResolvedSlackMedia());
expect(resolveSlackMedia).toHaveBeenCalledTimes(1);
expectResolveSlackMediaCalledWithDefaults();
});
});

View File

@@ -1,44 +1,35 @@
import { describe, expect, it, vi } from "vitest";
import { registerSlackMemberEvents } from "./members.js";
import {
createSlackSystemEventTestHarness,
type SlackSystemEventTestOverrides,
createSlackSystemEventTestHarness as initSlackHarness,
type SlackSystemEventTestOverrides as MemberOverrides,
} from "./system-event-test-harness.js";
const enqueueSystemEventMock = vi.fn();
const readAllowFromStoreMock = vi.fn();
const memberMocks = vi.hoisted(() => ({
enqueue: vi.fn(),
readAllow: vi.fn(),
}));
vi.mock("../../../infra/system-events.js", () => ({
enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args),
enqueueSystemEvent: memberMocks.enqueue,
}));
vi.mock("../../../pairing/pairing-store.js", () => ({
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
readChannelAllowFromStore: memberMocks.readAllow,
}));
type SlackMemberHandler = (args: {
event: Record<string, unknown>;
body: unknown;
}) => Promise<void>;
type MemberHandler = (args: { event: Record<string, unknown>; body: unknown }) => Promise<void>;
function createMembersContext(params?: {
overrides?: SlackSystemEventTestOverrides;
type MemberCaseArgs = {
event?: Record<string, unknown>;
body?: unknown;
overrides?: MemberOverrides;
handler?: "joined" | "left";
trackEvent?: () => void;
shouldDropMismatchedSlackEvent?: (body: unknown) => boolean;
}) {
const harness = createSlackSystemEventTestHarness(params?.overrides);
if (params?.shouldDropMismatchedSlackEvent) {
harness.ctx.shouldDropMismatchedSlackEvent = params.shouldDropMismatchedSlackEvent;
}
registerSlackMemberEvents({ ctx: harness.ctx, trackEvent: params?.trackEvent });
return {
getJoinedHandler: () =>
harness.getHandler("member_joined_channel") as SlackMemberHandler | null,
getLeftHandler: () => harness.getHandler("member_left_channel") as SlackMemberHandler | null,
};
}
};
function makeMemberEvent(overrides?: { user?: string; channel?: string }) {
function makeMemberEvent(overrides?: { channel?: string; user?: string }) {
return {
type: "member_joined_channel",
user: overrides?.user ?? "U1",
@@ -47,106 +38,90 @@ function makeMemberEvent(overrides?: { user?: string; channel?: string }) {
};
}
function getMemberHandlers(params: {
overrides?: MemberOverrides;
trackEvent?: () => void;
shouldDropMismatchedSlackEvent?: (body: unknown) => boolean;
}) {
const harness = initSlackHarness(params.overrides);
if (params.shouldDropMismatchedSlackEvent) {
harness.ctx.shouldDropMismatchedSlackEvent = params.shouldDropMismatchedSlackEvent;
}
registerSlackMemberEvents({ ctx: harness.ctx, trackEvent: params.trackEvent });
return {
joined: harness.getHandler("member_joined_channel") as MemberHandler | null,
left: harness.getHandler("member_left_channel") as MemberHandler | null,
};
}
async function runMemberCase(args: MemberCaseArgs = {}): Promise<void> {
memberMocks.enqueue.mockClear();
memberMocks.readAllow.mockReset().mockResolvedValue([]);
const handlers = getMemberHandlers({
overrides: args.overrides,
trackEvent: args.trackEvent,
shouldDropMismatchedSlackEvent: args.shouldDropMismatchedSlackEvent,
});
const key = args.handler ?? "joined";
const handler = handlers[key];
expect(handler).toBeTruthy();
await handler!({
event: (args.event ?? makeMemberEvent()) as Record<string, unknown>,
body: args.body ?? {},
});
}
describe("registerSlackMemberEvents", () => {
it("enqueues DM member events when dmPolicy is open", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getJoinedHandler } = createMembersContext({ overrides: { dmPolicy: "open" } });
const joinedHandler = getJoinedHandler();
expect(joinedHandler).toBeTruthy();
await joinedHandler!({
event: makeMemberEvent(),
body: {},
});
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
});
it("blocks DM member events when dmPolicy is disabled", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getJoinedHandler } = createMembersContext({ overrides: { dmPolicy: "disabled" } });
const joinedHandler = getJoinedHandler();
expect(joinedHandler).toBeTruthy();
await joinedHandler!({
event: makeMemberEvent(),
body: {},
});
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
});
it("blocks DM member events for unauthorized senders in allowlist mode", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getJoinedHandler } = createMembersContext({
overrides: { dmPolicy: "allowlist", allowFrom: ["U2"] },
});
const joinedHandler = getJoinedHandler();
expect(joinedHandler).toBeTruthy();
await joinedHandler!({
event: makeMemberEvent({ user: "U1" }),
body: {},
});
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
});
it("allows DM member events for authorized senders in allowlist mode", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getLeftHandler } = createMembersContext({
overrides: { dmPolicy: "allowlist", allowFrom: ["U1"] },
});
const leftHandler = getLeftHandler();
expect(leftHandler).toBeTruthy();
await leftHandler!({
event: {
...makeMemberEvent({ user: "U1" }),
type: "member_left_channel",
it.each([
{
name: "enqueues DM member events when dmPolicy is open",
args: { overrides: { dmPolicy: "open" } },
calls: 1,
},
{
name: "blocks DM member events when dmPolicy is disabled",
args: { overrides: { dmPolicy: "disabled" } },
calls: 0,
},
{
name: "blocks DM member events for unauthorized senders in allowlist mode",
args: {
overrides: { dmPolicy: "allowlist", allowFrom: ["U2"] },
event: makeMemberEvent({ user: "U1" }),
},
body: {},
});
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
});
it("blocks channel member events for users outside channel users allowlist", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getJoinedHandler } = createMembersContext({
overrides: {
dmPolicy: "open",
channelType: "channel",
channelUsers: ["U_OWNER"],
calls: 0,
},
{
name: "allows DM member events for authorized senders in allowlist mode",
args: {
handler: "left" as const,
overrides: { dmPolicy: "allowlist", allowFrom: ["U1"] },
event: { ...makeMemberEvent({ user: "U1" }), type: "member_left_channel" },
},
});
const joinedHandler = getJoinedHandler();
expect(joinedHandler).toBeTruthy();
await joinedHandler!({
event: makeMemberEvent({ channel: "C1", user: "U_ATTACKER" }),
body: {},
});
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
calls: 1,
},
{
name: "blocks channel member events for users outside channel users allowlist",
args: {
overrides: {
dmPolicy: "open",
channelType: "channel",
channelUsers: ["U_OWNER"],
},
event: makeMemberEvent({ channel: "C1", user: "U_ATTACKER" }),
},
calls: 0,
},
])("$name", async ({ args, calls }) => {
await runMemberCase(args);
expect(memberMocks.enqueue).toHaveBeenCalledTimes(calls);
});
it("does not track mismatched events", async () => {
const trackEvent = vi.fn();
const { getJoinedHandler } = createMembersContext({
await runMemberCase({
trackEvent,
shouldDropMismatchedSlackEvent: () => true,
});
const joinedHandler = getJoinedHandler();
expect(joinedHandler).toBeTruthy();
await joinedHandler!({
event: makeMemberEvent(),
body: { api_app_id: "A_OTHER" },
});
@@ -155,14 +130,7 @@ describe("registerSlackMemberEvents", () => {
it("tracks accepted member events", async () => {
const trackEvent = vi.fn();
const { getJoinedHandler } = createMembersContext({ trackEvent });
const joinedHandler = getJoinedHandler();
expect(joinedHandler).toBeTruthy();
await joinedHandler!({
event: makeMemberEvent(),
body: {},
});
await runMemberCase({ trackEvent });
expect(trackEvent).toHaveBeenCalledTimes(1);
});

View File

@@ -5,23 +5,26 @@ import {
type SlackSystemEventTestOverrides,
} from "./system-event-test-harness.js";
const enqueueSystemEventMock = vi.fn();
const readAllowFromStoreMock = vi.fn();
const messageQueueMock = vi.fn();
const messageAllowMock = vi.fn();
vi.mock("../../../infra/system-events.js", () => ({
enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args),
enqueueSystemEvent: (...args: unknown[]) => messageQueueMock(...args),
}));
vi.mock("../../../pairing/pairing-store.js", () => ({
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
readChannelAllowFromStore: (...args: unknown[]) => messageAllowMock(...args),
}));
type SlackMessageHandler = (args: {
event: Record<string, unknown>;
body: unknown;
}) => Promise<void>;
type MessageHandler = (args: { event: Record<string, unknown>; body: unknown }) => Promise<void>;
function createMessagesContext(overrides?: SlackSystemEventTestOverrides) {
type MessageCase = {
overrides?: SlackSystemEventTestOverrides;
event?: Record<string, unknown>;
body?: unknown;
};
function createMessageHandlers(overrides?: SlackSystemEventTestOverrides) {
const harness = createSlackSystemEventTestHarness(overrides);
const handleSlackMessage = vi.fn(async () => {});
registerSlackMessageEvents({
@@ -29,7 +32,7 @@ function createMessagesContext(overrides?: SlackSystemEventTestOverrides) {
handleSlackMessage,
});
return {
getMessageHandler: () => harness.getHandler("message") as SlackMessageHandler | null,
handler: harness.getHandler("message") as MessageHandler | null,
handleSlackMessage,
};
}
@@ -40,14 +43,8 @@ function makeChangedEvent(overrides?: { channel?: string; user?: string }) {
type: "message",
subtype: "message_changed",
channel: overrides?.channel ?? "D1",
message: {
ts: "123.456",
user,
},
previous_message: {
ts: "123.450",
user,
},
message: { ts: "123.456", user },
previous_message: { ts: "123.450", user },
event_ts: "123.456",
};
}
@@ -73,113 +70,78 @@ function makeThreadBroadcastEvent(overrides?: { channel?: string; user?: string
subtype: "thread_broadcast",
channel: overrides?.channel ?? "D1",
user,
message: {
ts: "123.456",
user,
},
message: { ts: "123.456", user },
event_ts: "123.456",
};
}
async function runMessageCase(input: MessageCase = {}): Promise<void> {
messageQueueMock.mockClear();
messageAllowMock.mockReset().mockResolvedValue([]);
const { handler } = createMessageHandlers(input.overrides);
expect(handler).toBeTruthy();
await handler!({
event: (input.event ?? makeChangedEvent()) as Record<string, unknown>,
body: input.body ?? {},
});
}
describe("registerSlackMessageEvents", () => {
it("enqueues message_changed system events when dmPolicy is open", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getMessageHandler } = createMessagesContext({ dmPolicy: "open" });
const messageHandler = getMessageHandler();
expect(messageHandler).toBeTruthy();
await messageHandler!({
event: makeChangedEvent(),
body: {},
});
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
});
it("blocks message_changed system events when dmPolicy is disabled", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getMessageHandler } = createMessagesContext({ dmPolicy: "disabled" });
const messageHandler = getMessageHandler();
expect(messageHandler).toBeTruthy();
await messageHandler!({
event: makeChangedEvent(),
body: {},
});
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
});
it("blocks message_changed system events for unauthorized senders in allowlist mode", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getMessageHandler } = createMessagesContext({
dmPolicy: "allowlist",
allowFrom: ["U2"],
});
const messageHandler = getMessageHandler();
expect(messageHandler).toBeTruthy();
await messageHandler!({
event: makeChangedEvent({ user: "U1" }),
body: {},
});
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
});
it("blocks message_deleted system events for users outside channel users allowlist", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getMessageHandler } = createMessagesContext({
dmPolicy: "open",
channelType: "channel",
channelUsers: ["U_OWNER"],
});
const messageHandler = getMessageHandler();
expect(messageHandler).toBeTruthy();
await messageHandler!({
event: makeDeletedEvent({ channel: "C1", user: "U_ATTACKER" }),
body: {},
});
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
});
it("blocks thread_broadcast system events without an authenticated sender", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getMessageHandler } = createMessagesContext({ dmPolicy: "open" });
const messageHandler = getMessageHandler();
expect(messageHandler).toBeTruthy();
await messageHandler!({
event: {
...makeThreadBroadcastEvent(),
user: undefined,
message: {
ts: "123.456",
it.each([
{
name: "enqueues message_changed system events when dmPolicy is open",
input: { overrides: { dmPolicy: "open" }, event: makeChangedEvent() },
calls: 1,
},
{
name: "blocks message_changed system events when dmPolicy is disabled",
input: { overrides: { dmPolicy: "disabled" }, event: makeChangedEvent() },
calls: 0,
},
{
name: "blocks message_changed system events for unauthorized senders in allowlist mode",
input: {
overrides: { dmPolicy: "allowlist", allowFrom: ["U2"] },
event: makeChangedEvent({ user: "U1" }),
},
calls: 0,
},
{
name: "blocks message_deleted system events for users outside channel users allowlist",
input: {
overrides: {
dmPolicy: "open",
channelType: "channel",
channelUsers: ["U_OWNER"],
},
event: makeDeletedEvent({ channel: "C1", user: "U_ATTACKER" }),
},
calls: 0,
},
{
name: "blocks thread_broadcast system events without an authenticated sender",
input: {
overrides: { dmPolicy: "open" },
event: {
...makeThreadBroadcastEvent(),
user: undefined,
message: { ts: "123.456" },
},
},
body: {},
});
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
calls: 0,
},
])("$name", async ({ input, calls }) => {
await runMessageCase(input);
expect(messageQueueMock).toHaveBeenCalledTimes(calls);
});
it("passes regular message events to the message handler", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getMessageHandler, handleSlackMessage } = createMessagesContext({
dmPolicy: "open",
});
const messageHandler = getMessageHandler();
expect(messageHandler).toBeTruthy();
messageQueueMock.mockClear();
messageAllowMock.mockReset().mockResolvedValue([]);
const { handler, handleSlackMessage } = createMessageHandlers({ dmPolicy: "open" });
expect(handler).toBeTruthy();
await messageHandler!({
await handler!({
event: {
type: "message",
channel: "D1",
@@ -191,6 +153,6 @@ describe("registerSlackMessageEvents", () => {
});
expect(handleSlackMessage).toHaveBeenCalledTimes(1);
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
expect(messageQueueMock).not.toHaveBeenCalled();
});
});

View File

@@ -1,40 +1,32 @@
import { describe, expect, it, vi } from "vitest";
import { registerSlackPinEvents } from "./pins.js";
import {
createSlackSystemEventTestHarness,
type SlackSystemEventTestOverrides,
createSlackSystemEventTestHarness as buildPinHarness,
type SlackSystemEventTestOverrides as PinOverrides,
} from "./system-event-test-harness.js";
const enqueueSystemEventMock = vi.fn();
const readAllowFromStoreMock = vi.fn();
vi.mock("../../../infra/system-events.js", () => ({
enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args),
}));
const pinEnqueueMock = vi.hoisted(() => vi.fn());
const pinAllowMock = vi.hoisted(() => vi.fn());
vi.mock("../../../infra/system-events.js", () => {
return { enqueueSystemEvent: pinEnqueueMock };
});
vi.mock("../../../pairing/pairing-store.js", () => ({
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
readChannelAllowFromStore: pinAllowMock,
}));
type SlackPinHandler = (args: { event: Record<string, unknown>; body: unknown }) => Promise<void>;
type PinHandler = (args: { event: Record<string, unknown>; body: unknown }) => Promise<void>;
function createPinContext(params?: {
overrides?: SlackSystemEventTestOverrides;
type PinCase = {
body?: unknown;
event?: Record<string, unknown>;
handler?: "added" | "removed";
overrides?: PinOverrides;
trackEvent?: () => void;
shouldDropMismatchedSlackEvent?: (body: unknown) => boolean;
}) {
const harness = createSlackSystemEventTestHarness(params?.overrides);
if (params?.shouldDropMismatchedSlackEvent) {
harness.ctx.shouldDropMismatchedSlackEvent = params.shouldDropMismatchedSlackEvent;
}
registerSlackPinEvents({ ctx: harness.ctx, trackEvent: params?.trackEvent });
return {
getAddedHandler: () => harness.getHandler("pin_added") as SlackPinHandler | null,
getRemovedHandler: () => harness.getHandler("pin_removed") as SlackPinHandler | null,
};
}
};
function makePinEvent(overrides?: { user?: string; channel?: string }) {
function makePinEvent(overrides?: { channel?: string; user?: string }) {
return {
type: "pin_added",
user: overrides?.user ?? "U1",
@@ -42,110 +34,92 @@ function makePinEvent(overrides?: { user?: string; channel?: string }) {
event_ts: "123.456",
item: {
type: "message",
message: {
ts: "123.456",
},
message: { ts: "123.456" },
},
};
}
function installPinHandlers(args: {
overrides?: PinOverrides;
trackEvent?: () => void;
shouldDropMismatchedSlackEvent?: (body: unknown) => boolean;
}) {
const harness = buildPinHarness(args.overrides);
if (args.shouldDropMismatchedSlackEvent) {
harness.ctx.shouldDropMismatchedSlackEvent = args.shouldDropMismatchedSlackEvent;
}
registerSlackPinEvents({ ctx: harness.ctx, trackEvent: args.trackEvent });
return {
added: harness.getHandler("pin_added") as PinHandler | null,
removed: harness.getHandler("pin_removed") as PinHandler | null,
};
}
async function runPinCase(input: PinCase = {}): Promise<void> {
pinEnqueueMock.mockClear();
pinAllowMock.mockReset().mockResolvedValue([]);
const { added, removed } = installPinHandlers({
overrides: input.overrides,
trackEvent: input.trackEvent,
shouldDropMismatchedSlackEvent: input.shouldDropMismatchedSlackEvent,
});
const handlerKey = input.handler ?? "added";
const handler = handlerKey === "removed" ? removed : added;
expect(handler).toBeTruthy();
const event = (input.event ?? makePinEvent()) as Record<string, unknown>;
const body = input.body ?? {};
await handler!({
body,
event,
});
}
describe("registerSlackPinEvents", () => {
it("enqueues DM pin system events when dmPolicy is open", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getAddedHandler } = createPinContext({ overrides: { dmPolicy: "open" } });
const addedHandler = getAddedHandler();
expect(addedHandler).toBeTruthy();
await addedHandler!({
event: makePinEvent(),
body: {},
});
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
});
it("blocks DM pin system events when dmPolicy is disabled", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getAddedHandler } = createPinContext({ overrides: { dmPolicy: "disabled" } });
const addedHandler = getAddedHandler();
expect(addedHandler).toBeTruthy();
await addedHandler!({
event: makePinEvent(),
body: {},
});
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
});
it("blocks DM pin system events for unauthorized senders in allowlist mode", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getAddedHandler } = createPinContext({
overrides: { dmPolicy: "allowlist", allowFrom: ["U2"] },
});
const addedHandler = getAddedHandler();
expect(addedHandler).toBeTruthy();
await addedHandler!({
event: makePinEvent({ user: "U1" }),
body: {},
});
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
});
it("allows DM pin system events for authorized senders in allowlist mode", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getAddedHandler } = createPinContext({
overrides: { dmPolicy: "allowlist", allowFrom: ["U1"] },
});
const addedHandler = getAddedHandler();
expect(addedHandler).toBeTruthy();
await addedHandler!({
event: makePinEvent({ user: "U1" }),
body: {},
});
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
});
it("blocks channel pin events for users outside channel users allowlist", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getAddedHandler } = createPinContext({
overrides: {
dmPolicy: "open",
channelType: "channel",
channelUsers: ["U_OWNER"],
it.each([
["enqueues DM pin system events when dmPolicy is open", { overrides: { dmPolicy: "open" } }, 1],
[
"blocks DM pin system events when dmPolicy is disabled",
{ overrides: { dmPolicy: "disabled" } },
0,
],
[
"blocks DM pin system events for unauthorized senders in allowlist mode",
{
overrides: { dmPolicy: "allowlist", allowFrom: ["U2"] },
event: makePinEvent({ user: "U1" }),
},
});
const addedHandler = getAddedHandler();
expect(addedHandler).toBeTruthy();
await addedHandler!({
event: makePinEvent({ channel: "C1", user: "U_ATTACKER" }),
body: {},
});
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
0,
],
[
"allows DM pin system events for authorized senders in allowlist mode",
{
overrides: { dmPolicy: "allowlist", allowFrom: ["U1"] },
event: makePinEvent({ user: "U1" }),
},
1,
],
[
"blocks channel pin events for users outside channel users allowlist",
{
overrides: {
dmPolicy: "open",
channelType: "channel",
channelUsers: ["U_OWNER"],
},
event: makePinEvent({ channel: "C1", user: "U_ATTACKER" }),
},
0,
],
])("%s", async (_name, args: PinCase, expectedCalls: number) => {
await runPinCase(args);
expect(pinEnqueueMock).toHaveBeenCalledTimes(expectedCalls);
});
it("does not track mismatched events", async () => {
const trackEvent = vi.fn();
const { getAddedHandler } = createPinContext({
await runPinCase({
trackEvent,
shouldDropMismatchedSlackEvent: () => true,
});
const addedHandler = getAddedHandler();
expect(addedHandler).toBeTruthy();
await addedHandler!({
event: makePinEvent(),
body: { api_app_id: "A_OTHER" },
});
@@ -154,14 +128,7 @@ describe("registerSlackPinEvents", () => {
it("tracks accepted pin events", async () => {
const trackEvent = vi.fn();
const { getAddedHandler } = createPinContext({ trackEvent });
const addedHandler = getAddedHandler();
expect(addedHandler).toBeTruthy();
await addedHandler!({
event: makePinEvent(),
body: {},
});
await runPinCase({ trackEvent });
expect(trackEvent).toHaveBeenCalledTimes(1);
});

View File

@@ -5,39 +5,33 @@ import {
type SlackSystemEventTestOverrides,
} from "./system-event-test-harness.js";
const enqueueSystemEventMock = vi.fn();
const readAllowFromStoreMock = vi.fn();
const reactionQueueMock = vi.fn();
const reactionAllowMock = vi.fn();
vi.mock("../../../infra/system-events.js", () => ({
enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args),
}));
vi.mock("../../../infra/system-events.js", () => {
return {
enqueueSystemEvent: (...args: unknown[]) => reactionQueueMock(...args),
};
});
vi.mock("../../../pairing/pairing-store.js", () => ({
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
}));
vi.mock("../../../pairing/pairing-store.js", () => {
return {
readChannelAllowFromStore: (...args: unknown[]) => reactionAllowMock(...args),
};
});
type SlackReactionHandler = (args: {
event: Record<string, unknown>;
body: unknown;
}) => Promise<void>;
type ReactionHandler = (args: { event: Record<string, unknown>; body: unknown }) => Promise<void>;
function createReactionContext(params?: {
type ReactionRunInput = {
handler?: "added" | "removed";
overrides?: SlackSystemEventTestOverrides;
event?: Record<string, unknown>;
body?: unknown;
trackEvent?: () => void;
shouldDropMismatchedSlackEvent?: (body: unknown) => boolean;
}) {
const harness = createSlackSystemEventTestHarness(params?.overrides);
if (params?.shouldDropMismatchedSlackEvent) {
harness.ctx.shouldDropMismatchedSlackEvent = params.shouldDropMismatchedSlackEvent;
}
registerSlackReactionEvents({ ctx: harness.ctx, trackEvent: params?.trackEvent });
return {
getAddedHandler: () => harness.getHandler("reaction_added") as SlackReactionHandler | null,
getRemovedHandler: () => harness.getHandler("reaction_removed") as SlackReactionHandler | null,
};
}
};
function makeReactionEvent(overrides?: { user?: string; channel?: string }) {
function buildReactionEvent(overrides?: { user?: string; channel?: string }) {
return {
type: "reaction_added",
user: overrides?.user ?? "U1",
@@ -51,123 +45,100 @@ function makeReactionEvent(overrides?: { user?: string; channel?: string }) {
};
}
function createReactionHandlers(params: {
overrides?: SlackSystemEventTestOverrides;
trackEvent?: () => void;
shouldDropMismatchedSlackEvent?: (body: unknown) => boolean;
}) {
const harness = createSlackSystemEventTestHarness(params.overrides);
if (params.shouldDropMismatchedSlackEvent) {
harness.ctx.shouldDropMismatchedSlackEvent = params.shouldDropMismatchedSlackEvent;
}
registerSlackReactionEvents({ ctx: harness.ctx, trackEvent: params.trackEvent });
return {
added: harness.getHandler("reaction_added") as ReactionHandler | null,
removed: harness.getHandler("reaction_removed") as ReactionHandler | null,
};
}
async function executeReactionCase(input: ReactionRunInput = {}) {
reactionQueueMock.mockClear();
reactionAllowMock.mockReset().mockResolvedValue([]);
const handlers = createReactionHandlers({
overrides: input.overrides,
trackEvent: input.trackEvent,
shouldDropMismatchedSlackEvent: input.shouldDropMismatchedSlackEvent,
});
const handler = handlers[input.handler ?? "added"];
expect(handler).toBeTruthy();
await handler!({
event: (input.event ?? buildReactionEvent()) as Record<string, unknown>,
body: input.body ?? {},
});
}
describe("registerSlackReactionEvents", () => {
it("enqueues DM reaction system events when dmPolicy is open", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getAddedHandler } = createReactionContext({ overrides: { dmPolicy: "open" } });
const addedHandler = getAddedHandler();
expect(addedHandler).toBeTruthy();
await addedHandler!({
event: makeReactionEvent(),
body: {},
});
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
});
it("blocks DM reaction system events when dmPolicy is disabled", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getAddedHandler } = createReactionContext({ overrides: { dmPolicy: "disabled" } });
const addedHandler = getAddedHandler();
expect(addedHandler).toBeTruthy();
await addedHandler!({
event: makeReactionEvent(),
body: {},
});
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
});
it("blocks DM reaction system events for unauthorized senders in allowlist mode", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getAddedHandler } = createReactionContext({
overrides: { dmPolicy: "allowlist", allowFrom: ["U2"] },
});
const addedHandler = getAddedHandler();
expect(addedHandler).toBeTruthy();
await addedHandler!({
event: makeReactionEvent({ user: "U1" }),
body: {},
});
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
});
it("allows DM reaction system events for authorized senders in allowlist mode", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getAddedHandler } = createReactionContext({
overrides: { dmPolicy: "allowlist", allowFrom: ["U1"] },
});
const addedHandler = getAddedHandler();
expect(addedHandler).toBeTruthy();
await addedHandler!({
event: makeReactionEvent({ user: "U1" }),
body: {},
});
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
});
it("enqueues channel reaction events regardless of dmPolicy", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getRemovedHandler } = createReactionContext({
overrides: { dmPolicy: "disabled", channelType: "channel" },
});
const removedHandler = getRemovedHandler();
expect(removedHandler).toBeTruthy();
await removedHandler!({
event: {
...makeReactionEvent({ channel: "C1" }),
type: "reaction_removed",
it.each([
{
name: "enqueues DM reaction system events when dmPolicy is open",
args: { overrides: { dmPolicy: "open" } },
expectedCalls: 1,
},
{
name: "blocks DM reaction system events when dmPolicy is disabled",
args: { overrides: { dmPolicy: "disabled" } },
expectedCalls: 0,
},
{
name: "blocks DM reaction system events for unauthorized senders in allowlist mode",
args: {
overrides: { dmPolicy: "allowlist", allowFrom: ["U2"] },
event: buildReactionEvent({ user: "U1" }),
},
body: {},
});
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
});
it("blocks channel reaction events for users outside channel users allowlist", async () => {
enqueueSystemEventMock.mockClear();
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
const { getAddedHandler } = createReactionContext({
overrides: {
dmPolicy: "open",
channelType: "channel",
channelUsers: ["U_OWNER"],
expectedCalls: 0,
},
{
name: "allows DM reaction system events for authorized senders in allowlist mode",
args: {
overrides: { dmPolicy: "allowlist", allowFrom: ["U1"] },
event: buildReactionEvent({ user: "U1" }),
},
});
const addedHandler = getAddedHandler();
expect(addedHandler).toBeTruthy();
await addedHandler!({
event: makeReactionEvent({ channel: "C1", user: "U_ATTACKER" }),
body: {},
});
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
expectedCalls: 1,
},
{
name: "enqueues channel reaction events regardless of dmPolicy",
args: {
handler: "removed" as const,
overrides: { dmPolicy: "disabled", channelType: "channel" },
event: {
...buildReactionEvent({ channel: "C1" }),
type: "reaction_removed",
},
},
expectedCalls: 1,
},
{
name: "blocks channel reaction events for users outside channel users allowlist",
args: {
overrides: {
dmPolicy: "open",
channelType: "channel",
channelUsers: ["U_OWNER"],
},
event: buildReactionEvent({ channel: "C1", user: "U_ATTACKER" }),
},
expectedCalls: 0,
},
])("$name", async ({ args, expectedCalls }) => {
await executeReactionCase(args);
expect(reactionQueueMock).toHaveBeenCalledTimes(expectedCalls);
});
it("does not track mismatched events", async () => {
const trackEvent = vi.fn();
const { getAddedHandler } = createReactionContext({
await executeReactionCase({
trackEvent,
shouldDropMismatchedSlackEvent: () => true,
});
const addedHandler = getAddedHandler();
expect(addedHandler).toBeTruthy();
await addedHandler!({
event: makeReactionEvent(),
body: { api_app_id: "A_OTHER" },
});
@@ -176,14 +147,7 @@ describe("registerSlackReactionEvents", () => {
it("tracks accepted message reactions", async () => {
const trackEvent = vi.fn();
const { getAddedHandler } = createReactionContext({ trackEvent });
const addedHandler = getAddedHandler();
expect(addedHandler).toBeTruthy();
await addedHandler!({
event: makeReactionEvent(),
body: {},
});
await executeReactionCase({ trackEvent });
expect(trackEvent).toHaveBeenCalledTimes(1);
});

View File

@@ -189,6 +189,73 @@ describe("slack prepareSlackMessage inbound contract", () => {
return prepareMessageWith(ctx, createThreadAccount(), createThreadReplyMessage(overrides));
}
function createDmScopeMainSlackCtx(): SlackMonitorContext {
const slackCtx = createInboundSlackCtx({
cfg: {
channels: { slack: { enabled: true } },
session: { dmScope: "main" },
} as OpenClawConfig,
});
// 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 });
return slackCtx;
}
function createMainScopedDmMessage(overrides: Partial<SlackMessageEvent>): SlackMessageEvent {
return createSlackMessage({
channel: "D0ACP6B1T8V",
user: "U1",
text: "hello from DM",
ts: "1.000",
...overrides,
});
}
function expectMainScopedDmClassification(
prepared: Awaited<ReturnType<typeof prepareSlackMessage>>,
options?: { includeFromCheck?: boolean },
) {
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");
if (options?.includeFromCheck) {
expect(prepared!.ctxPayload.From).toContain("slack:U1");
}
}
function createReplyToAllSlackCtx(params?: {
groupPolicy?: "open";
defaultRequireMention?: boolean;
asChannel?: boolean;
}): SlackMonitorContext {
const slackCtx = createInboundSlackCtx({
cfg: {
channels: {
slack: {
enabled: true,
replyToMode: "all",
...(params?.groupPolicy ? { groupPolicy: params.groupPolicy } : {}),
},
},
} as OpenClawConfig,
replyToMode: "all",
...(params?.defaultRequireMention === undefined
? {}
: { defaultRequireMention: params.defaultRequireMention }),
});
// oxlint-disable-next-line typescript/no-explicit-any
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
if (params?.asChannel) {
slackCtx.resolveChannelName = async () => ({ name: "general", type: "channel" });
}
return slackCtx;
}
it("produces a finalized MsgContext", async () => {
const message: SlackMessageEvent = {
channel: "D123",
@@ -331,179 +398,34 @@ describe("slack prepareSlackMessage inbound contract", () => {
});
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: [],
allowNameMatching: false,
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 prepared = await prepareMessageWith(
createDmScopeMainSlackCtx(),
createSlackAccount(),
createMainScopedDmMessage({
// Bug scenario: D-prefix channel but Slack event says channel_type: "channel"
channel_type: "channel",
}),
);
const account: ResolvedSlackAccount = {
accountId: "default",
enabled: true,
botTokenSource: "config",
appTokenSource: "config",
userTokenSource: "none",
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");
expectMainScopedDmClassification(prepared, { includeFromCheck: true });
});
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: [],
allowNameMatching: false,
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",
userTokenSource: "none",
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,
const message = createMainScopedDmMessage({});
delete message.channel_type;
const prepared = await prepareMessageWith(
createDmScopeMainSlackCtx(),
createSlackAccount(),
// channel_type missing — should infer from D-prefix.
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");
expectMainScopedDmClassification(prepared);
});
it("sets MessageThreadId for top-level messages when replyToMode=all", async () => {
const slackCtx = createInboundSlackCtx({
cfg: {
channels: { slack: { enabled: true, replyToMode: "all" } },
} as OpenClawConfig,
replyToMode: "all",
});
// oxlint-disable-next-line typescript/no-explicit-any
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
const prepared = await prepareMessageWith(
slackCtx,
createReplyToAllSlackCtx(),
createSlackAccount({ replyToMode: "all" }),
createSlackMessage({}),
);
@@ -513,17 +435,8 @@ describe("slack prepareSlackMessage inbound contract", () => {
});
it("respects replyToModeByChatType.direct override for DMs", async () => {
const slackCtx = createInboundSlackCtx({
cfg: {
channels: { slack: { enabled: true, replyToMode: "all" } },
} as OpenClawConfig,
replyToMode: "all",
});
// oxlint-disable-next-line typescript/no-explicit-any
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
const prepared = await prepareMessageWith(
slackCtx,
createReplyToAllSlackCtx(),
createSlackAccount({ replyToMode: "all", replyToModeByChatType: { direct: "off" } }),
createSlackMessage({}), // DM (channel_type: "im")
);
@@ -534,19 +447,12 @@ describe("slack prepareSlackMessage inbound contract", () => {
});
it("still threads channel messages when replyToModeByChatType.direct is off", async () => {
const slackCtx = createInboundSlackCtx({
cfg: {
channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } },
} as OpenClawConfig,
replyToMode: "all",
defaultRequireMention: false,
});
// oxlint-disable-next-line typescript/no-explicit-any
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
slackCtx.resolveChannelName = async () => ({ name: "general", type: "channel" });
const prepared = await prepareMessageWith(
slackCtx,
createReplyToAllSlackCtx({
groupPolicy: "open",
defaultRequireMention: false,
asChannel: true,
}),
createSlackAccount({ replyToMode: "all", replyToModeByChatType: { direct: "off" } }),
createSlackMessage({ channel: "C123", channel_type: "channel" }),
);
@@ -557,17 +463,8 @@ describe("slack prepareSlackMessage inbound contract", () => {
});
it("respects dm.replyToMode legacy override for DMs", async () => {
const slackCtx = createInboundSlackCtx({
cfg: {
channels: { slack: { enabled: true, replyToMode: "all" } },
} as OpenClawConfig,
replyToMode: "all",
});
// oxlint-disable-next-line typescript/no-explicit-any
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
const prepared = await prepareMessageWith(
slackCtx,
createReplyToAllSlackCtx(),
createSlackAccount({ replyToMode: "all", dm: { replyToMode: "off" } }),
createSlackMessage({}), // DM
);