mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 02:45:38 -06:00
245451b6a9
Keep WhatsApp QR login state synced across gateway, macOS, and UI wait flows. - Preserve the latest QR data URL/version while login polling rotates codes. - Keep the wait-result protocol bounded to current QR metadata. - Stabilize QR rendering and media fixture coverage after rebasing on main. Validation: - pnpm test extensions/whatsapp/src/login-qr.test.ts extensions/whatsapp/src/media.test.ts extensions/whatsapp/src/agent-tools-login.test.ts src/gateway/protocol/channels.schema.test.ts src/gateway/server-methods/web.start.test.ts ui/src/ui/controllers/channels.test.ts - pnpm test:extension whatsapp - cd apps/macos && swift test --filter ChannelsSettingsSmokeTests - GitHub PR checks: 62 success, 5 skipped
211 lines
5.4 KiB
TypeScript
211 lines
5.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { ChannelRuntimeSnapshot } from "../server-channel-runtime.types.js";
|
|
import type { GatewayRequestHandlerOptions } from "./types.js";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
listChannelPlugins: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../../channels/plugins/index.js", () => ({
|
|
listChannelPlugins: mocks.listChannelPlugins,
|
|
}));
|
|
|
|
import { webHandlers } from "./web.js";
|
|
|
|
function createOptions(
|
|
params: Record<string, unknown>,
|
|
overrides?: Partial<GatewayRequestHandlerOptions>,
|
|
): GatewayRequestHandlerOptions {
|
|
return {
|
|
req: { type: "req", id: "req-1", method: "web.login.start", params },
|
|
params,
|
|
client: null,
|
|
isWebchatConnect: () => false,
|
|
respond: vi.fn(),
|
|
context: {
|
|
stopChannel: vi.fn(),
|
|
startChannel: vi.fn(),
|
|
getRuntimeSnapshot: vi.fn(
|
|
(): ChannelRuntimeSnapshot => ({
|
|
channels: {
|
|
whatsapp: {
|
|
accountId: "default",
|
|
running: true,
|
|
},
|
|
},
|
|
channelAccounts: {
|
|
whatsapp: {
|
|
default: {
|
|
accountId: "default",
|
|
running: true,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
),
|
|
},
|
|
...overrides,
|
|
} as unknown as GatewayRequestHandlerOptions;
|
|
}
|
|
|
|
function createRunningWhatsappContext() {
|
|
const startChannel = vi.fn();
|
|
const stopChannel = vi.fn();
|
|
return {
|
|
startChannel,
|
|
stopChannel,
|
|
context: {
|
|
stopChannel,
|
|
startChannel,
|
|
getRuntimeSnapshot: vi.fn(
|
|
(): ChannelRuntimeSnapshot => ({
|
|
channels: {
|
|
whatsapp: {
|
|
accountId: "default",
|
|
running: true,
|
|
},
|
|
},
|
|
channelAccounts: {
|
|
whatsapp: {
|
|
default: {
|
|
accountId: "default",
|
|
running: true,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
),
|
|
} as unknown as GatewayRequestHandlerOptions["context"],
|
|
};
|
|
}
|
|
|
|
describe("webHandlers web.login.start", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("restarts a previously running channel when login start exits early without a QR", async () => {
|
|
const loginWithQrStart = vi.fn().mockResolvedValue({
|
|
code: "whatsapp-auth-unstable",
|
|
message: "retry later",
|
|
});
|
|
mocks.listChannelPlugins.mockReturnValue([
|
|
{
|
|
id: "whatsapp",
|
|
gatewayMethods: ["web.login.start"],
|
|
gateway: { loginWithQrStart },
|
|
},
|
|
]);
|
|
const { context, startChannel, stopChannel } = createRunningWhatsappContext();
|
|
const respond = vi.fn();
|
|
|
|
await webHandlers["web.login.start"](
|
|
createOptions(
|
|
{ accountId: "default" },
|
|
{
|
|
respond,
|
|
context,
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(stopChannel).toHaveBeenCalledWith("whatsapp", "default");
|
|
expect(startChannel).toHaveBeenCalledWith("whatsapp", "default");
|
|
expect(respond).toHaveBeenCalledWith(
|
|
true,
|
|
{
|
|
code: "whatsapp-auth-unstable",
|
|
message: "retry later",
|
|
},
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
it("keeps the channel stopped when login start has taken over with a QR flow", async () => {
|
|
const loginWithQrStart = vi.fn().mockResolvedValue({
|
|
qrDataUrl: "data:image/png;base64,qr",
|
|
message: "scan qr",
|
|
});
|
|
mocks.listChannelPlugins.mockReturnValue([
|
|
{
|
|
id: "whatsapp",
|
|
gatewayMethods: ["web.login.start"],
|
|
gateway: { loginWithQrStart },
|
|
},
|
|
]);
|
|
const { context, startChannel, stopChannel } = createRunningWhatsappContext();
|
|
|
|
await webHandlers["web.login.start"](
|
|
createOptions(
|
|
{ accountId: "default" },
|
|
{
|
|
context,
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(stopChannel).toHaveBeenCalledWith("whatsapp", "default");
|
|
expect(startChannel).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("webHandlers web.login.wait", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("passes refreshed QR payloads back to the client while login is still pending", async () => {
|
|
const loginWithQrWait = vi.fn().mockResolvedValue({
|
|
connected: false,
|
|
message: "QR refreshed. Scan the latest code in WhatsApp → Linked Devices.",
|
|
qrDataUrl: "data:image/png;base64,next-qr",
|
|
});
|
|
mocks.listChannelPlugins.mockReturnValue([
|
|
{
|
|
id: "whatsapp",
|
|
gatewayMethods: ["web.login.wait"],
|
|
gateway: { loginWithQrWait },
|
|
},
|
|
]);
|
|
const respond = vi.fn();
|
|
|
|
await webHandlers["web.login.wait"](
|
|
createOptions(
|
|
{
|
|
accountId: "default",
|
|
timeoutMs: 5000,
|
|
currentQrDataUrl: "data:image/png;base64,current-qr",
|
|
},
|
|
{
|
|
req: {
|
|
type: "req",
|
|
id: "req-2",
|
|
method: "web.login.wait",
|
|
params: {
|
|
accountId: "default",
|
|
timeoutMs: 5000,
|
|
currentQrDataUrl: "data:image/png;base64,current-qr",
|
|
},
|
|
} as GatewayRequestHandlerOptions["req"],
|
|
respond,
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(loginWithQrWait).toHaveBeenCalledWith({
|
|
accountId: "default",
|
|
timeoutMs: 5000,
|
|
currentQrDataUrl: "data:image/png;base64,current-qr",
|
|
});
|
|
expect(respond).toHaveBeenCalledWith(
|
|
true,
|
|
{
|
|
connected: false,
|
|
message: "QR refreshed. Scan the latest code in WhatsApp → Linked Devices.",
|
|
qrDataUrl: "data:image/png;base64,next-qr",
|
|
},
|
|
undefined,
|
|
);
|
|
});
|
|
});
|