495 lines · 16.1 KB · TypeScript
RawDownload
import { EnvironmentId, MessageId } from "@t3tools/contracts";import { createRef, type ReactNode, type Ref } from "react";import { renderToStaticMarkup } from "react-dom/server";import { beforeAll, describe, expect, it, vi } from "vite-plus/test";import type { LegendListRef } from "@legendapp/list/react"; vi.mock("@legendapp/list/react", async () => {  const legendListTestId = "legend-list";   const LegendList = (props: {    data: Array<{ id: string }>;    keyExtractor: (item: { id: string }) => string;    renderItem: (args: { item: { id: string } }) => ReactNode;    ListHeaderComponent?: ReactNode;    ListFooterComponent?: ReactNode;    anchoredEndSpace?: {      anchorIndex: number;      anchorMaxSize?: number;      anchorOffset?: number;      onReady?: (info: { anchorIndex: number }) => void;      onSizeChanged?: (size: number) => void;    };    contentInsetEndAdjustment?: number;    className?: string;    maintainScrollAtEnd?: boolean;    maintainVisibleContentPosition?:      | boolean      | {          data?: boolean;          size?: boolean;          shouldRestorePosition?: (item: { id: string }) => boolean;        };    ref?: Ref<LegendListRef>;  }) => {    if (props.anchoredEndSpace) {      props.anchoredEndSpace.onSizeChanged?.(240);      props.anchoredEndSpace.onReady?.({ anchorIndex: props.anchoredEndSpace.anchorIndex });    }    return (      <div        data-testid={legendListTestId}        data-anchor-index={props.anchoredEndSpace?.anchorIndex}        data-anchor-max-size={props.anchoredEndSpace?.anchorMaxSize}        data-anchor-offset={props.anchoredEndSpace?.anchorOffset}        data-anchor-on-ready={Boolean(props.anchoredEndSpace?.onReady)}        data-content-inset-end={props.contentInsetEndAdjustment}        data-class-name={props.className}        data-maintain-scroll-at-end={props.maintainScrollAtEnd}        data-maintain-visible-content-position={          typeof props.maintainVisibleContentPosition === "object"            ? "object"            : props.maintainVisibleContentPosition        }        data-maintain-visible-content-position-data={          typeof props.maintainVisibleContentPosition === "object"            ? props.maintainVisibleContentPosition.data            : undefined        }        data-maintain-visible-content-position-size={          typeof props.maintainVisibleContentPosition === "object"            ? props.maintainVisibleContentPosition.size            : undefined        }      >        {props.ListHeaderComponent}        {props.data.map((item) => (          <div key={props.keyExtractor(item)}>{props.renderItem({ item })}</div>        ))}        {props.ListFooterComponent}      </div>    );  };   return { LegendList };}); function MockFileDiff(props: {  fileDiff: { name?: string | null; prevName?: string | null };  renderCustomHeader?: (fileDiff: {    name?: string | null;    prevName?: string | null;  }) => React.ReactNode;}) {  return (    <div data-testid="file-diff">      {props.renderCustomHeader?.(props.fileDiff)}      {props.fileDiff.name ?? props.fileDiff.prevName ?? "diff"}    </div>  );} vi.mock("@pierre/diffs/react", () => {  return { FileDiff: MockFileDiff };}); function matchMedia() {  return {    matches: false,    addEventListener: () => {},    removeEventListener: () => {},  };} beforeAll(() => {  const classList = {    add: () => {},    remove: () => {},    toggle: () => {},    contains: () => false,  };   vi.stubGlobal("localStorage", {    getItem: () => null,    setItem: () => {},    removeItem: () => {},    clear: () => {},  });  vi.stubGlobal("window", {    matchMedia,    addEventListener: () => {},    removeEventListener: () => {},    requestAnimationFrame: (callback: FrameRequestCallback) => {      callback(0);      return 0;    },    cancelAnimationFrame: () => {},    desktopBridge: undefined,  });  vi.stubGlobal("document", {    documentElement: {      classList,      offsetHeight: 0,    },  });}); const ACTIVE_THREAD_ENVIRONMENT_ID = EnvironmentId.make("environment-local");const MESSAGE_CREATED_AT = "2026-03-17T19:12:28.000Z"; function buildProps() {  return {    isWorking: false,    activeTurnInProgress: false,    activeTurnStartedAt: null,    listRef: createRef<LegendListRef | null>(),    latestTurn: null,    runningTurnId: null,    turnDiffSummaryByAssistantMessageId: new Map(),    routeThreadKey: "environment-local:thread-1",    onOpenTurnDiff: () => {},    revertTurnCountByUserMessageId: new Map(),    onRevertUserMessage: () => {},    isRevertingCheckpoint: false,    onImageExpand: () => {},    activeThreadEnvironmentId: ACTIVE_THREAD_ENVIRONMENT_ID,    markdownCwd: undefined,    resolvedTheme: "light" as const,    timestampFormat: "locale" as const,    workspaceRoot: undefined,    anchorMessageId: null,    onAnchorReady: () => {},    onAnchorSizeChanged: () => {},    contentInsetEndAdjustment: 0,    onIsAtEndChange: () => {},  };} function buildLongUserMessageText(tail = "deep hidden detail only after expand") {  return Array.from({ length: 9 }, (_, index) =>    index === 8 ? tail : `Line ${index + 1}: ${"verbose prompt content ".repeat(8).trim()}`,  ).join("\n");} function buildUserTimelineEntry(text: string) {  return {    id: "entry-1",    kind: "message" as const,    createdAt: MESSAGE_CREATED_AT,    message: {      id: MessageId.make("message-1"),      role: "user" as const,      text,      turnId: null,      createdAt: MESSAGE_CREATED_AT,      updatedAt: MESSAGE_CREATED_AT,      streaming: false,    },  };} describe("MessagesTimeline", () => {  it("anchors a sent attachment message using its measured height", async () => {    const { MessagesTimeline } = await import("./MessagesTimeline");    const onAnchorReady = vi.fn();    const onAnchorSizeChanged = vi.fn();    const firstEntry = buildUserTimelineEntry("First prompt.");    const secondEntry = {      ...buildUserTimelineEntry("Newest prompt."),      id: "entry-2",      message: {        ...buildUserTimelineEntry("Newest prompt.").message,        id: MessageId.make("message-2"),        attachments: [          {            type: "image" as const,            id: "attachment-1",            name: "screenshot.png",            mimeType: "image/png",            sizeBytes: 1,            previewUrl: "data:image/png;base64,iVBORw0KGgo=",          },        ],      },    };    const markup = renderToStaticMarkup(      <MessagesTimeline        {...buildProps()}        anchorMessageId={secondEntry.message.id}        onAnchorReady={onAnchorReady}        onAnchorSizeChanged={onAnchorSizeChanged}        contentInsetEndAdjustment={144}        timelineEntries={[firstEntry, secondEntry]}      />,    );     expect(markup).toContain('data-anchor-index="1"');    expect(markup).toContain('data-anchor-offset="16"');    expect(markup).toContain('data-anchor-on-ready="true"');    expect(markup).not.toContain("data-anchor-max-size=");    expect(markup).toContain('data-content-inset-end="144"');    expect(markup).toContain("[overflow-anchor:none]");    expect(markup).not.toContain("data-maintain-scroll-at-end=");    expect(markup).toContain('data-maintain-visible-content-position="object"');    expect(markup).toContain('data-maintain-visible-content-position-data="true"');    expect(markup).toContain('data-maintain-visible-content-position-size="false"');    expect(onAnchorReady).toHaveBeenCalledOnce();    expect(onAnchorReady).toHaveBeenCalledWith(secondEntry.message.id, 1);    expect(onAnchorSizeChanged).toHaveBeenCalledWith(secondEntry.message.id, 240);  });   it("renders collapse controls for long user messages", async () => {    const { MessagesTimeline } = await import("./MessagesTimeline");    const markup = renderToStaticMarkup(      <MessagesTimeline        {...buildProps()}        timelineEntries={[buildUserTimelineEntry(buildLongUserMessageText())]}      />,    );     expect(markup).toContain("Show full message");    expect(markup).toContain('data-user-message-collapsed="true"');    expect(markup).toContain('data-user-message-fade="true"');    expect(markup).toContain('data-user-message-footer="true"');  });   it("does not render collapse controls for short user messages", async () => {    const { MessagesTimeline } = await import("./MessagesTimeline");    const markup = renderToStaticMarkup(      <MessagesTimeline        {...buildProps()}        timelineEntries={[buildUserTimelineEntry("Short prompt.")]}      />,    );     expect(markup).not.toContain("Show full message");    expect(markup).toContain('data-user-message-collapsible="false"');  });   it("renders inline terminal labels with the composer chip UI", async () => {    const { MessagesTimeline } = await import("./MessagesTimeline");    const markup = renderToStaticMarkup(      <MessagesTimeline        {...buildProps()}        timelineEntries={[          buildUserTimelineEntry(            [              buildLongUserMessageText("yoo what's @terminal-1:1-5 mean"),              "",              "<terminal_context>",              "- Terminal 1 lines 1-5:",              "  1 | julius@mac effect-http-ws-cli % bun i",              "  2 | bun install v1.3.9 (cf6cdbbb)",              "</terminal_context>",            ].join("\n"),          ),        ]}      />,    );     expect(markup).toContain("Terminal 1 lines 1-5");    expect(markup).toContain("lucide-terminal");    expect(markup).toContain("yoo what&#x27;s</p>");    expect(markup).toContain('<span aria-hidden="true"> </span>');    expect(markup).toContain("Show full message");  }, 20_000);   it("renders chips for standalone element-pick context messages", async () => {    const { MessagesTimeline } = await import("./MessagesTimeline");    const markup = renderToStaticMarkup(      <MessagesTimeline        {...buildProps()}        timelineEntries={[          buildUserTimelineEntry(            [              "<element_context>",              "- <SubmitButton> (Button.tsx:12):",              "  url: https://example.com/dashboard",              "  selector: button.submit",              "  source: /repo/src/Button.tsx:12:5",              "  html:",              '  <button class="submit">Save</button>',              "</element_context>",            ].join("\n"),          ),        ]}      />,    );     expect(markup).toContain("SubmitButton");    expect(markup).not.toContain("&lt;element_context");    expect(markup).not.toContain("<element_context");  });   it("keeps the copy button for collapsed long user messages", async () => {    const { MessagesTimeline } = await import("./MessagesTimeline");    const markup = renderToStaticMarkup(      <MessagesTimeline        {...buildProps()}        timelineEntries={[buildUserTimelineEntry(buildLongUserMessageText())]}      />,    );     expect(markup).toContain('aria-label="Copy link"');    expect(markup).toContain('data-user-message-collapsed="true"');    expect(markup).toContain('data-user-message-footer="true"');  });   it("renders context compaction entries in the normal work log", async () => {    const { MessagesTimeline } = await import("./MessagesTimeline");    const markup = renderToStaticMarkup(      <MessagesTimeline        {...buildProps()}        timelineEntries={[          {            id: "entry-1",            kind: "work",            createdAt: "2026-03-17T19:12:28.000Z",            entry: {              id: "work-1",              createdAt: "2026-03-17T19:12:28.000Z",              label: "Context compacted",              tone: "info",            },          },        ]}      />,    );     expect(markup).toContain("Context compacted");    expect(markup).toContain("Work Log");  });   it("formats changed file paths from the workspace root", async () => {    const { MessagesTimeline } = await import("./MessagesTimeline");    const markup = renderToStaticMarkup(      <MessagesTimeline        {...buildProps()}        timelineEntries={[          {            id: "entry-1",            kind: "work",            createdAt: "2026-03-17T19:12:28.000Z",            entry: {              id: "work-1",              createdAt: "2026-03-17T19:12:28.000Z",              label: "Updated files",              tone: "tool",              changedFiles: ["C:/Users/mike/dev-stuff/t3code/apps/web/src/session-logic.ts"],            },          },        ]}        workspaceRoot="C:/Users/mike/dev-stuff/t3code"      />,    );     expect(markup).toContain("t3code/apps/web/src/session-logic.ts");    expect(markup).not.toContain("C:/Users/mike/dev-stuff/t3code/apps/web/src/session-logic.ts");  });   it("renders review comment contexts as structured cards instead of raw tags", async () => {    const { MessagesTimeline } = await import("./MessagesTimeline");    const markup = renderToStaticMarkup(      <MessagesTimeline        {...buildProps()}        timelineEntries={[          {            id: "entry-1",            kind: "message",            createdAt: "2026-03-17T19:12:28.000Z",            message: {              id: MessageId.make("message-2"),              role: "user",              text: [                '<review_comment sectionId="turn:2" sectionTitle="Turn 2" filePath="apps/web/src/lib/contextWindow.test.ts" startIndex="3" endIndex="14" rangeLabel="+47 to +58">',                "Wadduo",                "```diff",                "@@ -0,0 +47,2 @@",                '+  it("keeps valid zero-usage snapshots", () => {',                "+    expect(snapshot).not.toBeNull();",                "```",                "</review_comment>",              ].join("\n"),              turnId: null,              createdAt: "2026-03-17T19:12:28.000Z",              updatedAt: "2026-03-17T19:12:28.000Z",              streaming: false,            },          },        ]}      />,    );     expect(markup).toContain("contextWindow.test.ts");    expect(markup).toContain("Wadduo");    expect(markup).toContain('data-testid="file-diff"');    expect(markup).not.toContain(">Review comment<");    expect(markup).not.toContain("&lt;review_comment");    expect(markup).not.toContain("&lt;/review_comment&gt;");  });   it("renders file review comments as source code instead of diffs", async () => {    const { MessagesTimeline } = await import("./MessagesTimeline");    const markup = renderToStaticMarkup(      <MessagesTimeline        {...buildProps()}        timelineEntries={[          {            id: "entry-1",            kind: "message",            createdAt: "2026-03-17T19:12:28.000Z",            message: {              id: MessageId.make("message-source-comment"),              role: "user",              text: [                '<review_comment sectionId="file:docs/plan.md" sectionTitle="File comment" filePath="docs/plan.md" startIndex="0" endIndex="1" rangeLabel="L1 to L2">',                "Clarify this.",                "```md",                "# Plan",                "- Step one",                "```",                "</review_comment>",              ].join("\n"),              turnId: null,              createdAt: "2026-03-17T19:12:28.000Z",              updatedAt: "2026-03-17T19:12:28.000Z",              streaming: false,            },          },        ]}      />,    );     expect(markup).toContain("plan.md");    expect(markup).toContain("Clarify this.");    expect(markup).toContain("# Plan");    expect(markup).not.toContain('data-testid="file-diff"');  });   it("renders a failure marker for failed tool lifecycle entries", async () => {    const { MessagesTimeline } = await import("./MessagesTimeline");    const markup = renderToStaticMarkup(      <MessagesTimeline        {...buildProps()}        timelineEntries={[          {            id: "entry-1",            kind: "work",            createdAt: "2026-03-17T19:12:28.000Z",            entry: {              id: "work-1",              createdAt: "2026-03-17T19:12:28.000Z",              label: "Glob",              tone: "tool",              toolLifecycleStatus: "failed",              detail: "No files found",            },          },        ]}      />,    );     expect(markup).toContain("lucide-x");    expect(markup).toContain('aria-label="Tool call failed"');  });});