fix: retry delivery when outbound adapter is unavailable (#119371)

* fix(outbound): preserve pre-dispatch retryability

* test(outbound): assert lazy runtime sender

* fix(feishu): preflight direct message runtime

* test(gateway): preserve scoped registry fixture
This commit is contained in:
Jason (Json)
2026-08-04 21:22:07 -06:00
committed by GitHub
parent fd1662f49c
commit ab7b3ffd1f
10 changed files with 474 additions and 28 deletions
@@ -1,5 +1,6 @@
// Channel outbound send tests cover CLI send runtime handoff to channel outbound adapters.
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PlatformMessageNotDispatchedError } from "../../infra/outbound/deliver-types.js";
const mocks = vi.hoisted(() => ({
loadChannelOutboundAdapter: vi.fn(),
@@ -23,6 +24,32 @@ describe("createChannelOutboundRuntimeSend", () => {
return params;
}
it.each(["discord", "telegram"] as const)(
"classifies unavailable %s adapters as definitely not dispatched",
async (channelId) => {
mocks.loadChannelOutboundAdapter.mockResolvedValue(undefined);
const unavailableMessage = `${channelId} outbound adapter is unavailable.`;
const { createChannelOutboundRuntimeSend } = await import("./channel-outbound-send.js");
const runtimeSend = createChannelOutboundRuntimeSend({
channelId,
unavailableMessage,
});
const error = await runtimeSend
.sendMessage("target", "hello", { cfg: {} })
.catch((caught: unknown) => caught);
expect(error).toBeInstanceOf(PlatformMessageNotDispatchedError);
expect(error).toMatchObject({
name: "PlatformMessageNotDispatchedError",
message: unavailableMessage,
cause: expect.objectContaining({
message: unavailableMessage,
}),
});
},
);
it("routes media sends through sendMedia and preserves media access", async () => {
const sendMedia = vi.fn(async () => ({ channel: "whatsapp", messageId: "wa-1" }));
mocks.loadChannelOutboundAdapter.mockResolvedValue({
@@ -4,6 +4,7 @@ import { loadChannelOutboundAdapter } from "../../channels/plugins/outbound/load
import type { ChannelId } from "../../channels/plugins/types.public.js";
import { getRuntimeConfig } from "../../config/config.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { PlatformMessageNotDispatchedError } from "../../infra/outbound/deliver-types.js";
import type { OutboundDeliveryFormattingOptions } from "../../infra/outbound/formatting.js";
import type { OutboundMediaAccess } from "../../media/load-options.js";
@@ -96,7 +97,8 @@ export function createChannelOutboundRuntimeSend(params: {
return await outbound.sendMedia(buildContext());
}
if (!outbound?.sendText) {
throw new Error(params.unavailableMessage);
const cause = new Error(params.unavailableMessage);
throw new PlatformMessageNotDispatchedError(params.unavailableMessage, { cause });
}
return await outbound.sendText(buildContext());
},