489 lines · 13.9 KB · TypeScript
RawDownload
import { describe, expect, it } from "vitest";import {  computeStableMessagesTimelineRows,  computeMessageDurationStart,  deriveMessagesTimelineRows,  normalizeCompactToolLabel,  resolveAssistantMessageCopyState,} from "./MessagesTimeline.logic"; describe("computeMessageDurationStart", () => {  it("returns message createdAt when there is no preceding user message", () => {    const result = computeMessageDurationStart([      {        id: "a1",        role: "assistant",        createdAt: "2026-01-01T00:00:05Z",        completedAt: "2026-01-01T00:00:10Z",      },    ]);    expect(result).toEqual(new Map([["a1", "2026-01-01T00:00:05Z"]]));  });   it("uses the user message createdAt for the first assistant response", () => {    const result = computeMessageDurationStart([      { id: "u1", role: "user", createdAt: "2026-01-01T00:00:00Z" },      {        id: "a1",        role: "assistant",        createdAt: "2026-01-01T00:00:30Z",        completedAt: "2026-01-01T00:00:30Z",      },    ]);     expect(result).toEqual(      new Map([        ["u1", "2026-01-01T00:00:00Z"],        ["a1", "2026-01-01T00:00:00Z"],      ]),    );  });   it("uses the previous assistant completedAt for subsequent assistant responses", () => {    const result = computeMessageDurationStart([      { id: "u1", role: "user", createdAt: "2026-01-01T00:00:00Z" },      {        id: "a1",        role: "assistant",        createdAt: "2026-01-01T00:00:30Z",        completedAt: "2026-01-01T00:00:30Z",      },      {        id: "a2",        role: "assistant",        createdAt: "2026-01-01T00:00:55Z",        completedAt: "2026-01-01T00:00:55Z",      },    ]);     expect(result).toEqual(      new Map([        ["u1", "2026-01-01T00:00:00Z"],        ["a1", "2026-01-01T00:00:00Z"],        ["a2", "2026-01-01T00:00:30Z"],      ]),    );  });   it("does not advance the boundary for a streaming message without completedAt", () => {    const result = computeMessageDurationStart([      { id: "u1", role: "user", createdAt: "2026-01-01T00:00:00Z" },      { id: "a1", role: "assistant", createdAt: "2026-01-01T00:00:30Z" },      {        id: "a2",        role: "assistant",        createdAt: "2026-01-01T00:00:55Z",        completedAt: "2026-01-01T00:00:55Z",      },    ]);     expect(result).toEqual(      new Map([        ["u1", "2026-01-01T00:00:00Z"],        ["a1", "2026-01-01T00:00:00Z"],        ["a2", "2026-01-01T00:00:00Z"],      ]),    );  });   it("resets the boundary on a new user message", () => {    const result = computeMessageDurationStart([      { id: "u1", role: "user", createdAt: "2026-01-01T00:00:00Z" },      {        id: "a1",        role: "assistant",        createdAt: "2026-01-01T00:00:30Z",        completedAt: "2026-01-01T00:00:30Z",      },      { id: "u2", role: "user", createdAt: "2026-01-01T00:01:00Z" },      {        id: "a2",        role: "assistant",        createdAt: "2026-01-01T00:01:20Z",        completedAt: "2026-01-01T00:01:20Z",      },    ]);     expect(result).toEqual(      new Map([        ["u1", "2026-01-01T00:00:00Z"],        ["a1", "2026-01-01T00:00:00Z"],        ["u2", "2026-01-01T00:01:00Z"],        ["a2", "2026-01-01T00:01:00Z"],      ]),    );  });   it("handles system messages without affecting the boundary", () => {    const result = computeMessageDurationStart([      { id: "u1", role: "user", createdAt: "2026-01-01T00:00:00Z" },      { id: "s1", role: "system", createdAt: "2026-01-01T00:00:01Z" },      {        id: "a1",        role: "assistant",        createdAt: "2026-01-01T00:00:30Z",        completedAt: "2026-01-01T00:00:30Z",      },    ]);     expect(result).toEqual(      new Map([        ["u1", "2026-01-01T00:00:00Z"],        ["s1", "2026-01-01T00:00:00Z"],        ["a1", "2026-01-01T00:00:00Z"],      ]),    );  });   it("returns empty map for empty input", () => {    expect(computeMessageDurationStart([])).toEqual(new Map());  });}); describe("normalizeCompactToolLabel", () => {  it("removes trailing completion wording from command labels", () => {    expect(normalizeCompactToolLabel("Ran command complete")).toBe("Ran command");  });   it("removes trailing completion wording from other labels", () => {    expect(normalizeCompactToolLabel("Read file completed")).toBe("Read file");  });}); describe("resolveAssistantMessageCopyState", () => {  it("returns enabled copy state for completed assistant messages", () => {    expect(      resolveAssistantMessageCopyState({        showCopyButton: true,        text: "Ship it",        streaming: false,      }),    ).toEqual({      text: "Ship it",      visible: true,    });  });   it("hides copy while an assistant message is still streaming", () => {    expect(      resolveAssistantMessageCopyState({        showCopyButton: true,        text: "Still streaming",        streaming: true,      }),    ).toEqual({      text: "Still streaming",      visible: false,    });  });   it("hides copy for empty completed assistant messages", () => {    expect(      resolveAssistantMessageCopyState({        showCopyButton: true,        text: "   ",        streaming: false,      }),    ).toEqual({      text: null,      visible: false,    });  });   it("hides copy for non-terminal assistant messages", () => {    expect(      resolveAssistantMessageCopyState({        showCopyButton: false,        text: "Interim thought",        streaming: false,      }),    ).toEqual({      text: "Interim thought",      visible: false,    });  });}); describe("deriveMessagesTimelineRows", () => {  it("only enables assistant copy for the terminal assistant message in a turn", () => {    const rows = deriveMessagesTimelineRows({      timelineEntries: [        {          id: "user-1-entry",          kind: "message",          createdAt: "2026-01-01T00:00:00Z",          message: {            id: "user-1" as never,            role: "user",            text: "Write a poem",            turnId: null,            createdAt: "2026-01-01T00:00:00Z",            streaming: false,          },        },        {          id: "assistant-thought-entry",          kind: "message",          createdAt: "2026-01-01T00:00:10Z",          message: {            id: "assistant-thought" as never,            role: "assistant",            text: "I should ground this first.",            turnId: "turn-1" as never,            createdAt: "2026-01-01T00:00:10Z",            completedAt: "2026-01-01T00:00:11Z",            streaming: false,          },        },        {          id: "assistant-final-entry",          kind: "message",          createdAt: "2026-01-01T00:00:20Z",          message: {            id: "assistant-final" as never,            role: "assistant",            text: "Here is the poem.",            turnId: "turn-1" as never,            createdAt: "2026-01-01T00:00:20Z",            completedAt: "2026-01-01T00:00:30Z",            streaming: false,          },        },      ],      completionDividerBeforeEntryId: "assistant-final-entry",      isWorking: false,      activeTurnStartedAt: null,      turnDiffSummaryByAssistantMessageId: new Map(),      revertTurnCountByUserMessageId: new Map(),    });     const assistantRows = rows.filter(      (row): row is Extract<(typeof rows)[number], { kind: "message" }> =>        row.kind === "message" && row.message.role === "assistant",    );     expect(assistantRows).toHaveLength(2);    expect(assistantRows[0]?.showAssistantCopyButton).toBe(false);    expect(assistantRows[1]?.showAssistantCopyButton).toBe(true);    expect(assistantRows[1]?.showCompletionDivider).toBe(true);  });   it("projects assistant diff summaries and user revert counts onto the affected rows", () => {    const assistantTurnDiffSummary = {      turnId: "turn-1" as never,      completedAt: "2026-01-01T00:00:30Z",      assistantMessageId: "assistant-1" as never,      checkpointTurnCount: 2,      files: [{ path: "src/index.ts", additions: 3, deletions: 1 }],    };     const rows = deriveMessagesTimelineRows({      timelineEntries: [        {          id: "user-entry",          kind: "message",          createdAt: "2026-01-01T00:00:00Z",          message: {            id: "user-1" as never,            role: "user",            text: "Do the thing",            turnId: null,            createdAt: "2026-01-01T00:00:00Z",            streaming: false,          },        },        {          id: "assistant-entry",          kind: "message",          createdAt: "2026-01-01T00:00:20Z",          message: {            id: "assistant-1" as never,            role: "assistant",            text: "Done",            turnId: "turn-1" as never,            createdAt: "2026-01-01T00:00:20Z",            completedAt: "2026-01-01T00:00:30Z",            streaming: false,          },        },      ],      completionDividerBeforeEntryId: null,      isWorking: false,      activeTurnStartedAt: null,      turnDiffSummaryByAssistantMessageId: new Map([        ["assistant-1" as never, assistantTurnDiffSummary],      ]),      revertTurnCountByUserMessageId: new Map([["user-1" as never, 1]]),    });     const userRow = rows.find(      (row): row is Extract<(typeof rows)[number], { kind: "message" }> =>        row.kind === "message" && row.message.role === "user",    );    const assistantRow = rows.find(      (row): row is Extract<(typeof rows)[number], { kind: "message" }> =>        row.kind === "message" && row.message.role === "assistant",    );     expect(userRow?.revertTurnCount).toBe(1);    expect(assistantRow?.assistantTurnDiffSummary).toBe(assistantTurnDiffSummary);  });}); describe("computeStableMessagesTimelineRows", () => {  it("returns the previous result when row order and content are unchanged", () => {    const firstUserMessage = {      id: "user-1" as never,      role: "user" as const,      text: "First",      turnId: null,      createdAt: "2026-01-01T00:00:00Z",      streaming: false,    };    const secondUserMessage = {      id: "user-2" as never,      role: "user" as const,      text: "Second",      turnId: null,      createdAt: "2026-01-01T00:00:10Z",      streaming: false,    };     const rows = deriveMessagesTimelineRows({      timelineEntries: [        {          id: "entry-user-1",          kind: "message",          createdAt: firstUserMessage.createdAt,          message: firstUserMessage,        },        {          id: "entry-user-2",          kind: "message",          createdAt: secondUserMessage.createdAt,          message: secondUserMessage,        },      ],      completionDividerBeforeEntryId: null,      isWorking: false,      activeTurnStartedAt: null,      turnDiffSummaryByAssistantMessageId: new Map(),      revertTurnCountByUserMessageId: new Map(),    });     const initial = computeStableMessagesTimelineRows(rows, {      byId: new Map(),      result: [],    });     const repeated = computeStableMessagesTimelineRows(rows, initial);     expect(repeated).toBe(initial);    expect(repeated.result).toBe(initial.result);  });   it("reuses work rows when equivalent timeline derivations create new grouped arrays", () => {    const firstWorkEntry = {      id: "work-1",      createdAt: "2026-01-01T00:00:00Z",      label: "thinking",      detail: "Inspecting repository state",      tone: "thinking" as const,    };    const secondWorkEntry = {      id: "work-2",      createdAt: "2026-01-01T00:00:01Z",      label: "read",      detail: "Reading package.json",      tone: "tool" as const,    };     const createRows = () =>      deriveMessagesTimelineRows({        timelineEntries: [          {            id: "entry-work-1",            kind: "work",            createdAt: firstWorkEntry.createdAt,            entry: firstWorkEntry,          },          {            id: "entry-work-2",            kind: "work",            createdAt: secondWorkEntry.createdAt,            entry: secondWorkEntry,          },        ],        completionDividerBeforeEntryId: null,        isWorking: false,        activeTurnStartedAt: null,        turnDiffSummaryByAssistantMessageId: new Map(),        revertTurnCountByUserMessageId: new Map(),      });     const firstRows = createRows();    const initial = computeStableMessagesTimelineRows(firstRows, {      byId: new Map(),      result: [],    });    const secondRows = createRows();     expect(secondRows[0]).not.toBe(firstRows[0]);     const repeated = computeStableMessagesTimelineRows(secondRows, initial);     expect(repeated).toBe(initial);    expect(repeated.result[0]).toBe(initial.result[0]);  });   it("returns a new result when row order changes without content changes", () => {    const firstUserMessage = {      id: "user-1" as never,      role: "user" as const,      text: "First",      turnId: null,      createdAt: "2026-01-01T00:00:00Z",      streaming: false,    };    const secondUserMessage = {      id: "user-2" as never,      role: "user" as const,      text: "Second",      turnId: null,      createdAt: "2026-01-01T00:00:10Z",      streaming: false,    };     const firstRows = deriveMessagesTimelineRows({      timelineEntries: [        {          id: "entry-user-1",          kind: "message",          createdAt: firstUserMessage.createdAt,          message: firstUserMessage,        },        {          id: "entry-user-2",          kind: "message",          createdAt: secondUserMessage.createdAt,          message: secondUserMessage,        },      ],      completionDividerBeforeEntryId: null,      isWorking: false,      activeTurnStartedAt: null,      turnDiffSummaryByAssistantMessageId: new Map(),      revertTurnCountByUserMessageId: new Map(),    });     const initial = computeStableMessagesTimelineRows(firstRows, {      byId: new Map(),      result: [],    });     const reordered = computeStableMessagesTimelineRows([firstRows[1]!, firstRows[0]!], initial);     expect(reordered).not.toBe(initial);    expect(reordered.result).toEqual([initial.result[1], initial.result[0]]);  });});