mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 12:26:38 -06:00
654 lines
20 KiB
TypeScript
654 lines
20 KiB
TypeScript
// Sms tests cover channel plugin behavior.
|
|
import { isChannelPartialDeliveryError } from "openclaw/plugin-sdk/channel-inbound";
|
|
import { PlatformMessageNotDispatchedError } from "openclaw/plugin-sdk/error-runtime";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { smsPlugin } from "./channel.js";
|
|
import type { SmsDeliveryRecord } from "./delivery-observations.js";
|
|
import type { probeSmsAccount as probeSmsAccountType } from "./status.js";
|
|
import type { sendSmsViaTwilio as sendSmsViaTwilioType } from "./twilio.js";
|
|
|
|
const sendSmsViaTwilio = vi.hoisted(() =>
|
|
vi.fn<typeof sendSmsViaTwilioType>(async ({ to, onPlatformSendDispatch }) => {
|
|
await onPlatformSendDispatch?.();
|
|
return {
|
|
sid: "SM-default",
|
|
to,
|
|
from: "+15557654321",
|
|
status: "queued",
|
|
};
|
|
}),
|
|
);
|
|
const hostedMediaMocks = vi.hoisted(() => {
|
|
const cleanup = vi.fn(async () => undefined);
|
|
return {
|
|
cleanup,
|
|
prepare: vi.fn(async () => ({
|
|
url: "https://gateway.example.com/webhooks/sms/media/abc?token=token",
|
|
cleanup,
|
|
})),
|
|
};
|
|
});
|
|
const listRecentSmsDeliveryRecords = vi.hoisted(() =>
|
|
vi.fn(async (): Promise<SmsDeliveryRecord[]> => []),
|
|
);
|
|
const recordInitialSmsDeliveryResult = vi.hoisted(() => vi.fn(async () => null));
|
|
const probeSmsAccount = vi.hoisted(() =>
|
|
vi.fn<typeof probeSmsAccountType>(async () => ({
|
|
ok: true,
|
|
webhook: { status: "skipped" as const, reason: "test" },
|
|
hints: [],
|
|
})),
|
|
);
|
|
|
|
vi.mock("./twilio.js", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("./twilio.js")>()),
|
|
sendSmsViaTwilio,
|
|
}));
|
|
vi.mock("./media.js", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("./media.js")>()),
|
|
prepareHostedSmsMedia: hostedMediaMocks.prepare,
|
|
}));
|
|
vi.mock("./delivery-observations.js", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("./delivery-observations.js")>()),
|
|
listRecentSmsDeliveryRecords,
|
|
recordInitialSmsDeliveryResult,
|
|
}));
|
|
vi.mock("./status.js", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("./status.js")>()),
|
|
probeSmsAccount,
|
|
}));
|
|
|
|
beforeEach(() => {
|
|
sendSmsViaTwilio.mockReset();
|
|
sendSmsViaTwilio.mockImplementation(async ({ to, onPlatformSendDispatch }) => {
|
|
await onPlatformSendDispatch?.();
|
|
return {
|
|
sid: "SM-default",
|
|
to,
|
|
from: "+15557654321",
|
|
status: "queued",
|
|
};
|
|
});
|
|
hostedMediaMocks.cleanup.mockReset();
|
|
hostedMediaMocks.cleanup.mockResolvedValue(undefined);
|
|
hostedMediaMocks.prepare.mockReset();
|
|
hostedMediaMocks.prepare.mockResolvedValue({
|
|
url: "https://gateway.example.com/webhooks/sms/media/abc?token=token",
|
|
cleanup: hostedMediaMocks.cleanup,
|
|
});
|
|
listRecentSmsDeliveryRecords.mockClear();
|
|
recordInitialSmsDeliveryResult.mockReset();
|
|
recordInitialSmsDeliveryResult.mockResolvedValue(null);
|
|
probeSmsAccount.mockClear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe("smsPlugin status", () => {
|
|
it("builds a status snapshot for configured SMS accounts", async () => {
|
|
const snapshot = await smsPlugin.status?.buildAccountSnapshot?.({
|
|
cfg: {},
|
|
account: {
|
|
accountId: "support",
|
|
enabled: true,
|
|
accountSid: "AC123",
|
|
authToken: "secret",
|
|
fromNumber: "+15557654321",
|
|
messagingServiceSid: "",
|
|
defaultTo: "",
|
|
webhookPath: "/webhooks/sms",
|
|
publicWebhookUrl: "",
|
|
dangerouslyDisableSignatureValidation: false,
|
|
dmPolicy: "pairing",
|
|
allowFrom: [],
|
|
textChunkLimit: 1500,
|
|
},
|
|
});
|
|
|
|
expect(snapshot).toMatchObject({
|
|
accountId: "support",
|
|
name: "+15557654321",
|
|
enabled: true,
|
|
configured: true,
|
|
statusState: "configured",
|
|
});
|
|
});
|
|
|
|
it("projects lifecycle from the runtime status record", async () => {
|
|
const snapshot = await smsPlugin.status?.buildAccountSnapshot?.({
|
|
cfg: {},
|
|
account: {
|
|
accountId: "support",
|
|
enabled: true,
|
|
accountSid: "AC123",
|
|
authToken: "secret",
|
|
fromNumber: "+15557654321",
|
|
messagingServiceSid: "",
|
|
defaultTo: "",
|
|
webhookPath: "/webhooks/sms",
|
|
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
|
|
dangerouslyDisableSignatureValidation: false,
|
|
dmPolicy: "pairing",
|
|
allowFrom: [],
|
|
textChunkLimit: 1500,
|
|
},
|
|
runtime: { accountId: "support", lifecycle: "blocked", terminalDisconnect: true },
|
|
});
|
|
|
|
expect(snapshot).toMatchObject({ lifecycle: "blocked", terminalDisconnect: true });
|
|
});
|
|
|
|
it("loads delivery observations with the full Twilio account identity", async () => {
|
|
const account = {
|
|
accountId: "support",
|
|
enabled: true,
|
|
accountSid: "AC-support",
|
|
authToken: "secret",
|
|
fromNumber: "+15557654321",
|
|
messagingServiceSid: "",
|
|
defaultTo: "",
|
|
webhookPath: "/webhooks/sms",
|
|
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
|
|
dangerouslyDisableSignatureValidation: false,
|
|
dmPolicy: "pairing" as const,
|
|
allowFrom: [],
|
|
textChunkLimit: 1500,
|
|
};
|
|
const records = [
|
|
{
|
|
accountId: "support",
|
|
accountSidHash: "account-sid-hash",
|
|
messageSid: "SM123",
|
|
status: "delivered",
|
|
firstObservedAt: 1,
|
|
lastObservedAt: 2,
|
|
observations: [],
|
|
},
|
|
];
|
|
listRecentSmsDeliveryRecords.mockResolvedValueOnce(records);
|
|
|
|
await smsPlugin.status?.probeAccount?.({
|
|
cfg: {},
|
|
account,
|
|
timeoutMs: 1000,
|
|
});
|
|
|
|
expect(listRecentSmsDeliveryRecords).toHaveBeenCalledWith(account);
|
|
expect(probeSmsAccount).toHaveBeenCalledWith({
|
|
account,
|
|
timeoutMs: expect.any(Number),
|
|
options: { deliveryRecords: records },
|
|
});
|
|
const remainingTimeoutMs = probeSmsAccount.mock.calls[0]?.[0]?.timeoutMs;
|
|
expect(remainingTimeoutMs).toBeGreaterThan(0);
|
|
expect(remainingTimeoutMs).toBeLessThanOrEqual(1000);
|
|
});
|
|
|
|
it("passes only the remaining probe budget after loading delivery observations", async () => {
|
|
vi.useFakeTimers();
|
|
const account = {
|
|
accountId: "support",
|
|
enabled: true,
|
|
accountSid: "AC-support",
|
|
authToken: "secret",
|
|
fromNumber: "+15557654321",
|
|
messagingServiceSid: "",
|
|
defaultTo: "",
|
|
webhookPath: "/webhooks/sms",
|
|
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
|
|
dangerouslyDisableSignatureValidation: false,
|
|
dmPolicy: "pairing" as const,
|
|
allowFrom: [],
|
|
textChunkLimit: 1500,
|
|
};
|
|
listRecentSmsDeliveryRecords.mockImplementationOnce(
|
|
async () =>
|
|
await new Promise<SmsDeliveryRecord[]>((resolve) => {
|
|
setTimeout(() => resolve([]), 250);
|
|
}),
|
|
);
|
|
|
|
const probe = smsPlugin.status?.probeAccount?.({
|
|
cfg: {},
|
|
account,
|
|
timeoutMs: 1000,
|
|
});
|
|
await vi.advanceTimersByTimeAsync(250);
|
|
await probe;
|
|
|
|
expect(probeSmsAccount).toHaveBeenCalledWith({
|
|
account,
|
|
timeoutMs: 750,
|
|
options: { deliveryRecords: [] },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("smsPlugin outbound", () => {
|
|
it("declares an active text chunker and account-aware chunk limit", () => {
|
|
expect(smsPlugin.configSchema).toBeDefined();
|
|
expect(smsPlugin.status?.probeAccount).toBeDefined();
|
|
expect(smsPlugin.status?.formatCapabilitiesProbe).toBeDefined();
|
|
expect(smsPlugin.secrets?.secretTargetRegistryEntries?.map((entry) => entry.id)).toEqual([
|
|
"channels.sms.accounts.*.authToken",
|
|
"channels.sms.authToken",
|
|
]);
|
|
expect(smsPlugin.messaging?.targetPrefixes).toEqual(["twilio-sms"]);
|
|
expect(smsPlugin.outbound?.chunker?.("alpha beta", 6)).toEqual(["alpha", "beta"]);
|
|
expect(
|
|
smsPlugin.outbound?.resolveEffectiveTextChunkLimit?.({
|
|
cfg: {
|
|
channels: {
|
|
sms: {
|
|
accountSid: "AC123",
|
|
authToken: "secret",
|
|
fromNumber: "+15557654321",
|
|
textChunkLimit: 42,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
).toBe(42);
|
|
expect(
|
|
smsPlugin.outbound?.resolveEffectiveTextChunkLimit?.({
|
|
cfg: {
|
|
channels: {
|
|
sms: {
|
|
defaultAccount: "support",
|
|
accounts: {
|
|
support: {
|
|
accountSid: "AC-support",
|
|
authToken: "support-token",
|
|
fromNumber: "+15551112222",
|
|
textChunkLimit: 700,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
).toBe(700);
|
|
});
|
|
|
|
it("uses defaultTo for targetless sends and preserves Twilio receipt metadata", async () => {
|
|
const result = await smsPlugin.outbound?.sendText?.({
|
|
cfg: {
|
|
channels: {
|
|
sms: {
|
|
accountSid: "AC123",
|
|
authToken: "secret",
|
|
fromNumber: "+15557654321",
|
|
defaultTo: "+15551234567",
|
|
},
|
|
},
|
|
},
|
|
to: "",
|
|
text: "hello",
|
|
});
|
|
|
|
expect(sendSmsViaTwilio).toHaveBeenCalledWith(
|
|
expect.objectContaining({ to: "+15551234567", text: "hello" }),
|
|
);
|
|
expect(result?.messageId).toBe("SM-default");
|
|
expect(result?.receipt?.raw?.[0]).toMatchObject({
|
|
messageId: "SM-default",
|
|
chatId: "+15551234567",
|
|
toJid: "+15551234567",
|
|
meta: {
|
|
from: "+15557654321",
|
|
status: "queued",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("resolves the configured default SMS target for outbound delivery", () => {
|
|
expect(
|
|
smsPlugin.outbound?.resolveTarget?.({
|
|
cfg: {
|
|
channels: {
|
|
sms: {
|
|
accountSid: "AC123",
|
|
authToken: "secret",
|
|
fromNumber: "+15557654321",
|
|
defaultTo: "+15551234567",
|
|
},
|
|
},
|
|
},
|
|
to: "",
|
|
}),
|
|
).toEqual({ ok: true, to: "+15551234567" });
|
|
});
|
|
|
|
it("hosts and sends outbound media as MMS with an ordered multipart receipt", async () => {
|
|
sendSmsViaTwilio
|
|
.mockResolvedValueOnce({
|
|
sid: "MM-first",
|
|
to: "+15551234567",
|
|
from: "+15557654321",
|
|
status: "queued",
|
|
})
|
|
.mockResolvedValueOnce({
|
|
sid: "SM-second",
|
|
to: "+15551234567",
|
|
from: "+15557654321",
|
|
status: "queued",
|
|
});
|
|
const ctx = {
|
|
cfg: {
|
|
channels: {
|
|
sms: {
|
|
accountSid: "AC123",
|
|
authToken: "secret",
|
|
fromNumber: "+15557654321",
|
|
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
|
|
textChunkLimit: 5,
|
|
},
|
|
},
|
|
},
|
|
to: "+15551234567",
|
|
text: "alpha beta",
|
|
kind: "media" as const,
|
|
mediaUrl: "/tmp/photo.jpg",
|
|
mediaLocalRoots: ["/tmp"],
|
|
mediaReadFile: async () => Buffer.from("photo"),
|
|
};
|
|
await smsPlugin.message?.send?.lifecycle?.beforeSendAttempt?.(ctx);
|
|
const result = await smsPlugin.message?.send?.media?.(ctx);
|
|
|
|
expect(hostedMediaMocks.prepare).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
mediaUrl: "/tmp/photo.jpg",
|
|
mediaLocalRoots: ["/tmp"],
|
|
}),
|
|
);
|
|
expect(sendSmsViaTwilio).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({
|
|
to: "+15551234567",
|
|
text: "alpha",
|
|
mediaUrls: ["https://gateway.example.com/webhooks/sms/media/abc?token=token"],
|
|
}),
|
|
);
|
|
expect(sendSmsViaTwilio).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({
|
|
to: "+15551234567",
|
|
text: " beta",
|
|
}),
|
|
);
|
|
expect(sendSmsViaTwilio.mock.calls[1]?.[0]).not.toHaveProperty("mediaUrls");
|
|
expect(result?.messageId).toBe("MM-first");
|
|
expect(result?.receipt.platformMessageIds).toEqual(["MM-first", "SM-second"]);
|
|
expect(result?.receipt.parts.map((part) => part.kind)).toEqual(["media", "text"]);
|
|
});
|
|
|
|
it("hosts durable MMS media in the lifecycle before platform send starts", async () => {
|
|
const events: string[] = [];
|
|
hostedMediaMocks.prepare.mockImplementationOnce(async () => {
|
|
events.push("prepare");
|
|
return {
|
|
url: "https://gateway.example.com/webhooks/sms/media/abc?token=token",
|
|
cleanup: hostedMediaMocks.cleanup,
|
|
};
|
|
});
|
|
sendSmsViaTwilio.mockImplementationOnce(async ({ to, onPlatformSendDispatch }) => {
|
|
await onPlatformSendDispatch?.();
|
|
events.push("send");
|
|
return { sid: "MM-first", to };
|
|
});
|
|
const ctx = {
|
|
cfg: {
|
|
channels: {
|
|
sms: {
|
|
accountSid: "AC123",
|
|
authToken: "secret",
|
|
fromNumber: "+15557654321",
|
|
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
|
|
},
|
|
},
|
|
},
|
|
to: "+15551234567",
|
|
text: "caption",
|
|
kind: "media" as const,
|
|
mediaUrl: "/tmp/photo.jpg",
|
|
mediaLocalRoots: ["/tmp"],
|
|
onPlatformSendDispatch: async () => {
|
|
events.push("dispatch");
|
|
},
|
|
};
|
|
|
|
await smsPlugin.message?.send?.lifecycle?.beforeSendAttempt?.(ctx);
|
|
events.push("platform-start");
|
|
await smsPlugin.message?.send?.media?.(ctx);
|
|
|
|
expect(hostedMediaMocks.prepare).toHaveBeenCalledOnce();
|
|
expect(events).toEqual(["prepare", "platform-start", "dispatch", "send"]);
|
|
await expect(smsPlugin.message?.send?.media?.(ctx)).rejects.toThrow(
|
|
"SMS message lifecycle did not prepare the MMS attachment.",
|
|
);
|
|
});
|
|
|
|
it("discards staged MMS media when the durable dispatch marker fails", async () => {
|
|
const ctx = {
|
|
cfg: {
|
|
channels: {
|
|
sms: {
|
|
accountSid: "AC123",
|
|
authToken: "secret",
|
|
fromNumber: "+15557654321",
|
|
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
|
|
},
|
|
},
|
|
},
|
|
to: "+15551234567",
|
|
text: "caption",
|
|
kind: "media" as const,
|
|
mediaUrl: "/tmp/photo.jpg",
|
|
onPlatformSendDispatch: async () => {
|
|
throw new Error("delivery marker failed");
|
|
},
|
|
};
|
|
const lifecycle = smsPlugin.message?.send?.lifecycle;
|
|
const attemptToken = await lifecycle?.beforeSendAttempt?.(ctx);
|
|
let observed: unknown;
|
|
try {
|
|
await smsPlugin.message?.send?.media?.(ctx);
|
|
} catch (error) {
|
|
observed = error;
|
|
}
|
|
|
|
expect(observed).toBeInstanceOf(PlatformMessageNotDispatchedError);
|
|
await lifecycle?.afterSendFailure?.({
|
|
...ctx,
|
|
error: observed,
|
|
attemptToken,
|
|
});
|
|
|
|
expect(hostedMediaMocks.cleanup).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("discards staged MMS media when core fails before entering the adapter", async () => {
|
|
const ctx = {
|
|
cfg: {
|
|
channels: {
|
|
sms: {
|
|
accountSid: "AC123",
|
|
authToken: "secret",
|
|
fromNumber: "+15557654321",
|
|
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
|
|
},
|
|
},
|
|
},
|
|
to: "+15551234567",
|
|
text: "caption",
|
|
kind: "media" as const,
|
|
mediaUrl: "/tmp/photo.jpg",
|
|
};
|
|
const lifecycle = smsPlugin.message?.send?.lifecycle;
|
|
const attemptToken = await lifecycle?.beforeSendAttempt?.(ctx);
|
|
|
|
await lifecycle?.afterSendFailure?.({
|
|
...ctx,
|
|
error: new Error("queue state rejected before adapter dispatch"),
|
|
attemptToken,
|
|
});
|
|
|
|
expect(hostedMediaMocks.cleanup).toHaveBeenCalledOnce();
|
|
expect(sendSmsViaTwilio).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("retains staged MMS media after an ambiguous Twilio failure", async () => {
|
|
const failure = new Error("Twilio response was lost");
|
|
sendSmsViaTwilio.mockImplementationOnce(async ({ onPlatformSendDispatch }) => {
|
|
await onPlatformSendDispatch?.();
|
|
throw failure;
|
|
});
|
|
const ctx = {
|
|
cfg: {
|
|
channels: {
|
|
sms: {
|
|
accountSid: "AC123",
|
|
authToken: "secret",
|
|
fromNumber: "+15557654321",
|
|
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
|
|
},
|
|
},
|
|
},
|
|
to: "+15551234567",
|
|
text: "caption",
|
|
kind: "media" as const,
|
|
mediaUrl: "/tmp/photo.jpg",
|
|
onPlatformSendDispatch: async () => undefined,
|
|
};
|
|
const lifecycle = smsPlugin.message?.send?.lifecycle;
|
|
const attemptToken = await lifecycle?.beforeSendAttempt?.(ctx);
|
|
let observed: unknown;
|
|
try {
|
|
await smsPlugin.message?.send?.media?.(ctx);
|
|
} catch (error) {
|
|
observed = error;
|
|
}
|
|
|
|
expect(observed).toBe(failure);
|
|
await lifecycle?.afterSendFailure?.({
|
|
...ctx,
|
|
error: observed,
|
|
attemptToken,
|
|
});
|
|
|
|
expect(hostedMediaMocks.cleanup).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("retains staged MMS media after a partial delivery", async () => {
|
|
sendSmsViaTwilio
|
|
.mockImplementationOnce(async ({ to, onPlatformSendDispatch }) => {
|
|
await onPlatformSendDispatch?.();
|
|
return { sid: "MM-first", to };
|
|
})
|
|
.mockRejectedValueOnce(
|
|
new PlatformMessageNotDispatchedError("second chunk rejected before dispatch", {
|
|
cause: new Error("provider rejected chunk"),
|
|
}),
|
|
);
|
|
const ctx = {
|
|
cfg: {
|
|
channels: {
|
|
sms: {
|
|
accountSid: "AC123",
|
|
authToken: "secret",
|
|
fromNumber: "+15557654321",
|
|
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
|
|
textChunkLimit: 5,
|
|
},
|
|
},
|
|
},
|
|
to: "+15551234567",
|
|
text: "alpha beta",
|
|
kind: "media" as const,
|
|
mediaUrl: "/tmp/photo.jpg",
|
|
onPlatformSendDispatch: async () => undefined,
|
|
};
|
|
const lifecycle = smsPlugin.message?.send?.lifecycle;
|
|
const attemptToken = await lifecycle?.beforeSendAttempt?.(ctx);
|
|
let observed: unknown;
|
|
try {
|
|
await smsPlugin.message?.send?.media?.(ctx);
|
|
} catch (error) {
|
|
observed = error;
|
|
}
|
|
|
|
expect(isChannelPartialDeliveryError(observed)).toBe(true);
|
|
await lifecycle?.afterSendFailure?.({
|
|
...ctx,
|
|
error: observed,
|
|
attemptToken,
|
|
});
|
|
|
|
expect(hostedMediaMocks.cleanup).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reports an accepted text chunk before a later durable send fails", async () => {
|
|
const failure = new Error("second text chunk failed");
|
|
const events: string[] = [];
|
|
sendSmsViaTwilio
|
|
.mockImplementationOnce(async ({ onPlatformSendDispatch }) => {
|
|
await onPlatformSendDispatch?.();
|
|
events.push("send:first");
|
|
return { sid: "SM-first", to: "+15551234567" };
|
|
})
|
|
.mockImplementationOnce(async ({ onPlatformSendDispatch }) => {
|
|
await onPlatformSendDispatch?.();
|
|
events.push("send:second");
|
|
throw failure;
|
|
});
|
|
const onDeliveryResult = vi.fn(async (result) => {
|
|
events.push(`delivery:${result.messageId}`);
|
|
});
|
|
const onPlatformSendDispatch = vi.fn(async () => {
|
|
events.push("dispatch");
|
|
});
|
|
|
|
let observed: unknown;
|
|
try {
|
|
await smsPlugin.message?.send?.text?.({
|
|
cfg: {
|
|
channels: {
|
|
sms: {
|
|
accountSid: "AC123",
|
|
authToken: "secret",
|
|
fromNumber: "+15557654321",
|
|
textChunkLimit: 5,
|
|
},
|
|
},
|
|
},
|
|
to: "+15551234567",
|
|
text: "alpha beta",
|
|
onPlatformSendDispatch,
|
|
onDeliveryResult,
|
|
});
|
|
} catch (error) {
|
|
observed = error;
|
|
}
|
|
|
|
expect(isChannelPartialDeliveryError(observed)).toBe(true);
|
|
expect(onDeliveryResult).toHaveBeenCalledExactlyOnceWith(
|
|
expect.objectContaining({
|
|
messageId: "SM-first",
|
|
receipt: expect.objectContaining({
|
|
platformMessageIds: ["SM-first"],
|
|
}),
|
|
}),
|
|
);
|
|
expect(onPlatformSendDispatch).toHaveBeenCalledTimes(2);
|
|
expect(events).toEqual([
|
|
"dispatch",
|
|
"send:first",
|
|
"delivery:SM-first",
|
|
"dispatch",
|
|
"send:second",
|
|
]);
|
|
});
|
|
});
|