Files
openclaw/src/telegram/bot/helpers.test.ts
2026-03-02 22:08:07 +00:00

398 lines
14 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
buildTelegramThreadParams,
buildTypingThreadParams,
describeReplyTarget,
expandTextLinks,
normalizeForwardedContext,
resolveTelegramDirectPeerId,
resolveTelegramForumThreadId,
} 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("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("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,
},
// oxlint-disable-next-line typescript/no-explicit-any
} as any);
expect(ctx).not.toBeNull();
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 },
// oxlint-disable-next-line typescript/no-explicit-any
} as any);
expect(ctx).not.toBeNull();
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,
},
// oxlint-disable-next-line typescript/no-explicit-any
} as any);
expect(ctx).not.toBeNull();
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",
},
// oxlint-disable-next-line typescript/no-explicit-any
} as any);
expect(ctx).not.toBeNull();
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,
},
// oxlint-disable-next-line typescript/no-explicit-any
} as any);
expect(ctx).not.toBeNull();
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,
},
// oxlint-disable-next-line typescript/no-explicit-any
} as any);
expect(ctx).not.toBeNull();
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,
},
// oxlint-disable-next-line typescript/no-explicit-any
} as any);
expect(ctx).not.toBeNull();
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(
// oxlint-disable-next-line typescript/no-explicit-any
{ 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 },
},
// oxlint-disable-next-line typescript/no-explicit-any
} as any);
expect(result).not.toBeNull();
expect(result?.body).toBe("Original message");
expect(result?.sender).toBe("Alice");
expect(result?.id).toBe("1");
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,
},
},
// oxlint-disable-next-line typescript/no-explicit-any
} as any);
expect(result).not.toBeNull();
expect(result?.body).toBe("This is the forwarded content");
expect(result?.id).toBe("2");
// The reply target's forwarded context should be included
expect(result?.forwardedFrom).toBeDefined();
expect(result?.forwardedFrom?.from).toBe("Bob Smith (@bobsmith)");
expect(result?.forwardedFrom?.fromType).toBe("user");
expect(result?.forwardedFrom?.fromId).toBe("999");
expect(result?.forwardedFrom?.date).toBe(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",
},
},
// oxlint-disable-next-line typescript/no-explicit-any
} as any);
expect(result).not.toBeNull();
expect(result?.forwardedFrom).toBeDefined();
expect(result?.forwardedFrom?.from).toBe("Tech News (Editor)");
expect(result?.forwardedFrom?.fromType).toBe("channel");
expect(result?.forwardedFrom?.fromMessageId).toBe(456);
});
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,
},
},
// oxlint-disable-next-line typescript/no-explicit-any
} as any);
expect(result).not.toBeNull();
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("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");
});
});