forked from bai/curriculum-project-hub
6227e1931b
- Add buildOutboundPayload: detect markdown format, isolate code blocks, force plain text for tables, fallback from post to text on API rejection - Add addReaction/removeReaction for processing status lifecycle - Replace THUMBSUP with Typing→(remove on success | CrossMark on failure) - Add unit tests for markdown rendering (15) and reactions (6)
142 lines
4.4 KiB
TypeScript
142 lines
4.4 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
_buildMarkdownPostRows,
|
|
buildOutboundPayload,
|
|
patchTextMessage,
|
|
sendTextMessage,
|
|
type FeishuRuntime,
|
|
} from "../../src/feishu/client.js";
|
|
|
|
describe("Feishu markdown outbound payloads", () => {
|
|
it("uses text messages for plain text", () => {
|
|
const payload = buildOutboundPayload("hello from cph");
|
|
|
|
expect(payload.msgType).toBe("text");
|
|
expect(JSON.parse(payload.content)).toEqual({ text: "hello from cph" });
|
|
});
|
|
|
|
it("keeps fenced code blocks in separate post rows", () => {
|
|
const content = "Before\n```ts\nconst x = 1;\n```\nAfter";
|
|
|
|
expect(_buildMarkdownPostRows(content)).toEqual([
|
|
[{ tag: "md", text: "Before\n" }],
|
|
[{ tag: "md", text: "```ts\nconst x = 1;\n```" }],
|
|
[{ tag: "md", text: "\nAfter" }],
|
|
]);
|
|
});
|
|
|
|
it("forces markdown tables to plain text", () => {
|
|
const content = "| A | B |\n|---|---|\n| 1 | 2 |";
|
|
const payload = buildOutboundPayload(content);
|
|
|
|
expect(payload.msgType).toBe("text");
|
|
expect(JSON.parse(payload.content)).toEqual({ text: content });
|
|
});
|
|
|
|
it("uses post messages for markdown headers, bold text, and lists", () => {
|
|
const content = "# Title\n\nThis is **important**.\n\n- first\n- second";
|
|
const payload = buildOutboundPayload(content);
|
|
|
|
expect(payload.msgType).toBe("post");
|
|
expect(JSON.parse(payload.content)).toEqual({
|
|
zh_cn: {
|
|
content: [[{ tag: "md", text: content }]],
|
|
},
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
["header", "# Heading"],
|
|
["list", "- item"],
|
|
["code block", "```ts\nconst x = 1;\n```"],
|
|
["inline code", "Use `code` here"],
|
|
["bold", "**bold**"],
|
|
["italic", "_italic_"],
|
|
["strikethrough", "~~removed~~"],
|
|
["link", "[docs](https://example.com)"],
|
|
["blockquote", "> quoted"],
|
|
])("detects markdown %s syntax", (_name, content) => {
|
|
expect(buildOutboundPayload(content).msgType).toBe("post");
|
|
});
|
|
|
|
it("falls back from rejected post payloads to stripped plain text", async () => {
|
|
const messageCreate = vi
|
|
.fn()
|
|
.mockRejectedValueOnce(new Error("content format of the post type is incorrect"))
|
|
.mockResolvedValueOnce({ data: { message_id: "message-1" } });
|
|
const rt = mockRuntime({ messageCreate });
|
|
|
|
await expect(sendTextMessage(rt, "chat-1", "# Title\n\n**bold** and `code`")).resolves.toBe("message-1");
|
|
|
|
expect(messageCreate).toHaveBeenCalledTimes(2);
|
|
expect(messageCreate).toHaveBeenNthCalledWith(1, {
|
|
params: { receive_id_type: "chat_id" },
|
|
data: {
|
|
receive_id: "chat-1",
|
|
msg_type: "post",
|
|
content: JSON.stringify({
|
|
zh_cn: {
|
|
content: [[{ tag: "md", text: "# Title\n\n**bold** and `code`" }]],
|
|
},
|
|
}),
|
|
},
|
|
});
|
|
expect(messageCreate).toHaveBeenNthCalledWith(2, {
|
|
params: { receive_id_type: "chat_id" },
|
|
data: {
|
|
receive_id: "chat-1",
|
|
msg_type: "text",
|
|
content: JSON.stringify({ text: "Title\n\nbold and code" }),
|
|
},
|
|
});
|
|
});
|
|
|
|
it("patches markdown post rows as separate interactive card elements", async () => {
|
|
const patch = vi.fn(async () => ({}));
|
|
const rt = mockRuntime({ patch });
|
|
|
|
await patchTextMessage(rt, "message-1", "Before\n```ts\nconst x = 1;\n```\nAfter");
|
|
|
|
expect(patch).toHaveBeenCalledWith({
|
|
path: { message_id: "message-1" },
|
|
params: { msg_type: "interactive" },
|
|
data: {
|
|
content: JSON.stringify({
|
|
elements: [
|
|
{ tag: "div", text: { tag: "lark_md", content: "Before\n" } },
|
|
{ tag: "div", text: { tag: "lark_md", content: "```ts\nconst x = 1;\n```" } },
|
|
{ tag: "div", text: { tag: "lark_md", content: "\nAfter" } },
|
|
],
|
|
}),
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
function mockRuntime(options: {
|
|
readonly messageCreate?: (payload: unknown) => Promise<unknown>;
|
|
readonly patch?: (payload: unknown) => Promise<unknown>;
|
|
}): FeishuRuntime {
|
|
return {
|
|
client: {
|
|
im: {
|
|
v1: {
|
|
message: {
|
|
create: options.messageCreate ?? vi.fn(async () => ({ data: { message_id: "message-1" } })),
|
|
patch: options.patch ?? vi.fn(async () => ({})),
|
|
},
|
|
},
|
|
},
|
|
} as unknown as FeishuRuntime["client"],
|
|
logger: {
|
|
info() {},
|
|
warn() {},
|
|
error() {},
|
|
debug() {},
|
|
fatal() {},
|
|
child() { return this; },
|
|
level: "silent",
|
|
} as unknown as FeishuRuntime["logger"],
|
|
};
|
|
}
|