openclaw/src/infra/outbound/message-action-runner.threading.test.ts
Clawdbot c5267396a4 fix(telegram): inject toolContext threadId into message tool params
When using the message tool in a Telegram DM with topics enabled,
media and text were sent to the General topic instead of the current
session topic. This happened because toolContext.currentThreadTs was
not mapped to params.threadId before the Telegram plugin action
handler read it.

Now runMessageAction injects toolContext.currentThreadTs into
params.threadId when not explicitly provided, ensuring messages
land in the correct topic.

Fixes #2777
2026-01-27 14:55:50 +01:00

228 lines
6.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { MoltbotConfig } from "../../config/config.js";
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
import { slackPlugin } from "../../../extensions/slack/src/channel.js";
import { telegramPlugin } from "../../../extensions/telegram/src/channel.js";
const mocks = vi.hoisted(() => ({
executeSendAction: vi.fn(),
recordSessionMetaFromInbound: vi.fn(async () => ({ ok: true })),
}));
vi.mock("./outbound-send-service.js", async () => {
const actual = await vi.importActual<typeof import("./outbound-send-service.js")>(
"./outbound-send-service.js",
);
return {
...actual,
executeSendAction: mocks.executeSendAction,
};
});
vi.mock("../../config/sessions.js", async () => {
const actual = await vi.importActual<typeof import("../../config/sessions.js")>(
"../../config/sessions.js",
);
return {
...actual,
recordSessionMetaFromInbound: mocks.recordSessionMetaFromInbound,
};
});
import { runMessageAction } from "./message-action-runner.js";
const slackConfig = {
channels: {
slack: {
botToken: "xoxb-test",
appToken: "xapp-test",
},
},
} as MoltbotConfig;
describe("runMessageAction Slack threading", () => {
beforeEach(async () => {
const { createPluginRuntime } = await import("../../plugins/runtime/index.js");
const { setSlackRuntime } = await import("../../../extensions/slack/src/runtime.js");
const runtime = createPluginRuntime();
setSlackRuntime(runtime);
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "slack",
source: "test",
plugin: slackPlugin,
},
]),
);
});
afterEach(() => {
setActivePluginRegistry(createTestRegistry([]));
mocks.executeSendAction.mockReset();
mocks.recordSessionMetaFromInbound.mockReset();
});
it("uses toolContext thread when auto-threading is active", async () => {
mocks.executeSendAction.mockResolvedValue({
handledBy: "plugin",
payload: {},
});
await runMessageAction({
cfg: slackConfig,
action: "send",
params: {
channel: "slack",
target: "channel:C123",
message: "hi",
},
toolContext: {
currentChannelId: "C123",
currentThreadTs: "111.222",
replyToMode: "all",
},
agentId: "main",
});
const call = mocks.executeSendAction.mock.calls[0]?.[0];
expect(call?.ctx?.mirror?.sessionKey).toBe("agent:main:slack:channel:c123:thread:111.222");
});
it("matches auto-threading when channel ids differ in case", async () => {
mocks.executeSendAction.mockResolvedValue({
handledBy: "plugin",
payload: {},
});
await runMessageAction({
cfg: slackConfig,
action: "send",
params: {
channel: "slack",
target: "channel:c123",
message: "hi",
},
toolContext: {
currentChannelId: "C123",
currentThreadTs: "333.444",
replyToMode: "all",
},
agentId: "main",
});
const call = mocks.executeSendAction.mock.calls[0]?.[0];
expect(call?.ctx?.mirror?.sessionKey).toBe("agent:main:slack:channel:c123:thread:333.444");
});
});
const telegramConfig = {
channels: {
telegram: {
enabled: true,
botToken: "test:token",
},
},
} as MoltbotConfig;
describe("runMessageAction thread id injection from toolContext", () => {
beforeEach(async () => {
const { createPluginRuntime } = await import("../../plugins/runtime/index.js");
const { setTelegramRuntime } = await import("../../../extensions/telegram/src/runtime.js");
const runtime = createPluginRuntime();
setTelegramRuntime(runtime);
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "telegram",
source: "test",
plugin: telegramPlugin,
},
]),
);
});
afterEach(() => {
setActivePluginRegistry(createTestRegistry([]));
mocks.executeSendAction.mockReset();
mocks.recordSessionMetaFromInbound.mockReset();
});
it("injects toolContext.currentThreadTs into params.threadId when not explicitly set", async () => {
mocks.executeSendAction.mockResolvedValue({
handledBy: "plugin",
payload: { ok: true, messageId: "123", chatId: "63448508" },
});
await runMessageAction({
cfg: telegramConfig,
action: "send",
params: {
channel: "telegram",
target: "63448508",
message: "hello from topic",
},
toolContext: {
currentChannelId: "63448508",
currentChannelProvider: "telegram",
currentThreadTs: "994409",
},
});
const call = mocks.executeSendAction.mock.calls[0]?.[0];
expect(call?.ctx?.params?.threadId).toBe("994409");
});
it("does not override explicit threadId with toolContext", async () => {
mocks.executeSendAction.mockResolvedValue({
handledBy: "plugin",
payload: { ok: true, messageId: "124", chatId: "63448508" },
});
await runMessageAction({
cfg: telegramConfig,
action: "send",
params: {
channel: "telegram",
target: "63448508",
message: "explicit thread",
threadId: "12345",
},
toolContext: {
currentChannelId: "63448508",
currentChannelProvider: "telegram",
currentThreadTs: "994409",
},
});
const call = mocks.executeSendAction.mock.calls[0]?.[0];
expect(call?.ctx?.params?.threadId).toBe("12345");
});
it("does not inject threadId when toolContext has no currentThreadTs", async () => {
mocks.executeSendAction.mockResolvedValue({
handledBy: "plugin",
payload: { ok: true, messageId: "125", chatId: "63448508" },
});
await runMessageAction({
cfg: telegramConfig,
action: "send",
params: {
channel: "telegram",
target: "63448508",
message: "no thread context",
},
toolContext: {
currentChannelId: "63448508",
currentChannelProvider: "telegram",
},
});
const call = mocks.executeSendAction.mock.calls[0]?.[0];
expect(call?.ctx?.params?.threadId).toBeUndefined();
});
});