test: tighten durable message send assertions

This commit is contained in:
Peter Steinberger
2026-05-09 22:45:47 +01:00
parent 4e56fb82b6
commit aa720a6bb7

View File

@@ -11,15 +11,43 @@ vi.mock("../../infra/outbound/deliver.js", () => ({
deliverOutboundPayloadsInternal: deliverOutboundPayloads,
}));
import { sendDurableMessageBatch, withDurableMessageSendContext } from "./send.js";
import {
sendDurableMessageBatch,
type DurableMessageBatchSendResult,
withDurableMessageSendContext,
} from "./send.js";
import type { DurableMessageSendIntent } from "./types.js";
type DeliveryIntentCallbackParams = {
onDeliveryIntent?: (intent: OutboundDeliveryIntent) => void;
onPayloadDeliveryOutcome?: (outcome: OutboundPayloadDeliveryOutcome) => void;
};
type DeliveryRequest = DeliveryIntentCallbackParams & {
abortSignal?: AbortSignal;
payloads?: unknown;
queuePolicy?: string;
replyToId?: string;
threadId?: string | number;
};
const cfg = {} as OpenClawConfig;
function latestDeliveryRequest(): DeliveryRequest {
const [request] = deliverOutboundPayloads.mock.calls.at(-1) as unknown as [DeliveryRequest];
return request;
}
function expectBatchStatus<TStatus extends DurableMessageBatchSendResult["status"]>(
result: DurableMessageBatchSendResult,
status: TStatus,
): asserts result is Extract<DurableMessageBatchSendResult, { status: TStatus }> {
expect(result.status).toBe(status);
if (result.status !== status) {
throw new Error(`expected durable batch status ${status}`);
}
}
describe("withDurableMessageSendContext", () => {
it("renders and sends through a durable send context", async () => {
deliverOutboundPayloads.mockImplementationOnce(async (params: DeliveryIntentCallbackParams) => {
@@ -42,58 +70,45 @@ describe("withDurableMessageSendContext", () => {
replyToId: "reply-1",
},
async (ctx) => {
expect(ctx).toEqual(
expect.objectContaining({
id: "telegram:chat-1",
channel: "telegram",
to: "chat-1",
durability: "required",
attempt: 1,
}),
);
expect(ctx.id).toBe("telegram:chat-1");
expect(ctx.channel).toBe("telegram");
expect(ctx.to).toBe("chat-1");
expect(ctx.durability).toBe("required");
expect(ctx.attempt).toBe(1);
const rendered = await ctx.render();
expect(rendered).toEqual({
payloads: [{ text: "hello" }],
plan: expect.objectContaining({
plan: {
payloadCount: 1,
textCount: 1,
mediaCount: 0,
voiceCount: 0,
presentationCount: 0,
interactiveCount: 0,
channelDataCount: 0,
items: [{ index: 0, kinds: ["text"] as const, text: "hello", mediaUrls: [] }],
}),
},
});
const send = await ctx.send(rendered);
expect(ctx.intent).toEqual(
expect.objectContaining({
id: "intent-1",
channel: "telegram",
to: "chat-1",
durability: "required",
renderedBatch: rendered,
}),
);
expect(ctx.intent?.id).toBe("intent-1");
expect(ctx.intent?.channel).toBe("telegram");
expect(ctx.intent?.to).toBe("chat-1");
expect(ctx.intent?.durability).toBe("required");
expect(ctx.intent?.renderedBatch).toBe(rendered);
return send;
},
);
expect(result).toEqual(
expect.objectContaining({
status: "sent",
deliveryIntent: expect.objectContaining({ id: "intent-1" }),
receipt: expect.objectContaining({
platformMessageIds: ["msg-1"],
threadId: "42",
replyToId: "reply-1",
}),
}),
);
expect(deliverOutboundPayloads).toHaveBeenCalledWith(
expect.objectContaining({
queuePolicy: "required",
payloads: [{ text: "hello" }],
threadId: 42,
replyToId: "reply-1",
}),
);
expectBatchStatus(result, "sent");
expect(result.deliveryIntent?.id).toBe("intent-1");
expect(result.receipt?.platformMessageIds).toEqual(["msg-1"]);
expect(result.receipt?.threadId).toBe("42");
expect(result.receipt?.replyToId).toBe("reply-1");
const request = latestDeliveryRequest();
expect(request.queuePolicy).toBe("required");
expect(request.payloads).toEqual([{ text: "hello" }]);
expect(request.threadId).toBe(42);
expect(request.replyToId).toBe("reply-1");
});
it("records a replayable rendered batch plan on the durable intent", async () => {
@@ -131,33 +146,28 @@ describe("withDurableMessageSendContext", () => {
},
);
expect(intent).toEqual(
expect.objectContaining({
renderedBatch: expect.objectContaining({
plan: {
payloadCount: 1,
textCount: 1,
mediaCount: 2,
voiceCount: 1,
presentationCount: 1,
interactiveCount: 1,
channelDataCount: 1,
items: [
{
index: 0,
kinds: ["text", "voice", "presentation", "interactive", "channelData"] as const,
text: "caption",
mediaUrls: ["file:///tmp/a.png", "file:///tmp/b.png"],
audioAsVoice: true,
presentationBlockCount: 1,
hasInteractive: true,
hasChannelData: true,
},
],
},
}),
}),
);
const renderedBatch = (intent as DurableMessageSendIntent | undefined)?.renderedBatch;
expect(renderedBatch?.plan).toEqual({
payloadCount: 1,
textCount: 1,
mediaCount: 2,
voiceCount: 1,
presentationCount: 1,
interactiveCount: 1,
channelDataCount: 1,
items: [
{
index: 0,
kinds: ["text", "voice", "presentation", "interactive", "channelData"] as const,
text: "caption",
mediaUrls: ["file:///tmp/a.png", "file:///tmp/b.png"],
audioAsVoice: true,
presentationBlockCount: 1,
hasInteractive: true,
hasChannelData: true,
},
],
});
});
it("forwards the durable send context signal to outbound delivery", async () => {
@@ -177,20 +187,11 @@ describe("withDurableMessageSendContext", () => {
signal: abortController.signal,
});
expect(result).toEqual(
expect.objectContaining({
status: "sent",
receipt: expect.objectContaining({
platformMessageIds: ["msg-1"],
}),
}),
);
expect(deliverOutboundPayloads).toHaveBeenLastCalledWith(
expect.objectContaining({
abortSignal: abortController.signal,
queuePolicy: "required",
}),
);
expectBatchStatus(result, "sent");
expect(result.receipt?.platformMessageIds).toEqual(["msg-1"]);
const request = latestDeliveryRequest();
expect(request.abortSignal).toBe(abortController.signal);
expect(request.queuePolicy).toBe("required");
});
it("maps best-effort durability to best-effort queue policy", async () => {
@@ -212,17 +213,9 @@ describe("withDurableMessageSendContext", () => {
durability: "best_effort",
});
expect(result).toEqual(
expect.objectContaining({
status: "sent",
deliveryIntent: expect.objectContaining({ id: "intent-best-effort" }),
}),
);
expect(deliverOutboundPayloads).toHaveBeenLastCalledWith(
expect.objectContaining({
queuePolicy: "best_effort",
}),
);
expectBatchStatus(result, "sent");
expect(result.deliveryIntent?.id).toBe("intent-best-effort");
expect(latestDeliveryRequest().queuePolicy).toBe("best_effort");
});
it("preserves adapter-provided multipart receipts in durable sends", async () => {
@@ -249,19 +242,15 @@ describe("withDurableMessageSendContext", () => {
payloads: [{ text: "hello" }],
});
expect(result).toEqual(
expect.objectContaining({
status: "sent",
receipt: expect.objectContaining({
primaryPlatformMessageId: "platform-1",
platformMessageIds: ["platform-1", "platform-2"],
parts: [
expect.objectContaining({ platformMessageId: "platform-1", kind: "text" }),
expect.objectContaining({ platformMessageId: "platform-2", kind: "media" }),
],
}),
}),
);
expectBatchStatus(result, "sent");
expect(result.receipt?.primaryPlatformMessageId).toBe("platform-1");
expect(result.receipt?.platformMessageIds).toEqual(["platform-1", "platform-2"]);
expect(
result.receipt?.parts.map(({ platformMessageId, kind }) => ({ platformMessageId, kind })),
).toEqual([
{ platformMessageId: "platform-1", kind: "text" },
{ platformMessageId: "platform-2", kind: "media" },
]);
});
it("supports preview, edit, and delete send-context hooks", async () => {
@@ -303,10 +292,13 @@ describe("withDurableMessageSendContext", () => {
},
);
expect(onEditReceipt).toHaveBeenCalledWith(
receipt,
expect.objectContaining({ payloads: [{ text: "final" }] }),
);
expect(onEditReceipt).toHaveBeenCalledTimes(1);
const [editReceiptArg, renderedArg] = onEditReceipt.mock.calls[0] as unknown as [
unknown,
{ payloads?: unknown },
];
expect(editReceiptArg).toBe(receipt);
expect(renderedArg.payloads).toEqual([{ text: "final" }]);
expect(onDeleteReceipt).toHaveBeenCalledWith(editedReceipt);
});
@@ -357,18 +349,14 @@ describe("withDurableMessageSendContext", () => {
onCommitReceipt,
});
expect(result).toEqual(
expect.objectContaining({
status: "suppressed",
reason: "no_visible_result",
deliveryIntent: expect.objectContaining({ id: "intent-2" }),
}),
);
expect(onCommitReceipt).toHaveBeenCalledWith(
expect.objectContaining({
platformMessageIds: [],
}),
);
expectBatchStatus(result, "suppressed");
expect(result.reason).toBe("no_visible_result");
expect(result.deliveryIntent?.id).toBe("intent-2");
expect(onCommitReceipt).toHaveBeenCalledTimes(1);
const [receiptArg] = onCommitReceipt.mock.calls[0] as unknown as [
{ platformMessageIds?: unknown },
];
expect(receiptArg.platformMessageIds).toEqual([]);
});
it("reports hook-cancelled deliveries as explicit suppressed sends", async () => {
@@ -391,22 +379,19 @@ describe("withDurableMessageSendContext", () => {
onCommitReceipt,
});
expect(result).toEqual(
expect.objectContaining({
status: "suppressed",
reason: "cancelled_by_message_sending_hook",
payloadOutcomes: [
expect.objectContaining({
status: "suppressed",
reason: "cancelled_by_message_sending_hook",
hookEffect: { cancelReason: "owned-by-other-agent" },
}),
],
}),
);
expect(onCommitReceipt).toHaveBeenCalledWith(
expect.objectContaining({ platformMessageIds: [] }),
);
expectBatchStatus(result, "suppressed");
expect(result.reason).toBe("cancelled_by_message_sending_hook");
expect(result.payloadOutcomes?.[0]).toEqual({
index: 0,
status: "suppressed",
reason: "cancelled_by_message_sending_hook",
hookEffect: { cancelReason: "owned-by-other-agent" },
});
expect(onCommitReceipt).toHaveBeenCalledTimes(1);
const [receiptArg] = onCommitReceipt.mock.calls[0] as unknown as [
{ platformMessageIds?: unknown },
];
expect(receiptArg.platformMessageIds).toEqual([]);
});
it("forwards payload delivery outcomes to callers while collecting durable outcomes", async () => {
@@ -428,25 +413,22 @@ describe("withDurableMessageSendContext", () => {
onPayloadDeliveryOutcome,
});
expect(result).toEqual(
expect.objectContaining({
status: "suppressed",
payloadOutcomes: [
expect.objectContaining({
index: 0,
status: "suppressed",
reason: "cancelled_by_message_sending_hook",
}),
],
}),
);
expect(onPayloadDeliveryOutcome).toHaveBeenCalledWith(
expect.objectContaining({
index: 0,
status: "suppressed",
reason: "cancelled_by_message_sending_hook",
}),
);
expectBatchStatus(result, "suppressed");
expect(result.payloadOutcomes?.[0]).toEqual({
index: 0,
status: "suppressed",
reason: "cancelled_by_message_sending_hook",
});
expect(onPayloadDeliveryOutcome).toHaveBeenCalledTimes(1);
const [outcomeArg] = onPayloadDeliveryOutcome.mock.calls[0] as unknown as [
OutboundPayloadDeliveryOutcome,
];
expect(outcomeArg.index).toBe(0);
expect(outcomeArg.status).toBe("suppressed");
if (outcomeArg.status !== "suppressed") {
throw new Error("expected suppressed payload outcome");
}
expect(outcomeArg.reason).toBe("cancelled_by_message_sending_hook");
});
it("reports zero-result failed best-effort payloads as failed sends", async () => {
@@ -474,21 +456,16 @@ describe("withDurableMessageSendContext", () => {
onSendFailure,
});
expect(result).toEqual(
expect.objectContaining({
status: "failed",
error,
stage: "platform_send",
payloadOutcomes: [
expect.objectContaining({
index: 0,
status: "failed",
error,
stage: "platform_send",
}),
],
}),
);
expectBatchStatus(result, "failed");
expect(result.error).toBe(error);
expect(result.stage).toBe("platform_send");
expect(result.payloadOutcomes?.[0]).toEqual({
index: 0,
status: "failed",
error,
sentBeforeError: false,
stage: "platform_send",
});
expect(onCommitReceipt).not.toHaveBeenCalled();
expect(onSendFailure).toHaveBeenCalledWith(error);
});
@@ -520,15 +497,11 @@ describe("withDurableMessageSendContext", () => {
onSendFailure,
});
expect(result).toEqual(
expect.objectContaining({
status: "partial_failed",
results: [{ channel: "telegram", messageId: "msg-1" }],
receipt: expect.objectContaining({ platformMessageIds: ["msg-1"] }),
error,
sentBeforeError: true,
}),
);
expectBatchStatus(result, "partial_failed");
expect(result.results).toEqual([{ channel: "telegram", messageId: "msg-1" }]);
expect(result.receipt?.platformMessageIds).toEqual(["msg-1"]);
expect(result.error).toBe(error);
expect(result.sentBeforeError).toBe(true);
expect(onSendFailure).toHaveBeenCalledWith(error);
});
@@ -564,15 +537,11 @@ describe("withDurableMessageSendContext", () => {
onSendFailure,
});
expect(result).toEqual(
expect.objectContaining({
status: "partial_failed",
results: [{ channel: "telegram", messageId: "msg-1" }],
receipt: expect.objectContaining({ platformMessageIds: ["msg-1"] }),
error,
sentBeforeError: true,
}),
);
expectBatchStatus(result, "partial_failed");
expect(result.results).toEqual([{ channel: "telegram", messageId: "msg-1" }]);
expect(result.receipt?.platformMessageIds).toEqual(["msg-1"]);
expect(result.error).toBe(error);
expect(result.sentBeforeError).toBe(true);
expect(onSendFailure).toHaveBeenCalledWith(error);
});