| 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: "タナカ タロウ", |
| }, |
| }); |
|
|
| |
| 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)", () => { |
| |
| |
| 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); |
|
|
| |
| expect(result.entities).toHaveLength(1); |
| expect(result.entities[0]?.mentioned.id).toBe("a1b2c3d4-e5f6-7890-abcd-ef1234567890"); |
| expect(result.entities[0]?.mentioned.name).toBe("タナカ タロウ"); |
|
|
| |
| 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); |
| |
| 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>"); |
| }); |
| }); |
|
|