fix(signal): bind reactions to normalized target (#112607)

This commit is contained in:
Josh Avant
2026-07-22 04:30:49 -05:00
committed by GitHub
parent c377673990
commit b0ccaa6eec
2 changed files with 114 additions and 14 deletions

View File

@@ -154,14 +154,14 @@ describe("signalMessageActions", () => {
name: "normalizes uuid recipients",
cfg: { channels: { signal: { account: "+15550001111" } } } as OpenClawConfig,
params: {
recipient: "uuid:123e4567-e89b-12d3-a456-426614174000",
to: "uuid:123e4567-e89b-12d3-a456-426614174000",
messageId: "123",
emoji: "🔥",
},
expectedRecipient: "123e4567-e89b-12d3-a456-426614174000",
expectedTimestamp: 123,
expectedEmoji: "🔥",
expectedOptions: {},
expectedOptions: { accountId: "default" },
},
{
name: "passes groupId and targetAuthor for group reactions",
@@ -176,6 +176,7 @@ describe("signalMessageActions", () => {
expectedTimestamp: 123,
expectedEmoji: "✅",
expectedOptions: {
accountId: "default",
groupId: "group-id",
targetAuthor: "uuid:123e4567-e89b-12d3-a456-426614174000",
},
@@ -187,7 +188,7 @@ describe("signalMessageActions", () => {
expectedRecipient: "+15559999999",
expectedTimestamp: 1737630212345,
expectedEmoji: "🔥",
expectedOptions: {},
expectedOptions: { accountId: "default" },
toolContext: { currentMessageId: "1737630212345" },
},
] as const;
@@ -224,6 +225,86 @@ describe("signalMessageActions", () => {
}
});
it("binds provider reactions to the canonical target", async () => {
const cfg = {
channels: { signal: { account: "+15550001111" } },
} as OpenClawConfig;
await signalMessageActions.handleAction?.({
channel: "signal",
action: "react",
params: {
to: "+15559999999",
recipient: "+15558888888",
messageId: "123",
emoji: "✅",
},
cfg,
});
expect(sendReactionSignalMock).toHaveBeenCalledWith(
"+15559999999",
123,
"✅",
expect.objectContaining({ accountId: "default" }),
);
await signalMessageActions.handleAction?.({
channel: "signal",
action: "react",
params: {
to: "+15559999999",
recipient: "+15558888888",
messageId: "123",
emoji: "✅",
remove: true,
},
cfg,
});
expect(removeReactionSignalMock).toHaveBeenCalledWith(
"+15559999999",
123,
"✅",
expect.objectContaining({ accountId: "default" }),
);
});
it.each([
{
name: "disabled",
cfg: {
channels: {
signal: {
account: "+15550001111",
accounts: { work: { enabled: false, account: "+15550002222" } },
},
},
},
accountId: "work",
error: /account "work" is disabled/,
},
{
name: "unconfigured",
cfg: { channels: { signal: {} } },
accountId: "default",
error: /account "default" is not configured/,
},
])("rejects $name accounts before provider dispatch", async ({ cfg, accountId, error }) => {
await expect(
signalMessageActions.handleAction?.({
channel: "signal",
action: "react",
params: { to: "+15559999999", messageId: "123", emoji: "✅" },
cfg: cfg as OpenClawConfig,
accountId,
}),
).rejects.toThrow(error);
expect(sendReactionSignalMock).not.toHaveBeenCalled();
expect(removeReactionSignalMock).not.toHaveBeenCalled();
});
it("rejects invalid reaction inputs before dispatch", async () => {
const cfg = {
channels: { signal: { account: "+15550001111" } },
@@ -256,5 +337,19 @@ describe("signalMessageActions", () => {
cfg,
}),
).rejects.toThrow(/targetAuthor/);
await expect(
signalMessageActions.handleAction?.({
channel: "signal",
action: "react",
params: {
recipient: "+15559999999",
messageId: "123",
emoji: "✅",
},
cfg,
}),
).rejects.toThrow(/recipient.*required/);
expect(sendReactionSignalMock).not.toHaveBeenCalled();
});
});

View File

@@ -116,9 +116,17 @@ export const signalMessageActions: ChannelMessageActionAdapter = {
}
if (action === "react") {
const account = resolveSignalAccount({ cfg, accountId });
if (!account.enabled) {
throw new Error(`Signal account "${account.accountId}" is disabled.`);
}
if (!account.configured) {
throw new Error(`Signal account "${account.accountId}" is not configured.`);
}
const reactionLevelInfo = resolveSignalReactionLevel({
cfg,
accountId: accountId ?? undefined,
accountId: account.accountId,
});
if (!reactionLevelInfo.agentReactionsEnabled) {
throw new Error(
@@ -127,18 +135,15 @@ export const signalMessageActions: ChannelMessageActionAdapter = {
);
}
const actionConfig = resolveSignalAccount({ cfg, accountId }).config.actions;
const isActionEnabled = createActionGate(actionConfig);
const isActionEnabled = createActionGate(account.config.actions);
if (!isActionEnabled("reactions")) {
throw new Error("Signal reactions are disabled via actions.reactions.");
}
const recipientRaw =
readStringParam(params, "recipient") ??
readStringParam(params, "to", {
required: true,
label: "recipient (UUID, phone number, or group)",
});
const recipientRaw = readStringParam(params, "to", {
required: true,
label: "recipient (UUID, phone number, or group)",
});
const target = resolveSignalReactionTarget(recipientRaw);
if (!target.recipient && !target.groupId) {
throw new Error("recipient or group required");
@@ -171,7 +176,7 @@ export const signalMessageActions: ChannelMessageActionAdapter = {
}
return await mutateSignalReaction({
cfg,
accountId: accountId ?? undefined,
accountId: account.accountId,
target,
timestamp,
emoji,
@@ -186,7 +191,7 @@ export const signalMessageActions: ChannelMessageActionAdapter = {
}
return await mutateSignalReaction({
cfg,
accountId: accountId ?? undefined,
accountId: account.accountId,
target,
timestamp,
emoji,