import { describe, expect, it, vi } from "vitest" ;
import type { LineAutoReplyDeps } from "./auto-reply-delivery.js" ;
import { deliverLineAutoReply } from "./auto-reply-delivery.js" ;
import { sendLineReplyChunks } from "./reply-chunks.js" ;
const createFlexMessage = (altText: string, contents: unknown) => ({
type: "flex" as const ,
altText,
contents,
});
const createImageMessage = (url: string) => ({
type: "image" as const ,
originalContentUrl: url,
previewImageUrl: url,
});
const createLocationMessage = (location: {
title: string;
address: string;
latitude: number;
longitude: number;
}) => ({
type: "location" as const ,
...location,
});
describe("deliverLineAutoReply" , () => {
const LINE_TEST_CFG = { channels: { line: { accounts: { acc: {} } } } };
const baseDeliveryParams = {
cfg: LINE_TEST_CFG,
to: "line:user:1" ,
replyToken: "token" ,
replyTokenUsed: false ,
accountId: "acc" ,
textLimit: 5000 ,
};
function createDeps(overrides?: Partial<LineAutoReplyDeps>) {
const replyMessageLine = vi.fn(async () => ({}));
const pushMessageLine = vi.fn(async () => ({}));
const pushTextMessageWithQuickReplies = vi.fn(async () => ({}));
const createTextMessageWithQuickReplies = vi.fn((text: string) => ({
type: "text" as const ,
text,
}));
const createQuickReplyItems = vi.fn((labels: string[]) => ({ items: labels }));
const pushMessagesLine = vi.fn(async () => ({ messageId: "push" , chatId: "u1" }));
const deps: LineAutoReplyDeps = {
buildTemplateMessageFromPayload: () => null ,
processLineMessage: (text) => ({ text, flexMessages: [] }),
chunkMarkdownText: (text) => [text],
sendLineReplyChunks,
replyMessageLine,
pushMessageLine,
pushTextMessageWithQuickReplies,
createTextMessageWithQuickReplies,
createQuickReplyItems: createQuickReplyItems as LineAutoReplyDeps["createQuickReplyItems" ],
pushMessagesLine,
createFlexMessage: createFlexMessage as LineAutoReplyDeps["createFlexMessage" ],
createImageMessage,
createLocationMessage,
...overrides,
};
return {
deps,
replyMessageLine,
pushMessageLine,
pushTextMessageWithQuickReplies,
createTextMessageWithQuickReplies,
createQuickReplyItems,
pushMessagesLine,
};
}
it("uses reply token for text before sending rich messages" , async () => {
const lineData = {
flexMessage: { altText: "Card" , contents: { type: "bubble" } },
};
const { deps, replyMessageLine, pushMessagesLine, createQuickReplyItems } = createDeps();
const result = await deliverLineAutoReply({
...baseDeliveryParams,
payload: { text: "hello" , channelData: { line: lineData } },
lineData,
deps,
});
expect(result.replyTokenUsed).toBe(true );
expect(replyMessageLine).toHaveBeenCalledTimes(1 );
expect(replyMessageLine).toHaveBeenCalledWith("token" , [{ type: "text" , text: "hello" }], {
cfg: LINE_TEST_CFG,
accountId: "acc" ,
});
expect(pushMessagesLine).toHaveBeenCalledTimes(1 );
expect(pushMessagesLine).toHaveBeenCalledWith(
"line:user:1" ,
[createFlexMessage("Card" , { type: "bubble" })],
{ cfg: LINE_TEST_CFG, accountId: "acc" },
);
expect(createQuickReplyItems).not.toHaveBeenCalled();
});
it("uses reply token for rich-only payloads" , async () => {
const lineData = {
flexMessage: { altText: "Card" , contents: { type: "bubble" } },
quickReplies: ["A" ],
};
const { deps, replyMessageLine, pushMessagesLine, createQuickReplyItems } = createDeps({
processLineMessage: () => ({ text: "" , flexMessages: [] }),
chunkMarkdownText: () => [],
sendLineReplyChunks: vi.fn(async () => ({ replyTokenUsed: false })),
});
const result = await deliverLineAutoReply({
...baseDeliveryParams,
payload: { channelData: { line: lineData } },
lineData,
deps,
});
expect(result.replyTokenUsed).toBe(true );
expect(replyMessageLine).toHaveBeenCalledTimes(1 );
expect(replyMessageLine).toHaveBeenCalledWith(
"token" ,
[
{
...createFlexMessage("Card" , { type: "bubble" }),
quickReply: { items: ["A" ] },
},
],
{ cfg: LINE_TEST_CFG, accountId: "acc" },
);
expect(pushMessagesLine).not.toHaveBeenCalled();
expect(createQuickReplyItems).toHaveBeenCalledWith(["A" ]);
});
it("sends rich messages before quick-reply text so quick replies remain visible" , async () => {
const createTextMessageWithQuickReplies = vi.fn((text: string, _quickReplies: string[]) => ({
type: "text" as const ,
text,
quickReply: { items: ["A" ] },
}));
const lineData = {
flexMessage: { altText: "Card" , contents: { type: "bubble" } },
quickReplies: ["A" ],
};
const { deps, pushMessagesLine, replyMessageLine } = createDeps({
createTextMessageWithQuickReplies:
createTextMessageWithQuickReplies as LineAutoReplyDeps["createTextMessageWithQuickReplies" ],
});
await deliverLineAutoReply({
...baseDeliveryParams,
payload: { text: "hello" , channelData: { line: lineData } },
lineData,
deps,
});
expect(pushMessagesLine).toHaveBeenCalledWith(
"line:user:1" ,
[createFlexMessage("Card" , { type: "bubble" })],
{ cfg: LINE_TEST_CFG, accountId: "acc" },
);
expect(replyMessageLine).toHaveBeenCalledWith(
"token" ,
[
{
type: "text" ,
text: "hello" ,
quickReply: { items: ["A" ] },
},
],
{ cfg: LINE_TEST_CFG, accountId: "acc" },
);
const pushOrder = pushMessagesLine.mock.invocationCallOrder[0 ];
const replyOrder = replyMessageLine.mock.invocationCallOrder[0 ];
expect(pushOrder).toBeLessThan(replyOrder);
});
it("falls back to push when reply token delivery fails" , async () => {
const lineData = {
flexMessage: { altText: "Card" , contents: { type: "bubble" } },
};
const failingReplyMessageLine = vi.fn(async () => {
throw new Error("reply failed" );
});
const { deps, pushMessagesLine } = createDeps({
processLineMessage: () => ({ text: "" , flexMessages: [] }),
chunkMarkdownText: () => [],
replyMessageLine: failingReplyMessageLine as LineAutoReplyDeps["replyMessageLine" ],
});
const result = await deliverLineAutoReply({
...baseDeliveryParams,
payload: { channelData: { line: lineData } },
lineData,
deps,
});
expect(result.replyTokenUsed).toBe(true );
expect(failingReplyMessageLine).toHaveBeenCalledTimes(1 );
expect(pushMessagesLine).toHaveBeenCalledWith(
"line:user:1" ,
[createFlexMessage("Card" , { type: "bubble" })],
{ cfg: LINE_TEST_CFG, accountId: "acc" },
);
});
});
Messung V0.5 in Prozent C=94 H=95 G=94
¤ Dauer der Verarbeitung: 0.13 Sekunden
(vorverarbeitet am 2026-06-05)
¤
*© Formatika GbR, Deutschland