
























1+import {
2+buildExecApprovalPendingReplyPayload,
3+buildPluginApprovalPendingReplyPayload,
4+} from "openclaw/plugin-sdk/approval-reply-runtime";
15// Signal tests cover approval reactions plugin behavior.
26import { beforeEach, describe, expect, it, vi } from "vitest";
37import {
48addSignalApprovalReactionHintToText,
5-appendSignalApprovalReactionHintForOutboundMessage,
9+addSignalApprovalReactionHintToStructuredPayload,
610buildSignalApprovalReactionHint,
711clearSignalApprovalReactionTargetsForTest,
812maybeResolveSignalApprovalReaction,
9-registerSignalApprovalReactionTargetForOutboundMessage,
13+registerSignalApprovalReactionTargetForDeliveredPayload,
1014registerSignalApprovalReactionTarget,
1115resolveSignalApprovalReactionTargetWithPersistence,
1216} from "./approval-reactions.js";
@@ -78,85 +82,334 @@ describe("Signal approval reactions", () => {
7882).toBe(prompt);
7983});
808481-it("registers target-mode outbound approval prompts for reactions", async () => {
85+it("registers delivered structured approval payloads for reactions", async () => {
8286const cfg = {
8387channels: {
8488signal: {
8589allowFrom: ["+15551230000"],
8690},
8791},
8892approvals: {
89-plugin: {
93+exec: {
9094enabled: true,
9195mode: "targets" as const,
9296targets: [{ channel: "signal", to: "+15551230000" }],
9397},
9498},
9599};
96-const text =
97-"Plugin approval required\nID: plugin:abc\n\nReply with: /approve plugin:abc allow-once|deny";
98-const textWithHint = appendSignalApprovalReactionHintForOutboundMessage({
100+const payload = buildExecApprovalPendingReplyPayload({
101+approvalId: "exec-structured-approval",
102+approvalSlug: "exec-str",
103+allowedDecisions: ["allow-once", "deny"],
104+command: "printf test",
105+host: "gateway",
106+agentId: "main",
107+sessionKey: "agent:main:signal:direct:+15551230000",
108+});
109+const deliveredPayload = addSignalApprovalReactionHintToStructuredPayload({
99110 cfg,
100111accountId: "default",
101112to: "+15551230000",
102-text,
113+payload,
103114targetAuthor: "+15550009999",
104115});
105116106-expect(textWithHint).toContain("React with:\n\n👍 Allow Once\n👎 Deny");
107117expect(
108-registerSignalApprovalReactionTargetForOutboundMessage({
118+registerSignalApprovalReactionTargetForDeliveredPayload({
109119 cfg,
120+target: {
121+channel: "signal",
122+to: "+15551230000",
123+accountId: "default",
124+},
125+payload: deliveredPayload!,
126+results: [
127+{
128+channel: "signal",
129+messageId: "1700000000012",
130+toJid: "+15551230000",
131+},
132+],
133+targetAuthor: "+15550009999",
134+}),
135+).toBe(true);
136+137+await expect(
138+resolveSignalApprovalReactionTargetWithPersistence({
110139accountId: "default",
140+conversationKey: "+15551230000",
141+messageId: "1700000000012",
142+reactionKey: "👍",
143+targetAuthor: "+15550009999",
144+}),
145+).resolves.toEqual({
146+approvalId: "exec-structured-approval",
147+approvalKind: "exec",
148+decision: "allow-once",
149+route: {
150+deliveryMode: "target",
111151to: "+15551230000",
112-messageId: "1700000000009",
113-text: textWithHint,
152+accountId: "default",
153+agentId: "main",
154+sessionKey: "agent:main:signal:direct:+15551230000",
155+},
156+});
157+});
158+159+it("does not register metadata-only approval payloads without visible reaction hints", async () => {
160+const cfg = {
161+channels: {
162+signal: {
163+allowFrom: ["+15551230000"],
164+},
165+},
166+approvals: {
167+exec: {
168+enabled: true,
169+mode: "targets" as const,
170+targets: [{ channel: "signal", to: "+15551230000" }],
171+},
172+},
173+};
174+const payload = buildExecApprovalPendingReplyPayload({
175+approvalId: "exec-hidden-reaction",
176+approvalSlug: "exec-hid",
177+allowedDecisions: ["allow-once", "deny"],
178+command: "printf hidden",
179+host: "gateway",
180+agentId: "main",
181+sessionKey: "agent:main:signal:direct:+15551230000",
182+});
183+184+expect(
185+registerSignalApprovalReactionTargetForDeliveredPayload({
186+ cfg,
187+target: {
188+channel: "signal",
189+to: "+15551230000",
190+accountId: "default",
191+},
192+ payload,
193+results: [
194+{
195+channel: "signal",
196+messageId: "1700000000015",
197+},
198+],
114199targetAuthor: "+15550009999",
115200}),
116-).toBe(true);
201+).toBe(false);
117202118-const handled = await maybeResolveSignalApprovalReaction({
203+await expect(
204+resolveSignalApprovalReactionTargetWithPersistence({
205+accountId: "default",
206+conversationKey: "+15551230000",
207+messageId: "1700000000015",
208+reactionKey: "👍",
209+targetAuthor: "+15550009999",
210+}),
211+).resolves.toBeNull();
212+});
213+214+it("registers only delivered chunks that contain visible reaction hints", async () => {
215+const cfg = {
216+channels: {
217+signal: {
218+allowFrom: ["+15551230000"],
219+},
220+},
221+approvals: {
222+exec: {
223+enabled: true,
224+mode: "targets" as const,
225+targets: [{ channel: "signal", to: "+15551230000" }],
226+},
227+},
228+};
229+const payload = buildExecApprovalPendingReplyPayload({
230+approvalId: "exec-chunked-reaction",
231+approvalSlug: "exec-ch",
232+allowedDecisions: ["allow-once", "deny"],
233+command: "printf chunked",
234+host: "gateway",
235+agentId: "main",
236+sessionKey: "agent:main:signal:direct:+15551230000",
237+});
238+const deliveredPayload = addSignalApprovalReactionHintToStructuredPayload({
119239 cfg,
120240accountId: "default",
121-conversationKey: "+15551230000",
122-messageId: "1700000000009",
123-reactionKey: "👍",
124-actorId: "+15551230000",
241+to: "+15551230000",
242+ payload,
125243targetAuthor: "+15550009999",
126244});
127245128-expect(handled).toBe(true);
129-expect(resolverMocks.resolveSignalApproval).toHaveBeenCalledWith({
246+expect(
247+registerSignalApprovalReactionTargetForDeliveredPayload({
248+ cfg,
249+target: {
250+channel: "signal",
251+to: "+15551230000",
252+accountId: "default",
253+},
254+payload: deliveredPayload!,
255+results: [
256+{
257+channel: "signal",
258+messageId: "1700000000016",
259+meta: {
260+signalVisibleText: "Exec approval required\n\nReact with:\n\n👍 Allow Once\n👎 Deny",
261+},
262+},
263+{
264+channel: "signal",
265+messageId: "1700000000017",
266+meta: {
267+signalVisibleText: "Continuation chunk without controls",
268+},
269+},
270+],
271+targetAuthor: "+15550009999",
272+}),
273+).toBe(true);
274+275+await expect(
276+resolveSignalApprovalReactionTargetWithPersistence({
277+accountId: "default",
278+conversationKey: "+15551230000",
279+messageId: "1700000000016",
280+reactionKey: "👍",
281+targetAuthor: "+15550009999",
282+}),
283+).resolves.toMatchObject({
284+approvalId: "exec-chunked-reaction",
285+decision: "allow-once",
286+});
287+await expect(
288+resolveSignalApprovalReactionTargetWithPersistence({
289+accountId: "default",
290+conversationKey: "+15551230000",
291+messageId: "1700000000017",
292+reactionKey: "👍",
293+targetAuthor: "+15550009999",
294+}),
295+).resolves.toBeNull();
296+});
297+298+it("registers delivered structured plugin approval payloads using metadata kind", async () => {
299+const cfg = {
300+channels: {
301+signal: {
302+allowFrom: ["+15551230000"],
303+},
304+},
305+approvals: {
306+plugin: {
307+enabled: true,
308+mode: "targets" as const,
309+targets: [{ channel: "signal", to: "+15551230000" }],
310+},
311+},
312+};
313+const payload = buildPluginApprovalPendingReplyPayload({
314+request: {
315+id: "plugin-structured-approval",
316+request: {
317+title: "Sensitive plugin action",
318+description: "Needs approval",
319+allowedDecisions: ["allow-once", "deny"],
320+},
321+createdAtMs: 1_000,
322+expiresAtMs: 61_000,
323+},
324+nowMs: 1_000,
325+});
326+const deliveredPayload = addSignalApprovalReactionHintToStructuredPayload({
130327 cfg,
131-approvalId: "plugin:abc",
328+accountId: "default",
329+to: "+15551230000",
330+ payload,
331+targetAuthor: "+15550009999",
332+});
333+334+expect(
335+registerSignalApprovalReactionTargetForDeliveredPayload({
336+ cfg,
337+target: {
338+channel: "signal",
339+to: "+15551230000",
340+accountId: "default",
341+},
342+payload: deliveredPayload!,
343+results: [
344+{
345+channel: "signal",
346+messageId: "1700000000013",
347+},
348+],
349+targetAuthor: "+15550009999",
350+}),
351+).toBe(true);
352+353+await expect(
354+resolveSignalApprovalReactionTargetWithPersistence({
355+accountId: "default",
356+conversationKey: "+15551230000",
357+messageId: "1700000000013",
358+reactionKey: "👍",
359+targetAuthor: "+15550009999",
360+}),
361+).resolves.toMatchObject({
362+approvalId: "plugin-structured-approval",
363+approvalKind: "plugin",
132364decision: "allow-once",
133-senderId: "+15551230000",
134-gatewayUrl: undefined,
135365});
136366});
137367138-it("keeps target-mode outbound prompts manual when the target route is disabled", () => {
139-const text =
140-"Plugin approval required\nID: plugin:abc\n\nReply with: /approve plugin:abc allow-once|deny";
368+it("does not register delivered structured approval payloads without explicit approvers", () => {
369+const payload = buildExecApprovalPendingReplyPayload({
370+approvalId: "exec-no-approvers",
371+approvalSlug: "exec-no",
372+allowedDecisions: ["allow-once", "deny"],
373+command: "printf test",
374+host: "gateway",
375+});
376+const deliveredPayload = {
377+ ...payload,
378+text: addSignalApprovalReactionHintToText({
379+text: payload.text ?? "",
380+allowedDecisions: ["allow-once", "deny"],
381+}),
382+};
141383142384expect(
143-appendSignalApprovalReactionHintForOutboundMessage({
385+registerSignalApprovalReactionTargetForDeliveredPayload({
144386cfg: {
145-channels: { signal: { allowFrom: ["+15551230000"] } },
387+channels: {
388+signal: {},
389+},
146390approvals: {
147-plugin: {
148-enabled: false,
391+exec: {
392+enabled: true,
149393mode: "targets",
150394targets: [{ channel: "signal", to: "+15551230000" }],
151395},
152396},
153397},
154-accountId: "default",
155-to: "+15551230000",
156- text,
398+target: {
399+channel: "signal",
400+to: "+15551230000",
401+accountId: "default",
402+},
403+payload: deliveredPayload,
404+results: [
405+{
406+channel: "signal",
407+messageId: "1700000000014",
408+},
409+],
157410targetAuthor: "+15550009999",
158411}),
159-).toBe(text);
412+).toBe(false);
160413});
161414162415it("registers reaction state when only allow-always is available", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。