144 lines
4.8 KiB
TypeScript
144 lines
4.8 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { Hono } from "hono";
|
|
import {
|
|
normalizeWhatsAppPayload,
|
|
createWhatsAppAdapter,
|
|
WHATSAPP_ERROR_REPLY,
|
|
} from "../src/channels/whatsapp/webhook.js";
|
|
import type { WaToolboxPayload } from "../src/channels/whatsapp/webhook.js";
|
|
import { RATE_LIMITED_REPLY } from "../src/core/message-service.js";
|
|
import type { MessageService } from "../src/core/message-service.js";
|
|
|
|
describe("normalizeWhatsAppPayload", () => {
|
|
it("keys identity on m_phone and conversation on the sender @lid", () => {
|
|
const p: WaToolboxPayload = {
|
|
m_phone: "60175885290",
|
|
m_user: "144311035379840@lid",
|
|
m_text: "testing message",
|
|
};
|
|
const r = normalizeWhatsAppPayload(p);
|
|
expect(r.channel).toBe("whatsapp");
|
|
expect(r.externalUserId).toBe("60175885290");
|
|
expect(r.externalConversationId).toBe("144311035379840@lid");
|
|
expect(r.text).toBe("testing message");
|
|
});
|
|
|
|
it("falls back to m_user when m_phone is absent", () => {
|
|
const r = normalizeWhatsAppPayload({
|
|
m_user: "144311035379840@lid",
|
|
m_text: "hi",
|
|
});
|
|
expect(r.externalUserId).toBe("144311035379840@lid");
|
|
});
|
|
|
|
it("uses the group id as the conversation key in group chats", () => {
|
|
const r = normalizeWhatsAppPayload({
|
|
m_phone: "60175885290",
|
|
m_user: "144311035379840@lid",
|
|
m_gid: "[email protected]",
|
|
m_text: "hello group",
|
|
});
|
|
expect(r.externalConversationId).toBe("[email protected]");
|
|
expect(r.externalUserId).toBe("60175885290");
|
|
});
|
|
|
|
it("reads text from m_text, then m_content, then msg", () => {
|
|
expect(normalizeWhatsAppPayload({ m_text: "a" }).text).toBe("a");
|
|
expect(normalizeWhatsAppPayload({ m_content: "b" }).text).toBe("b");
|
|
expect(normalizeWhatsAppPayload({ msg: "c" }).text).toBe("c");
|
|
expect(normalizeWhatsAppPayload({}).text).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("createWhatsAppAdapter handler", () => {
|
|
function makeAdapter(
|
|
handle: MessageService["handle"],
|
|
trialReply?: string,
|
|
) {
|
|
const messages = { handle } as unknown as MessageService;
|
|
const trial = { handleMessage: async () => trialReply } as never;
|
|
const logger = {
|
|
info: () => {},
|
|
warn: () => {},
|
|
error: () => {},
|
|
} as never;
|
|
const adapter = createWhatsAppAdapter({ messages, logger, trial });
|
|
const app = new Hono();
|
|
app.post("/wa", (c) => adapter.handler(c));
|
|
return app;
|
|
}
|
|
|
|
it("routes trial-flow messages before the LLM", async () => {
|
|
const app = makeAdapter(
|
|
async () => ({ handled: true as const, reply: "LLM", conversationId: 1 }),
|
|
"TRIAL FLOW REPLY",
|
|
);
|
|
const res = await post(app, { m_phone: "60103181872", m_text: "I want try CZH03" });
|
|
expect(res.status).toBe(200);
|
|
expect(await res.json()).toEqual({ msg: "TRIAL FLOW REPLY" });
|
|
});
|
|
|
|
async function post(app: Hono, body: unknown) {
|
|
return app.request("/wa", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
it("returns the reply in the msg field", async () => {
|
|
const app = makeAdapter(async () => ({
|
|
handled: true as const,
|
|
reply: "Hello from the bot",
|
|
conversationId: 1,
|
|
}));
|
|
const res = await post(app, { m_phone: "60175885290", m_text: "hi" });
|
|
expect(res.status).toBe(200);
|
|
expect(await res.json()).toEqual({ msg: "Hello from the bot" });
|
|
});
|
|
|
|
it("returns the rate-limited notice once, then silence", async () => {
|
|
const app = makeAdapter(async () => ({
|
|
handled: false as const,
|
|
reason: "rate_limited" as const,
|
|
retryAfterMs: 5000,
|
|
notify: true,
|
|
}));
|
|
const res = await post(app, { m_phone: "60175885290", m_text: "spam" });
|
|
expect(await res.json()).toEqual({ msg: RATE_LIMITED_REPLY });
|
|
|
|
const app2 = makeAdapter(async () => ({
|
|
handled: false as const,
|
|
reason: "rate_limited" as const,
|
|
retryAfterMs: 5000,
|
|
notify: false,
|
|
}));
|
|
const res2 = await post(app2, { m_phone: "60175885290", m_text: "spam" });
|
|
expect(await res2.json()).toEqual({ msg: "" });
|
|
});
|
|
|
|
it("answers 200 with a friendly error when the pipeline throws", async () => {
|
|
const app = makeAdapter(async () => {
|
|
throw new Error("boom");
|
|
});
|
|
const res = await post(app, { m_phone: "60175885290", m_text: "hi" });
|
|
expect(res.status).toBe(200);
|
|
expect(await res.json()).toEqual({ msg: WHATSAPP_ERROR_REPLY });
|
|
});
|
|
|
|
it("rejects a non-JSON body with 400 and empty msg", async () => {
|
|
const app = makeAdapter(async () => ({
|
|
handled: true as const,
|
|
reply: "x",
|
|
conversationId: 1,
|
|
}));
|
|
const res = await app.request("/wa", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: "not-json",
|
|
});
|
|
expect(res.status).toBe(400);
|
|
expect(await res.json()).toEqual({ msg: "" });
|
|
});
|
|
});
|