mirror of https://github.com/openclaw/openclaw.git
236 lines
7.6 KiB
TypeScript
236 lines
7.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { buildMentionEntities, formatMentionText, parseMentions } from "./mentions.js";
|
|
|
|
describe("parseMentions", () => {
|
|
it("parses single mention", () => {
|
|
const result = parseMentions("Hello @[John Doe](28:a1b2c3-d4e5f6)!");
|
|
|
|
expect(result.text).toBe("Hello <at>John Doe</at>!");
|
|
expect(result.entities).toHaveLength(1);
|
|
expect(result.entities[0]).toEqual({
|
|
type: "mention",
|
|
text: "<at>John Doe</at>",
|
|
mentioned: {
|
|
id: "28:a1b2c3-d4e5f6",
|
|
name: "John Doe",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("parses multiple mentions", () => {
|
|
const result = parseMentions("Hey @[Alice](28:aaa) and @[Bob](28:bbb), can you review this?");
|
|
|
|
expect(result.text).toBe("Hey <at>Alice</at> and <at>Bob</at>, can you review this?");
|
|
expect(result.entities).toHaveLength(2);
|
|
expect(result.entities[0]).toEqual({
|
|
type: "mention",
|
|
text: "<at>Alice</at>",
|
|
mentioned: {
|
|
id: "28:aaa",
|
|
name: "Alice",
|
|
},
|
|
});
|
|
expect(result.entities[1]).toEqual({
|
|
type: "mention",
|
|
text: "<at>Bob</at>",
|
|
mentioned: {
|
|
id: "28:bbb",
|
|
name: "Bob",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("handles text without mentions", () => {
|
|
const result = parseMentions("Hello world!");
|
|
|
|
expect(result.text).toBe("Hello world!");
|
|
expect(result.entities).toHaveLength(0);
|
|
});
|
|
|
|
it("handles empty text", () => {
|
|
const result = parseMentions("");
|
|
|
|
expect(result.text).toBe("");
|
|
expect(result.entities).toHaveLength(0);
|
|
});
|
|
|
|
it("handles mention with spaces in name", () => {
|
|
const result = parseMentions("@[John Peter Smith](28:a1b2c3)");
|
|
|
|
expect(result.text).toBe("<at>John Peter Smith</at>");
|
|
expect(result.entities[0]?.mentioned.name).toBe("John Peter Smith");
|
|
});
|
|
|
|
it("trims whitespace from id and name", () => {
|
|
const result = parseMentions("@[ John Doe ]( 28:a1b2c3 )");
|
|
|
|
expect(result.entities[0]).toEqual({
|
|
type: "mention",
|
|
text: "<at>John Doe</at>",
|
|
mentioned: {
|
|
id: "28:a1b2c3",
|
|
name: "John Doe",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("handles Japanese characters in mention at start of message", () => {
|
|
const input = "@[タナカ タロウ](a1b2c3d4-e5f6-7890-abcd-ef1234567890) スキル化完了しました!";
|
|
const result = parseMentions(input);
|
|
|
|
expect(result.text).toBe("<at>タナカ タロウ</at> スキル化完了しました!");
|
|
expect(result.entities).toHaveLength(1);
|
|
expect(result.entities[0]).toEqual({
|
|
type: "mention",
|
|
text: "<at>タナカ タロウ</at>",
|
|
mentioned: {
|
|
id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
name: "タナカ タロウ",
|
|
},
|
|
});
|
|
|
|
// Verify entity text exactly matches what's in the formatted text
|
|
const entityText = result.entities[0]?.text;
|
|
expect(result.text).toContain(entityText);
|
|
expect(result.text.indexOf(entityText)).toBe(0);
|
|
});
|
|
|
|
it("skips mention-like patterns with non-Teams IDs (e.g. in code blocks)", () => {
|
|
// This reproduces the actual failing payload: the message contains a real mention
|
|
// plus `@[表示名](ユーザーID)` as documentation text inside backticks.
|
|
const input =
|
|
"@[タナカ タロウ](a1b2c3d4-e5f6-7890-abcd-ef1234567890) スキル化完了しました!📋\n\n" +
|
|
"**作成したスキル:** `teams-mention`\n" +
|
|
"- 機能: Teamsでのメンション形式 `@[表示名](ユーザーID)`\n\n" +
|
|
"**追加対応:**\n" +
|
|
"- ユーザーのID `a1b2c3d4-e5f6-7890-abcd-ef1234567890` を登録済み";
|
|
const result = parseMentions(input);
|
|
|
|
// Only the real mention should be parsed; the documentation example should be left as-is
|
|
expect(result.entities).toHaveLength(1);
|
|
expect(result.entities[0]?.mentioned.id).toBe("a1b2c3d4-e5f6-7890-abcd-ef1234567890");
|
|
expect(result.entities[0]?.mentioned.name).toBe("タナカ タロウ");
|
|
|
|
// The documentation pattern must remain untouched in the text
|
|
expect(result.text).toContain("`@[表示名](ユーザーID)`");
|
|
});
|
|
|
|
it("accepts Bot Framework IDs (28:xxx)", () => {
|
|
const result = parseMentions("@[Bot](28:abc-123)");
|
|
expect(result.entities).toHaveLength(1);
|
|
expect(result.entities[0]?.mentioned.id).toBe("28:abc-123");
|
|
});
|
|
|
|
it("accepts Bot Framework IDs with non-hex payloads (29:xxx)", () => {
|
|
const result = parseMentions("@[Bot](29:08q2j2o3jc09au90eucae)");
|
|
expect(result.entities).toHaveLength(1);
|
|
expect(result.entities[0]?.mentioned.id).toBe("29:08q2j2o3jc09au90eucae");
|
|
});
|
|
|
|
it("accepts org-scoped IDs with extra segments (8:orgid:...)", () => {
|
|
const result = parseMentions("@[User](8:orgid:2d8c2d2c-1111-2222-3333-444444444444)");
|
|
expect(result.entities).toHaveLength(1);
|
|
expect(result.entities[0]?.mentioned.id).toBe("8:orgid:2d8c2d2c-1111-2222-3333-444444444444");
|
|
});
|
|
|
|
it("accepts AAD object IDs (UUIDs)", () => {
|
|
const result = parseMentions("@[User](a1b2c3d4-e5f6-7890-abcd-ef1234567890)");
|
|
expect(result.entities).toHaveLength(1);
|
|
expect(result.entities[0]?.mentioned.id).toBe("a1b2c3d4-e5f6-7890-abcd-ef1234567890");
|
|
});
|
|
|
|
it("rejects non-ID strings as mention targets", () => {
|
|
const result = parseMentions("See @[docs](https://example.com) for details");
|
|
expect(result.entities).toHaveLength(0);
|
|
// Original text preserved
|
|
expect(result.text).toBe("See @[docs](https://example.com) for details");
|
|
});
|
|
});
|
|
|
|
describe("buildMentionEntities", () => {
|
|
it("builds entities from mention info", () => {
|
|
const mentions = [
|
|
{ id: "28:aaa", name: "Alice" },
|
|
{ id: "28:bbb", name: "Bob" },
|
|
];
|
|
|
|
const entities = buildMentionEntities(mentions);
|
|
|
|
expect(entities).toHaveLength(2);
|
|
expect(entities[0]).toEqual({
|
|
type: "mention",
|
|
text: "<at>Alice</at>",
|
|
mentioned: {
|
|
id: "28:aaa",
|
|
name: "Alice",
|
|
},
|
|
});
|
|
expect(entities[1]).toEqual({
|
|
type: "mention",
|
|
text: "<at>Bob</at>",
|
|
mentioned: {
|
|
id: "28:bbb",
|
|
name: "Bob",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("handles empty list", () => {
|
|
const entities = buildMentionEntities([]);
|
|
expect(entities).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("formatMentionText", () => {
|
|
it("formats text with single mention", () => {
|
|
const text = "Hello @John!";
|
|
const mentions = [{ id: "28:xxx", name: "John" }];
|
|
|
|
const result = formatMentionText(text, mentions);
|
|
|
|
expect(result).toBe("Hello <at>John</at>!");
|
|
});
|
|
|
|
it("formats text with multiple mentions", () => {
|
|
const text = "Hey @Alice and @Bob";
|
|
const mentions = [
|
|
{ id: "28:aaa", name: "Alice" },
|
|
{ id: "28:bbb", name: "Bob" },
|
|
];
|
|
|
|
const result = formatMentionText(text, mentions);
|
|
|
|
expect(result).toBe("Hey <at>Alice</at> and <at>Bob</at>");
|
|
});
|
|
|
|
it("handles case-insensitive matching", () => {
|
|
const text = "Hey @alice and @ALICE";
|
|
const mentions = [{ id: "28:aaa", name: "Alice" }];
|
|
|
|
const result = formatMentionText(text, mentions);
|
|
|
|
expect(result).toBe("Hey <at>Alice</at> and <at>Alice</at>");
|
|
});
|
|
|
|
it("handles text without mentions", () => {
|
|
const text = "Hello world";
|
|
const mentions = [{ id: "28:xxx", name: "John" }];
|
|
|
|
const result = formatMentionText(text, mentions);
|
|
|
|
expect(result).toBe("Hello world");
|
|
});
|
|
|
|
it("escapes regex metacharacters in names", () => {
|
|
const text = "Hey @John(Test) and @Alice.Smith";
|
|
const mentions = [
|
|
{ id: "28:xxx", name: "John(Test)" },
|
|
{ id: "28:yyy", name: "Alice.Smith" },
|
|
];
|
|
|
|
const result = formatMentionText(text, mentions);
|
|
|
|
expect(result).toBe("Hey <at>John(Test)</at> and <at>Alice.Smith</at>");
|
|
});
|
|
});
|