diff --git a/extensions/whatsapp/src/agent-tools-login.test.ts b/extensions/whatsapp/src/agent-tools-login.test.ts index 1ee31e26b05..3862000b879 100644 --- a/extensions/whatsapp/src/agent-tools-login.test.ts +++ b/extensions/whatsapp/src/agent-tools-login.test.ts @@ -26,7 +26,7 @@ describe("createWhatsAppLoginTool", () => { const tool = createWhatsAppLoginTool(); const result = await tool.execute("tool-call-1", { action: "wait", - timeoutMs: 5000, + timeoutMs: "5000", accountId, currentQrDataUrl: "data:image/png;base64,current-qr", }); @@ -56,6 +56,39 @@ describe("createWhatsAppLoginTool", () => { }); }); + it("passes string timeoutMs through to start actions", async () => { + startWebLoginWithQrMock.mockResolvedValueOnce({ + connected: false, + message: "Scan this QR in WhatsApp → Linked Devices.", + qrDataUrl: "data:image/png;base64,current-qr", + }); + + const tool = createWhatsAppLoginTool(); + await tool.execute("tool-call-start", { + action: "start", + timeoutMs: "6000", + accountId: "account-3", + }); + + expect(startWebLoginWithQrMock).toHaveBeenCalledWith({ + accountId: "account-3", + timeoutMs: 6000, + force: false, + }); + }); + + it("rejects fractional timeoutMs before login actions", async () => { + const tool = createWhatsAppLoginTool(); + + await expect( + tool.execute("tool-call-start", { + action: "start", + timeoutMs: "6000.5", + }), + ).rejects.toThrow("timeoutMs must be a positive integer"); + expect(startWebLoginWithQrMock).not.toHaveBeenCalled(); + }); + it("does not retain QR state across tool actions", async () => { const accountId = "account-2"; startWebLoginWithQrMock.mockResolvedValueOnce({ diff --git a/extensions/whatsapp/src/agent-tools-login.ts b/extensions/whatsapp/src/agent-tools-login.ts index 7dff7e9f436..da6a5ab7b05 100644 --- a/extensions/whatsapp/src/agent-tools-login.ts +++ b/extensions/whatsapp/src/agent-tools-login.ts @@ -1,3 +1,7 @@ +import { + optionalPositiveIntegerSchema, + readPositiveIntegerParam, +} from "openclaw/plugin-sdk/channel-actions"; import type { ChannelAgentTool } from "openclaw/plugin-sdk/channel-contract"; import { Type } from "typebox"; import { startWebLoginWithQr, waitForWebLogin } from "../login-qr-api.js"; @@ -20,7 +24,7 @@ export function createWhatsAppLoginTool(): ChannelAgentTool { type: "string", enum: ["start", "wait"], }), - timeoutMs: Type.Optional(Type.Number()), + timeoutMs: optionalPositiveIntegerSchema(), force: Type.Optional(Type.Boolean()), accountId: Type.Optional(Type.String()), currentQrDataUrl: Type.Optional( @@ -54,13 +58,11 @@ export function createWhatsAppLoginTool(): ChannelAgentTool { const action = (args as { action?: string })?.action ?? "start"; const accountId = readOptionalString((args as { accountId?: unknown }).accountId); + const timeoutMs = readPositiveIntegerParam(args as Record, "timeoutMs"); if (action === "wait") { const result = await waitForWebLogin({ accountId, - timeoutMs: - typeof (args as { timeoutMs?: unknown }).timeoutMs === "number" - ? (args as { timeoutMs?: number }).timeoutMs - : undefined, + timeoutMs, currentQrDataUrl: readOptionalString( (args as { currentQrDataUrl?: unknown }).currentQrDataUrl, ), @@ -80,10 +82,7 @@ export function createWhatsAppLoginTool(): ChannelAgentTool { const result = await startWebLoginWithQr({ accountId, - timeoutMs: - typeof (args as { timeoutMs?: unknown }).timeoutMs === "number" - ? (args as { timeoutMs?: number }).timeoutMs - : undefined, + timeoutMs, force: typeof (args as { force?: unknown }).force === "boolean" ? (args as { force?: boolean }).force