mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
399 lines
12 KiB
TypeScript
399 lines
12 KiB
TypeScript
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
|
import {
|
|
createPluginStateKeyedStoreForTests,
|
|
createPluginStateSyncKeyedStoreForTests,
|
|
resetPluginStateStoreForTests,
|
|
} from "openclaw/plugin-sdk/plugin-state-test-runtime";
|
|
import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { buildTelegramGroupPeerId } from "./bot/helpers.js";
|
|
import { recordTelegramGroupHistoryEntry } from "./group-history-window.js";
|
|
import { resolveTelegramMessageCacheScope } from "./message-cache-persistence.js";
|
|
import {
|
|
createTelegramMessageCache,
|
|
hasProviderObservedTelegramThreadBinding,
|
|
} from "./message-cache.js";
|
|
import {
|
|
recordOutboundMessageForPromptContext,
|
|
registerTelegramOutboundGroupHistoryRecorder,
|
|
} from "./outbound-message-context.js";
|
|
import { setTelegramRuntime } from "./runtime.js";
|
|
import {
|
|
clearTelegramRuntimeForTest as clearTelegramRuntime,
|
|
resetTelegramMessageCacheForTest as resetTelegramMessageCacheBucketsForTest,
|
|
} from "./runtime.test-support.js";
|
|
import type { TelegramRuntime } from "./runtime.types.js";
|
|
|
|
const cfg = {
|
|
session: { store: "/tmp/openclaw-telegram-outbound-context-test.json" },
|
|
} satisfies OpenClawConfig;
|
|
|
|
function installTelegramStateRuntimeForTest(): void {
|
|
setTelegramRuntime({
|
|
state: {
|
|
openKeyedStore: ((options) =>
|
|
createPluginStateKeyedStoreForTests(
|
|
"telegram",
|
|
options,
|
|
)) as TelegramRuntime["state"]["openKeyedStore"],
|
|
openSyncKeyedStore: ((options) =>
|
|
createPluginStateSyncKeyedStoreForTests(
|
|
"telegram",
|
|
options,
|
|
)) as TelegramRuntime["state"]["openSyncKeyedStore"],
|
|
},
|
|
channel: {},
|
|
} as TelegramRuntime);
|
|
}
|
|
|
|
async function recordAndRead(
|
|
params: Omit<Parameters<typeof recordOutboundMessageForPromptContext>[0], "cfg">,
|
|
) {
|
|
await recordOutboundMessageForPromptContext({ cfg, ...params });
|
|
const cache = createPromptContextCache();
|
|
return await cache.get({
|
|
accountId: params.account.accountId,
|
|
chatId: params.chatId,
|
|
messageId: String(params.messageId),
|
|
});
|
|
}
|
|
|
|
function createPromptContextCache() {
|
|
return createTelegramMessageCache({
|
|
scope: resolveTelegramMessageCacheScope(resolveStorePath(cfg.session.store)),
|
|
});
|
|
}
|
|
|
|
describe("recordOutboundMessageForPromptContext", () => {
|
|
beforeEach(() => {
|
|
resetPluginStateStoreForTests();
|
|
resetTelegramMessageCacheBucketsForTest();
|
|
installTelegramStateRuntimeForTest();
|
|
});
|
|
|
|
afterEach(() => {
|
|
clearTelegramRuntime();
|
|
resetTelegramMessageCacheBucketsForTest();
|
|
resetPluginStateStoreForTests();
|
|
});
|
|
|
|
it("uses the configured self name and drops stale Telegram display-name fields", async () => {
|
|
const cached = await recordAndRead({
|
|
account: { accountId: "default", name: " Configured Agent " },
|
|
chatId: 42,
|
|
message: {
|
|
chat: { id: 42, type: "private" },
|
|
date: 1_736_380_700,
|
|
from: {
|
|
id: 999,
|
|
is_bot: true,
|
|
first_name: "Provisioning",
|
|
last_name: "Placeholder",
|
|
username: "openclaw_bot",
|
|
},
|
|
message_id: 700,
|
|
text: "Bot just replied",
|
|
},
|
|
messageId: 700,
|
|
text: "Bot just replied",
|
|
});
|
|
|
|
expect(cached).toMatchObject({
|
|
sender: "Configured Agent (you)",
|
|
senderId: "999",
|
|
senderUsername: "openclaw_bot",
|
|
sourceMessage: {
|
|
from: {
|
|
id: 999,
|
|
is_bot: true,
|
|
first_name: "Configured Agent (you)",
|
|
username: "openclaw_bot",
|
|
},
|
|
},
|
|
});
|
|
expect(cached?.sourceMessage.from).not.toHaveProperty("last_name");
|
|
});
|
|
|
|
it("binds topics only when the successful provider response identifies the thread", async () => {
|
|
const common = {
|
|
account: { accountId: "default", name: "Configured Agent" },
|
|
chatId: -1001,
|
|
messageId: 700,
|
|
text: "Bot just replied",
|
|
messageThreadId: 77,
|
|
} as const;
|
|
const callerOnlyThread = await recordAndRead({
|
|
...common,
|
|
successfulSendThread: { id: 77, scope: "forum" },
|
|
message: {
|
|
chat: { id: -1001, type: "supergroup", title: "QA" },
|
|
date: 1_736_380_700,
|
|
from: { id: 999, is_bot: true, first_name: "OpenClaw" },
|
|
message_id: 700,
|
|
text: "Bot just replied",
|
|
},
|
|
});
|
|
expect(hasProviderObservedTelegramThreadBinding(callerOnlyThread, 77)).toBe(false);
|
|
|
|
const providerThread = await recordAndRead({
|
|
...common,
|
|
messageId: 701,
|
|
successfulSendThread: { id: 77, scope: "forum" },
|
|
message: {
|
|
chat: { id: -1001, type: "supergroup", title: "QA" },
|
|
date: 1_736_380_701,
|
|
from: { id: 999, is_bot: true, first_name: "OpenClaw" },
|
|
message_id: 701,
|
|
message_thread_id: 77,
|
|
text: "Bot replied in the topic",
|
|
},
|
|
});
|
|
expect(hasProviderObservedTelegramThreadBinding(providerThread, 77)).toBe(true);
|
|
});
|
|
|
|
it("records the successful channel Direct Messages spec ahead of raw message_thread_id", async () => {
|
|
const cached = await recordAndRead({
|
|
account: { accountId: "default", name: "Configured Agent" },
|
|
chatId: -1002,
|
|
messageId: 704,
|
|
messageThreadId: 999,
|
|
successfulSendThread: { id: 77, scope: "direct-messages" },
|
|
message: {
|
|
chat: {
|
|
id: -1002,
|
|
type: "supergroup",
|
|
title: "Channel replies",
|
|
},
|
|
date: 1_736_380_704,
|
|
from: { id: 999, is_bot: true, first_name: "OpenClaw" },
|
|
message_id: 704,
|
|
message_thread_id: 999,
|
|
direct_messages_topic: { topic_id: 77 },
|
|
text: "Bot replied in channel Direct Messages",
|
|
},
|
|
});
|
|
|
|
expect(cached?.threadId).toBe("77");
|
|
expect(cached?.threadBinding?.threadSpec).toEqual({ scope: "direct-messages", id: 77 });
|
|
});
|
|
|
|
it("records forum and channel Direct Messages replies with the same topic ID in separate histories", async () => {
|
|
const chatId = -1001;
|
|
const history = new Map<string, Array<{ sender: string; body: string; messageId: string }>>();
|
|
const unregister = registerTelegramOutboundGroupHistoryRecorder({
|
|
accountId: "default",
|
|
recorder: (record) =>
|
|
recordTelegramGroupHistoryEntry({
|
|
historyMap: history,
|
|
historyKey: buildTelegramGroupPeerId(record.chatId, record.threadSpec),
|
|
limit: 10,
|
|
entry: {
|
|
sender: "Configured Agent (you)",
|
|
body: record.text ?? "<media>",
|
|
messageId: String(record.messageId),
|
|
},
|
|
}),
|
|
});
|
|
|
|
try {
|
|
for (const { scope, messageId, body } of [
|
|
{ scope: "forum", messageId: 710, body: "Forum reply" },
|
|
{ scope: "direct-messages", messageId: 711, body: "Direct-topic reply" },
|
|
] as const) {
|
|
await recordOutboundMessageForPromptContext({
|
|
cfg,
|
|
account: { accountId: "default", name: "Configured Agent" },
|
|
chatId,
|
|
messageId,
|
|
messageThreadId: 77,
|
|
successfulSendThread: { scope, id: 77 },
|
|
message: {
|
|
chat: { id: chatId, type: "supergroup" },
|
|
date: 1_736_380_700,
|
|
message_id: messageId,
|
|
...(scope === "forum"
|
|
? { message_thread_id: 77 }
|
|
: { direct_messages_topic: { topic_id: 77 } }),
|
|
text: body,
|
|
},
|
|
});
|
|
}
|
|
|
|
expect(history.get("-1001:direct-topic:77")).toEqual([
|
|
expect.objectContaining({ body: "Direct-topic reply", messageId: "711" }),
|
|
]);
|
|
expect(history.get("-1001:topic:77")).toEqual([
|
|
expect.objectContaining({ body: "Forum reply", messageId: "710" }),
|
|
]);
|
|
} finally {
|
|
unregister();
|
|
}
|
|
});
|
|
|
|
it("binds a successful General-topic response from trusted send context", async () => {
|
|
const cached = await recordAndRead({
|
|
account: { accountId: "default", name: "Configured Agent" },
|
|
chatId: -1001,
|
|
message: {
|
|
chat: { id: -1001, type: "supergroup", title: "QA" },
|
|
date: 1_736_380_700,
|
|
from: { id: 999, is_bot: true, first_name: "OpenClaw" },
|
|
message_id: 702,
|
|
text: "Bot replied in General",
|
|
},
|
|
messageId: 702,
|
|
messageThreadId: 1,
|
|
successfulSendThread: { id: 1, scope: "forum" },
|
|
});
|
|
|
|
expect(hasProviderObservedTelegramThreadBinding(cached, 1)).toBe(true);
|
|
});
|
|
|
|
it("does not infer a General-topic binding for DM thread context", async () => {
|
|
const cached = await recordAndRead({
|
|
account: { accountId: "default", name: "Configured Agent" },
|
|
chatId: 42,
|
|
message: {
|
|
chat: { id: 42, type: "private" },
|
|
date: 1_736_380_700,
|
|
from: { id: 999, is_bot: true, first_name: "OpenClaw" },
|
|
message_id: 703,
|
|
text: "Bot replied in a DM topic",
|
|
},
|
|
messageId: 703,
|
|
messageThreadId: 1,
|
|
successfulSendThread: { id: 1, scope: "dm" },
|
|
});
|
|
|
|
expect(hasProviderObservedTelegramThreadBinding(cached, 1)).toBe(false);
|
|
});
|
|
|
|
it("falls back to the Telegram bot name when no configured name exists", async () => {
|
|
const cached = await recordAndRead({
|
|
account: { accountId: "default", name: "" },
|
|
chatId: 42,
|
|
message: {
|
|
chat: { id: 42, type: "private" },
|
|
date: 1_736_380_700,
|
|
from: {
|
|
id: 999,
|
|
is_bot: true,
|
|
first_name: "Atlas",
|
|
username: "atlas_bot",
|
|
},
|
|
message_id: 701,
|
|
text: "Bot just replied",
|
|
},
|
|
messageId: 701,
|
|
text: "Bot just replied",
|
|
});
|
|
|
|
expect(cached).toMatchObject({
|
|
sender: "Atlas (you)",
|
|
senderId: "999",
|
|
senderUsername: "atlas_bot",
|
|
});
|
|
});
|
|
|
|
it("preserves the sending bot identity for Telegram Business messages", async () => {
|
|
const cached = await recordAndRead({
|
|
account: { accountId: "default", name: "Configured Agent" },
|
|
chatId: 42,
|
|
message: {
|
|
chat: { id: 42, type: "private" },
|
|
date: 1_736_380_700,
|
|
from: {
|
|
id: 777,
|
|
is_bot: false,
|
|
first_name: "Business Account",
|
|
username: "business_account",
|
|
},
|
|
sender_business_bot: {
|
|
id: 999,
|
|
is_bot: true,
|
|
first_name: "Telegram Bot Name",
|
|
username: "openclaw_bot",
|
|
},
|
|
message_id: 702,
|
|
text: "Business reply",
|
|
},
|
|
messageId: 702,
|
|
text: "Business reply",
|
|
});
|
|
|
|
expect(cached).toMatchObject({
|
|
sender: "Configured Agent (you)",
|
|
senderId: "777",
|
|
senderUsername: "business_account",
|
|
sourceMessage: {
|
|
sender_business_bot: { id: 999, is_bot: true, username: "openclaw_bot" },
|
|
},
|
|
});
|
|
});
|
|
|
|
it("uses the synthetic sender identity for a finalized streamed message without from", async () => {
|
|
const initial = await recordAndRead({
|
|
account: { accountId: "default", name: "StreamBot" },
|
|
chatId: 42,
|
|
message: { message_id: 1497 },
|
|
messageId: 1497,
|
|
text: "Final streamed reply",
|
|
});
|
|
|
|
expect(initial).toMatchObject({
|
|
sender: "StreamBot (you)",
|
|
senderId: "0",
|
|
sourceMessage: {
|
|
from: {
|
|
id: 0,
|
|
is_bot: true,
|
|
first_name: "StreamBot (you)",
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("ignores Telegram's fake sender identity across channel post echoes", async () => {
|
|
const initial = await recordAndRead({
|
|
account: { accountId: "default" },
|
|
chatId: -1001,
|
|
message: {
|
|
message_id: 1498,
|
|
from: { id: 777_000, is_bot: false, first_name: "Telegram" },
|
|
sender_chat: { id: -1001, title: "Announcements" },
|
|
},
|
|
messageId: 1498,
|
|
text: "Channel announcement",
|
|
});
|
|
expect(initial).toMatchObject({ sender: "OpenClaw (you)", senderId: "0" });
|
|
|
|
const cache = createPromptContextCache();
|
|
await cache.record({
|
|
accountId: "default",
|
|
chatId: -1001,
|
|
msg: {
|
|
message_id: 1498,
|
|
date: 1_736_380_900,
|
|
chat: { id: -1001, type: "supergroup", title: "Announcements" },
|
|
from: { id: 777_000, is_bot: false, first_name: "Telegram" },
|
|
sender_chat: { id: -1001, type: "channel", title: "Announcements" },
|
|
text: "Channel announcement",
|
|
},
|
|
});
|
|
|
|
const merged = await cache.get({
|
|
accountId: "default",
|
|
chatId: -1001,
|
|
messageId: "1498",
|
|
});
|
|
expect(merged).toMatchObject({
|
|
sender: "OpenClaw (you)",
|
|
senderId: "0",
|
|
sourceMessage: {
|
|
from: { id: 0, is_bot: true, first_name: "OpenClaw (you)" },
|
|
sender_chat: { id: -1001, type: "channel", title: "Announcements" },
|
|
},
|
|
});
|
|
});
|
|
});
|