

























@@ -4,11 +4,33 @@ import type { OpenClawConfig } from "./runtime-api.js";
4455const hoisted = vi.hoisted(() => ({
66handleWhatsAppAction: vi.fn(async () => ({ content: [{ type: "text", text: '{"ok":true}' }] })),
7+resolveAuthorizedWhatsAppOutboundTarget: vi.fn(
8+({
9+ chatJid,
10+ accountId,
11+}: {
12+chatJid: string;
13+accountId?: string;
14+}): { to: string; accountId: string } => ({
15+to: chatJid,
16+accountId: accountId ?? "default",
17+}),
18+),
19+resolveWhatsAppAccount: vi.fn(() => ({ accountId: "default", mediaMaxMb: 50 })),
20+resolveWhatsAppMediaMaxBytes: vi.fn(() => 50 * 1024 * 1024),
21+sendMessageWhatsApp: vi.fn(async () => ({
22+messageId: "msg-media-1",
23+toJid: "1555@s.whatsapp.net",
24+})),
725}));
826927vi.mock("./channel-react-action.runtime.js", async () => {
1028return {
1129handleWhatsAppAction: hoisted.handleWhatsAppAction,
30+resolveAuthorizedWhatsAppOutboundTarget: hoisted.resolveAuthorizedWhatsAppOutboundTarget,
31+resolveWhatsAppAccount: hoisted.resolveWhatsAppAccount,
32+resolveWhatsAppMediaMaxBytes: hoisted.resolveWhatsAppMediaMaxBytes,
33+sendMessageWhatsApp: hoisted.sendMessageWhatsApp,
1234resolveReactionMessageId: ({
1335 args,
1436 toolContext,
@@ -41,7 +63,7 @@ vi.mock("./channel-react-action.runtime.js", async () => {
4163readStringParam: (
4264params: Record<string, unknown>,
4365key: string,
44-options?: { required?: boolean; allowEmpty?: boolean },
66+options?: { required?: boolean; allowEmpty?: boolean; trim?: boolean },
4567) => {
4668const value = params[key];
4769if (value == null) {
@@ -73,6 +95,145 @@ describe("whatsapp react action messageId resolution", () => {
73957496beforeEach(() => {
7597hoisted.handleWhatsAppAction.mockClear();
98+hoisted.resolveAuthorizedWhatsAppOutboundTarget.mockClear();
99+hoisted.resolveWhatsAppAccount.mockClear();
100+hoisted.resolveWhatsAppMediaMaxBytes.mockClear();
101+hoisted.resolveWhatsAppAccount.mockReturnValue({ accountId: "default", mediaMaxMb: 50 });
102+hoisted.resolveWhatsAppMediaMaxBytes.mockReturnValue(50 * 1024 * 1024);
103+hoisted.sendMessageWhatsApp.mockClear();
104+});
105+106+it("sends upload-file through the WhatsApp media send path", async () => {
107+const mediaReadFile = vi.fn(async () => Buffer.from("media"));
108+109+const result = await handleWhatsAppReactAction({
110+action: "upload-file",
111+params: {
112+to: "+1555",
113+filePath: "/tmp/pic.png",
114+caption: "picture caption",
115+forceDocument: "true",
116+gifPlayback: true,
117+asVoice: "true",
118+},
119+cfg: baseCfg,
120+accountId: "default",
121+mediaLocalRoots: ["/tmp"],
122+ mediaReadFile,
123+});
124+125+expect(hoisted.resolveAuthorizedWhatsAppOutboundTarget).toHaveBeenCalledWith({
126+cfg: baseCfg,
127+chatJid: "+1555",
128+accountId: "default",
129+actionLabel: "upload-file",
130+});
131+expect(hoisted.sendMessageWhatsApp).toHaveBeenCalledWith("+1555", "picture caption", {
132+verbose: false,
133+cfg: baseCfg,
134+mediaUrl: "/tmp/pic.png",
135+mediaAccess: undefined,
136+mediaLocalRoots: ["/tmp"],
137+ mediaReadFile,
138+gifPlayback: true,
139+audioAsVoice: true,
140+forceDocument: true,
141+accountId: "default",
142+});
143+expect(result.details).toMatchObject({
144+ok: true,
145+channel: "whatsapp",
146+action: "upload-file",
147+messageId: "msg-media-1",
148+toJid: "1555@s.whatsapp.net",
149+});
150+});
151+152+it("does not send upload-file when target authorization fails", async () => {
153+hoisted.resolveAuthorizedWhatsAppOutboundTarget.mockImplementationOnce(() => {
154+throw new Error("WhatsApp upload-file blocked");
155+});
156+157+await expect(
158+handleWhatsAppReactAction({
159+action: "upload-file",
160+params: {
161+to: "+1555",
162+filePath: "/tmp/pic.png",
163+},
164+cfg: baseCfg,
165+accountId: "default",
166+}),
167+).rejects.toThrow("WhatsApp upload-file blocked");
168+expect(hoisted.sendMessageWhatsApp).not.toHaveBeenCalled();
169+});
170+171+it("sends upload-file from the hydrated buffer payload", async () => {
172+await handleWhatsAppReactAction({
173+action: "upload-file",
174+params: {
175+to: "+1555",
176+buffer: Buffer.from("hello").toString("base64"),
177+contentType: "text/plain",
178+filename: "hello.txt",
179+filePath: "/tmp/hello.txt",
180+forceDocument: true,
181+message: "file caption",
182+},
183+cfg: baseCfg,
184+accountId: "default",
185+});
186+187+expect(hoisted.sendMessageWhatsApp).toHaveBeenCalledWith("+1555", "file caption", {
188+verbose: false,
189+cfg: baseCfg,
190+mediaPayload: {
191+buffer: Buffer.from("hello"),
192+contentType: "text/plain",
193+fileName: "hello.txt",
194+},
195+mediaAccess: undefined,
196+mediaLocalRoots: undefined,
197+mediaReadFile: undefined,
198+gifPlayback: undefined,
199+audioAsVoice: undefined,
200+forceDocument: true,
201+accountId: "default",
202+});
203+});
204+205+it("rejects upload-file buffers above the WhatsApp media limit", async () => {
206+hoisted.resolveWhatsAppMediaMaxBytes.mockReturnValueOnce(4);
207+208+await expect(
209+handleWhatsAppReactAction({
210+action: "upload-file",
211+params: {
212+to: "+1555",
213+buffer: Buffer.from("hello").toString("base64"),
214+contentType: "text/plain",
215+filename: "hello.txt",
216+},
217+cfg: baseCfg,
218+accountId: "default",
219+}),
220+).rejects.toThrow("WhatsApp upload-file buffer exceeds configured media limit");
221+expect(hoisted.sendMessageWhatsApp).not.toHaveBeenCalled();
222+});
223+224+it("requires upload-file media path input", async () => {
225+await expect(
226+handleWhatsAppReactAction({
227+action: "upload-file",
228+params: {
229+to: "+1555",
230+caption: "missing media",
231+},
232+cfg: baseCfg,
233+accountId: "default",
234+}),
235+).rejects.toThrow("WhatsApp upload-file requires media");
236+expect(hoisted.sendMessageWhatsApp).not.toHaveBeenCalled();
76237});
7723878239it("uses explicit messageId when provided", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。