* refactor: move WhatsApp channel from src/web/ to extensions/whatsapp/ Move all WhatsApp implementation code (77 source/test files + 9 channel plugin files) from src/web/ and src/channels/plugins/*/whatsapp* to extensions/whatsapp/src/. - Leave thin re-export shims at all original locations so cross-cutting imports continue to resolve - Update plugin-sdk/whatsapp.ts to only re-export generic framework utilities; channel-specific functions imported locally by the extension - Update vi.mock paths in 15 cross-cutting test files - Rename outbound.ts -> send.ts to match extension naming conventions and avoid false positive in cfg-threading guard test - Widen tsconfig.plugin-sdk.dts.json rootDir to support shim->extension cross-directory references Part of the core-channels-to-extensions migration (PR 6/10). * style: format WhatsApp extension files * fix: correct stale import paths in WhatsApp extension tests Fix vi.importActual, test mock, and hardcoded source paths that weren't updated during the file move: - media.test.ts: vi.importActual path - onboarding.test.ts: vi.importActual path - test-helpers.ts: test/mocks/baileys.js path - monitor-inbox.test-harness.ts: incomplete media/store mock - login.test.ts: hardcoded source file path - message-action-runner.media.test.ts: vi.mock/importActual path
269 lines
8.5 KiB
TypeScript
269 lines
8.5 KiB
TypeScript
import { 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 type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
|
import { whatsappOnboardingAdapter } from "./onboarding.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("../../../src/channel-web.js", () => ({
|
|
loginWeb: loginWebMock,
|
|
}));
|
|
|
|
vi.mock("../../../src/utils.js", async () => {
|
|
const actual =
|
|
await vi.importActual<typeof import("../../../src/utils.js")>("../../../src/utils.js");
|
|
return {
|
|
...actual,
|
|
pathExists: pathExistsMock,
|
|
};
|
|
});
|
|
|
|
vi.mock("./accounts.js", () => ({
|
|
listWhatsAppAccountIds: listWhatsAppAccountIdsMock,
|
|
resolveDefaultWhatsAppAccountId: resolveDefaultWhatsAppAccountIdMock,
|
|
resolveWhatsAppAuthDir: resolveWhatsAppAuthDirMock,
|
|
}));
|
|
|
|
function createPrompterHarness(params?: {
|
|
selectValues?: string[];
|
|
textValues?: string[];
|
|
confirmValues?: boolean[];
|
|
}) {
|
|
const selectValues = [...(params?.selectValues ?? [])];
|
|
const textValues = [...(params?.textValues ?? [])];
|
|
const confirmValues = [...(params?.confirmValues ?? [])];
|
|
|
|
const intro = vi.fn(async () => undefined);
|
|
const outro = vi.fn(async () => undefined);
|
|
const note = vi.fn(async () => undefined);
|
|
const select = vi.fn(async () => selectValues.shift() ?? "");
|
|
const multiselect = vi.fn(async () => [] as string[]);
|
|
const text = vi.fn(async () => textValues.shift() ?? "");
|
|
const confirm = vi.fn(async () => confirmValues.shift() ?? false);
|
|
const progress = vi.fn(() => ({
|
|
update: vi.fn(),
|
|
stop: vi.fn(),
|
|
}));
|
|
|
|
return {
|
|
intro,
|
|
outro,
|
|
note,
|
|
select,
|
|
multiselect,
|
|
text,
|
|
confirm,
|
|
progress,
|
|
prompter: {
|
|
intro,
|
|
outro,
|
|
note,
|
|
select,
|
|
multiselect,
|
|
text,
|
|
confirm,
|
|
progress,
|
|
} as WizardPrompter,
|
|
};
|
|
}
|
|
|
|
function createRuntime(): RuntimeEnv {
|
|
return {
|
|
error: vi.fn(),
|
|
} as unknown as RuntimeEnv;
|
|
}
|
|
|
|
async function runConfigureWithHarness(params: {
|
|
harness: ReturnType<typeof createPrompterHarness>;
|
|
cfg?: Parameters<typeof whatsappOnboardingAdapter.configure>[0]["cfg"];
|
|
runtime?: RuntimeEnv;
|
|
options?: Parameters<typeof whatsappOnboardingAdapter.configure>[0]["options"];
|
|
accountOverrides?: Parameters<typeof whatsappOnboardingAdapter.configure>[0]["accountOverrides"];
|
|
shouldPromptAccountIds?: boolean;
|
|
forceAllowFrom?: boolean;
|
|
}) {
|
|
return await whatsappOnboardingAdapter.configure({
|
|
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 createPrompterHarness({
|
|
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("whatsappOnboardingAdapter.configure", () => {
|
|
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 = createPrompterHarness({
|
|
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 = createPrompterHarness({
|
|
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 = createPrompterHarness({
|
|
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",
|
|
);
|
|
});
|
|
});
|