openclaw/src/test-utils/send-payload-contract.ts

139 lines
4.0 KiB
TypeScript

import { expect, it, type Mock } from "vitest";
type PayloadLike = {
mediaUrl?: string;
mediaUrls?: string[];
text?: string;
};
type SendResultLike = {
messageId: string;
[key: string]: unknown;
};
type ChunkingMode =
| {
longTextLength: number;
maxChunkLength: number;
mode: "split";
}
| {
longTextLength: number;
mode: "passthrough";
};
export function installSendPayloadContractSuite(params: {
channel: string;
chunking: ChunkingMode;
createHarness: (params: { payload: PayloadLike; sendResults?: SendResultLike[] }) => {
run: () => Promise<Record<string, unknown>>;
sendMock: Mock;
to: string;
};
}) {
it("text-only delegates to sendText", async () => {
const { run, sendMock, to } = params.createHarness({
payload: { text: "hello" },
});
const result = await run();
expect(sendMock).toHaveBeenCalledTimes(1);
expect(sendMock).toHaveBeenCalledWith(to, "hello", expect.any(Object));
expect(result).toMatchObject({ channel: params.channel });
});
it("single media delegates to sendMedia", async () => {
const { run, sendMock, to } = params.createHarness({
payload: { text: "cap", mediaUrl: "https://example.com/a.jpg" },
});
const result = await run();
expect(sendMock).toHaveBeenCalledTimes(1);
expect(sendMock).toHaveBeenCalledWith(
to,
"cap",
expect.objectContaining({ mediaUrl: "https://example.com/a.jpg" }),
);
expect(result).toMatchObject({ channel: params.channel });
});
it("multi-media iterates URLs with caption on first", async () => {
const { run, sendMock, to } = params.createHarness({
payload: {
text: "caption",
mediaUrls: ["https://example.com/1.jpg", "https://example.com/2.jpg"],
},
sendResults: [{ messageId: "m-1" }, { messageId: "m-2" }],
});
const result = await run();
expect(sendMock).toHaveBeenCalledTimes(2);
expect(sendMock).toHaveBeenNthCalledWith(
1,
to,
"caption",
expect.objectContaining({ mediaUrl: "https://example.com/1.jpg" }),
);
expect(sendMock).toHaveBeenNthCalledWith(
2,
to,
"",
expect.objectContaining({ mediaUrl: "https://example.com/2.jpg" }),
);
expect(result).toMatchObject({ channel: params.channel, messageId: "m-2" });
});
it("empty payload returns no-op", async () => {
const { run, sendMock } = params.createHarness({ payload: {} });
const result = await run();
expect(sendMock).not.toHaveBeenCalled();
expect(result).toEqual({ channel: params.channel, messageId: "" });
});
if (params.chunking.mode === "passthrough") {
it("text exceeding chunk limit is sent as-is when chunker is null", async () => {
const text = "a".repeat(params.chunking.longTextLength);
const { run, sendMock, to } = params.createHarness({ payload: { text } });
const result = await run();
expect(sendMock).toHaveBeenCalledTimes(1);
expect(sendMock).toHaveBeenCalledWith(to, text, expect.any(Object));
expect(result).toMatchObject({ channel: params.channel });
});
return;
}
const chunking = params.chunking;
it("chunking splits long text", async () => {
const text = "a".repeat(chunking.longTextLength);
const { run, sendMock } = params.createHarness({
payload: { text },
sendResults: [{ messageId: "c-1" }, { messageId: "c-2" }],
});
const result = await run();
expect(sendMock.mock.calls.length).toBeGreaterThanOrEqual(2);
for (const call of sendMock.mock.calls) {
expect((call[1] as string).length).toBeLessThanOrEqual(chunking.maxChunkLength);
}
expect(result).toMatchObject({ channel: params.channel });
});
}
export function primeSendMock(
sendMock: Mock,
fallbackResult: Record<string, unknown>,
sendResults: SendResultLike[] = [],
) {
sendMock.mockReset();
if (sendResults.length === 0) {
sendMock.mockResolvedValue(fallbackResult);
return;
}
for (const result of sendResults) {
sendMock.mockResolvedValueOnce(result);
}
}