openskynet / src /slack /monitor /message-handler.app-mention-race.test.ts
Darochin's picture
Mirror OpenSkyNet workspace snapshot from Git HEAD
fc93158 verified
import { beforeEach, describe, expect, it, vi } from "vitest";
const prepareSlackMessageMock =
vi.fn<
(params: {
opts: { source: "message" | "app_mention"; wasMentioned?: boolean };
}) => Promise<unknown>
>();
const dispatchPreparedSlackMessageMock = vi.fn<(prepared: unknown) => Promise<void>>();
vi.mock("../../channels/inbound-debounce-policy.js", () => ({
shouldDebounceTextInbound: () => false,
createChannelInboundDebouncer: (params: {
onFlush: (
entries: Array<{
message: Record<string, unknown>;
opts: { source: "message" | "app_mention"; wasMentioned?: boolean };
}>,
) => Promise<void>;
}) => ({
debounceMs: 0,
debouncer: {
enqueue: async (entry: {
message: Record<string, unknown>;
opts: { source: "message" | "app_mention"; wasMentioned?: boolean };
}) => {
await params.onFlush([entry]);
},
flushKey: async (_key: string) => {},
},
}),
}));
vi.mock("./thread-resolution.js", () => ({
createSlackThreadTsResolver: () => ({
resolve: async ({ message }: { message: Record<string, unknown> }) => message,
}),
}));
vi.mock("./message-handler/prepare.js", () => ({
prepareSlackMessage: (
params: Parameters<typeof prepareSlackMessageMock>[0],
): ReturnType<typeof prepareSlackMessageMock> => prepareSlackMessageMock(params),
}));
vi.mock("./message-handler/dispatch.js", () => ({
dispatchPreparedSlackMessage: (
prepared: Parameters<typeof dispatchPreparedSlackMessageMock>[0],
): ReturnType<typeof dispatchPreparedSlackMessageMock> =>
dispatchPreparedSlackMessageMock(prepared),
}));
import { createSlackMessageHandler } from "./message-handler.js";
function createMarkMessageSeen() {
const seen = new Set<string>();
return (channel: string | undefined, ts: string | undefined) => {
if (!channel || !ts) {
return false;
}
const key = `${channel}:${ts}`;
if (seen.has(key)) {
return true;
}
seen.add(key);
return false;
};
}
function createTestHandler() {
return createSlackMessageHandler({
ctx: {
cfg: {},
accountId: "default",
app: { client: {} },
runtime: {},
markMessageSeen: createMarkMessageSeen(),
} as Parameters<typeof createSlackMessageHandler>[0]["ctx"],
account: { accountId: "default" } as Parameters<typeof createSlackMessageHandler>[0]["account"],
});
}
function createSlackEvent(params: { type: "message" | "app_mention"; ts: string; text: string }) {
return { type: params.type, channel: "C1", ts: params.ts, text: params.text } as never;
}
async function sendMessageEvent(handler: ReturnType<typeof createTestHandler>, ts: string) {
await handler(createSlackEvent({ type: "message", ts, text: "hello" }), { source: "message" });
}
async function sendMentionEvent(handler: ReturnType<typeof createTestHandler>, ts: string) {
await handler(createSlackEvent({ type: "app_mention", ts, text: "<@U_BOT> hello" }), {
source: "app_mention",
wasMentioned: true,
});
}
async function createInFlightMessageScenario(ts: string) {
let resolveMessagePrepare: ((value: unknown) => void) | undefined;
const messagePrepare = new Promise<unknown>((resolve) => {
resolveMessagePrepare = resolve;
});
prepareSlackMessageMock.mockImplementation(async ({ opts }) => {
if (opts.source === "message") {
return messagePrepare;
}
return { ctxPayload: {} };
});
const handler = createTestHandler();
const messagePending = handler(createSlackEvent({ type: "message", ts, text: "hello" }), {
source: "message",
});
await Promise.resolve();
return { handler, messagePending, resolveMessagePrepare };
}
describe("createSlackMessageHandler app_mention race handling", () => {
beforeEach(() => {
prepareSlackMessageMock.mockReset();
dispatchPreparedSlackMessageMock.mockReset();
});
it("allows a single app_mention retry when message event was dropped before dispatch", async () => {
prepareSlackMessageMock.mockImplementation(async ({ opts }) => {
if (opts.source === "message") {
return null;
}
return { ctxPayload: {} };
});
const handler = createTestHandler();
await sendMessageEvent(handler, "1700000000.000100");
await sendMentionEvent(handler, "1700000000.000100");
await sendMentionEvent(handler, "1700000000.000100");
expect(prepareSlackMessageMock).toHaveBeenCalledTimes(2);
expect(dispatchPreparedSlackMessageMock).toHaveBeenCalledTimes(1);
});
it("allows app_mention while message handling is still in-flight, then keeps later duplicates deduped", async () => {
const { handler, messagePending, resolveMessagePrepare } =
await createInFlightMessageScenario("1700000000.000150");
await sendMentionEvent(handler, "1700000000.000150");
resolveMessagePrepare?.(null);
await messagePending;
await sendMentionEvent(handler, "1700000000.000150");
expect(prepareSlackMessageMock).toHaveBeenCalledTimes(2);
expect(dispatchPreparedSlackMessageMock).toHaveBeenCalledTimes(1);
});
it("suppresses message dispatch when app_mention already dispatched during in-flight race", async () => {
const { handler, messagePending, resolveMessagePrepare } =
await createInFlightMessageScenario("1700000000.000175");
await sendMentionEvent(handler, "1700000000.000175");
resolveMessagePrepare?.({ ctxPayload: {} });
await messagePending;
expect(prepareSlackMessageMock).toHaveBeenCalledTimes(2);
expect(dispatchPreparedSlackMessageMock).toHaveBeenCalledTimes(1);
});
it("keeps app_mention deduped when message event already dispatched", async () => {
prepareSlackMessageMock.mockResolvedValue({ ctxPayload: {} });
const handler = createTestHandler();
await sendMessageEvent(handler, "1700000000.000200");
await sendMentionEvent(handler, "1700000000.000200");
expect(prepareSlackMessageMock).toHaveBeenCalledTimes(1);
expect(dispatchPreparedSlackMessageMock).toHaveBeenCalledTimes(1);
});
});