Download src/slack/monitor.tool-result.test.ts from Darochin/openskynet: direct link, hf CLI and curl.
- Browser
- Download file 21.3 kB
-
https://huggingface.co/Darochin/openskynet/resolve/main/src/slack/monitor.tool-result.test.ts
- Command line
-
hf download hf://Darochin/openskynet/src/slack/monitor.tool-result.test.ts
-
curl -L -o monitor.tool-result.test.ts https://huggingface.co/Darochin/openskynet/resolve/main/src/slack/monitor.tool-result.test.ts
21.3 kB
| import { beforeEach, describe, expect, it, vi } from "vitest"; | |
| import { HISTORY_CONTEXT_MARKER } from "../auto-reply/reply/history.js"; | |
| import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js"; | |
| import { CURRENT_MESSAGE_MARKER } from "../auto-reply/reply/mentions.js"; | |
| import { | |
| defaultSlackTestConfig, | |
| getSlackTestState, | |
| getSlackClient, | |
| getSlackHandlers, | |
| getSlackHandlerOrThrow, | |
| flush, | |
| resetSlackTestState, | |
| runSlackMessageOnce, | |
| startSlackMonitor, | |
| stopSlackMonitor, | |
| } from "./monitor.test-helpers.js"; | |
| const { monitorSlackProvider } = await import("./monitor.js"); | |
| const slackTestState = getSlackTestState(); | |
| const { sendMock, replyMock, reactMock, upsertPairingRequestMock } = slackTestState; | |
| beforeEach(() => { | |
| resetInboundDedupe(); | |
| resetSlackTestState(defaultSlackTestConfig()); | |
| }); | |
| describe("monitorSlackProvider tool results", () => { | |
| type SlackMessageEvent = { | |
| type: "message"; | |
| user: string; | |
| text: string; | |
| ts: string; | |
| channel: string; | |
| channel_type: "im" | "channel"; | |
| thread_ts?: string; | |
| parent_user_id?: string; | |
| }; | |
| const baseSlackMessageEvent = Object.freeze({ | |
| type: "message", | |
| user: "U1", | |
| text: "hello", | |
| ts: "123", | |
| channel: "C1", | |
| channel_type: "im", | |
| }) as SlackMessageEvent; | |
| function makeSlackMessageEvent(overrides: Partial<SlackMessageEvent> = {}): SlackMessageEvent { | |
| return { ...baseSlackMessageEvent, ...overrides }; | |
| } | |
| function setDirectMessageReplyMode(replyToMode: "off" | "all" | "first") { | |
| slackTestState.config = { | |
| messages: { | |
| responsePrefix: "PFX", | |
| ackReaction: "๐", | |
| ackReactionScope: "group-mentions", | |
| }, | |
| channels: { | |
| slack: { | |
| dm: { enabled: true, policy: "open", allowFrom: ["*"] }, | |
| replyToMode, | |
| }, | |
| }, | |
| }; | |
| } | |
| function firstReplyCtx(): { WasMentioned?: boolean } { | |
| return (replyMock.mock.calls[0]?.[0] ?? {}) as { WasMentioned?: boolean }; | |
| } | |
| function setRequireMentionChannelConfig(mentionPatterns?: string[]) { | |
| slackTestState.config = { | |
| ...(mentionPatterns | |
| ? { | |
| messages: { | |
| responsePrefix: "PFX", | |
| groupChat: { mentionPatterns }, | |
| }, | |
| } | |
| : {}), | |
| channels: { | |
| slack: { | |
| dm: { enabled: true, policy: "open", allowFrom: ["*"] }, | |
| channels: { C1: { allow: true, requireMention: true } }, | |
| }, | |
| }, | |
| }; | |
| } | |
| async function runDirectMessageEvent(ts: string, extraEvent: Record<string, unknown> = {}) { | |
| await runSlackMessageOnce(monitorSlackProvider, { | |
| event: makeSlackMessageEvent({ ts, ...extraEvent }), | |
| }); | |
| } | |
| async function runChannelThreadReplyEvent() { | |
| await runSlackMessageOnce(monitorSlackProvider, { | |
| event: makeSlackMessageEvent({ | |
| text: "thread reply", | |
| ts: "123.456", | |
| thread_ts: "111.222", | |
| channel_type: "channel", | |
| }), | |
| }); | |
| } | |
| async function runChannelMessageEvent( | |
| text: string, | |
| overrides: Partial<SlackMessageEvent> = {}, | |
| ): Promise<void> { | |
| await runSlackMessageOnce(monitorSlackProvider, { | |
| event: makeSlackMessageEvent({ | |
| text, | |
| channel_type: "channel", | |
| ...overrides, | |
| }), | |
| }); | |
| } | |
| function setHistoryCaptureConfig(channels: Record<string, unknown>) { | |
| slackTestState.config = { | |
| messages: { ackReactionScope: "group-mentions" }, | |
| channels: { | |
| slack: { | |
| historyLimit: 5, | |
| dm: { enabled: true, policy: "open", allowFrom: ["*"] }, | |
| channels, | |
| }, | |
| }, | |
| }; | |
| } | |
| function captureReplyContexts<T extends Record<string, unknown>>() { | |
| const contexts: T[] = []; | |
| replyMock.mockImplementation(async (ctx: unknown) => { | |
| contexts.push((ctx ?? {}) as T); | |
| return undefined; | |
| }); | |
| return contexts; | |
| } | |
| async function runMonitoredSlackMessages(events: SlackMessageEvent[]) { | |
| const { controller, run } = startSlackMonitor(monitorSlackProvider); | |
| const handler = await getSlackHandlerOrThrow("message"); | |
| for (const event of events) { | |
| await handler({ event }); | |
| } | |
| await stopSlackMonitor({ controller, run }); | |
| } | |
| function setPairingOnlyDirectMessages() { | |
| const currentConfig = slackTestState.config as { | |
| channels?: { slack?: Record<string, unknown> }; | |
| }; | |
| slackTestState.config = { | |
| ...currentConfig, | |
| channels: { | |
| ...currentConfig.channels, | |
| slack: { | |
| ...currentConfig.channels?.slack, | |
| dm: { enabled: true, policy: "pairing", allowFrom: [] }, | |
| }, | |
| }, | |
| }; | |
| } | |
| function setOpenChannelDirectMessages(params?: { | |
| bindings?: Array<Record<string, unknown>>; | |
| groupPolicy?: "open"; | |
| includeAckReactionConfig?: boolean; | |
| replyToMode?: "off" | "all" | "first"; | |
| threadInheritParent?: boolean; | |
| }) { | |
| const slackChannelConfig: Record<string, unknown> = { | |
| dm: { enabled: true, policy: "open", allowFrom: ["*"] }, | |
| channels: { C1: { allow: true, requireMention: false } }, | |
| ...(params?.groupPolicy ? { groupPolicy: params.groupPolicy } : {}), | |
| ...(params?.replyToMode ? { replyToMode: params.replyToMode } : {}), | |
| ...(params?.threadInheritParent ? { thread: { inheritParent: true } } : {}), | |
| }; | |
| slackTestState.config = { | |
| messages: params?.includeAckReactionConfig | |
| ? { | |
| responsePrefix: "PFX", | |
| ackReaction: "๐", | |
| ackReactionScope: "group-mentions", | |
| } | |
| : { responsePrefix: "PFX" }, | |
| channels: { slack: slackChannelConfig }, | |
| ...(params?.bindings ? { bindings: params.bindings } : {}), | |
| }; | |
| } | |
| function getFirstReplySessionCtx(): { | |
| SessionKey?: string; | |
| ParentSessionKey?: string; | |
| ThreadStarterBody?: string; | |
| ThreadLabel?: string; | |
| } { | |
| return (replyMock.mock.calls[0]?.[0] ?? {}) as { | |
| SessionKey?: string; | |
| ParentSessionKey?: string; | |
| ThreadStarterBody?: string; | |
| ThreadLabel?: string; | |
| }; | |
| } | |
| function expectSingleSendWithThread(threadTs: string | undefined) { | |
| expect(sendMock).toHaveBeenCalledTimes(1); | |
| expect(sendMock.mock.calls[0][2]).toMatchObject({ threadTs }); | |
| } | |
| async function runDefaultMessageAndExpectSentText(expectedText: string) { | |
| replyMock.mockResolvedValue({ text: expectedText.replace(/^PFX /, "") }); | |
| await runSlackMessageOnce(monitorSlackProvider, { | |
| event: makeSlackMessageEvent(), | |
| }); | |
| expect(sendMock).toHaveBeenCalledTimes(1); | |
| expect(sendMock.mock.calls[0][1]).toBe(expectedText); | |
| } | |
| it("skips socket startup when Slack channel is disabled", async () => { | |
| slackTestState.config = { | |
| channels: { | |
| slack: { | |
| enabled: false, | |
| mode: "socket", | |
| botToken: "xoxb-config", | |
| appToken: "xapp-config", | |
| }, | |
| }, | |
| }; | |
| const client = getSlackClient(); | |
| if (!client) { | |
| throw new Error("Slack client not registered"); | |
| } | |
| client.auth.test.mockClear(); | |
| const { controller, run } = startSlackMonitor(monitorSlackProvider); | |
| await flush(); | |
| controller.abort(); | |
| await run; | |
| expect(client.auth.test).not.toHaveBeenCalled(); | |
| expect(getSlackHandlers()?.size ?? 0).toBe(0); | |
| }); | |
| it("skips tool summaries with responsePrefix", async () => { | |
| await runDefaultMessageAndExpectSentText("PFX final reply"); | |
| }); | |
| it("drops events with mismatched api_app_id", async () => { | |
| const client = getSlackClient(); | |
| if (!client) { | |
| throw new Error("Slack client not registered"); | |
| } | |
| (client.auth as { test: ReturnType<typeof vi.fn> }).test.mockResolvedValue({ | |
| user_id: "bot-user", | |
| team_id: "T1", | |
| api_app_id: "A1", | |
| }); | |
| await runSlackMessageOnce( | |
| monitorSlackProvider, | |
| { | |
| body: { api_app_id: "A2", team_id: "T1" }, | |
| event: makeSlackMessageEvent(), | |
| }, | |
| { appToken: "xapp-1-A1-abc" }, | |
| ); | |
| expect(sendMock).not.toHaveBeenCalled(); | |
| expect(replyMock).not.toHaveBeenCalled(); | |
| }); | |
| it("does not derive responsePrefix from routed agent identity when unset", async () => { | |
| slackTestState.config = { | |
| agents: { | |
| list: [ | |
| { | |
| id: "main", | |
| default: true, | |
| identity: { name: "Mainbot", theme: "space lobster", emoji: "๐ฆ" }, | |
| }, | |
| { | |
| id: "rich", | |
| identity: { name: "Richbot", theme: "lion bot", emoji: "๐ฆ" }, | |
| }, | |
| ], | |
| }, | |
| bindings: [ | |
| { | |
| agentId: "rich", | |
| match: { channel: "slack", peer: { kind: "direct", id: "U1" } }, | |
| }, | |
| ], | |
| messages: { | |
| ackReaction: "๐", | |
| ackReactionScope: "group-mentions", | |
| }, | |
| channels: { | |
| slack: { dm: { enabled: true, policy: "open", allowFrom: ["*"] } }, | |
| }, | |
| }; | |
| await runDefaultMessageAndExpectSentText("final reply"); | |
| }); | |
| it("preserves RawBody without injecting processed room history", async () => { | |
| setHistoryCaptureConfig({ "*": { requireMention: false } }); | |
| const capturedCtx = captureReplyContexts<{ | |
| Body?: string; | |
| RawBody?: string; | |
| CommandBody?: string; | |
| }>(); | |
| await runMonitoredSlackMessages([ | |
| makeSlackMessageEvent({ user: "U1", text: "first", ts: "123", channel_type: "channel" }), | |
| makeSlackMessageEvent({ user: "U2", text: "second", ts: "124", channel_type: "channel" }), | |
| ]); | |
| expect(replyMock).toHaveBeenCalledTimes(2); | |
| const latestCtx = capturedCtx.at(-1) ?? {}; | |
| expect(latestCtx.Body).not.toContain(HISTORY_CONTEXT_MARKER); | |
| expect(latestCtx.Body).not.toContain(CURRENT_MESSAGE_MARKER); | |
| expect(latestCtx.Body).not.toContain("first"); | |
| expect(latestCtx.RawBody).toBe("second"); | |
| expect(latestCtx.CommandBody).toBe("second"); | |
| }); | |
| it("scopes thread history to the thread by default", async () => { | |
| setHistoryCaptureConfig({ C1: { allow: true, requireMention: true } }); | |
| const capturedCtx = captureReplyContexts<{ Body?: string }>(); | |
| await runMonitoredSlackMessages([ | |
| makeSlackMessageEvent({ | |
| user: "U1", | |
| text: "thread-a-one", | |
| ts: "200", | |
| thread_ts: "100", | |
| channel_type: "channel", | |
| }), | |
| makeSlackMessageEvent({ | |
| user: "U1", | |
| text: "<@bot-user> thread-a-two", | |
| ts: "201", | |
| thread_ts: "100", | |
| channel_type: "channel", | |
| }), | |
| makeSlackMessageEvent({ | |
| user: "U2", | |
| text: "<@bot-user> thread-b-one", | |
| ts: "301", | |
| thread_ts: "300", | |
| channel_type: "channel", | |
| }), | |
| ]); | |
| expect(replyMock).toHaveBeenCalledTimes(2); | |
| expect(capturedCtx[0]?.Body).toContain("thread-a-one"); | |
| expect(capturedCtx[1]?.Body).not.toContain("thread-a-one"); | |
| expect(capturedCtx[1]?.Body).not.toContain("thread-a-two"); | |
| }); | |
| it("updates assistant thread status when replies start", async () => { | |
| replyMock.mockImplementation(async (...args: unknown[]) => { | |
| const opts = (args[1] ?? {}) as { onReplyStart?: () => Promise<void> | void }; | |
| await opts?.onReplyStart?.(); | |
| return { text: "final reply" }; | |
| }); | |
| setDirectMessageReplyMode("all"); | |
| await runSlackMessageOnce(monitorSlackProvider, { | |
| event: makeSlackMessageEvent(), | |
| }); | |
| const client = getSlackClient() as { | |
| assistant?: { threads?: { setStatus?: ReturnType<typeof vi.fn> } }; | |
| }; | |
| const setStatus = client.assistant?.threads?.setStatus; | |
| expect(setStatus).toHaveBeenCalledTimes(2); | |
| expect(setStatus).toHaveBeenNthCalledWith(1, { | |
| token: "bot-token", | |
| channel_id: "C1", | |
| thread_ts: "123", | |
| status: "is typing...", | |
| }); | |
| expect(setStatus).toHaveBeenNthCalledWith(2, { | |
| token: "bot-token", | |
| channel_id: "C1", | |
| thread_ts: "123", | |
| status: "", | |
| }); | |
| }); | |
| async function expectMentionPatternMessageAccepted(text: string): Promise<void> { | |
| setRequireMentionChannelConfig(["\\bopenclaw\\b"]); | |
| replyMock.mockResolvedValue({ text: "hi" }); | |
| await runSlackMessageOnce(monitorSlackProvider, { | |
| event: makeSlackMessageEvent({ | |
| text, | |
| channel_type: "channel", | |
| }), | |
| }); | |
| expect(replyMock).toHaveBeenCalledTimes(1); | |
| expect(firstReplyCtx().WasMentioned).toBe(true); | |
| } | |
| it("accepts channel messages when mentionPatterns match", async () => { | |
| await expectMentionPatternMessageAccepted("openclaw: hello"); | |
| }); | |
| it("accepts channel messages when mentionPatterns match even if another user is mentioned", async () => { | |
| await expectMentionPatternMessageAccepted("openclaw: hello <@U2>"); | |
| }); | |
| it("treats replies to bot threads as implicit mentions", async () => { | |
| setRequireMentionChannelConfig(); | |
| replyMock.mockResolvedValue({ text: "hi" }); | |
| await runSlackMessageOnce(monitorSlackProvider, { | |
| event: makeSlackMessageEvent({ | |
| text: "following up", | |
| ts: "124", | |
| thread_ts: "123", | |
| parent_user_id: "bot-user", | |
| channel_type: "channel", | |
| }), | |
| }); | |
| expect(replyMock).toHaveBeenCalledTimes(1); | |
| expect(firstReplyCtx().WasMentioned).toBe(true); | |
| }); | |
| it("accepts channel messages without mention when channels.slack.requireMention is false", async () => { | |
| slackTestState.config = { | |
| channels: { | |
| slack: { | |
| dm: { enabled: true, policy: "open", allowFrom: ["*"] }, | |
| groupPolicy: "open", | |
| requireMention: false, | |
| }, | |
| }, | |
| }; | |
| replyMock.mockResolvedValue({ text: "hi" }); | |
| await runSlackMessageOnce(monitorSlackProvider, { | |
| event: makeSlackMessageEvent({ | |
| channel_type: "channel", | |
| }), | |
| }); | |
| expect(replyMock).toHaveBeenCalledTimes(1); | |
| expect(firstReplyCtx().WasMentioned).toBe(false); | |
| expect(sendMock).toHaveBeenCalledTimes(1); | |
| }); | |
| it("treats control commands as mentions for group bypass", async () => { | |
| replyMock.mockResolvedValue({ text: "ok" }); | |
| await runChannelMessageEvent("/elevated off"); | |
| expect(replyMock).toHaveBeenCalledTimes(1); | |
| expect(firstReplyCtx().WasMentioned).toBe(true); | |
| }); | |
| it("threads replies when incoming message is in a thread", async () => { | |
| replyMock.mockResolvedValue({ text: "thread reply" }); | |
| setOpenChannelDirectMessages({ | |
| includeAckReactionConfig: true, | |
| groupPolicy: "open", | |
| replyToMode: "off", | |
| }); | |
| await runChannelThreadReplyEvent(); | |
| expectSingleSendWithThread("111.222"); | |
| }); | |
| it("ignores replyToId directive when replyToMode is off", async () => { | |
| replyMock.mockResolvedValue({ text: "forced reply", replyToId: "555" }); | |
| slackTestState.config = { | |
| messages: { | |
| responsePrefix: "PFX", | |
| ackReaction: "๐", | |
| ackReactionScope: "group-mentions", | |
| }, | |
| channels: { | |
| slack: { | |
| dmPolicy: "open", | |
| allowFrom: ["*"], | |
| dm: { enabled: true }, | |
| replyToMode: "off", | |
| }, | |
| }, | |
| }; | |
| await runSlackMessageOnce(monitorSlackProvider, { | |
| event: makeSlackMessageEvent({ | |
| ts: "789", | |
| }), | |
| }); | |
| expectSingleSendWithThread(undefined); | |
| }); | |
| it("keeps replyToId directive threading when replyToMode is all", async () => { | |
| replyMock.mockResolvedValue({ text: "forced reply", replyToId: "555" }); | |
| setDirectMessageReplyMode("all"); | |
| await runSlackMessageOnce(monitorSlackProvider, { | |
| event: makeSlackMessageEvent({ | |
| ts: "789", | |
| }), | |
| }); | |
| expectSingleSendWithThread("555"); | |
| }); | |
| it("reacts to mention-gated room messages when ackReaction is enabled", async () => { | |
| replyMock.mockResolvedValue(undefined); | |
| const client = getSlackClient(); | |
| if (!client) { | |
| throw new Error("Slack client not registered"); | |
| } | |
| const conversations = client.conversations as { | |
| info: ReturnType<typeof vi.fn>; | |
| }; | |
| conversations.info.mockResolvedValueOnce({ | |
| channel: { name: "general", is_channel: true }, | |
| }); | |
| await runSlackMessageOnce(monitorSlackProvider, { | |
| event: makeSlackMessageEvent({ | |
| text: "<@bot-user> hello", | |
| ts: "456", | |
| channel_type: "channel", | |
| }), | |
| }); | |
| expect(reactMock).toHaveBeenCalledWith({ | |
| channel: "C1", | |
| timestamp: "456", | |
| name: "๐", | |
| }); | |
| }); | |
| it("replies with pairing code when dmPolicy is pairing and no allowFrom is set", async () => { | |
| setPairingOnlyDirectMessages(); | |
| await runSlackMessageOnce(monitorSlackProvider, { | |
| event: makeSlackMessageEvent(), | |
| }); | |
| expect(replyMock).not.toHaveBeenCalled(); | |
| expect(upsertPairingRequestMock).toHaveBeenCalled(); | |
| expect(sendMock).toHaveBeenCalledTimes(1); | |
| expect(sendMock.mock.calls[0]?.[1]).toContain("Your Slack user id: U1"); | |
| expect(sendMock.mock.calls[0]?.[1]).toContain("Pairing code: PAIRCODE"); | |
| }); | |
| it("does not resend pairing code when a request is already pending", async () => { | |
| setPairingOnlyDirectMessages(); | |
| upsertPairingRequestMock | |
| .mockResolvedValueOnce({ code: "PAIRCODE", created: true }) | |
| .mockResolvedValueOnce({ code: "PAIRCODE", created: false }); | |
| const { controller, run } = startSlackMonitor(monitorSlackProvider); | |
| const handler = await getSlackHandlerOrThrow("message"); | |
| const baseEvent = makeSlackMessageEvent(); | |
| await handler({ event: baseEvent }); | |
| await handler({ event: { ...baseEvent, ts: "124", text: "hello again" } }); | |
| await stopSlackMonitor({ controller, run }); | |
| expect(sendMock).toHaveBeenCalledTimes(1); | |
| }); | |
| it("threads top-level replies when replyToMode is all", async () => { | |
| replyMock.mockResolvedValue({ text: "thread reply" }); | |
| setDirectMessageReplyMode("all"); | |
| await runDirectMessageEvent("123"); | |
| expectSingleSendWithThread("123"); | |
| }); | |
| it("treats parent_user_id as a thread reply even when thread_ts matches ts", async () => { | |
| replyMock.mockResolvedValue({ text: "thread reply" }); | |
| await runSlackMessageOnce(monitorSlackProvider, { | |
| event: makeSlackMessageEvent({ | |
| thread_ts: "123", | |
| parent_user_id: "U2", | |
| }), | |
| }); | |
| expect(replyMock).toHaveBeenCalledTimes(1); | |
| const ctx = getFirstReplySessionCtx(); | |
| expect(ctx.SessionKey).toBe("agent:main:main:thread:123"); | |
| expect(ctx.ParentSessionKey).toBeUndefined(); | |
| }); | |
| it("keeps thread parent inheritance opt-in", async () => { | |
| replyMock.mockResolvedValue({ text: "thread reply" }); | |
| setOpenChannelDirectMessages({ threadInheritParent: true }); | |
| await runSlackMessageOnce(monitorSlackProvider, { | |
| event: makeSlackMessageEvent({ | |
| thread_ts: "111.222", | |
| channel_type: "channel", | |
| }), | |
| }); | |
| expect(replyMock).toHaveBeenCalledTimes(1); | |
| const ctx = getFirstReplySessionCtx(); | |
| expect(ctx.SessionKey).toBe("agent:main:slack:channel:c1:thread:111.222"); | |
| expect(ctx.ParentSessionKey).toBe("agent:main:slack:channel:c1"); | |
| }); | |
| it("injects starter context for thread replies", async () => { | |
| replyMock.mockResolvedValue({ text: "ok" }); | |
| const client = getSlackClient(); | |
| if (client?.conversations?.info) { | |
| client.conversations.info.mockResolvedValue({ | |
| channel: { name: "general", is_channel: true }, | |
| }); | |
| } | |
| if (client?.conversations?.replies) { | |
| client.conversations.replies.mockResolvedValue({ | |
| messages: [{ text: "starter message", user: "U2", ts: "111.222" }], | |
| }); | |
| } | |
| setOpenChannelDirectMessages(); | |
| await runChannelThreadReplyEvent(); | |
| expect(replyMock).toHaveBeenCalledTimes(1); | |
| const ctx = getFirstReplySessionCtx(); | |
| expect(ctx.SessionKey).toBe("agent:main:slack:channel:c1:thread:111.222"); | |
| expect(ctx.ParentSessionKey).toBeUndefined(); | |
| expect(ctx.ThreadStarterBody).toContain("starter message"); | |
| expect(ctx.ThreadLabel).toContain("Slack thread #general"); | |
| }); | |
| it("scopes thread session keys to the routed agent", async () => { | |
| replyMock.mockResolvedValue({ text: "ok" }); | |
| setOpenChannelDirectMessages({ | |
| bindings: [{ agentId: "support", match: { channel: "slack", teamId: "T1" } }], | |
| }); | |
| const client = getSlackClient(); | |
| if (client?.auth?.test) { | |
| client.auth.test.mockResolvedValue({ | |
| user_id: "bot-user", | |
| team_id: "T1", | |
| }); | |
| } | |
| if (client?.conversations?.info) { | |
| client.conversations.info.mockResolvedValue({ | |
| channel: { name: "general", is_channel: true }, | |
| }); | |
| } | |
| await runChannelThreadReplyEvent(); | |
| expect(replyMock).toHaveBeenCalledTimes(1); | |
| const ctx = getFirstReplySessionCtx(); | |
| expect(ctx.SessionKey).toBe("agent:support:slack:channel:c1:thread:111.222"); | |
| expect(ctx.ParentSessionKey).toBeUndefined(); | |
| }); | |
| it("keeps replies in channel root when message is not threaded (replyToMode off)", async () => { | |
| replyMock.mockResolvedValue({ text: "root reply" }); | |
| setDirectMessageReplyMode("off"); | |
| await runDirectMessageEvent("789"); | |
| expectSingleSendWithThread(undefined); | |
| }); | |
| it("threads first reply when replyToMode is first and message is not threaded", async () => { | |
| replyMock.mockResolvedValue({ text: "first reply" }); | |
| setDirectMessageReplyMode("first"); | |
| await runDirectMessageEvent("789"); | |
| expectSingleSendWithThread("789"); | |
| }); | |
| }); | |