fix(outbound): report broadcast delivery truth (#122605)

Derive broadcast success from structured send outcomes instead of treating every returned send as delivered.
This commit is contained in:
Peter Steinberger
2026-08-12 05:09:32 -07:00
committed by GitHub
parent 5209d6cb05
commit c97f3207a0
2 changed files with 153 additions and 1 deletions
@@ -16,6 +16,7 @@ import {
runMessageAction,
setMessageActionTestPlugin as setTestPlugin,
} from "./message-action-runner.test-helpers.js";
import type { MessageSendResult } from "./message.js";
const requireLabeledRecord = createRequireRecord("record", "expected-label");
@@ -375,6 +376,127 @@ describe("runMessageAction plugin dispatch", () => {
expect(mocks.executeSendAction).not.toHaveBeenCalled();
});
it.each<{
name: string;
delivery: Partial<MessageSendResult>;
outcome: { ok: boolean; error?: string; sentBeforeError?: true };
}>([
{
name: "sent",
delivery: { deliveryStatus: "sent" },
outcome: { ok: true },
},
{
name: "suppressed",
delivery: {
deliveryStatus: "suppressed",
suppressionReason: "cancelled_by_message_sending_hook",
},
outcome: {
ok: false,
error: "Broadcast send suppressed: cancelled_by_message_sending_hook.",
},
},
{
name: "failed",
delivery: {
deliveryStatus: "failed",
error: "provider rejected the message",
},
outcome: { ok: false, error: "provider rejected the message" },
},
{
name: "failed without an error",
delivery: { deliveryStatus: "failed" },
outcome: { ok: false, error: "Broadcast send failed." },
},
{
name: "partial_failed",
delivery: {
deliveryStatus: "partial_failed",
error: "second payload failed",
sentBeforeError: true,
},
outcome: { ok: false, error: "second payload failed", sentBeforeError: true },
},
{
name: "partial_failed without an error",
delivery: { deliveryStatus: "partial_failed", sentBeforeError: true },
outcome: {
ok: false,
error: "Broadcast send partially failed.",
sentBeforeError: true,
},
},
{
name: "legacy result without deliveryStatus",
delivery: {
via: "gateway",
result: { messageId: "legacy-message-1" },
},
outcome: { ok: true },
},
])("derives broadcast truth from a $name send result", async ({ delivery, outcome }) => {
const nestedPayload = { ok: true, nested: "payload" };
const sendResult = {
channel: "gatewaychat",
to: "user-123",
via: "direct",
mediaUrl: null,
...delivery,
} satisfies MessageSendResult;
const gatewayPlugin = createGatewayActionPlugin({
pluginId: "gatewaychat",
label: "Gateway Chat",
blurb: "Gateway Chat delivery truth test plugin.",
actions: ["send"],
messaging: {
targetResolver: {
looksLikeId: () => true,
},
},
handleAction: vi.fn(async () => jsonResult({ ok: true })),
});
setTestPlugin(gatewayPlugin, "gatewaychat");
mocks.executeSendAction.mockResolvedValue({
handledBy: "core",
payload: nestedPayload,
sendResult,
});
const result = await runMessageAction({
cfg: {
channels: {
gatewaychat: {
enabled: true,
},
},
} as OpenClawConfig,
action: "broadcast",
params: {
channel: "gatewaychat",
targets: ["user-123"],
message: "hello from broadcast",
},
});
expect(result.kind).toBe("broadcast");
if (result.kind !== "broadcast") {
throw new Error("expected broadcast result");
}
expect(result.payload.results).toEqual([
{
channel: "gatewaychat",
to: "user-123",
...outcome,
payload: nestedPayload,
result: sendResult,
},
]);
expect(result.payload.results[0]?.payload).toBe(nestedPayload);
expect(result.payload.results[0]?.result).toBe(sendResult);
});
it("preserves partial-delivery evidence from failed broadcast sends", async () => {
const gatewayPlugin = createGatewayActionPlugin({
pluginId: "gatewaychat",
+31 -1
View File
@@ -57,6 +57,34 @@ function withSendNormalization(
return normalization && result.kind === "send" ? { ...result, normalization } : result;
}
function deriveBroadcastEntryOutcome(
sendResult?: MessageSendResult,
): { ok: true } | { ok: false; error: string; sentBeforeError?: true } {
if (
!sendResult ||
sendResult.deliveryStatus === undefined ||
sendResult.deliveryStatus === "sent"
) {
return { ok: true };
}
switch (sendResult.deliveryStatus) {
case "suppressed":
return {
ok: false,
error: `Broadcast send suppressed: ${sendResult.suppressionReason ?? "unknown reason"}.`,
};
case "failed":
return { ok: false, error: sendResult.error ?? "Broadcast send failed." };
case "partial_failed":
return {
ok: false,
error: sendResult.error ?? "Broadcast send partially failed.",
sentBeforeError: true,
};
}
return sendResult.deliveryStatus satisfies never;
}
async function handleBroadcastAction(
input: MessageActionInput,
params: Record<string, unknown>,
@@ -147,7 +175,9 @@ async function handleBroadcastAction(
results.push({
channel: targetChannel,
to: resolved.to,
ok: true,
...deriveBroadcastEntryOutcome(
sendResult.kind === "send" ? sendResult.sendResult : undefined,
),
payload: sendResult.kind === "send" ? sendResult.payload : undefined,
result: sendResult.kind === "send" ? sendResult.sendResult : undefined,
});