mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-17 12:11:20 +00:00
* msteams: extract structured quote/reply context from Teams HTML attachments * msteams: address PR #51647 review feedback
222 lines
7.7 KiB
TypeScript
222 lines
7.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
decodeHtmlEntities,
|
|
extractMSTeamsQuoteInfo,
|
|
htmlToPlainText,
|
|
normalizeMSTeamsConversationId,
|
|
parseMSTeamsActivityTimestamp,
|
|
stripMSTeamsMentionTags,
|
|
wasMSTeamsBotMentioned,
|
|
} from "./inbound.js";
|
|
|
|
describe("msteams inbound", () => {
|
|
describe("stripMSTeamsMentionTags", () => {
|
|
it("removes <at>...</at> tags and trims", () => {
|
|
expect(stripMSTeamsMentionTags("<at>Bot</at> hi")).toBe("hi");
|
|
expect(stripMSTeamsMentionTags("hi <at>Bot</at>")).toBe("hi");
|
|
});
|
|
|
|
it("removes <at ...> tags with attributes", () => {
|
|
expect(stripMSTeamsMentionTags('<at id="1">Bot</at> hi')).toBe("hi");
|
|
expect(stripMSTeamsMentionTags('hi <at itemid="2">Bot</at>')).toBe("hi");
|
|
});
|
|
});
|
|
|
|
describe("normalizeMSTeamsConversationId", () => {
|
|
it("strips the ;messageid suffix", () => {
|
|
expect(normalizeMSTeamsConversationId("19:abc@thread.tacv2;messageid=deadbeef")).toBe(
|
|
"19:abc@thread.tacv2",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("parseMSTeamsActivityTimestamp", () => {
|
|
it("returns undefined for empty/invalid values", () => {
|
|
expect(parseMSTeamsActivityTimestamp(undefined)).toBeUndefined();
|
|
expect(parseMSTeamsActivityTimestamp("not-a-date")).toBeUndefined();
|
|
});
|
|
|
|
it("parses string timestamps", () => {
|
|
const ts = parseMSTeamsActivityTimestamp("2024-01-01T00:00:00.000Z");
|
|
if (!ts) {
|
|
throw new Error("expected MSTeams timestamp parser to return a Date");
|
|
}
|
|
expect(ts.toISOString()).toBe("2024-01-01T00:00:00.000Z");
|
|
});
|
|
|
|
it("passes through Date instances", () => {
|
|
const d = new Date("2024-01-01T00:00:00.000Z");
|
|
expect(parseMSTeamsActivityTimestamp(d)).toBe(d);
|
|
});
|
|
});
|
|
|
|
describe("wasMSTeamsBotMentioned", () => {
|
|
it("returns true when a mention entity matches recipient.id", () => {
|
|
expect(
|
|
wasMSTeamsBotMentioned({
|
|
recipient: { id: "bot" },
|
|
entities: [{ type: "mention", mentioned: { id: "bot" } }],
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("returns false when there is no matching mention", () => {
|
|
expect(
|
|
wasMSTeamsBotMentioned({
|
|
recipient: { id: "bot" },
|
|
entities: [{ type: "mention", mentioned: { id: "other" } }],
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("decodeHtmlEntities", () => {
|
|
it("decodes common entities", () => {
|
|
expect(decodeHtmlEntities("&<>"'' ")).toBe("&<>\"'' ");
|
|
});
|
|
|
|
it("leaves plain text unchanged", () => {
|
|
expect(decodeHtmlEntities("hello world")).toBe("hello world");
|
|
});
|
|
|
|
it("prevents double-decoding: &lt; should become < not <", () => {
|
|
// If & were decoded first, &lt; → < → < (wrong).
|
|
// With & decoded last, &lt; stays as < (correct).
|
|
expect(decodeHtmlEntities("&lt;b&gt;")).toBe("<b>");
|
|
});
|
|
});
|
|
|
|
describe("htmlToPlainText", () => {
|
|
it("strips tags and decodes entities", () => {
|
|
expect(htmlToPlainText("<strong>Hello & world</strong>")).toBe("Hello & world");
|
|
});
|
|
|
|
it("collapses whitespace from tag removal", () => {
|
|
expect(htmlToPlainText("<p>foo</p><p>bar</p>")).toBe("foo bar");
|
|
});
|
|
|
|
it("trims leading and trailing whitespace", () => {
|
|
expect(htmlToPlainText(" <span>hi</span> ")).toBe("hi");
|
|
});
|
|
});
|
|
|
|
describe("extractMSTeamsQuoteInfo", () => {
|
|
const replyAttachment = (overrides?: { content?: string; contentType?: string }) => ({
|
|
contentType: overrides?.contentType ?? "text/html",
|
|
content:
|
|
overrides?.content ??
|
|
'<blockquote itemtype="http://schema.skype.com/Reply" itemscope>' +
|
|
'<strong itemprop="mri">Alice</strong>' +
|
|
'<p itemprop="copy">Hello world</p>' +
|
|
"</blockquote>",
|
|
});
|
|
|
|
it("extracts sender and body from a Teams reply attachment", () => {
|
|
const result = extractMSTeamsQuoteInfo([replyAttachment()]);
|
|
expect(result).toEqual({ sender: "Alice", body: "Hello world" });
|
|
});
|
|
|
|
it("returns undefined for empty attachments array", () => {
|
|
expect(extractMSTeamsQuoteInfo([])).toBeUndefined();
|
|
});
|
|
|
|
it("returns undefined when no reply blockquote is present", () => {
|
|
expect(
|
|
extractMSTeamsQuoteInfo([{ contentType: "text/html", content: "<p>just a message</p>" }]),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("uses 'unknown' as sender when sender element is absent", () => {
|
|
const result = extractMSTeamsQuoteInfo([
|
|
{
|
|
contentType: "text/html",
|
|
content:
|
|
'<blockquote itemtype="http://schema.skype.com/Reply" itemscope>' +
|
|
'<p itemprop="copy">quoted text</p>' +
|
|
"</blockquote>",
|
|
},
|
|
]);
|
|
expect(result).toEqual({ sender: "unknown", body: "quoted text" });
|
|
});
|
|
|
|
it("returns undefined when body element is absent", () => {
|
|
const result = extractMSTeamsQuoteInfo([
|
|
{
|
|
contentType: "text/html",
|
|
content:
|
|
'<blockquote itemtype="http://schema.skype.com/Reply" itemscope>' +
|
|
'<strong itemprop="mri">Alice</strong>' +
|
|
"</blockquote>",
|
|
},
|
|
]);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("decodes HTML entities in body text", () => {
|
|
const result = extractMSTeamsQuoteInfo([
|
|
{
|
|
contentType: "text/html",
|
|
content:
|
|
'<blockquote itemtype="http://schema.skype.com/Reply" itemscope>' +
|
|
'<strong itemprop="mri">Bob</strong>' +
|
|
'<p itemprop="copy">2 < 3 & 4 > 1</p>' +
|
|
"</blockquote>",
|
|
},
|
|
]);
|
|
expect(result).toEqual({ sender: "Bob", body: "2 < 3 & 4 > 1" });
|
|
});
|
|
|
|
it("handles multiline body by collapsing whitespace", () => {
|
|
const result = extractMSTeamsQuoteInfo([
|
|
{
|
|
contentType: "text/html",
|
|
content:
|
|
'<blockquote itemtype="http://schema.skype.com/Reply" itemscope>' +
|
|
'<strong itemprop="mri">Carol</strong>' +
|
|
'<p itemprop="copy">line one\nline two</p>' +
|
|
"</blockquote>",
|
|
},
|
|
]);
|
|
expect(result?.body).toBe("line one line two");
|
|
});
|
|
|
|
it("skips non-string content values", () => {
|
|
expect(
|
|
extractMSTeamsQuoteInfo([{ contentType: "application/json", content: { foo: "bar" } }]),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("handles object content with .text property containing the reply HTML", () => {
|
|
const htmlContent =
|
|
'<blockquote itemtype="http://schema.skype.com/Reply" itemscope>' +
|
|
'<strong itemprop="mri">Dave</strong>' +
|
|
'<p itemprop="copy">hello from object</p>' +
|
|
"</blockquote>";
|
|
const result = extractMSTeamsQuoteInfo([
|
|
{ contentType: "text/html", content: { text: htmlContent } },
|
|
]);
|
|
expect(result).toEqual({ sender: "Dave", body: "hello from object" });
|
|
});
|
|
|
|
it("handles object content with .body property containing the reply HTML", () => {
|
|
const htmlContent =
|
|
'<blockquote itemtype="http://schema.skype.com/Reply" itemscope>' +
|
|
'<strong itemprop="mri">Eve</strong>' +
|
|
'<p itemprop="copy">hello from body field</p>' +
|
|
"</blockquote>";
|
|
const result = extractMSTeamsQuoteInfo([
|
|
{ contentType: "text/html", content: { body: htmlContent } },
|
|
]);
|
|
expect(result).toEqual({ sender: "Eve", body: "hello from body field" });
|
|
});
|
|
|
|
it("finds quote in second attachment when first has no quote", () => {
|
|
const result = extractMSTeamsQuoteInfo([
|
|
{ contentType: "text/plain", content: "plain text" },
|
|
replyAttachment(),
|
|
]);
|
|
expect(result).toEqual({ sender: "Alice", body: "Hello world" });
|
|
});
|
|
});
|
|
});
|