test: collapse telegram and whatsapp target suites

This commit is contained in:
Peter Steinberger
2026-03-25 05:18:59 +00:00
parent 793b36c5d2
commit 6f137fff76
10 changed files with 588 additions and 595 deletions

View File

@@ -1,26 +0,0 @@
import { describe, expect, it } from "vitest";
import { isNumericTelegramUserId, normalizeTelegramAllowFromEntry } from "./allow-from.js";
describe("telegram allow-from helpers", () => {
it("normalizes tg/telegram prefixes", () => {
const cases = [
{ value: " TG:123 ", expected: "123" },
{ value: "telegram:@someone", expected: "@someone" },
] as const;
for (const testCase of cases) {
expect(normalizeTelegramAllowFromEntry(testCase.value)).toBe(testCase.expected);
}
});
it("accepts signed numeric IDs", () => {
const cases = [
{ value: "123456789", expected: true },
{ value: "-1001234567890", expected: true },
{ value: "@someone", expected: false },
{ value: "12 34", expected: false },
] as const;
for (const testCase of cases) {
expect(isNumericTelegramUserId(testCase.value)).toBe(testCase.expected);
}
});
});

View File

@@ -1,23 +0,0 @@
import { describe, expect, it } from "vitest";
import { looksLikeTelegramTargetId, normalizeTelegramMessagingTarget } from "./normalize.js";
describe("telegram target normalization", () => {
it("normalizes telegram prefixes, group targets, and topic suffixes", () => {
expect(normalizeTelegramMessagingTarget("telegram:123456")).toBe("telegram:123456");
expect(normalizeTelegramMessagingTarget("tg:group:-100123")).toBe("telegram:group:-100123");
expect(normalizeTelegramMessagingTarget("telegram:-100123:topic:99")).toBe(
"telegram:-100123:topic:99",
);
});
it("returns undefined for invalid telegram recipients", () => {
expect(normalizeTelegramMessagingTarget("telegram:")).toBeUndefined();
expect(normalizeTelegramMessagingTarget(" ")).toBeUndefined();
});
it("detects valid telegram target identifiers", () => {
expect(looksLikeTelegramTargetId("telegram:123456")).toBe(true);
expect(looksLikeTelegramTargetId("tg:group:-100123")).toBe(true);
expect(looksLikeTelegramTargetId("hello world")).toBe(false);
});
});

View File

@@ -1,46 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { resolveTelegramAllowFromEntries } from "./setup-core.js";
describe("resolveTelegramAllowFromEntries", () => {
it("passes apiRoot through username lookups", async () => {
const globalFetch = vi.fn(async () => {
throw new Error("global fetch should not be called");
});
const fetchMock = vi.fn(async () => ({
ok: true,
json: async () => ({ ok: true, result: { id: 12345 } }),
}));
vi.stubGlobal("fetch", globalFetch);
const proxyFetch = vi.fn();
const fetchModule = await import("./fetch.js");
const proxyModule = await import("./proxy.js");
const resolveTelegramFetch = vi.spyOn(fetchModule, "resolveTelegramFetch");
const makeProxyFetch = vi.spyOn(proxyModule, "makeProxyFetch");
makeProxyFetch.mockReturnValue(proxyFetch as unknown as typeof fetch);
resolveTelegramFetch.mockReturnValue(fetchMock as unknown as typeof fetch);
try {
const resolved = await resolveTelegramAllowFromEntries({
entries: ["@user"],
credentialValue: "tok",
apiRoot: "https://custom.telegram.test/root/",
proxyUrl: "http://127.0.0.1:8080",
network: { autoSelectFamily: false, dnsResultOrder: "ipv4first" },
});
expect(resolved).toEqual([{ input: "@user", resolved: true, id: "12345" }]);
expect(makeProxyFetch).toHaveBeenCalledWith("http://127.0.0.1:8080");
expect(resolveTelegramFetch).toHaveBeenCalledWith(proxyFetch, {
network: { autoSelectFamily: false, dnsResultOrder: "ipv4first" },
});
expect(fetchMock).toHaveBeenCalledWith(
"https://custom.telegram.test/root/bottok/getChat?chat_id=%40user",
undefined,
);
} finally {
makeProxyFetch.mockRestore();
resolveTelegramFetch.mockRestore();
vi.unstubAllGlobals();
}
});
});

View File

@@ -6,6 +6,7 @@ import {
runSetupWizardFinalize,
runSetupWizardPrepare,
} from "../../../test/helpers/extensions/setup-wizard.js";
import { resolveTelegramAllowFromEntries } from "./setup-core.js";
import { telegramSetupWizard } from "./setup-surface.js";
async function runPrepare(cfg: OpenClawConfig, accountId: string) {
@@ -160,3 +161,47 @@ describe("telegramSetupWizard.finalize", () => {
expect(note).not.toHaveBeenCalled();
});
});
describe("resolveTelegramAllowFromEntries", () => {
it("passes apiRoot through username lookups", async () => {
const globalFetch = vi.fn(async () => {
throw new Error("global fetch should not be called");
});
const fetchMock = vi.fn(async () => ({
ok: true,
json: async () => ({ ok: true, result: { id: 12345 } }),
}));
vi.stubGlobal("fetch", globalFetch);
const proxyFetch = vi.fn();
const fetchModule = await import("./fetch.js");
const proxyModule = await import("./proxy.js");
const resolveTelegramFetch = vi.spyOn(fetchModule, "resolveTelegramFetch");
const makeProxyFetch = vi.spyOn(proxyModule, "makeProxyFetch");
makeProxyFetch.mockReturnValue(proxyFetch as unknown as typeof fetch);
resolveTelegramFetch.mockReturnValue(fetchMock as unknown as typeof fetch);
try {
const resolved = await resolveTelegramAllowFromEntries({
entries: ["@user"],
credentialValue: "tok",
apiRoot: "https://custom.telegram.test/root/",
proxyUrl: "http://127.0.0.1:8080",
network: { autoSelectFamily: false, dnsResultOrder: "ipv4first" },
});
expect(resolved).toEqual([{ input: "@user", resolved: true, id: "12345" }]);
expect(makeProxyFetch).toHaveBeenCalledWith("http://127.0.0.1:8080");
expect(resolveTelegramFetch).toHaveBeenCalledWith(proxyFetch, {
network: { autoSelectFamily: false, dnsResultOrder: "ipv4first" },
});
expect(fetchMock).toHaveBeenCalledWith(
"https://custom.telegram.test/root/bottok/getChat?chat_id=%40user",
undefined,
);
} finally {
makeProxyFetch.mockRestore();
resolveTelegramFetch.mockRestore();
vi.unstubAllGlobals();
}
});
});

View File

@@ -1,183 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../../src/config/config.js";
const readConfigFileSnapshotForWrite = vi.fn();
const writeConfigFile = vi.fn();
const loadCronStore = vi.fn();
const resolveCronStorePath = vi.fn();
const saveCronStore = vi.fn();
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
return {
...actual,
readConfigFileSnapshotForWrite,
writeConfigFile,
loadCronStore,
resolveCronStorePath,
saveCronStore,
};
});
describe("maybePersistResolvedTelegramTarget", () => {
let maybePersistResolvedTelegramTarget: typeof import("./target-writeback.js").maybePersistResolvedTelegramTarget;
beforeEach(async () => {
vi.resetModules();
({ maybePersistResolvedTelegramTarget } = await import("./target-writeback.js"));
readConfigFileSnapshotForWrite.mockReset();
writeConfigFile.mockReset();
loadCronStore.mockReset();
resolveCronStorePath.mockReset();
saveCronStore.mockReset();
resolveCronStorePath.mockReturnValue("/tmp/cron/jobs.json");
});
it("skips writeback when target is already numeric", async () => {
await maybePersistResolvedTelegramTarget({
cfg: {} as OpenClawConfig,
rawTarget: "-100123",
resolvedChatId: "-100123",
});
expect(readConfigFileSnapshotForWrite).not.toHaveBeenCalled();
expect(loadCronStore).not.toHaveBeenCalled();
});
it("writes back matching config and cron targets", async () => {
readConfigFileSnapshotForWrite.mockResolvedValue({
snapshot: {
config: {
channels: {
telegram: {
defaultTo: "t.me/mychannel",
accounts: {
alerts: {
defaultTo: "@mychannel",
},
},
},
},
},
},
writeOptions: { expectedConfigPath: "/tmp/openclaw.json" },
});
loadCronStore.mockResolvedValue({
version: 1,
jobs: [
{ id: "a", delivery: { channel: "telegram", to: "https://t.me/mychannel" } },
{ id: "b", delivery: { channel: "slack", to: "C123" } },
],
});
await maybePersistResolvedTelegramTarget({
cfg: {
cron: { store: "/tmp/cron/jobs.json" },
} as OpenClawConfig,
rawTarget: "t.me/mychannel",
resolvedChatId: "-100123",
});
expect(writeConfigFile).toHaveBeenCalledTimes(1);
expect(writeConfigFile).toHaveBeenCalledWith(
expect.objectContaining({
channels: {
telegram: {
defaultTo: "-100123",
accounts: {
alerts: {
defaultTo: "-100123",
},
},
},
},
}),
expect.objectContaining({ expectedConfigPath: "/tmp/openclaw.json" }),
);
expect(saveCronStore).toHaveBeenCalledTimes(1);
expect(saveCronStore).toHaveBeenCalledWith(
"/tmp/cron/jobs.json",
expect.objectContaining({
jobs: [
{ id: "a", delivery: { channel: "telegram", to: "-100123" } },
{ id: "b", delivery: { channel: "slack", to: "C123" } },
],
}),
);
});
it("preserves topic suffix style in writeback target", async () => {
readConfigFileSnapshotForWrite.mockResolvedValue({
snapshot: {
config: {
channels: {
telegram: {
defaultTo: "t.me/mychannel:topic:9",
},
},
},
},
writeOptions: {},
});
loadCronStore.mockResolvedValue({ version: 1, jobs: [] });
await maybePersistResolvedTelegramTarget({
cfg: {} as OpenClawConfig,
rawTarget: "t.me/mychannel:topic:9",
resolvedChatId: "-100123",
});
expect(writeConfigFile).toHaveBeenCalledWith(
expect.objectContaining({
channels: {
telegram: {
defaultTo: "-100123:topic:9",
},
},
}),
expect.any(Object),
);
});
it("matches username targets case-insensitively", async () => {
readConfigFileSnapshotForWrite.mockResolvedValue({
snapshot: {
config: {
channels: {
telegram: {
defaultTo: "https://t.me/mychannel",
},
},
},
},
writeOptions: {},
});
loadCronStore.mockResolvedValue({
version: 1,
jobs: [{ id: "a", delivery: { channel: "telegram", to: "https://t.me/mychannel" } }],
});
await maybePersistResolvedTelegramTarget({
cfg: {} as OpenClawConfig,
rawTarget: "@MyChannel",
resolvedChatId: "-100123",
});
expect(writeConfigFile).toHaveBeenCalledWith(
expect.objectContaining({
channels: {
telegram: {
defaultTo: "-100123",
},
},
}),
expect.any(Object),
);
expect(saveCronStore).toHaveBeenCalledWith(
"/tmp/cron/jobs.json",
expect.objectContaining({
jobs: [{ id: "a", delivery: { channel: "telegram", to: "-100123" } }],
}),
);
});
});

View File

@@ -1,8 +1,11 @@
import { describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../../src/config/config.js";
import { isNumericTelegramUserId, normalizeTelegramAllowFromEntry } from "./allow-from.js";
import {
resolveTelegramGroupRequireMention,
resolveTelegramGroupToolPolicy,
} from "./group-policy.js";
import { looksLikeTelegramTargetId, normalizeTelegramMessagingTarget } from "./normalize.js";
import {
isNumericTelegramChatId,
normalizeTelegramChatId,
@@ -11,6 +14,24 @@ import {
stripTelegramInternalPrefixes,
} from "./targets.js";
const readConfigFileSnapshotForWrite = vi.fn();
const writeConfigFile = vi.fn();
const loadCronStore = vi.fn();
const resolveCronStorePath = vi.fn();
const saveCronStore = vi.fn();
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
return {
...actual,
readConfigFileSnapshotForWrite,
writeConfigFile,
loadCronStore,
resolveCronStorePath,
saveCronStore,
};
});
describe("stripTelegramInternalPrefixes", () => {
it("strips telegram prefix", () => {
expect(stripTelegramInternalPrefixes("telegram:123")).toBe("123");
@@ -168,3 +189,211 @@ describe("telegram group policy", () => {
);
});
});
describe("telegram allow-from helpers", () => {
it("normalizes tg/telegram prefixes", () => {
const cases = [
{ value: " TG:123 ", expected: "123" },
{ value: "telegram:@someone", expected: "@someone" },
] as const;
for (const testCase of cases) {
expect(normalizeTelegramAllowFromEntry(testCase.value)).toBe(testCase.expected);
}
});
it("accepts signed numeric IDs", () => {
const cases = [
{ value: "123456789", expected: true },
{ value: "-1001234567890", expected: true },
{ value: "@someone", expected: false },
{ value: "12 34", expected: false },
] as const;
for (const testCase of cases) {
expect(isNumericTelegramUserId(testCase.value)).toBe(testCase.expected);
}
});
});
describe("telegram target normalization", () => {
it("normalizes telegram prefixes, group targets, and topic suffixes", () => {
expect(normalizeTelegramMessagingTarget("telegram:123456")).toBe("telegram:123456");
expect(normalizeTelegramMessagingTarget("tg:group:-100123")).toBe("telegram:group:-100123");
expect(normalizeTelegramMessagingTarget("telegram:-100123:topic:99")).toBe(
"telegram:-100123:topic:99",
);
});
it("returns undefined for invalid telegram recipients", () => {
expect(normalizeTelegramMessagingTarget("telegram:")).toBeUndefined();
expect(normalizeTelegramMessagingTarget(" ")).toBeUndefined();
});
it("detects valid telegram target identifiers", () => {
expect(looksLikeTelegramTargetId("telegram:123456")).toBe(true);
expect(looksLikeTelegramTargetId("tg:group:-100123")).toBe(true);
expect(looksLikeTelegramTargetId("hello world")).toBe(false);
});
});
describe("maybePersistResolvedTelegramTarget", () => {
let maybePersistResolvedTelegramTarget: typeof import("./target-writeback.js").maybePersistResolvedTelegramTarget;
beforeEach(async () => {
vi.resetModules();
({ maybePersistResolvedTelegramTarget } = await import("./target-writeback.js"));
readConfigFileSnapshotForWrite.mockReset();
writeConfigFile.mockReset();
loadCronStore.mockReset();
resolveCronStorePath.mockReset();
saveCronStore.mockReset();
resolveCronStorePath.mockReturnValue("/tmp/cron/jobs.json");
});
it("skips writeback when target is already numeric", async () => {
await maybePersistResolvedTelegramTarget({
cfg: {} as OpenClawConfig,
rawTarget: "-100123",
resolvedChatId: "-100123",
});
expect(readConfigFileSnapshotForWrite).not.toHaveBeenCalled();
expect(loadCronStore).not.toHaveBeenCalled();
});
it("writes back matching config and cron targets", async () => {
readConfigFileSnapshotForWrite.mockResolvedValue({
snapshot: {
config: {
channels: {
telegram: {
defaultTo: "t.me/mychannel",
accounts: {
alerts: {
defaultTo: "@mychannel",
},
},
},
},
},
},
writeOptions: { expectedConfigPath: "/tmp/openclaw.json" },
});
loadCronStore.mockResolvedValue({
version: 1,
jobs: [
{ id: "a", delivery: { channel: "telegram", to: "https://t.me/mychannel" } },
{ id: "b", delivery: { channel: "slack", to: "C123" } },
],
});
await maybePersistResolvedTelegramTarget({
cfg: {
cron: { store: "/tmp/cron/jobs.json" },
} as OpenClawConfig,
rawTarget: "t.me/mychannel",
resolvedChatId: "-100123",
});
expect(writeConfigFile).toHaveBeenCalledTimes(1);
expect(writeConfigFile).toHaveBeenCalledWith(
expect.objectContaining({
channels: {
telegram: {
defaultTo: "-100123",
accounts: {
alerts: {
defaultTo: "-100123",
},
},
},
},
}),
expect.objectContaining({ expectedConfigPath: "/tmp/openclaw.json" }),
);
expect(saveCronStore).toHaveBeenCalledTimes(1);
expect(saveCronStore).toHaveBeenCalledWith(
"/tmp/cron/jobs.json",
expect.objectContaining({
jobs: [
{ id: "a", delivery: { channel: "telegram", to: "-100123" } },
{ id: "b", delivery: { channel: "slack", to: "C123" } },
],
}),
);
});
it("preserves topic suffix style in writeback target", async () => {
readConfigFileSnapshotForWrite.mockResolvedValue({
snapshot: {
config: {
channels: {
telegram: {
defaultTo: "t.me/mychannel:topic:9",
},
},
},
},
writeOptions: {},
});
loadCronStore.mockResolvedValue({ version: 1, jobs: [] });
await maybePersistResolvedTelegramTarget({
cfg: {} as OpenClawConfig,
rawTarget: "t.me/mychannel:topic:9",
resolvedChatId: "-100123",
});
expect(writeConfigFile).toHaveBeenCalledWith(
expect.objectContaining({
channels: {
telegram: {
defaultTo: "-100123:topic:9",
},
},
}),
expect.any(Object),
);
});
it("matches username targets case-insensitively", async () => {
readConfigFileSnapshotForWrite.mockResolvedValue({
snapshot: {
config: {
channels: {
telegram: {
defaultTo: "https://t.me/mychannel",
},
},
},
},
writeOptions: {},
});
loadCronStore.mockResolvedValue({
version: 1,
jobs: [{ id: "a", delivery: { channel: "telegram", to: "https://t.me/mychannel" } }],
});
await maybePersistResolvedTelegramTarget({
cfg: {} as OpenClawConfig,
rawTarget: "@MyChannel",
resolvedChatId: "-100123",
});
expect(writeConfigFile).toHaveBeenCalledWith(
expect.objectContaining({
channels: {
telegram: {
defaultTo: "-100123",
},
},
}),
expect.any(Object),
);
expect(saveCronStore).toHaveBeenCalledWith(
"/tmp/cron/jobs.json",
expect.objectContaining({
jobs: [{ id: "a", delivery: { channel: "telegram", to: "-100123" } }],
}),
);
});
});

View File

@@ -1,4 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
import type { RuntimeEnv } from "../../../src/runtime.js";
import {
createWhatsAppPollFixture,
expectWhatsAppPollSent,
@@ -7,6 +9,11 @@ import {
createDirectoryTestRuntime,
expectDirectorySurface,
} from "../../../test/helpers/extensions/directory.ts";
import {
createPluginSetupWizardConfigure,
createQueuedWizardPrompter,
runSetupWizardConfigure,
} from "../../../test/helpers/extensions/setup-wizard.js";
import { whatsappPlugin } from "./channel.js";
import {
resolveWhatsAppGroupRequireMention,
@@ -16,6 +23,13 @@ import type { OpenClawConfig } from "./runtime-api.js";
const hoisted = vi.hoisted(() => ({
sendPollWhatsApp: vi.fn(async () => ({ messageId: "wa-poll-1", toJid: "1555@s.whatsapp.net" })),
loginWeb: vi.fn(async () => {}),
pathExists: vi.fn(async () => false),
listWhatsAppAccountIds: vi.fn(() => [] as string[]),
resolveDefaultWhatsAppAccountId: vi.fn(() => DEFAULT_ACCOUNT_ID),
resolveWhatsAppAuthDir: vi.fn(() => ({
authDir: "/tmp/openclaw-whatsapp-test",
})),
}));
vi.mock("./runtime.js", () => ({
@@ -31,6 +45,79 @@ vi.mock("./runtime.js", () => ({
}),
}));
vi.mock("./login.js", () => ({
loginWeb: hoisted.loginWeb,
}));
vi.mock("openclaw/plugin-sdk/setup", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/setup")>(
"openclaw/plugin-sdk/setup",
);
return {
...actual,
pathExists: hoisted.pathExists,
};
});
vi.mock("./accounts.js", async () => {
const actual = await vi.importActual<typeof import("./accounts.js")>("./accounts.js");
return {
...actual,
listWhatsAppAccountIds: hoisted.listWhatsAppAccountIds,
resolveDefaultWhatsAppAccountId: hoisted.resolveDefaultWhatsAppAccountId,
resolveWhatsAppAuthDir: hoisted.resolveWhatsAppAuthDir,
};
});
function createRuntime(): RuntimeEnv {
return {
error: vi.fn(),
} as unknown as RuntimeEnv;
}
let whatsappConfigure: ReturnType<typeof createPluginSetupWizardConfigure>;
async function runConfigureWithHarness(params: {
harness: ReturnType<typeof createQueuedWizardPrompter>;
cfg?: Parameters<typeof whatsappConfigure>[0]["cfg"];
runtime?: RuntimeEnv;
options?: Parameters<typeof whatsappConfigure>[0]["options"];
accountOverrides?: Parameters<typeof whatsappConfigure>[0]["accountOverrides"];
shouldPromptAccountIds?: boolean;
forceAllowFrom?: boolean;
}) {
return await runSetupWizardConfigure({
configure: whatsappConfigure,
cfg: params.cfg ?? {},
runtime: params.runtime ?? createRuntime(),
prompter: params.harness.prompter,
options: params.options ?? {},
accountOverrides: params.accountOverrides ?? {},
shouldPromptAccountIds: params.shouldPromptAccountIds ?? false,
forceAllowFrom: params.forceAllowFrom ?? false,
});
}
function createSeparatePhoneHarness(params: { selectValues: string[]; textValues?: string[] }) {
return createQueuedWizardPrompter({
confirmValues: [false],
selectValues: params.selectValues,
textValues: params.textValues,
});
}
async function runSeparatePhoneFlow(params: { selectValues: string[]; textValues?: string[] }) {
hoisted.pathExists.mockResolvedValue(true);
const harness = createSeparatePhoneHarness({
selectValues: params.selectValues,
textValues: params.textValues,
});
const result = await runConfigureWithHarness({
harness,
});
return { harness, result };
}
describe("whatsappPlugin outbound sendMedia", () => {
it("forwards mediaLocalRoots to sendMessageWhatsApp", async () => {
const sendWhatsApp = vi.fn(async () => ({
@@ -149,6 +236,159 @@ describe("whatsapp directory", () => {
});
});
describe("whatsapp setup wizard", () => {
beforeAll(() => {
whatsappConfigure = createPluginSetupWizardConfigure(whatsappPlugin);
});
beforeEach(() => {
hoisted.loginWeb.mockReset();
hoisted.pathExists.mockReset();
hoisted.pathExists.mockResolvedValue(false);
hoisted.listWhatsAppAccountIds.mockReset();
hoisted.listWhatsAppAccountIds.mockReturnValue([]);
hoisted.resolveDefaultWhatsAppAccountId.mockReset();
hoisted.resolveDefaultWhatsAppAccountId.mockReturnValue(DEFAULT_ACCOUNT_ID);
hoisted.resolveWhatsAppAuthDir.mockReset();
hoisted.resolveWhatsAppAuthDir.mockReturnValue({ authDir: "/tmp/openclaw-whatsapp-test" });
});
it("applies owner allowlist when forceAllowFrom is enabled", async () => {
const harness = createQueuedWizardPrompter({
confirmValues: [false],
textValues: ["+1 (555) 555-0123"],
});
const result = await runConfigureWithHarness({
harness,
forceAllowFrom: true,
});
expect(result.accountId).toBe(DEFAULT_ACCOUNT_ID);
expect(hoisted.loginWeb).not.toHaveBeenCalled();
expect(result.cfg.channels?.whatsapp?.selfChatMode).toBe(true);
expect(result.cfg.channels?.whatsapp?.dmPolicy).toBe("allowlist");
expect(result.cfg.channels?.whatsapp?.allowFrom).toEqual(["+15555550123"]);
expect(harness.text).toHaveBeenCalledWith(
expect.objectContaining({
message: "Your personal WhatsApp number (the phone you will message from)",
}),
);
});
it("supports disabled DM policy for separate-phone setup", async () => {
const { harness, result } = await runSeparatePhoneFlow({
selectValues: ["separate", "disabled"],
});
expect(result.cfg.channels?.whatsapp?.selfChatMode).toBe(false);
expect(result.cfg.channels?.whatsapp?.dmPolicy).toBe("disabled");
expect(result.cfg.channels?.whatsapp?.allowFrom).toBeUndefined();
expect(harness.text).not.toHaveBeenCalled();
});
it("normalizes allowFrom entries when list mode is selected", async () => {
const { result } = await runSeparatePhoneFlow({
selectValues: ["separate", "allowlist", "list"],
textValues: ["+1 (555) 555-0123, +15555550123, *"],
});
expect(result.cfg.channels?.whatsapp?.selfChatMode).toBe(false);
expect(result.cfg.channels?.whatsapp?.dmPolicy).toBe("allowlist");
expect(result.cfg.channels?.whatsapp?.allowFrom).toEqual(["+15555550123", "*"]);
});
it("enables allowlist self-chat mode for personal-phone setup", async () => {
hoisted.pathExists.mockResolvedValue(true);
const harness = createQueuedWizardPrompter({
confirmValues: [false],
selectValues: ["personal"],
textValues: ["+1 (555) 111-2222"],
});
const result = await runConfigureWithHarness({
harness,
});
expect(result.cfg.channels?.whatsapp?.selfChatMode).toBe(true);
expect(result.cfg.channels?.whatsapp?.dmPolicy).toBe("allowlist");
expect(result.cfg.channels?.whatsapp?.allowFrom).toEqual(["+15551112222"]);
});
it("forces wildcard allowFrom for open policy without allowFrom follow-up prompts", async () => {
hoisted.pathExists.mockResolvedValue(true);
const harness = createSeparatePhoneHarness({
selectValues: ["separate", "open"],
});
const result = await runConfigureWithHarness({
harness,
cfg: {
channels: {
whatsapp: {
allowFrom: ["+15555550123"],
},
},
},
});
expect(result.cfg.channels?.whatsapp?.selfChatMode).toBe(false);
expect(result.cfg.channels?.whatsapp?.dmPolicy).toBe("open");
expect(result.cfg.channels?.whatsapp?.allowFrom).toEqual(["*", "+15555550123"]);
expect(harness.select).toHaveBeenCalledTimes(2);
expect(harness.text).not.toHaveBeenCalled();
});
it("runs WhatsApp login when not linked and user confirms linking", async () => {
hoisted.pathExists.mockResolvedValue(false);
const harness = createQueuedWizardPrompter({
confirmValues: [true],
selectValues: ["separate", "disabled"],
});
const runtime = createRuntime();
await runConfigureWithHarness({
harness,
runtime,
});
expect(hoisted.loginWeb).toHaveBeenCalledWith(false, undefined, runtime, DEFAULT_ACCOUNT_ID);
});
it("skips relink note when already linked and relink is declined", async () => {
hoisted.pathExists.mockResolvedValue(true);
const harness = createSeparatePhoneHarness({
selectValues: ["separate", "disabled"],
});
await runConfigureWithHarness({
harness,
});
expect(hoisted.loginWeb).not.toHaveBeenCalled();
expect(harness.note).not.toHaveBeenCalledWith(
expect.stringContaining("openclaw channels login"),
"WhatsApp",
);
});
it("shows follow-up login command note when not linked and linking is skipped", async () => {
hoisted.pathExists.mockResolvedValue(false);
const harness = createSeparatePhoneHarness({
selectValues: ["separate", "disabled"],
});
await runConfigureWithHarness({
harness,
});
expect(harness.note).toHaveBeenCalledWith(
expect.stringContaining("openclaw channels login"),
"WhatsApp",
);
});
});
describe("whatsapp group policy", () => {
it("uses generic channel group policy helpers", () => {
const cfg = {

View File

@@ -1,73 +0,0 @@
import { describe, expect, it } from "vitest";
import {
isWhatsAppGroupJid,
isWhatsAppUserTarget,
normalizeWhatsAppTarget,
} from "./normalize-target.js";
describe("normalizeWhatsAppTarget", () => {
it("preserves group JIDs", () => {
expect(normalizeWhatsAppTarget("120363401234567890@g.us")).toBe("120363401234567890@g.us");
expect(normalizeWhatsAppTarget("123456789-987654321@g.us")).toBe("123456789-987654321@g.us");
expect(normalizeWhatsAppTarget("whatsapp:120363401234567890@g.us")).toBe(
"120363401234567890@g.us",
);
});
it("normalizes direct JIDs to E.164", () => {
expect(normalizeWhatsAppTarget("1555123@s.whatsapp.net")).toBe("+1555123");
});
it("normalizes user JIDs with device suffix to E.164", () => {
expect(normalizeWhatsAppTarget("41796666864:0@s.whatsapp.net")).toBe("+41796666864");
expect(normalizeWhatsAppTarget("1234567890:123@s.whatsapp.net")).toBe("+1234567890");
expect(normalizeWhatsAppTarget("41796666864@s.whatsapp.net")).toBe("+41796666864");
});
it("normalizes LID JIDs to E.164", () => {
expect(normalizeWhatsAppTarget("123456789@lid")).toBe("+123456789");
expect(normalizeWhatsAppTarget("123456789@LID")).toBe("+123456789");
});
it("rejects invalid targets", () => {
expect(normalizeWhatsAppTarget("wat")).toBeNull();
expect(normalizeWhatsAppTarget("whatsapp:")).toBeNull();
expect(normalizeWhatsAppTarget("@g.us")).toBeNull();
expect(normalizeWhatsAppTarget("whatsapp:group:@g.us")).toBeNull();
expect(normalizeWhatsAppTarget("whatsapp:group:120363401234567890@g.us")).toBeNull();
expect(normalizeWhatsAppTarget("group:123456789-987654321@g.us")).toBeNull();
expect(normalizeWhatsAppTarget(" WhatsApp:Group:123456789-987654321@G.US ")).toBeNull();
expect(normalizeWhatsAppTarget("abc@s.whatsapp.net")).toBeNull();
});
it("handles repeated prefixes", () => {
expect(normalizeWhatsAppTarget("whatsapp:whatsapp:+1555")).toBe("+1555");
expect(normalizeWhatsAppTarget("group:group:120@g.us")).toBeNull();
});
});
describe("isWhatsAppUserTarget", () => {
it("detects user JIDs with various formats", () => {
expect(isWhatsAppUserTarget("41796666864:0@s.whatsapp.net")).toBe(true);
expect(isWhatsAppUserTarget("1234567890@s.whatsapp.net")).toBe(true);
expect(isWhatsAppUserTarget("123456789@lid")).toBe(true);
expect(isWhatsAppUserTarget("123456789@LID")).toBe(true);
expect(isWhatsAppUserTarget("123@lid:0")).toBe(false);
expect(isWhatsAppUserTarget("abc@s.whatsapp.net")).toBe(false);
expect(isWhatsAppUserTarget("123456789-987654321@g.us")).toBe(false);
expect(isWhatsAppUserTarget("+1555123")).toBe(false);
});
});
describe("isWhatsAppGroupJid", () => {
it("detects group JIDs with or without prefixes", () => {
expect(isWhatsAppGroupJid("120363401234567890@g.us")).toBe(true);
expect(isWhatsAppGroupJid("123456789-987654321@g.us")).toBe(true);
expect(isWhatsAppGroupJid("whatsapp:120363401234567890@g.us")).toBe(true);
expect(isWhatsAppGroupJid("whatsapp:group:120363401234567890@g.us")).toBe(false);
expect(isWhatsAppGroupJid("x@g.us")).toBe(false);
expect(isWhatsAppGroupJid("@g.us")).toBe(false);
expect(isWhatsAppGroupJid("120@g.usx")).toBe(false);
expect(isWhatsAppGroupJid("+1555123")).toBe(false);
});
});

View File

@@ -1,5 +1,10 @@
import { installCommonResolveTargetErrorCases } from "openclaw/plugin-sdk/testing";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
isWhatsAppGroupJid,
isWhatsAppUserTarget,
normalizeWhatsAppTarget,
} from "./normalize-target.js";
vi.mock("./runtime-api.js", async () => {
const actual = await vi.importActual<typeof import("./runtime-api.js")>("./runtime-api.js");
@@ -161,3 +166,70 @@ describe("whatsapp resolveTarget", () => {
});
});
});
describe("normalizeWhatsAppTarget", () => {
it("preserves group JIDs", () => {
expect(normalizeWhatsAppTarget("120363401234567890@g.us")).toBe("120363401234567890@g.us");
expect(normalizeWhatsAppTarget("123456789-987654321@g.us")).toBe("123456789-987654321@g.us");
expect(normalizeWhatsAppTarget("whatsapp:120363401234567890@g.us")).toBe(
"120363401234567890@g.us",
);
});
it("normalizes direct JIDs to E.164", () => {
expect(normalizeWhatsAppTarget("1555123@s.whatsapp.net")).toBe("+1555123");
});
it("normalizes user JIDs with device suffix to E.164", () => {
expect(normalizeWhatsAppTarget("41796666864:0@s.whatsapp.net")).toBe("+41796666864");
expect(normalizeWhatsAppTarget("1234567890:123@s.whatsapp.net")).toBe("+1234567890");
expect(normalizeWhatsAppTarget("41796666864@s.whatsapp.net")).toBe("+41796666864");
});
it("normalizes LID JIDs to E.164", () => {
expect(normalizeWhatsAppTarget("123456789@lid")).toBe("+123456789");
expect(normalizeWhatsAppTarget("123456789@LID")).toBe("+123456789");
});
it("rejects invalid targets", () => {
expect(normalizeWhatsAppTarget("wat")).toBeNull();
expect(normalizeWhatsAppTarget("whatsapp:")).toBeNull();
expect(normalizeWhatsAppTarget("@g.us")).toBeNull();
expect(normalizeWhatsAppTarget("whatsapp:group:@g.us")).toBeNull();
expect(normalizeWhatsAppTarget("whatsapp:group:120363401234567890@g.us")).toBeNull();
expect(normalizeWhatsAppTarget("group:123456789-987654321@g.us")).toBeNull();
expect(normalizeWhatsAppTarget(" WhatsApp:Group:123456789-987654321@G.US ")).toBeNull();
expect(normalizeWhatsAppTarget("abc@s.whatsapp.net")).toBeNull();
});
it("handles repeated prefixes", () => {
expect(normalizeWhatsAppTarget("whatsapp:whatsapp:+1555")).toBe("+1555");
expect(normalizeWhatsAppTarget("group:group:120@g.us")).toBeNull();
});
});
describe("isWhatsAppUserTarget", () => {
it("detects user JIDs with various formats", () => {
expect(isWhatsAppUserTarget("41796666864:0@s.whatsapp.net")).toBe(true);
expect(isWhatsAppUserTarget("1234567890@s.whatsapp.net")).toBe(true);
expect(isWhatsAppUserTarget("123456789@lid")).toBe(true);
expect(isWhatsAppUserTarget("123456789@LID")).toBe(true);
expect(isWhatsAppUserTarget("123@lid:0")).toBe(false);
expect(isWhatsAppUserTarget("abc@s.whatsapp.net")).toBe(false);
expect(isWhatsAppUserTarget("123456789-987654321@g.us")).toBe(false);
expect(isWhatsAppUserTarget("+1555123")).toBe(false);
});
});
describe("isWhatsAppGroupJid", () => {
it("detects group JIDs with or without prefixes", () => {
expect(isWhatsAppGroupJid("120363401234567890@g.us")).toBe(true);
expect(isWhatsAppGroupJid("123456789-987654321@g.us")).toBe(true);
expect(isWhatsAppGroupJid("whatsapp:120363401234567890@g.us")).toBe(true);
expect(isWhatsAppGroupJid("whatsapp:group:120363401234567890@g.us")).toBe(false);
expect(isWhatsAppGroupJid("x@g.us")).toBe(false);
expect(isWhatsAppGroupJid("@g.us")).toBe(false);
expect(isWhatsAppGroupJid("120@g.usx")).toBe(false);
expect(isWhatsAppGroupJid("+1555123")).toBe(false);
});
});

View File

@@ -1,242 +0,0 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
import type { RuntimeEnv } from "../../../src/runtime.js";
import {
createPluginSetupWizardConfigure,
createQueuedWizardPrompter,
runSetupWizardConfigure,
} from "../../../test/helpers/extensions/setup-wizard.js";
const loginWebMock = vi.hoisted(() => vi.fn(async () => {}));
const pathExistsMock = vi.hoisted(() => vi.fn(async () => false));
const listWhatsAppAccountIdsMock = vi.hoisted(() => vi.fn(() => [] as string[]));
const resolveDefaultWhatsAppAccountIdMock = vi.hoisted(() => vi.fn(() => DEFAULT_ACCOUNT_ID));
const resolveWhatsAppAuthDirMock = vi.hoisted(() =>
vi.fn(() => ({
authDir: "/tmp/openclaw-whatsapp-test",
})),
);
vi.mock("./login.js", () => ({
loginWeb: loginWebMock,
}));
vi.mock("openclaw/plugin-sdk/setup", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/setup")>(
"openclaw/plugin-sdk/setup",
);
return {
...actual,
pathExists: pathExistsMock,
};
});
vi.mock("./accounts.js", async () => {
const actual = await vi.importActual<typeof import("./accounts.js")>("./accounts.js");
return {
...actual,
listWhatsAppAccountIds: listWhatsAppAccountIdsMock,
resolveDefaultWhatsAppAccountId: resolveDefaultWhatsAppAccountIdMock,
resolveWhatsAppAuthDir: resolveWhatsAppAuthDirMock,
};
});
function createRuntime(): RuntimeEnv {
return {
error: vi.fn(),
} as unknown as RuntimeEnv;
}
let whatsappConfigure: ReturnType<typeof createPluginSetupWizardConfigure>;
async function runConfigureWithHarness(params: {
harness: ReturnType<typeof createQueuedWizardPrompter>;
cfg?: Parameters<typeof whatsappConfigure>[0]["cfg"];
runtime?: RuntimeEnv;
options?: Parameters<typeof whatsappConfigure>[0]["options"];
accountOverrides?: Parameters<typeof whatsappConfigure>[0]["accountOverrides"];
shouldPromptAccountIds?: boolean;
forceAllowFrom?: boolean;
}) {
return await runSetupWizardConfigure({
configure: whatsappConfigure,
cfg: params.cfg ?? {},
runtime: params.runtime ?? createRuntime(),
prompter: params.harness.prompter,
options: params.options ?? {},
accountOverrides: params.accountOverrides ?? {},
shouldPromptAccountIds: params.shouldPromptAccountIds ?? false,
forceAllowFrom: params.forceAllowFrom ?? false,
});
}
function createSeparatePhoneHarness(params: { selectValues: string[]; textValues?: string[] }) {
return createQueuedWizardPrompter({
confirmValues: [false],
selectValues: params.selectValues,
textValues: params.textValues,
});
}
async function runSeparatePhoneFlow(params: { selectValues: string[]; textValues?: string[] }) {
pathExistsMock.mockResolvedValue(true);
const harness = createSeparatePhoneHarness({
selectValues: params.selectValues,
textValues: params.textValues,
});
const result = await runConfigureWithHarness({
harness,
});
return { harness, result };
}
describe("whatsapp setup wizard", () => {
beforeAll(async () => {
vi.resetModules();
const { whatsappPlugin } = await import("./channel.js");
whatsappConfigure = createPluginSetupWizardConfigure(whatsappPlugin);
});
beforeEach(() => {
vi.clearAllMocks();
pathExistsMock.mockResolvedValue(false);
listWhatsAppAccountIdsMock.mockReturnValue([]);
resolveDefaultWhatsAppAccountIdMock.mockReturnValue(DEFAULT_ACCOUNT_ID);
resolveWhatsAppAuthDirMock.mockReturnValue({ authDir: "/tmp/openclaw-whatsapp-test" });
});
it("applies owner allowlist when forceAllowFrom is enabled", async () => {
const harness = createQueuedWizardPrompter({
confirmValues: [false],
textValues: ["+1 (555) 555-0123"],
});
const result = await runConfigureWithHarness({
harness,
forceAllowFrom: true,
});
expect(result.accountId).toBe(DEFAULT_ACCOUNT_ID);
expect(loginWebMock).not.toHaveBeenCalled();
expect(result.cfg.channels?.whatsapp?.selfChatMode).toBe(true);
expect(result.cfg.channels?.whatsapp?.dmPolicy).toBe("allowlist");
expect(result.cfg.channels?.whatsapp?.allowFrom).toEqual(["+15555550123"]);
expect(harness.text).toHaveBeenCalledWith(
expect.objectContaining({
message: "Your personal WhatsApp number (the phone you will message from)",
}),
);
});
it("supports disabled DM policy for separate-phone setup", async () => {
const { harness, result } = await runSeparatePhoneFlow({
selectValues: ["separate", "disabled"],
});
expect(result.cfg.channels?.whatsapp?.selfChatMode).toBe(false);
expect(result.cfg.channels?.whatsapp?.dmPolicy).toBe("disabled");
expect(result.cfg.channels?.whatsapp?.allowFrom).toBeUndefined();
expect(harness.text).not.toHaveBeenCalled();
});
it("normalizes allowFrom entries when list mode is selected", async () => {
const { result } = await runSeparatePhoneFlow({
selectValues: ["separate", "allowlist", "list"],
textValues: ["+1 (555) 555-0123, +15555550123, *"],
});
expect(result.cfg.channels?.whatsapp?.selfChatMode).toBe(false);
expect(result.cfg.channels?.whatsapp?.dmPolicy).toBe("allowlist");
expect(result.cfg.channels?.whatsapp?.allowFrom).toEqual(["+15555550123", "*"]);
});
it("enables allowlist self-chat mode for personal-phone setup", async () => {
pathExistsMock.mockResolvedValue(true);
const harness = createQueuedWizardPrompter({
confirmValues: [false],
selectValues: ["personal"],
textValues: ["+1 (555) 111-2222"],
});
const result = await runConfigureWithHarness({
harness,
});
expect(result.cfg.channels?.whatsapp?.selfChatMode).toBe(true);
expect(result.cfg.channels?.whatsapp?.dmPolicy).toBe("allowlist");
expect(result.cfg.channels?.whatsapp?.allowFrom).toEqual(["+15551112222"]);
});
it("forces wildcard allowFrom for open policy without allowFrom follow-up prompts", async () => {
pathExistsMock.mockResolvedValue(true);
const harness = createSeparatePhoneHarness({
selectValues: ["separate", "open"],
});
const result = await runConfigureWithHarness({
harness,
cfg: {
channels: {
whatsapp: {
allowFrom: ["+15555550123"],
},
},
},
});
expect(result.cfg.channels?.whatsapp?.selfChatMode).toBe(false);
expect(result.cfg.channels?.whatsapp?.dmPolicy).toBe("open");
expect(result.cfg.channels?.whatsapp?.allowFrom).toEqual(["*", "+15555550123"]);
expect(harness.select).toHaveBeenCalledTimes(2);
expect(harness.text).not.toHaveBeenCalled();
});
it("runs WhatsApp login when not linked and user confirms linking", async () => {
pathExistsMock.mockResolvedValue(false);
const harness = createQueuedWizardPrompter({
confirmValues: [true],
selectValues: ["separate", "disabled"],
});
const runtime = createRuntime();
await runConfigureWithHarness({
harness,
runtime,
});
expect(loginWebMock).toHaveBeenCalledWith(false, undefined, runtime, DEFAULT_ACCOUNT_ID);
});
it("skips relink note when already linked and relink is declined", async () => {
pathExistsMock.mockResolvedValue(true);
const harness = createSeparatePhoneHarness({
selectValues: ["separate", "disabled"],
});
await runConfigureWithHarness({
harness,
});
expect(loginWebMock).not.toHaveBeenCalled();
expect(harness.note).not.toHaveBeenCalledWith(
expect.stringContaining("openclaw channels login"),
"WhatsApp",
);
});
it("shows follow-up login command note when not linked and linking is skipped", async () => {
pathExistsMock.mockResolvedValue(false);
const harness = createSeparatePhoneHarness({
selectValues: ["separate", "disabled"],
});
await runConfigureWithHarness({
harness,
});
expect(harness.note).toHaveBeenCalledWith(
expect.stringContaining("openclaw channels login"),
"WhatsApp",
);
});
});