mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-13 17:30:43 +00:00
test: tighten durable message send assertions
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user