Skip to content

Commit 5768ee8

Browse files
committed
test deltas
1 parent 5532335 commit 5768ee8

File tree

1 file changed

+262
-0
lines changed

1 file changed

+262
-0
lines changed

src/deltas.test.ts

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, it, expect } from "vitest";
22
import {
33
blankUIMessage,
4+
combineUIMessages,
45
deriveUIMessagesFromTextStreamParts,
56
updateFromTextStreamParts,
67
updateFromUIMessageChunks,
@@ -534,3 +535,264 @@ describe("mergeDeltas", () => {
534535
]);
535536
});
536537
});
538+
539+
describe("combineUIMessages", () => {
540+
it("combines messages spanning two pages correctly", () => {
541+
const message1 = {
542+
id: "msg1",
543+
key: "msg1-key",
544+
_creationTime: Date.now(),
545+
order: 1,
546+
stepOrder: 1,
547+
status: "success" as const,
548+
role: "assistant" as const,
549+
text: "",
550+
parts: [
551+
{
552+
type: "dynamic-tool" as const,
553+
state: "input-available" as const,
554+
toolCallId: "call_123",
555+
toolName: "calculator",
556+
input: { operation: "add", a: 2, b: 3 },
557+
},
558+
],
559+
};
560+
561+
const message2 = {
562+
id: "msg2",
563+
key: "msg2-key",
564+
_creationTime: Date.now() + 1,
565+
order: 1,
566+
stepOrder: 2,
567+
status: "success" as const,
568+
role: "assistant" as const,
569+
text: "The result is 5.",
570+
parts: [
571+
{
572+
type: "tool-calculator" as const,
573+
state: "output-available" as const,
574+
toolCallId: "call_123",
575+
input: { operation: "add", a: 2, b: 3 },
576+
output: { result: 5 },
577+
},
578+
{
579+
type: "text" as const,
580+
text: "The result is 5.",
581+
state: "done" as const,
582+
},
583+
],
584+
};
585+
586+
const combined = combineUIMessages([message1, message2]);
587+
588+
expect(combined).toHaveLength(1);
589+
expect(combined[0].role).toBe("assistant");
590+
expect(combined[0].text).toBe("The result is 5.");
591+
expect(combined[0].parts).toHaveLength(2);
592+
593+
const toolPart = combined[0].parts.find(p => p.type === "tool-calculator");
594+
expect(toolPart).toMatchObject({
595+
type: "tool-calculator",
596+
state: "output-available",
597+
toolCallId: "call_123",
598+
input: { operation: "add", a: 2, b: 3 },
599+
output: { result: 5 },
600+
});
601+
602+
const textPart = combined[0].parts.find(p => p.type === "text");
603+
expect(textPart).toMatchObject({
604+
type: "text",
605+
text: "The result is 5.",
606+
state: "done",
607+
});
608+
});
609+
610+
it("preserves separate messages with different roles", () => {
611+
const userMessage = {
612+
id: "user1",
613+
key: "user1-key",
614+
_creationTime: Date.now(),
615+
order: 1,
616+
stepOrder: 0,
617+
status: "success" as const,
618+
role: "user" as const,
619+
text: "Calculate 2 + 3",
620+
parts: [{ type: "text" as const, text: "Calculate 2 + 3" }],
621+
};
622+
623+
const assistantMessage = {
624+
id: "assistant1",
625+
key: "assistant1-key",
626+
_creationTime: Date.now() + 1,
627+
order: 2,
628+
stepOrder: 0,
629+
status: "success" as const,
630+
role: "assistant" as const,
631+
text: "The result is 5.",
632+
parts: [{ type: "text" as const, text: "The result is 5.", state: "done" as const }],
633+
};
634+
635+
const combined = combineUIMessages([userMessage, assistantMessage]);
636+
637+
expect(combined).toHaveLength(2);
638+
expect(combined[0]).toEqual(userMessage);
639+
expect(combined[1]).toEqual(assistantMessage);
640+
});
641+
642+
it("combines multiple tool calls across pages", () => {
643+
const message1 = {
644+
id: "msg1",
645+
key: "msg1-key",
646+
_creationTime: Date.now(),
647+
order: 1,
648+
stepOrder: 1,
649+
status: "success" as const,
650+
role: "assistant" as const,
651+
text: "",
652+
parts: [
653+
{
654+
type: "dynamic-tool" as const,
655+
state: "input-available" as const,
656+
toolCallId: "call_1",
657+
toolName: "calculator",
658+
input: { operation: "add", a: 2, b: 3 },
659+
},
660+
{
661+
type: "dynamic-tool" as const,
662+
state: "input-available" as const,
663+
toolCallId: "call_2",
664+
toolName: "formatter",
665+
input: { text: "result" },
666+
},
667+
],
668+
};
669+
670+
const message2 = {
671+
id: "msg2",
672+
key: "msg2-key",
673+
_creationTime: Date.now() + 1,
674+
order: 1,
675+
stepOrder: 2,
676+
status: "success" as const,
677+
role: "assistant" as const,
678+
text: "The formatted result is: 5",
679+
parts: [
680+
{
681+
type: "tool-calculator" as const,
682+
state: "output-available" as const,
683+
toolCallId: "call_1",
684+
input: { operation: "add", a: 2, b: 3 },
685+
output: { result: 5 },
686+
},
687+
{
688+
type: "tool-formatter" as const,
689+
state: "output-available" as const,
690+
toolCallId: "call_2",
691+
input: { text: "result" },
692+
output: { formatted: "The formatted result is: 5" },
693+
},
694+
{
695+
type: "text" as const,
696+
text: "The formatted result is: 5",
697+
state: "done" as const,
698+
},
699+
],
700+
};
701+
702+
const combined = combineUIMessages([message1, message2]);
703+
704+
expect(combined).toHaveLength(1);
705+
expect(combined[0].role).toBe("assistant");
706+
expect(combined[0].text).toBe("The formatted result is: 5");
707+
expect(combined[0].parts).toHaveLength(3);
708+
709+
const calculatorPart = combined[0].parts.find(p =>
710+
p.type === "tool-calculator" && "toolCallId" in p && p.toolCallId === "call_1"
711+
);
712+
expect(calculatorPart).toMatchObject({
713+
type: "tool-calculator",
714+
state: "output-available",
715+
toolCallId: "call_1",
716+
input: { operation: "add", a: 2, b: 3 },
717+
output: { result: 5 },
718+
});
719+
720+
const formatterPart = combined[0].parts.find(p =>
721+
p.type === "tool-formatter" && "toolCallId" in p && p.toolCallId === "call_2"
722+
);
723+
expect(formatterPart).toMatchObject({
724+
type: "tool-formatter",
725+
state: "output-available",
726+
toolCallId: "call_2",
727+
input: { text: "result" },
728+
output: { formatted: "The formatted result is: 5" },
729+
});
730+
});
731+
732+
it("handles tool call without corresponding output", () => {
733+
const message1 = {
734+
id: "msg1",
735+
key: "msg1-key",
736+
_creationTime: Date.now(),
737+
order: 1,
738+
stepOrder: 1,
739+
status: "success" as const,
740+
role: "assistant" as const,
741+
text: "",
742+
parts: [
743+
{
744+
type: "dynamic-tool" as const,
745+
state: "input-available" as const,
746+
toolCallId: "call_orphan",
747+
toolName: "calculator",
748+
input: { operation: "add", a: 2, b: 3 },
749+
},
750+
],
751+
};
752+
753+
const message2 = {
754+
id: "msg2",
755+
key: "msg2-key",
756+
_creationTime: Date.now() + 1,
757+
order: 1,
758+
stepOrder: 2,
759+
status: "success" as const,
760+
role: "assistant" as const,
761+
text: "Still processing...",
762+
parts: [
763+
{
764+
type: "text" as const,
765+
text: "Still processing...",
766+
state: "done" as const,
767+
},
768+
],
769+
};
770+
771+
const combined = combineUIMessages([message1, message2]);
772+
773+
expect(combined).toHaveLength(1);
774+
expect(combined[0].role).toBe("assistant");
775+
expect(combined[0].text).toBe("Still processing...");
776+
expect(combined[0].parts).toHaveLength(2);
777+
778+
const toolPart = combined[0].parts.find(p =>
779+
p.type === "dynamic-tool"
780+
);
781+
expect(toolPart).toMatchObject({
782+
type: "dynamic-tool",
783+
state: "input-available",
784+
toolCallId: "call_orphan",
785+
toolName: "calculator",
786+
input: { operation: "add", a: 2, b: 3 },
787+
});
788+
789+
const textPart = combined[0].parts.find(p =>
790+
p.type === "text"
791+
);
792+
expect(textPart).toMatchObject({
793+
type: "text",
794+
text: "Still processing...",
795+
state: "done",
796+
});
797+
});
798+
});

0 commit comments

Comments
 (0)