Files

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: "" });
});
});