@convex-dev/agent
Version:
A agent component for Convex.
335 lines (300 loc) • 9.58 kB
text/typescript
import { describe, it, expect } from "vitest";
import {
dedupeMessages,
mergeUIMessages,
type UIMessageLike,
} from "./useUIMessages.js";
type TestMessage = {
order: number;
stepOrder: number;
status: "pending" | "success" | "failed" | "streaming";
id: string;
};
type TestUIMessage = UIMessageLike & {
id: string;
key: string;
text: string;
_creationTime: number;
};
function testUIMessage({
id,
order,
stepOrder,
status,
text,
}: {
id: string;
order: number;
stepOrder: number;
status: TestUIMessage["status"];
text: string;
}): TestUIMessage {
return {
id,
key: `thread-${order}-${stepOrder}`,
order,
stepOrder,
status,
role: "assistant",
parts: [{ type: "text", text }],
text,
_creationTime: 0,
};
}
describe("dedupeMessages", () => {
it("should prefer messages from messages list when streaming messages are absent", () => {
const messages: TestMessage[] = [
{ order: 1, stepOrder: 0, status: "success", id: "msg1" },
{ order: 2, stepOrder: 0, status: "success", id: "msg2" },
];
const streamMessages: TestMessage[] = [];
const result = dedupeMessages(messages, streamMessages);
expect(result).toHaveLength(2);
expect(result).toEqual(messages);
});
it("should prefer streaming messages when messages list is empty", () => {
const messages: TestMessage[] = [];
const streamMessages: TestMessage[] = [
{ order: 1, stepOrder: 0, status: "streaming", id: "stream1" },
{ order: 2, stepOrder: 0, status: "streaming", id: "stream2" },
];
const result = dedupeMessages(messages, streamMessages);
expect(result).toHaveLength(2);
expect(result).toEqual(streamMessages);
});
it("should prefer non-pending messages over pending messages", () => {
const messages: TestMessage[] = [
{ order: 1, stepOrder: 0, status: "pending", id: "pending1" },
{ order: 2, stepOrder: 0, status: "success", id: "success1" },
];
const streamMessages: TestMessage[] = [
{ order: 1, stepOrder: 0, status: "success", id: "finalized1" },
{ order: 2, stepOrder: 0, status: "streaming", id: "streaming1" },
];
const result = dedupeMessages(messages, streamMessages);
expect(result).toHaveLength(2);
expect(result[0]).toEqual({
order: 1,
stepOrder: 0,
status: "success",
id: "finalized1",
});
expect(result[1]).toEqual({
order: 2,
stepOrder: 0,
status: "success",
id: "success1",
});
});
it("should prefer streaming over pending messages", () => {
const messages: TestMessage[] = [
{ order: 1, stepOrder: 0, status: "pending", id: "pending1" },
];
const streamMessages: TestMessage[] = [
{ order: 1, stepOrder: 0, status: "streaming", id: "streaming1" },
];
const result = dedupeMessages(messages, streamMessages);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
order: 1,
stepOrder: 0,
status: "streaming",
id: "streaming1",
});
});
it("should prefer non-pending messages from messages list over streaming messages", () => {
const messages: TestMessage[] = [
{ order: 1, stepOrder: 0, status: "success", id: "success1" },
];
const streamMessages: TestMessage[] = [
{ order: 1, stepOrder: 0, status: "streaming", id: "streaming1" },
];
const result = dedupeMessages(messages, streamMessages);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
order: 1,
stepOrder: 0,
status: "success",
id: "success1",
});
});
it("should handle complex scenarios with multiple duplicates", () => {
const messages: TestMessage[] = [
{ order: 1, stepOrder: 0, status: "pending", id: "pending1" },
{ order: 2, stepOrder: 0, status: "success", id: "success2" },
{ order: 3, stepOrder: 0, status: "failed", id: "failed3" },
{ order: 4, stepOrder: 0, status: "success", id: "success4" },
];
const streamMessages: TestMessage[] = [
{ order: 1, stepOrder: 0, status: "streaming", id: "streaming1" },
{ order: 2, stepOrder: 0, status: "streaming", id: "streaming2" },
{ order: 5, stepOrder: 0, status: "streaming", id: "streaming5" },
];
const result = dedupeMessages(messages, streamMessages);
expect(result).toHaveLength(5);
// Order 1: streaming should replace pending
expect(result[0]).toEqual({
order: 1,
stepOrder: 0,
status: "streaming",
id: "streaming1",
});
// Order 2: success should be kept over streaming
expect(result[1]).toEqual({
order: 2,
stepOrder: 0,
status: "success",
id: "success2",
});
// Order 3: failed should be kept (no streaming equivalent)
expect(result[2]).toEqual({
order: 3,
stepOrder: 0,
status: "failed",
id: "failed3",
});
// Order 4: success should be kept (no streaming equivalent)
expect(result[3]).toEqual({
order: 4,
stepOrder: 0,
status: "success",
id: "success4",
});
// Order 5: streaming should be added (no messages equivalent)
expect(result[4]).toEqual({
order: 5,
stepOrder: 0,
status: "streaming",
id: "streaming5",
});
});
it("should handle different stepOrder values correctly", () => {
const messages: TestMessage[] = [
{ order: 1, stepOrder: 0, status: "pending", id: "pending1-0" },
{ order: 1, stepOrder: 1, status: "success", id: "success1-1" },
];
const streamMessages: TestMessage[] = [
{ order: 1, stepOrder: 0, status: "streaming", id: "streaming1-0" },
{ order: 1, stepOrder: 2, status: "streaming", id: "streaming1-2" },
];
const result = dedupeMessages(messages, streamMessages);
expect(result).toHaveLength(3);
// Order 1, stepOrder 0: streaming should replace pending
expect(result[0]).toEqual({
order: 1,
stepOrder: 0,
status: "streaming",
id: "streaming1-0",
});
// Order 1, stepOrder 1: success should be kept
expect(result[1]).toEqual({
order: 1,
stepOrder: 1,
status: "success",
id: "success1-1",
});
// Order 1, stepOrder 2: streaming should be added
expect(result[2]).toEqual({
order: 1,
stepOrder: 2,
status: "streaming",
id: "streaming1-2",
});
});
it("should maintain proper sorting by order and stepOrder", () => {
const messages: TestMessage[] = [
{ order: 3, stepOrder: 0, status: "success", id: "msg3" },
{ order: 1, stepOrder: 1, status: "success", id: "msg1-1" },
{ order: 1, stepOrder: 0, status: "pending", id: "msg1-0" },
];
const streamMessages: TestMessage[] = [
{ order: 2, stepOrder: 0, status: "streaming", id: "stream2" },
{ order: 1, stepOrder: 0, status: "streaming", id: "stream1-0" },
];
const result = dedupeMessages(messages, streamMessages);
expect(result).toHaveLength(4);
// Should be sorted by order, then stepOrder
expect(result[0]).toEqual({
order: 1,
stepOrder: 0,
status: "streaming",
id: "stream1-0",
});
expect(result[1]).toEqual({
order: 1,
stepOrder: 1,
status: "success",
id: "msg1-1",
});
expect(result[2]).toEqual({
order: 2,
stepOrder: 0,
status: "streaming",
id: "stream2",
});
expect(result[3]).toEqual({
order: 3,
stepOrder: 0,
status: "success",
id: "msg3",
});
});
it("should handle empty arrays", () => {
const result = dedupeMessages([], []);
expect(result).toEqual([]);
});
it("should demonstrate order dependency when messages and streamMessages have same order/stepOrder", () => {
// This test shows that the current implementation doesn't guarantee
// which message wins when they have the same order/stepOrder and status
const messages: TestMessage[] = [
{ order: 1, stepOrder: 0, status: "success", id: "messages-success" },
];
const streamMessages: TestMessage[] = [
{ order: 1, stepOrder: 0, status: "success", id: "stream-success" },
];
const result = dedupeMessages(messages, streamMessages);
expect(result).toHaveLength(1);
// The result depends on which array comes first in messages.concat(streamMessages)
// Since messages comes first, it should win when both have same status
expect(result[0].id).toBe("messages-success");
});
});
describe("mergeUIMessages", () => {
it("dedupes streaming steps before combining assistant messages", () => {
const messages = [
testUIMessage({
id: "persisted-step-1",
order: 1,
stepOrder: 1,
status: "success",
text: "7",
}),
testUIMessage({
id: "persisted-step-2",
order: 1,
stepOrder: 2,
status: "success",
text: "8",
}),
];
const streamMessages = [
testUIMessage({
id: "stream-step-2",
order: 1,
stepOrder: 2,
status: "streaming",
text: "8",
}),
];
const result = mergeUIMessages(messages, streamMessages);
expect(result).toHaveLength(1);
expect(result[0].id).toBe("persisted-step-1");
expect(result[0].stepOrder).toBe(1);
expect(result[0].status).toBe("success");
expect(result[0].text).toBe("7 8");
expect(result[0].parts).toEqual([
{ type: "text", text: "7" },
{ type: "text", text: "8" },
]);
});
});