mirror of https://github.com/openclaw/openclaw.git
229 lines
6.8 KiB
TypeScript
229 lines
6.8 KiB
TypeScript
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const {
|
|
pushMessageMock,
|
|
replyMessageMock,
|
|
showLoadingAnimationMock,
|
|
getProfileMock,
|
|
MessagingApiClientMock,
|
|
loadConfigMock,
|
|
resolveLineAccountMock,
|
|
resolveLineChannelAccessTokenMock,
|
|
recordChannelActivityMock,
|
|
logVerboseMock,
|
|
} = vi.hoisted(() => {
|
|
const pushMessageMock = vi.fn();
|
|
const replyMessageMock = vi.fn();
|
|
const showLoadingAnimationMock = vi.fn();
|
|
const getProfileMock = vi.fn();
|
|
const MessagingApiClientMock = vi.fn(function () {
|
|
return {
|
|
pushMessage: pushMessageMock,
|
|
replyMessage: replyMessageMock,
|
|
showLoadingAnimation: showLoadingAnimationMock,
|
|
getProfile: getProfileMock,
|
|
};
|
|
});
|
|
const loadConfigMock = vi.fn(() => ({}));
|
|
const resolveLineAccountMock = vi.fn(() => ({ accountId: "default" }));
|
|
const resolveLineChannelAccessTokenMock = vi.fn(() => "line-token");
|
|
const recordChannelActivityMock = vi.fn();
|
|
const logVerboseMock = vi.fn();
|
|
return {
|
|
pushMessageMock,
|
|
replyMessageMock,
|
|
showLoadingAnimationMock,
|
|
getProfileMock,
|
|
MessagingApiClientMock,
|
|
loadConfigMock,
|
|
resolveLineAccountMock,
|
|
resolveLineChannelAccessTokenMock,
|
|
recordChannelActivityMock,
|
|
logVerboseMock,
|
|
};
|
|
});
|
|
|
|
vi.mock("@line/bot-sdk", () => ({
|
|
messagingApi: { MessagingApiClient: MessagingApiClientMock },
|
|
}));
|
|
|
|
vi.mock("../config/config.js", () => ({
|
|
loadConfig: loadConfigMock,
|
|
}));
|
|
|
|
vi.mock("./accounts.js", () => ({
|
|
resolveLineAccount: resolveLineAccountMock,
|
|
}));
|
|
|
|
vi.mock("./channel-access-token.js", () => ({
|
|
resolveLineChannelAccessToken: resolveLineChannelAccessTokenMock,
|
|
}));
|
|
|
|
vi.mock("../infra/channel-activity.js", () => ({
|
|
recordChannelActivity: recordChannelActivityMock,
|
|
}));
|
|
|
|
vi.mock("../globals.js", () => ({
|
|
logVerbose: logVerboseMock,
|
|
}));
|
|
|
|
let sendModule: typeof import("./send.js");
|
|
|
|
describe("LINE send helpers", () => {
|
|
beforeAll(async () => {
|
|
sendModule = await import("./send.js");
|
|
});
|
|
|
|
beforeEach(() => {
|
|
pushMessageMock.mockReset();
|
|
replyMessageMock.mockReset();
|
|
showLoadingAnimationMock.mockReset();
|
|
getProfileMock.mockReset();
|
|
MessagingApiClientMock.mockClear();
|
|
loadConfigMock.mockReset();
|
|
resolveLineAccountMock.mockReset();
|
|
resolveLineChannelAccessTokenMock.mockReset();
|
|
recordChannelActivityMock.mockReset();
|
|
logVerboseMock.mockReset();
|
|
|
|
loadConfigMock.mockReturnValue({});
|
|
resolveLineAccountMock.mockReturnValue({ accountId: "default" });
|
|
resolveLineChannelAccessTokenMock.mockReturnValue("line-token");
|
|
pushMessageMock.mockResolvedValue({});
|
|
replyMessageMock.mockResolvedValue({});
|
|
showLoadingAnimationMock.mockResolvedValue({});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("limits quick reply items to 13", () => {
|
|
const labels = Array.from({ length: 20 }, (_, index) => `Option ${index + 1}`);
|
|
const quickReply = sendModule.createQuickReplyItems(labels);
|
|
|
|
expect(quickReply.items).toHaveLength(13);
|
|
});
|
|
|
|
it("pushes images via normalized LINE target", async () => {
|
|
const result = await sendModule.pushImageMessage(
|
|
"line:user:U123",
|
|
"https://example.com/original.jpg",
|
|
undefined,
|
|
{ verbose: true },
|
|
);
|
|
|
|
expect(pushMessageMock).toHaveBeenCalledWith({
|
|
to: "U123",
|
|
messages: [
|
|
{
|
|
type: "image",
|
|
originalContentUrl: "https://example.com/original.jpg",
|
|
previewImageUrl: "https://example.com/original.jpg",
|
|
},
|
|
],
|
|
});
|
|
expect(recordChannelActivityMock).toHaveBeenCalledWith({
|
|
channel: "line",
|
|
accountId: "default",
|
|
direction: "outbound",
|
|
});
|
|
expect(logVerboseMock).toHaveBeenCalledWith("line: pushed image to U123");
|
|
expect(result).toEqual({ messageId: "push", chatId: "U123" });
|
|
});
|
|
|
|
it("replies when reply token is provided", async () => {
|
|
const result = await sendModule.sendMessageLine("line:group:C1", "Hello", {
|
|
replyToken: "reply-token",
|
|
mediaUrl: "https://example.com/media.jpg",
|
|
verbose: true,
|
|
});
|
|
|
|
expect(replyMessageMock).toHaveBeenCalledTimes(1);
|
|
expect(pushMessageMock).not.toHaveBeenCalled();
|
|
expect(replyMessageMock).toHaveBeenCalledWith({
|
|
replyToken: "reply-token",
|
|
messages: [
|
|
{
|
|
type: "image",
|
|
originalContentUrl: "https://example.com/media.jpg",
|
|
previewImageUrl: "https://example.com/media.jpg",
|
|
},
|
|
{
|
|
type: "text",
|
|
text: "Hello",
|
|
},
|
|
],
|
|
});
|
|
expect(logVerboseMock).toHaveBeenCalledWith("line: replied to C1");
|
|
expect(result).toEqual({ messageId: "reply", chatId: "C1" });
|
|
});
|
|
|
|
it("throws when push messages are empty", async () => {
|
|
await expect(sendModule.pushMessagesLine("U123", [])).rejects.toThrow(
|
|
"Message must be non-empty for LINE sends",
|
|
);
|
|
});
|
|
|
|
it("logs HTTP body when push fails", async () => {
|
|
const err = new Error("LINE push failed") as Error & {
|
|
status: number;
|
|
statusText: string;
|
|
body: string;
|
|
};
|
|
err.status = 400;
|
|
err.statusText = "Bad Request";
|
|
err.body = "invalid flex payload";
|
|
pushMessageMock.mockRejectedValueOnce(err);
|
|
|
|
await expect(
|
|
sendModule.pushMessagesLine("U999", [{ type: "text", text: "hello" }]),
|
|
).rejects.toThrow("LINE push failed");
|
|
|
|
expect(logVerboseMock).toHaveBeenCalledWith(
|
|
"line: push message failed (400 Bad Request): invalid flex payload",
|
|
);
|
|
});
|
|
|
|
it("caches profile results by default", async () => {
|
|
getProfileMock.mockResolvedValue({
|
|
displayName: "Peter",
|
|
pictureUrl: "https://example.com/peter.jpg",
|
|
});
|
|
|
|
const first = await sendModule.getUserProfile("U-cache");
|
|
const second = await sendModule.getUserProfile("U-cache");
|
|
|
|
expect(first).toEqual({
|
|
displayName: "Peter",
|
|
pictureUrl: "https://example.com/peter.jpg",
|
|
});
|
|
expect(second).toEqual(first);
|
|
expect(getProfileMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("continues when loading animation is unsupported", async () => {
|
|
showLoadingAnimationMock.mockRejectedValueOnce(new Error("unsupported"));
|
|
|
|
await expect(sendModule.showLoadingAnimation("line:room:R1")).resolves.toBeUndefined();
|
|
|
|
expect(logVerboseMock).toHaveBeenCalledWith(
|
|
expect.stringContaining("line: loading animation failed (non-fatal)"),
|
|
);
|
|
});
|
|
|
|
it("pushes quick-reply text and caps to 13 buttons", async () => {
|
|
await sendModule.pushTextMessageWithQuickReplies(
|
|
"U-quick",
|
|
"Pick one",
|
|
Array.from({ length: 20 }, (_, index) => `Choice ${index + 1}`),
|
|
);
|
|
|
|
expect(pushMessageMock).toHaveBeenCalledTimes(1);
|
|
const firstCall = pushMessageMock.mock.calls[0] as [
|
|
{ messages: Array<{ quickReply?: { items: unknown[] } }> },
|
|
];
|
|
expect(firstCall[0].messages[0].quickReply?.items).toHaveLength(13);
|
|
});
|
|
});
|