1037 lines · 32.4 KB · TypeScript
RawDownload
import "../../index.css"; import { MessageId, type TurnId } from "@t3tools/contracts";import { page } from "vitest/browser";import { useCallback, useState, type ComponentProps } from "react";import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";import { render } from "vitest-browser-react"; import { deriveTimelineEntries, type WorkLogEntry } from "../../session-logic";import { type ChatMessage, type ProposedPlan, type TurnDiffSummary } from "../../types";import { MessagesTimeline } from "./MessagesTimeline";import {  deriveMessagesTimelineRows,  estimateMessagesTimelineRowHeight,} from "./MessagesTimeline.logic"; const DEFAULT_VIEWPORT = {  width: 960,  height: 1_100,};const MARKDOWN_CWD = "/repo/project"; interface RowMeasurement {  actualHeightPx: number;  estimatedHeightPx: number;  timelineWidthPx: number;  virtualizerSizePx: number;  renderedInVirtualizedRegion: boolean;} interface VirtualizationScenario {  name: string;  targetRowId: string;  props: Omit<ComponentProps<typeof MessagesTimeline>, "scrollContainer">;  maxEstimateDeltaPx: number;} interface VirtualizerSnapshot {  totalSize: number;  measurements: ReadonlyArray<{    id: string;    kind: string;    index: number;    size: number;    start: number;    end: number;  }>;} function MessagesTimelineBrowserHarness(  props: Omit<ComponentProps<typeof MessagesTimeline>, "scrollContainer">,) {  const [scrollContainer, setScrollContainer] = useState<HTMLDivElement | null>(null);  const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>(    () => props.expandedWorkGroups,  );  const handleToggleWorkGroup = useCallback(    (groupId: string) => {      setExpandedWorkGroups((current) => ({        ...current,        [groupId]: !(current[groupId] ?? false),      }));      props.onToggleWorkGroup(groupId);    },    [props],  );   return (    <div      ref={setScrollContainer}      data-testid="messages-timeline-scroll-container"      className="h-full overflow-y-auto overscroll-y-contain"    >      <MessagesTimeline        {...props}        scrollContainer={scrollContainer}        expandedWorkGroups={expandedWorkGroups}        onToggleWorkGroup={handleToggleWorkGroup}      />    </div>  );} function isoAt(offsetSeconds: number): string {  return new Date(Date.UTC(2026, 2, 17, 19, 12, 28) + offsetSeconds * 1_000).toISOString();} function createMessage(input: {  id: string;  role: ChatMessage["role"];  text: string;  offsetSeconds: number;  attachments?: ChatMessage["attachments"];}): ChatMessage {  return {    id: MessageId.makeUnsafe(input.id),    role: input.role,    text: input.text,    ...(input.attachments ? { attachments: input.attachments } : {}),    createdAt: isoAt(input.offsetSeconds),    ...(input.role === "assistant" ? { completedAt: isoAt(input.offsetSeconds + 1) } : {}),    streaming: false,  };} function createToolWorkEntry(input: {  id: string;  offsetSeconds: number;  label?: string;  detail?: string;}): WorkLogEntry {  return {    id: input.id,    createdAt: isoAt(input.offsetSeconds),    label: input.label ?? "exec_command completed",    ...(input.detail ? { detail: input.detail } : {}),    tone: "tool",    toolTitle: "exec_command",  };} function createPlan(input: {  id: string;  offsetSeconds: number;  planMarkdown: string;}): ProposedPlan {  return {    id: input.id as ProposedPlan["id"],    turnId: null,    planMarkdown: input.planMarkdown,    implementedAt: null,    implementationThreadId: null,    createdAt: isoAt(input.offsetSeconds),    updatedAt: isoAt(input.offsetSeconds + 1),  };} function createBaseTimelineProps(input: {  messages?: ChatMessage[];  proposedPlans?: ProposedPlan[];  workEntries?: WorkLogEntry[];  expandedWorkGroups?: Record<string, boolean>;  completionDividerBeforeEntryId?: string | null;  turnDiffSummaryByAssistantMessageId?: Map<MessageId, TurnDiffSummary>;  onVirtualizerSnapshot?: ComponentProps<typeof MessagesTimeline>["onVirtualizerSnapshot"];}): Omit<ComponentProps<typeof MessagesTimeline>, "scrollContainer"> {  return {    hasMessages: true,    isWorking: false,    activeTurnInProgress: false,    activeTurnStartedAt: null,    timelineEntries: deriveTimelineEntries(      input.messages ?? [],      input.proposedPlans ?? [],      input.workEntries ?? [],    ),    completionDividerBeforeEntryId: input.completionDividerBeforeEntryId ?? null,    completionSummary: null,    turnDiffSummaryByAssistantMessageId: input.turnDiffSummaryByAssistantMessageId ?? new Map(),    nowIso: isoAt(10_000),    expandedWorkGroups: input.expandedWorkGroups ?? {},    onToggleWorkGroup: () => {},    onOpenTurnDiff: () => {},    revertTurnCountByUserMessageId: new Map(),    onRevertUserMessage: () => {},    isRevertingCheckpoint: false,    onImageExpand: () => {},    markdownCwd: MARKDOWN_CWD,    resolvedTheme: "light",    timestampFormat: "locale",    workspaceRoot: MARKDOWN_CWD,    ...(input.onVirtualizerSnapshot ? { onVirtualizerSnapshot: input.onVirtualizerSnapshot } : {}),  };} function createFillerMessages(input: {  prefix: string;  startOffsetSeconds: number;  pairCount: number;}): ChatMessage[] {  const messages: ChatMessage[] = [];  for (let index = 0; index < input.pairCount; index += 1) {    const baseOffset = input.startOffsetSeconds + index * 4;    messages.push(      createMessage({        id: `${input.prefix}-user-${index}`,        role: "user",        text: `filler user message ${index}`,        offsetSeconds: baseOffset,      }),    );    messages.push(      createMessage({        id: `${input.prefix}-assistant-${index}`,        role: "assistant",        text: `filler assistant message ${index}`,        offsetSeconds: baseOffset + 1,      }),    );  }  return messages;} function createChangedFilesSummary(  targetMessageId: MessageId,  files: TurnDiffSummary["files"],): Map<MessageId, TurnDiffSummary> {  return new Map([    [      targetMessageId,      {        turnId: "turn-changed-files" as TurnId,        completedAt: isoAt(10),        assistantMessageId: targetMessageId,        files,      },    ],  ]);} function createChangedFilesScenario(input: {  name: string;  rowId: string;  files: TurnDiffSummary["files"];  maxEstimateDeltaPx?: number;}): VirtualizationScenario {  const beforeMessages = createFillerMessages({    prefix: `${input.rowId}-before`,    startOffsetSeconds: 0,    pairCount: 2,  });  const afterMessages = createFillerMessages({    prefix: `${input.rowId}-after`,    startOffsetSeconds: 40,    pairCount: 8,  });  const changedFilesMessage = createMessage({    id: input.rowId,    role: "assistant",    text: "Validation passed on the merged tree.",    offsetSeconds: 12,  });   return {    name: input.name,    targetRowId: changedFilesMessage.id,    props: createBaseTimelineProps({      messages: [...beforeMessages, changedFilesMessage, ...afterMessages],      turnDiffSummaryByAssistantMessageId: createChangedFilesSummary(        changedFilesMessage.id,        input.files,      ),    }),    maxEstimateDeltaPx: input.maxEstimateDeltaPx ?? 72,  };} function createAssistantMessageScenario(input: {  name: string;  rowId: string;  text: string;  maxEstimateDeltaPx?: number;}): VirtualizationScenario {  const beforeMessages = createFillerMessages({    prefix: `${input.rowId}-before`,    startOffsetSeconds: 0,    pairCount: 2,  });  const afterMessages = createFillerMessages({    prefix: `${input.rowId}-after`,    startOffsetSeconds: 40,    pairCount: 8,  });  const assistantMessage = createMessage({    id: input.rowId,    role: "assistant",    text: input.text,    offsetSeconds: 12,  });   return {    name: input.name,    targetRowId: assistantMessage.id,    props: createBaseTimelineProps({      messages: [...beforeMessages, assistantMessage, ...afterMessages],    }),    maxEstimateDeltaPx: input.maxEstimateDeltaPx ?? 16,  };} function buildStaticScenarios(): VirtualizationScenario[] {  const beforeMessages = createFillerMessages({    prefix: "before",    startOffsetSeconds: 0,    pairCount: 2,  });  const afterMessages = createFillerMessages({    prefix: "after",    startOffsetSeconds: 40,    pairCount: 8,  });   const longUserMessage = createMessage({    id: "target-user-long",    role: "user",    text: "x".repeat(3_200),    offsetSeconds: 12,  });  const workEntries = Array.from({ length: 4 }, (_, index) =>    createToolWorkEntry({      id: `target-work-${index}`,      offsetSeconds: 12 + index,      detail: `tool output line ${index + 1}`,    }),  );  const moderatePlan = createPlan({    id: "target-plan",    offsetSeconds: 12,    planMarkdown: [      "# Stabilize virtualization",      "",      "- Gather baseline measurements",      "- Add browser harness coverage",      "- Compare estimated and rendered heights",      "- Fix the broken rows without broad refactors",      "- Re-run lint and typecheck",    ].join("\n"),  });  return [    {      name: "long user message",      targetRowId: longUserMessage.id,      props: createBaseTimelineProps({        messages: [...beforeMessages, longUserMessage, ...afterMessages],      }),      maxEstimateDeltaPx: 56,    },    {      name: "grouped work log row",      targetRowId: workEntries[0]!.id,      props: createBaseTimelineProps({        messages: [...beforeMessages, ...afterMessages],        workEntries,      }),      maxEstimateDeltaPx: 56,    },    {      name: "expanded grouped work log row with show more enabled",      targetRowId: "target-work-expanded-0",      props: createBaseTimelineProps({        messages: [...beforeMessages, ...afterMessages],        workEntries: Array.from({ length: 10 }, (_, index) =>          createToolWorkEntry({            id: `target-work-expanded-${index}`,            offsetSeconds: 12 + index,            detail: `tool output line ${index + 1}`,          }),        ),        expandedWorkGroups: {          "target-work-expanded-0": true,        },      }),      maxEstimateDeltaPx: 72,    },    {      name: "proposed plan row",      targetRowId: moderatePlan.id,      props: createBaseTimelineProps({        messages: [...beforeMessages, ...afterMessages],        proposedPlans: [moderatePlan],      }),      maxEstimateDeltaPx: 96,    },    createAssistantMessageScenario({      name: "assistant single-paragraph row with plain prose",      rowId: "target-assistant-plain-prose",      text: [        "The host is still expanding to content somewhere in the grid layout.",        "I'm stripping it back further to a plain block container so the test width",        "is actually the timeline width.",      ].join(" "),    }),    createAssistantMessageScenario({      name: "assistant single-paragraph row with inline code",      rowId: "target-assistant-inline-code",      text: [        "Typecheck found one exact-optional-property issue in the browser harness:",        "I was always passing `onVirtualizerSnapshot`, including `undefined`.",        "I'm tightening that object construction and rerunning the checks.",      ].join(" "),      maxEstimateDeltaPx: 28,    }),    createChangedFilesScenario({      name: "assistant changed-files row with a compacted single-chain directory",      rowId: "target-assistant-changed-files-single-chain",      files: [        { path: "apps/web/src/components/chat/ChangedFilesTree.tsx", additions: 37, deletions: 45 },        {          path: "apps/web/src/components/chat/ChangedFilesTree.test.tsx",          additions: 0,          deletions: 26,        },      ],    }),    createChangedFilesScenario({      name: "assistant changed-files row with a branch after compaction",      rowId: "target-assistant-changed-files-branch-point",      files: [        { path: "apps/server/src/git/Layers/GitCore.ts", additions: 44, deletions: 38 },        { path: "apps/server/src/git/Layers/GitCore.test.ts", additions: 18, deletions: 9 },        {          path: "apps/server/src/provider/Layers/CodexAdapter.ts",          additions: 27,          deletions: 8,        },        {          path: "apps/server/src/provider/Layers/CodexAdapter.test.ts",          additions: 36,          deletions: 0,        },      ],    }),    createChangedFilesScenario({      name: "assistant changed-files row with mixed root and nested entries",      rowId: "target-assistant-changed-files-mixed-root",      files: [        { path: "README.md", additions: 5, deletions: 1 },        { path: "packages/contracts/src/orchestration.ts", additions: 13, deletions: 3 },        { path: "packages/shared/src/git.ts", additions: 8, deletions: 2 },      ],    }),  ];} async function nextFrame(): Promise<void> {  await new Promise<void>((resolve) => {    window.requestAnimationFrame(() => resolve());  });} async function waitForLayout(): Promise<void> {  await nextFrame();  await nextFrame();  await nextFrame();} async function setViewport(viewport: { width: number; height: number }): Promise<void> {  await page.viewport(viewport.width, viewport.height);  await waitForLayout();} async function waitForProductionStyles(): Promise<void> {  await vi.waitFor(    () => {      expect(        getComputedStyle(document.documentElement).getPropertyValue("--background").trim(),      ).not.toBe("");      expect(getComputedStyle(document.body).marginTop).toBe("0px");    },    { timeout: 4_000, interval: 16 },  );} async function waitForElement<T extends Element>(  query: () => T | null,  errorMessage: string,): Promise<T> {  let element: T | null = null;  await vi.waitFor(    () => {      element = query();      expect(element, errorMessage).toBeTruthy();    },    { timeout: 8_000, interval: 16 },  );  if (!element) {    throw new Error(errorMessage);  }  return element;} async function measureTimelineRow(input: {  host: HTMLElement;  props: Omit<ComponentProps<typeof MessagesTimeline>, "scrollContainer">;  targetRowId: string;}): Promise<RowMeasurement> {  const scrollContainer = await waitForElement(    () =>      input.host.querySelector<HTMLDivElement>(        '[data-testid="messages-timeline-scroll-container"]',      ),    "Unable to find MessagesTimeline scroll container.",  );   const rowSelector = `[data-timeline-row-id="${input.targetRowId}"]`;  const virtualRowSelector = `[data-virtual-row-id="${input.targetRowId}"]`;   let timelineWidthPx = 0;  let actualHeightPx = 0;  let virtualizerSizePx = 0;

Lines 1–500 of 1037.