mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-14 23:00:47 +00:00
796 lines
24 KiB
TypeScript
796 lines
24 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
buildTelegramRoutingTarget,
|
|
buildTelegramThreadParams,
|
|
buildTypingThreadParams,
|
|
describeReplyTarget,
|
|
expandTextLinks,
|
|
getTelegramTextParts,
|
|
hasBotMention,
|
|
isBinaryContent,
|
|
normalizeForwardedContext,
|
|
resolveTelegramDirectPeerId,
|
|
resolveTelegramForumFlag,
|
|
resolveTelegramForumThreadId,
|
|
resetTelegramForumFlagCacheForTest,
|
|
shouldUseTelegramDmThreadSession,
|
|
} from "./helpers.js";
|
|
|
|
describe("resolveTelegramForumThreadId", () => {
|
|
it.each([
|
|
{ isForum: false, messageThreadId: 42 },
|
|
{ isForum: false, messageThreadId: undefined },
|
|
{ isForum: undefined, messageThreadId: 99 },
|
|
])("returns undefined for non-forum groups", (params) => {
|
|
// Reply threads in regular groups should not create separate sessions.
|
|
expect(resolveTelegramForumThreadId(params)).toBeUndefined();
|
|
});
|
|
|
|
it.each([
|
|
{ isForum: true, messageThreadId: undefined, expected: 1 },
|
|
{ isForum: true, messageThreadId: null, expected: 1 },
|
|
{ isForum: true, messageThreadId: 99, expected: 99 },
|
|
])("resolves forum topic ids", ({ expected, ...params }) => {
|
|
expect(resolveTelegramForumThreadId(params)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe("resolveTelegramForumFlag", () => {
|
|
beforeEach(() => {
|
|
resetTelegramForumFlagCacheForTest();
|
|
});
|
|
|
|
it("keeps explicit forum metadata when Telegram already provides it", async () => {
|
|
const getChat = vi.fn(async () => ({ is_forum: false }));
|
|
await expect(
|
|
resolveTelegramForumFlag({
|
|
chatId: -100123,
|
|
chatType: "supergroup",
|
|
isGroup: true,
|
|
isForum: true,
|
|
getChat,
|
|
}),
|
|
).resolves.toBe(true);
|
|
expect(getChat).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("falls back to getChat for supergroups when is_forum is omitted", async () => {
|
|
const getChat = vi.fn(async () => ({ is_forum: true }));
|
|
await expect(
|
|
resolveTelegramForumFlag({
|
|
chatId: -100789,
|
|
chatType: "supergroup",
|
|
isGroup: true,
|
|
getChat,
|
|
}),
|
|
).resolves.toBe(true);
|
|
expect(getChat).toHaveBeenCalledWith(-100789);
|
|
});
|
|
|
|
it("reuses resolved forum metadata for later supergroup updates", async () => {
|
|
const getChat = vi.fn(async () => ({ is_forum: true }));
|
|
const params = {
|
|
chatId: -100456,
|
|
chatType: "supergroup" as const,
|
|
isGroup: true,
|
|
getChat,
|
|
};
|
|
await expect(resolveTelegramForumFlag(params)).resolves.toBe(true);
|
|
await expect(resolveTelegramForumFlag(params)).resolves.toBe(true);
|
|
expect(getChat).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("refreshes cached forum metadata from explicit Telegram updates", async () => {
|
|
const getChat = vi.fn(async () => ({ is_forum: true }));
|
|
const params = {
|
|
chatId: -100654,
|
|
chatType: "supergroup" as const,
|
|
isGroup: true,
|
|
getChat,
|
|
};
|
|
await expect(resolveTelegramForumFlag(params)).resolves.toBe(true);
|
|
await expect(resolveTelegramForumFlag({ ...params, isForum: false })).resolves.toBe(false);
|
|
await expect(resolveTelegramForumFlag(params)).resolves.toBe(false);
|
|
expect(getChat).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("returns false when forum lookup is unavailable", async () => {
|
|
const getChat = vi.fn(async () => {
|
|
throw new Error("lookup failed");
|
|
});
|
|
await expect(
|
|
resolveTelegramForumFlag({
|
|
chatId: -100999,
|
|
chatType: "supergroup",
|
|
isGroup: true,
|
|
getChat,
|
|
}),
|
|
).resolves.toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("buildTelegramThreadParams", () => {
|
|
it.each([
|
|
{ input: { id: 1, scope: "forum" as const }, expected: undefined },
|
|
{ input: { id: 99, scope: "forum" as const }, expected: { message_thread_id: 99 } },
|
|
{ input: { id: 1, scope: "dm" as const }, expected: { message_thread_id: 1 } },
|
|
{ input: { id: 2, scope: "dm" as const }, expected: { message_thread_id: 2 } },
|
|
{ input: { id: 0, scope: "dm" as const }, expected: undefined },
|
|
{ input: { id: -1, scope: "dm" as const }, expected: undefined },
|
|
{ input: { id: 1.9, scope: "dm" as const }, expected: { message_thread_id: 1 } },
|
|
// id=0 should be included for forum and none scopes (not falsy)
|
|
{ input: { id: 0, scope: "forum" as const }, expected: { message_thread_id: 0 } },
|
|
{ input: { id: 0, scope: "none" as const }, expected: { message_thread_id: 0 } },
|
|
])("builds thread params", ({ input, expected }) => {
|
|
expect(buildTelegramThreadParams(input)).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe("shouldUseTelegramDmThreadSession", () => {
|
|
it("keeps incidental DM thread ids flat by default", () => {
|
|
expect(shouldUseTelegramDmThreadSession({ dmThreadId: 42 })).toBe(false);
|
|
});
|
|
|
|
it("uses DM thread sessions for explicit or topic-required configs", () => {
|
|
expect(
|
|
shouldUseTelegramDmThreadSession({
|
|
dmThreadId: 42,
|
|
directConfig: { threadReplies: "inbound" },
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldUseTelegramDmThreadSession({
|
|
dmThreadId: 42,
|
|
directConfig: { requireTopic: true },
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldUseTelegramDmThreadSession({
|
|
dmThreadId: 42,
|
|
topicConfig: { agentId: "support" },
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("buildTelegramRoutingTarget", () => {
|
|
it.each([
|
|
{
|
|
name: "keeps General forum topic chat-scoped",
|
|
chatId: -100123,
|
|
thread: { id: 1, scope: "forum" as const },
|
|
expected: "telegram:-100123",
|
|
},
|
|
{
|
|
name: "includes real forum topic ids",
|
|
chatId: -100123,
|
|
thread: { id: 42, scope: "forum" as const },
|
|
expected: "telegram:-100123:topic:42",
|
|
},
|
|
{
|
|
name: "falls back to bare chat when thread is missing",
|
|
chatId: -100123,
|
|
thread: null,
|
|
expected: "telegram:-100123",
|
|
},
|
|
])("$name", ({ chatId, thread, expected }) => {
|
|
expect(buildTelegramRoutingTarget(chatId, thread)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe("buildTypingThreadParams", () => {
|
|
it.each([
|
|
{ input: undefined, expected: undefined },
|
|
{ input: 1, expected: { message_thread_id: 1 } },
|
|
])("builds typing params", ({ input, expected }) => {
|
|
expect(buildTypingThreadParams(input)).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe("resolveTelegramDirectPeerId", () => {
|
|
it("prefers sender id when available", () => {
|
|
expect(resolveTelegramDirectPeerId({ chatId: 777777777, senderId: 123456789 })).toBe(
|
|
"123456789",
|
|
);
|
|
});
|
|
|
|
it("falls back to chat id when sender id is missing", () => {
|
|
expect(resolveTelegramDirectPeerId({ chatId: 777777777, senderId: undefined })).toBe(
|
|
"777777777",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("thread id normalization", () => {
|
|
it.each([
|
|
{
|
|
build: () => buildTelegramThreadParams({ id: 42.9, scope: "forum" }),
|
|
expected: { message_thread_id: 42 },
|
|
},
|
|
{
|
|
build: () => buildTypingThreadParams(42.9),
|
|
expected: { message_thread_id: 42 },
|
|
},
|
|
])("normalizes thread ids to integers", ({ build, expected }) => {
|
|
expect(build()).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe("normalizeForwardedContext", () => {
|
|
it("handles forward_origin users", () => {
|
|
const ctx = normalizeForwardedContext({
|
|
forward_origin: {
|
|
type: "user",
|
|
sender_user: { first_name: "Ada", last_name: "Lovelace", username: "ada", id: 42 },
|
|
date: 123,
|
|
},
|
|
} as any);
|
|
expect(ctx?.from).toBe("Ada Lovelace (@ada)");
|
|
expect(ctx?.fromType).toBe("user");
|
|
expect(ctx?.fromId).toBe("42");
|
|
expect(ctx?.fromUsername).toBe("ada");
|
|
expect(ctx?.fromTitle).toBe("Ada Lovelace");
|
|
expect(ctx?.date).toBe(123);
|
|
});
|
|
|
|
it("handles hidden forward_origin names", () => {
|
|
const ctx = normalizeForwardedContext({
|
|
forward_origin: { type: "hidden_user", sender_user_name: "Hidden Name", date: 456 },
|
|
} as any);
|
|
expect(ctx?.from).toBe("Hidden Name");
|
|
expect(ctx?.fromType).toBe("hidden_user");
|
|
expect(ctx?.fromTitle).toBe("Hidden Name");
|
|
expect(ctx?.date).toBe(456);
|
|
});
|
|
|
|
it("handles forward_origin channel with author_signature and message_id", () => {
|
|
const ctx = normalizeForwardedContext({
|
|
forward_origin: {
|
|
type: "channel",
|
|
chat: {
|
|
title: "Tech News",
|
|
username: "technews",
|
|
id: -1001234,
|
|
type: "channel",
|
|
},
|
|
date: 500,
|
|
author_signature: "Editor",
|
|
message_id: 42,
|
|
},
|
|
} as any);
|
|
expect(ctx?.from).toBe("Tech News (Editor)");
|
|
expect(ctx?.fromType).toBe("channel");
|
|
expect(ctx?.fromId).toBe("-1001234");
|
|
expect(ctx?.fromUsername).toBe("technews");
|
|
expect(ctx?.fromTitle).toBe("Tech News");
|
|
expect(ctx?.fromSignature).toBe("Editor");
|
|
expect(ctx?.fromChatType).toBe("channel");
|
|
expect(ctx?.fromMessageId).toBe(42);
|
|
expect(ctx?.date).toBe(500);
|
|
});
|
|
|
|
it("handles forward_origin chat with sender_chat and author_signature", () => {
|
|
const ctx = normalizeForwardedContext({
|
|
forward_origin: {
|
|
type: "chat",
|
|
sender_chat: {
|
|
title: "Discussion Group",
|
|
id: -1005678,
|
|
type: "supergroup",
|
|
},
|
|
date: 600,
|
|
author_signature: "Admin",
|
|
},
|
|
} as any);
|
|
expect(ctx?.from).toBe("Discussion Group (Admin)");
|
|
expect(ctx?.fromType).toBe("chat");
|
|
expect(ctx?.fromId).toBe("-1005678");
|
|
expect(ctx?.fromTitle).toBe("Discussion Group");
|
|
expect(ctx?.fromSignature).toBe("Admin");
|
|
expect(ctx?.fromChatType).toBe("supergroup");
|
|
expect(ctx?.date).toBe(600);
|
|
});
|
|
|
|
it("uses author_signature from forward_origin", () => {
|
|
const ctx = normalizeForwardedContext({
|
|
forward_origin: {
|
|
type: "channel",
|
|
chat: { title: "My Channel", id: -100999, type: "channel" },
|
|
date: 700,
|
|
author_signature: "New Sig",
|
|
message_id: 1,
|
|
},
|
|
} as any);
|
|
expect(ctx?.fromSignature).toBe("New Sig");
|
|
expect(ctx?.from).toBe("My Channel (New Sig)");
|
|
});
|
|
|
|
it("returns undefined signature when author_signature is blank", () => {
|
|
const ctx = normalizeForwardedContext({
|
|
forward_origin: {
|
|
type: "channel",
|
|
chat: { title: "Updates", id: -100333, type: "channel" },
|
|
date: 860,
|
|
author_signature: " ",
|
|
message_id: 1,
|
|
},
|
|
} as any);
|
|
expect(ctx?.fromSignature).toBeUndefined();
|
|
expect(ctx?.from).toBe("Updates");
|
|
});
|
|
|
|
it("handles forward_origin channel without author_signature", () => {
|
|
const ctx = normalizeForwardedContext({
|
|
forward_origin: {
|
|
type: "channel",
|
|
chat: { title: "News", id: -100111, type: "channel" },
|
|
date: 900,
|
|
message_id: 1,
|
|
},
|
|
} as any);
|
|
expect(ctx?.from).toBe("News");
|
|
expect(ctx?.fromSignature).toBeUndefined();
|
|
expect(ctx?.fromChatType).toBe("channel");
|
|
});
|
|
});
|
|
|
|
describe("describeReplyTarget", () => {
|
|
it("returns null when no reply_to_message", () => {
|
|
const result = describeReplyTarget({
|
|
message_id: 1,
|
|
date: 1000,
|
|
chat: { id: 1, type: "private" },
|
|
} as any);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("extracts basic reply info", () => {
|
|
const result = describeReplyTarget({
|
|
message_id: 2,
|
|
date: 1000,
|
|
chat: { id: 1, type: "private" },
|
|
reply_to_message: {
|
|
message_id: 1,
|
|
date: 900,
|
|
chat: { id: 1, type: "private" },
|
|
text: "Original message",
|
|
from: { id: 42, first_name: "Alice", is_bot: false },
|
|
},
|
|
} as any);
|
|
expect(result?.body).toBe("Original message");
|
|
expect(result?.sender).toBe("Alice");
|
|
expect(result?.id).toBe("1");
|
|
expect(result?.kind).toBe("reply");
|
|
expect(result?.source).toBe("reply_to_message");
|
|
});
|
|
|
|
it("handles non-string reply text gracefully (issue #27201)", () => {
|
|
const result = describeReplyTarget({
|
|
message_id: 2,
|
|
date: 1000,
|
|
chat: { id: 1, type: "private" },
|
|
reply_to_message: {
|
|
message_id: 1,
|
|
date: 900,
|
|
chat: { id: 1, type: "private" },
|
|
// Simulate edge case where text is an unexpected non-string value
|
|
text: { some: "object" },
|
|
from: { id: 42, first_name: "Alice", is_bot: false },
|
|
},
|
|
} as any);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("falls back to caption when reply text is malformed", () => {
|
|
const result = describeReplyTarget({
|
|
message_id: 2,
|
|
date: 1000,
|
|
chat: { id: 1, type: "private" },
|
|
reply_to_message: {
|
|
message_id: 1,
|
|
date: 900,
|
|
chat: { id: 1, type: "private" },
|
|
text: { some: "object" },
|
|
caption: "Caption body",
|
|
from: { id: 42, first_name: "Alice", is_bot: false },
|
|
},
|
|
} as any);
|
|
expect(result?.body).toBe("Caption body");
|
|
expect(result?.kind).toBe("reply");
|
|
});
|
|
|
|
it("drops binary reply captions with no safe fallback", () => {
|
|
const result = describeReplyTarget({
|
|
message_id: 2,
|
|
date: 1000,
|
|
chat: { id: 1, type: "private" },
|
|
reply_to_message: {
|
|
message_id: 1,
|
|
date: 900,
|
|
chat: { id: 1, type: "private" },
|
|
caption: "PK\x00\x03\x04binary",
|
|
from: { id: 42, first_name: "Alice", is_bot: false },
|
|
},
|
|
} as any);
|
|
expect(result?.id).toBe("1");
|
|
expect(result?.sender).toBe("Alice");
|
|
expect(result?.body).toBeUndefined();
|
|
});
|
|
|
|
it("falls back to reply text when quote text is binary", () => {
|
|
const result = describeReplyTarget({
|
|
message_id: 2,
|
|
date: 1000,
|
|
chat: { id: 1, type: "private" },
|
|
quote: {
|
|
text: "\x00\x01\x02binary quote",
|
|
},
|
|
reply_to_message: {
|
|
message_id: 1,
|
|
date: 900,
|
|
chat: { id: 1, type: "private" },
|
|
text: "Original message",
|
|
from: { id: 42, first_name: "Alice", is_bot: false },
|
|
},
|
|
} as any);
|
|
expect(result?.body).toBe("Original message");
|
|
expect(result?.kind).toBe("reply");
|
|
});
|
|
|
|
it("falls back to external reply text when external quote text is binary", () => {
|
|
const result = describeReplyTarget({
|
|
message_id: 5,
|
|
date: 1300,
|
|
chat: { id: 1, type: "private" },
|
|
text: "Comment on forwarded message",
|
|
external_reply: {
|
|
message_id: 4,
|
|
date: 1200,
|
|
chat: { id: 1, type: "private" },
|
|
text: "Forwarded from elsewhere",
|
|
quote: {
|
|
text: "PK\x00\x03\x04binary quote",
|
|
},
|
|
from: { id: 123, first_name: "Eve", is_bot: false },
|
|
},
|
|
} as any);
|
|
expect(result?.body).toBe("Forwarded from elsewhere");
|
|
expect(result?.kind).toBe("reply");
|
|
});
|
|
|
|
it("extracts forwarded context from reply_to_message (issue #9619)", () => {
|
|
// When user forwards a message with a comment, the comment message has
|
|
// reply_to_message pointing to the forwarded message. We should extract
|
|
// the forward_origin from the reply target.
|
|
const result = describeReplyTarget({
|
|
message_id: 3,
|
|
date: 1100,
|
|
chat: { id: 1, type: "private" },
|
|
text: "Here is my comment about this forwarded content",
|
|
reply_to_message: {
|
|
message_id: 2,
|
|
date: 1000,
|
|
chat: { id: 1, type: "private" },
|
|
text: "This is the forwarded content",
|
|
forward_origin: {
|
|
type: "user",
|
|
sender_user: {
|
|
id: 999,
|
|
first_name: "Bob",
|
|
last_name: "Smith",
|
|
username: "bobsmith",
|
|
is_bot: false,
|
|
},
|
|
date: 500,
|
|
},
|
|
},
|
|
} as any);
|
|
expect(result?.body).toBe("This is the forwarded content");
|
|
expect(result?.id).toBe("2");
|
|
expect(result?.forwardedFrom).toMatchObject({
|
|
from: "Bob Smith (@bobsmith)",
|
|
fromType: "user",
|
|
fromId: "999",
|
|
date: 500,
|
|
});
|
|
});
|
|
|
|
it("extracts forwarded context from channel forward in reply_to_message", () => {
|
|
const result = describeReplyTarget({
|
|
message_id: 4,
|
|
date: 1200,
|
|
chat: { id: 1, type: "private" },
|
|
text: "Interesting article!",
|
|
reply_to_message: {
|
|
message_id: 3,
|
|
date: 1100,
|
|
chat: { id: 1, type: "private" },
|
|
text: "Channel post content here",
|
|
forward_origin: {
|
|
type: "channel",
|
|
chat: { id: -1001234567, title: "Tech News", username: "technews", type: "channel" },
|
|
date: 800,
|
|
message_id: 456,
|
|
author_signature: "Editor",
|
|
},
|
|
},
|
|
} as any);
|
|
expect(result?.forwardedFrom).toMatchObject({
|
|
from: "Tech News (Editor)",
|
|
fromType: "channel",
|
|
fromMessageId: 456,
|
|
});
|
|
});
|
|
|
|
it("marks top-level quote metadata on external replies as external targets", () => {
|
|
const result = describeReplyTarget({
|
|
message_id: 5,
|
|
date: 1300,
|
|
chat: { id: 1, type: "private" },
|
|
text: "Comment on forwarded message",
|
|
quote: {
|
|
text: "quoted slice",
|
|
position: 4,
|
|
entities: [{ type: "italic", offset: 0, length: 6 }],
|
|
},
|
|
external_reply: {
|
|
message_id: 4,
|
|
date: 1200,
|
|
chat: { id: 1, type: "private" },
|
|
text: "Forwarded from elsewhere",
|
|
from: { id: 123, first_name: "Eve", is_bot: false },
|
|
},
|
|
} as any);
|
|
|
|
expect(result?.id).toBe("4");
|
|
expect(result?.kind).toBe("quote");
|
|
expect(result?.source).toBe("external_reply");
|
|
expect(result?.quoteText).toBe("quoted slice");
|
|
expect(result?.quotePosition).toBe(4);
|
|
expect(result?.quoteEntities).toEqual([{ type: "italic", offset: 0, length: 6 }]);
|
|
});
|
|
|
|
it("extracts forwarded context from external_reply", () => {
|
|
const result = describeReplyTarget({
|
|
message_id: 5,
|
|
date: 1300,
|
|
chat: { id: 1, type: "private" },
|
|
text: "Comment on forwarded message",
|
|
external_reply: {
|
|
message_id: 4,
|
|
date: 1200,
|
|
chat: { id: 1, type: "private" },
|
|
text: "Forwarded from elsewhere",
|
|
forward_origin: {
|
|
type: "user",
|
|
sender_user: {
|
|
id: 123,
|
|
first_name: "Eve",
|
|
last_name: "Stone",
|
|
username: "eve",
|
|
is_bot: false,
|
|
},
|
|
date: 700,
|
|
},
|
|
},
|
|
} as any);
|
|
expect(result?.id).toBe("4");
|
|
expect(result?.forwardedFrom?.from).toBe("Eve Stone (@eve)");
|
|
expect(result?.forwardedFrom?.fromType).toBe("user");
|
|
expect(result?.forwardedFrom?.fromId).toBe("123");
|
|
expect(result?.forwardedFrom?.date).toBe(700);
|
|
});
|
|
});
|
|
|
|
describe("isBinaryContent", () => {
|
|
it("returns false for normal user text", () => {
|
|
expect(isBinaryContent("Hello, world!")).toBe(false);
|
|
});
|
|
|
|
it("returns false for text with common whitespace (tabs, newlines)", () => {
|
|
expect(isBinaryContent("line one\nline two\ttab")).toBe(false);
|
|
});
|
|
|
|
it("returns true for string containing null bytes", () => {
|
|
expect(isBinaryContent("PK\x00\x03\x04")).toBe(true);
|
|
});
|
|
|
|
it("returns true for typical binary file header bytes", () => {
|
|
const mobiBinarySnippet = "\x00\x00\x00\x01BOOKMOBI\x00\x00\x02\x0E";
|
|
expect(isBinaryContent(mobiBinarySnippet)).toBe(true);
|
|
});
|
|
|
|
it("returns false for empty string", () => {
|
|
expect(isBinaryContent("")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("getTelegramTextParts — binary caption filtering (#66647)", () => {
|
|
it("strips binary caption content to prevent token explosion", () => {
|
|
const binaryCaption = "PK\x03\x04\x14\x00\x08binary-ebook-data";
|
|
const result = getTelegramTextParts({
|
|
caption: binaryCaption,
|
|
caption_entities: [{ type: "mention", offset: 0, length: 5 }],
|
|
chat: { id: 1, type: "private" },
|
|
date: 1,
|
|
message_id: 1,
|
|
} as any);
|
|
expect(result.text).toBe("");
|
|
expect(result.entities).toStrictEqual([]);
|
|
});
|
|
|
|
it("preserves normal caption text", () => {
|
|
const result = getTelegramTextParts({
|
|
caption: "Here is my document",
|
|
caption_entities: [],
|
|
chat: { id: 1, type: "private" },
|
|
date: 1,
|
|
message_id: 1,
|
|
} as any);
|
|
expect(result.text).toBe("Here is my document");
|
|
});
|
|
|
|
it("strips binary content in msg.text as well", () => {
|
|
const result = getTelegramTextParts({
|
|
text: "\x00\x01\x02 binary junk",
|
|
entities: [{ type: "bold", offset: 0, length: 3 }],
|
|
chat: { id: 1, type: "private" },
|
|
date: 1,
|
|
message_id: 1,
|
|
} as any);
|
|
expect(result.text).toBe("");
|
|
expect(result.entities).toStrictEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("hasBotMention", () => {
|
|
it("prefers caption text and caption entities when message text is absent", () => {
|
|
expect(
|
|
getTelegramTextParts({
|
|
caption: "@gaian hello",
|
|
caption_entities: [{ type: "mention", offset: 0, length: 6 }],
|
|
chat: { id: 1, type: "private" },
|
|
date: 1,
|
|
message_id: 1,
|
|
} as any),
|
|
).toEqual({
|
|
text: "@gaian hello",
|
|
entities: [{ type: "mention", offset: 0, length: 6 }],
|
|
});
|
|
});
|
|
|
|
it("matches exact username mentions from plain text", () => {
|
|
expect(
|
|
hasBotMention(
|
|
{
|
|
text: "@gaian what is the group id?",
|
|
chat: { id: 1, type: "supergroup" },
|
|
} as any,
|
|
"gaian",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("does not match mention prefixes from longer bot usernames", () => {
|
|
expect(
|
|
hasBotMention(
|
|
{
|
|
text: "@GaianChat_Bot what is the group id?",
|
|
chat: { id: 1, type: "supergroup" },
|
|
} as any,
|
|
"gaian",
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("still matches exact mention entities", () => {
|
|
expect(
|
|
hasBotMention(
|
|
{
|
|
text: "@GaianChat_Bot hi @gaian",
|
|
entities: [{ type: "mention", offset: 18, length: 6 }],
|
|
chat: { id: 1, type: "supergroup" },
|
|
} as any,
|
|
"gaian",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("matches mention followed by punctuation", () => {
|
|
expect(
|
|
hasBotMention(
|
|
{
|
|
text: "@gaian, what's up?",
|
|
chat: { id: 1, type: "supergroup" },
|
|
} as any,
|
|
"gaian",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("matches mention followed by space", () => {
|
|
expect(
|
|
hasBotMention(
|
|
{
|
|
text: "@gaian how are you",
|
|
chat: { id: 1, type: "supergroup" },
|
|
} as any,
|
|
"gaian",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("does not match substring of a longer username", () => {
|
|
expect(
|
|
hasBotMention(
|
|
{
|
|
text: "@gaianchat_bot hello",
|
|
chat: { id: 1, type: "supergroup" },
|
|
} as any,
|
|
"gaian",
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("does not match when mention is a prefix of another word", () => {
|
|
expect(
|
|
hasBotMention(
|
|
{
|
|
text: "@gaianbot do something",
|
|
chat: { id: 1, type: "supergroup" },
|
|
} as any,
|
|
"gaian",
|
|
),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
describe("expandTextLinks", () => {
|
|
it("returns text unchanged when no entities are provided", () => {
|
|
expect(expandTextLinks("Hello world")).toBe("Hello world");
|
|
expect(expandTextLinks("Hello world", null)).toBe("Hello world");
|
|
expect(expandTextLinks("Hello world", [])).toBe("Hello world");
|
|
});
|
|
|
|
it("returns text unchanged when there are no text_link entities", () => {
|
|
const entities = [
|
|
{ type: "mention", offset: 0, length: 5 },
|
|
{ type: "bold", offset: 6, length: 5 },
|
|
];
|
|
expect(expandTextLinks("@user hello", entities)).toBe("@user hello");
|
|
});
|
|
|
|
it("expands a single text_link entity", () => {
|
|
const text = "Check this link for details";
|
|
const entities = [{ type: "text_link", offset: 11, length: 4, url: "https://example.com" }];
|
|
expect(expandTextLinks(text, entities)).toBe(
|
|
"Check this [link](https://example.com) for details",
|
|
);
|
|
});
|
|
|
|
it("expands multiple text_link entities", () => {
|
|
const text = "Visit Google or GitHub for more";
|
|
const entities = [
|
|
{ type: "text_link", offset: 6, length: 6, url: "https://google.com" },
|
|
{ type: "text_link", offset: 16, length: 6, url: "https://github.com" },
|
|
];
|
|
expect(expandTextLinks(text, entities)).toBe(
|
|
"Visit [Google](https://google.com) or [GitHub](https://github.com) for more",
|
|
);
|
|
});
|
|
|
|
it("handles adjacent text_link entities", () => {
|
|
const text = "AB";
|
|
const entities = [
|
|
{ type: "text_link", offset: 0, length: 1, url: "https://a.example" },
|
|
{ type: "text_link", offset: 1, length: 1, url: "https://b.example" },
|
|
];
|
|
expect(expandTextLinks(text, entities)).toBe("[A](https://a.example)[B](https://b.example)");
|
|
});
|
|
|
|
it("preserves offsets from the original string", () => {
|
|
const text = " Hello world";
|
|
const entities = [{ type: "text_link", offset: 1, length: 5, url: "https://example.com" }];
|
|
expect(expandTextLinks(text, entities)).toBe(" [Hello](https://example.com) world");
|
|
});
|
|
});
|