Files
openclaw/src/web/auto-reply/web-auto-reply-utils.test.ts
2026-02-22 07:44:57 +00:00

267 lines
8.6 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { saveSessionStore } from "../../config/sessions.js";
import { withTempDir } from "../../test-utils/temp-dir.js";
import {
debugMention,
isBotMentionedFromTargets,
resolveMentionTargets,
resolveOwnerList,
} 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>): 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;
describe("isBotMentionedFromTargets", () => {
const mentionCfg = { mentionRegexes: [/\bopenclaw\b/i] };
function expectMentioned(
msg: WebInboundMsg,
cfg: { mentionRegexes: RegExp[]; allowFrom?: Array<string | number> },
expected: boolean,
) {
const targets = resolveMentionTargets(msg);
expect(isBotMentionedFromTargets(msg, cfg, targets)).toBe(expected);
}
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",
});
expectMentioned(msg, mentionCfg, false);
});
it("matches explicit self mentions", () => {
const msg = makeMsg({
body: "hey",
mentionedJids: ["15551234567@s.whatsapp.net"],
selfE164: "+15551234567",
selfJid: "15551234567@s.whatsapp.net",
});
expectMentioned(msg, mentionCfg, 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",
});
expectMentioned(msg, mentionCfg, 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",
});
expectMentioned(msg, cfg, false);
const msgTextMention = makeMsg({
body: "openclaw ping",
selfE164: "+999",
selfJid: "999@s.whatsapp.net",
});
expectMentioned(msgTextMention, cfg, 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",
});
expectMentioned(msg, { mentionRegexes: [] }, 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<typeof getSessionSnapshot>[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("mentions diagnostics", () => {
it("returns normalized debug fields and mention outcome", () => {
const msg = makeMsg({
from: "777@lid",
body: "openclaw ping",
selfE164: "+15551234567",
selfJid: "15551234567@s.whatsapp.net",
});
const result = debugMention(msg, { mentionRegexes: [/\bopenclaw\b/i] });
expect(result.wasMentioned).toBe(true);
expect(result.details.bodyClean).toBe("openclaw ping");
expect(result.details.normalizedMentionedJids).toBeNull();
});
it("resolves owner list from allowFrom or falls back to self", () => {
expect(
resolveOwnerList(
{
mentionRegexes: [],
allowFrom: ["*", " +1 555 000 1111 "],
},
null,
),
).toEqual(["+15550001111"]);
expect(resolveOwnerList({ mentionRegexes: [] }, "+1 555 000 2222")).toEqual(["+15550002222"]);
});
});
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("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<string, unknown> = {};
circular.self = circular;
expect(isLikelyWhatsAppCryptoError(circular)).toBe(false);
});
const cases: Array<{ name: string; value: unknown; expected: boolean }> = [
{ name: "returns false for non-matching Error", value: new Error("boom"), expected: false },
{ name: "returns false for non-matching string", value: "boom", expected: false },
{
name: "returns false for bad-mac object without whatsapp/baileys markers",
value: { message: "bad mac" },
expected: false,
},
{
name: "matches known Baileys crypto auth errors (string, unsupported state)",
value: "baileys: unsupported state or unable to authenticate data (noise-handler)",
expected: true,
},
{
name: "matches known Baileys crypto auth errors (string, bad mac)",
value: "bad mac in aesDecryptGCM (baileys)",
expected: true,
},
{ name: "handles null reason without throwing", value: null, expected: false },
{ name: "handles number reason without throwing", value: 123, expected: false },
{ name: "handles boolean reason without throwing", value: true, expected: false },
{ name: "handles bigint reason without throwing", value: 123n, expected: false },
{ name: "handles symbol reason without throwing", value: Symbol("bad mac"), expected: false },
{
name: "handles function reason without throwing",
value: function namedFn() {},
expected: false,
},
];
for (const testCase of cases) {
it(testCase.name, () => {
expect(isLikelyWhatsAppCryptoError(testCase.value)).toBe(testCase.expected);
});
}
});
});