import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import { saveSessionStore } from "../../config/sessions.js"; import { isBotMentionedFromTargets, resolveMentionTargets } from "./mentions.js"; import { getSessionSnapshot } from "./session-snapshot.js"; import type { WebInboundMsg } from "./types.js"; import { elide, isLikelyWhatsAppCryptoError } from "./util.js"; const makeMsg = (overrides: Partial): WebInboundMsg => ({ id: "m1", from: "120363401234567890@g.us", conversationId: "120363401234567890@g.us", to: "15551234567@s.whatsapp.net", accountId: "default", body: "", chatType: "group", chatId: "120363401234567890@g.us", sendComposing: async () => {}, reply: async () => {}, sendMedia: async () => {}, ...overrides, }) as WebInboundMsg; async function withTempDir(prefix: string, run: (dir: string) => Promise): Promise { const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); try { return await run(dir); } finally { await fs.rm(dir, { recursive: true, force: true }); } } describe("isBotMentionedFromTargets", () => { const mentionCfg = { mentionRegexes: [/\bopenclaw\b/i] }; it("ignores regex matches when other mentions are present", () => { const msg = makeMsg({ body: "@OpenClaw please help", mentionedJids: ["19998887777@s.whatsapp.net"], selfE164: "+15551234567", selfJid: "15551234567@s.whatsapp.net", }); const targets = resolveMentionTargets(msg); expect(isBotMentionedFromTargets(msg, mentionCfg, targets)).toBe(false); }); it("matches explicit self mentions", () => { const msg = makeMsg({ body: "hey", mentionedJids: ["15551234567@s.whatsapp.net"], selfE164: "+15551234567", selfJid: "15551234567@s.whatsapp.net", }); const targets = resolveMentionTargets(msg); expect(isBotMentionedFromTargets(msg, mentionCfg, targets)).toBe(true); }); it("falls back to regex when no mentions are present", () => { const msg = makeMsg({ body: "openclaw can you help?", selfE164: "+15551234567", selfJid: "15551234567@s.whatsapp.net", }); const targets = resolveMentionTargets(msg); expect(isBotMentionedFromTargets(msg, mentionCfg, targets)).toBe(true); }); it("ignores JID mentions in self-chat mode", () => { const cfg = { mentionRegexes: [/\bopenclaw\b/i], allowFrom: ["+999"] }; const msg = makeMsg({ body: "@owner ping", mentionedJids: ["999@s.whatsapp.net"], selfE164: "+999", selfJid: "999@s.whatsapp.net", }); const targets = resolveMentionTargets(msg); expect(isBotMentionedFromTargets(msg, cfg, targets)).toBe(false); const msgTextMention = makeMsg({ body: "openclaw ping", selfE164: "+999", selfJid: "999@s.whatsapp.net", }); const targetsText = resolveMentionTargets(msgTextMention); expect(isBotMentionedFromTargets(msgTextMention, cfg, targetsText)).toBe(true); }); it("matches fallback number mentions when regexes do not match", () => { const msg = makeMsg({ body: "please check +1 555 123 4567", selfE164: "+15551234567", selfJid: "15551234567@s.whatsapp.net", }); const targets = resolveMentionTargets(msg); expect(isBotMentionedFromTargets(msg, { mentionRegexes: [] }, targets)).toBe(true); }); }); describe("resolveMentionTargets with @lid mapping", () => { it("uses @lid reverse mapping for mentions and self identity", async () => { await withTempDir("openclaw-lid-mapping-", async (authDir) => { await fs.writeFile( path.join(authDir, "lid-mapping-777_reverse.json"), JSON.stringify("+1777"), ); const mentionTargets = resolveMentionTargets( makeMsg({ body: "ping", mentionedJids: ["777@lid"], selfE164: "+15551234567", selfJid: "15551234567@s.whatsapp.net", }), authDir, ); expect(mentionTargets.normalizedMentions).toContain("+1777"); const selfTargets = resolveMentionTargets( makeMsg({ body: "ping", selfJid: "777@lid", }), authDir, ); expect(selfTargets.selfE164).toBe("+1777"); }); }); }); describe("getSessionSnapshot", () => { it("uses channel reset overrides when configured", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date(2026, 0, 18, 5, 0, 0)); try { await withTempDir("openclaw-snapshot-", async (root) => { const storePath = path.join(root, "sessions.json"); const sessionKey = "agent:main:whatsapp:dm:s1"; await saveSessionStore(storePath, { [sessionKey]: { sessionId: "snapshot-session", updatedAt: new Date(2026, 0, 18, 3, 30, 0).getTime(), lastChannel: "whatsapp", }, }); const cfg = { session: { store: storePath, reset: { mode: "daily", atHour: 4, idleMinutes: 240 }, resetByChannel: { whatsapp: { mode: "idle", idleMinutes: 360 }, }, }, } as Parameters[0]; const snapshot = getSessionSnapshot(cfg, "whatsapp:+15550001111", true, { sessionKey, }); expect(snapshot.resetPolicy.mode).toBe("idle"); expect(snapshot.resetPolicy.idleMinutes).toBe(360); expect(snapshot.fresh).toBe(true); expect(snapshot.dailyResetAt).toBeUndefined(); }); } finally { vi.useRealTimers(); } }); }); describe("web auto-reply util", () => { describe("elide", () => { it("returns undefined for undefined input", () => { expect(elide(undefined)).toBe(undefined); }); it("returns input when under limit", () => { expect(elide("hi", 10)).toBe("hi"); }); it("truncates and annotates when over limit", () => { expect(elide("abcdef", 3)).toBe("abc… (truncated 3 chars)"); }); }); describe("isLikelyWhatsAppCryptoError", () => { it("returns false for non-matching reasons", () => { expect(isLikelyWhatsAppCryptoError(new Error("boom"))).toBe(false); expect(isLikelyWhatsAppCryptoError("boom")).toBe(false); expect(isLikelyWhatsAppCryptoError({ message: "bad mac" })).toBe(false); }); it("matches known Baileys crypto auth errors (string)", () => { expect( isLikelyWhatsAppCryptoError( "baileys: unsupported state or unable to authenticate data (noise-handler)", ), ).toBe(true); expect(isLikelyWhatsAppCryptoError("bad mac in aesDecryptGCM (baileys)")).toBe(true); }); it("matches known Baileys crypto auth errors (Error)", () => { const err = new Error("bad mac"); err.stack = "at something\nat @whiskeysockets/baileys/noise-handler\n"; expect(isLikelyWhatsAppCryptoError(err)).toBe(true); }); it("does not throw on circular objects", () => { const circular: Record = {}; circular.self = circular; expect(isLikelyWhatsAppCryptoError(circular)).toBe(false); }); it("handles non-string reasons without throwing", () => { expect(isLikelyWhatsAppCryptoError(null)).toBe(false); expect(isLikelyWhatsAppCryptoError(123)).toBe(false); expect(isLikelyWhatsAppCryptoError(true)).toBe(false); expect(isLikelyWhatsAppCryptoError(123n)).toBe(false); expect(isLikelyWhatsAppCryptoError(Symbol("bad mac"))).toBe(false); expect(isLikelyWhatsAppCryptoError(function namedFn() {})).toBe(false); }); }); });