1import { describe, expect, it } from "vite-plus/test";
2import { MessageId } from "@t3tools/contracts";
3import type { MessagesTimelineRow } from "./MessagesTimeline.logic";
4import { deriveTimelineMinimapItems, resolveTimelineMinimapPreview } from "./timelineMinimapItems";
5import type { ChatMessage } from "../../types";
6
7function rows(
8 entries: ReadonlyArray<readonly ["user" | "assistant", string]>,
9): MessagesTimelineRow[] {
10 const messages: ChatMessage[] = entries.map(([role, text], index) => ({
11 id: MessageId.make(`message-${index}`),
12 role,
13 text,
14 streaming: false,
15 runId: null,
16 createdAt: new Date(index * 1000).toISOString(),
17 updatedAt: new Date(index * 1000).toISOString(),
18 }));
19 return messages.map((message) => ({
20 kind: "message",
21 id: message.id,
22 createdAt: message.createdAt,
23 message,
24 durationStart: message.createdAt,
25 showAssistantMeta: false,
26 showAssistantCopyButton: false,
27 assistantCopyStreaming: false,
28 }));
29}
30
31describe("timeline minimap previews", () => {
32 it("previews the last assistant response before the next prompt and retains jump targets", () => {
33 const source = rows([
34 ["user", " Inspect\n this "],
35 ["assistant", "Working"],
36 ["assistant", " Done\t now "],
37 ["user", "Next"],
38 ["assistant", "Second answer"],
39 ]);
40 const items = deriveTimelineMinimapItems(source);
41 expect(items).toHaveLength(2);
42 expect(resolveTimelineMinimapPreview(items[0]!)).toEqual({
43 ...items[0],
44 userText: "Inspect this",
45 assistantText: "Done now",
46 });
47 expect(source[items[0]!.rowIndex]!.id).toBe(items[0]!.id);
48 expect(resolveTimelineMinimapPreview(items[1]!)?.assistantText).toBe("Second answer");
49 expect(items[0]?.assistantText).toBe(" Done\t now ");
50 });
51
52 it("handles an unanswered prompt, empty responses, and a closed preview", () => {
53 const items = deriveTimelineMinimapItems(
54 rows([
55 ["user", "First"],
56 ["assistant", " \n\t"],
57 ["user", "Next"],
58 ]),
59 );
60 expect(items.map((item) => resolveTimelineMinimapPreview(item)?.assistantText)).toEqual([
61 null,
62 null,
63 ]);
64 expect(resolveTimelineMinimapPreview(null)).toBeNull();
65 });
66
67 it("shows fresh streaming text without changing the jump target", () => {
68 const first = deriveTimelineMinimapItems(
69 rows([
70 ["user", "Explain"],
71 ["assistant", "First"],
72 ]),
73 )[0]!;
74 const next = { ...first, assistantText: "First\n second" };
75 expect(resolveTimelineMinimapPreview(next)).toEqual({
76 ...first,
77 assistantText: "First second",
78 });
79 expect(resolveTimelineMinimapPreview(first)?.assistantText).toBe("First");
80 });
81});