3071 lines · 98.6 KB · TypeScript
RawDownload
import { describe, expect, it } from "vite-plus/test";import {  CheckpointRef,  EnvironmentId,  EventId,  MessageId,  ProjectId,  ProviderInstanceId,  ThreadId,  TurnId,  type OrchestrationThread,} from "@t3tools/contracts";import {  applyThreadDetailEvent,  createEnvironmentThreadDetailAtoms,  EMPTY_ENVIRONMENT_THREAD_STATE,} from "@t3tools/client-runtime/state/threads";import * as Option from "effect/Option";import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity";import {  computeStableMessagesTimelineRows,  computeMessageDurationStart,  deriveMessagesTimelineRows,  deriveMessagesTimelineRowsWithState,  liveWorkEntryLabel,  normalizeCompactToolLabel,  resolveAssistantMessageCopyState,  resolveWorkGroupScrollIndex,  shouldFollowWorkGroupAppend,  shouldPreserveAssistantLineBreaks,  type MessagesTimelineRow,  type MessagesTimelineRowsProjection,  workEntryDisplayLabel,} from "./MessagesTimeline.logic";import {  createMessageAttachmentPreviewProjector,  deriveTimelineEntries,  deriveTimelineEntriesWithState,  type WorkLogEntry,  type TimelineEntriesProjection,} from "../../session-logic";import { isImageAttachment, type ChatMessage, type TurnDiffSummary } from "../../types"; describe("streaming row projection", () => {  function fixture(text = "") {    const turnId = TurnId.make("live-turn");    const historyTurnId = TurnId.make("history-turn");    const time = (second: number) => new Date(Date.UTC(2026, 8, 4, 0, 0, second)).toISOString();    const messages: ChatMessage[] = [      {        id: MessageId.make("history-user"),        role: "user",        text: "Inspect",        turnId: null,        createdAt: time(0),        updatedAt: time(0),        streaming: false,      },      {        id: MessageId.make("history-assistant"),        role: "assistant",        text: "Done",        turnId: historyTurnId,        createdAt: time(3),        updatedAt: time(4),        streaming: false,      },      {        id: MessageId.make("live-user"),        role: "user",        text: "Continue",        turnId: null,        createdAt: time(5),        updatedAt: time(5),        streaming: false,      },      {        id: MessageId.make("live-assistant"),        role: "assistant",        text,        turnId,        createdAt: time(7),        updatedAt: time(7),        streaming: true,      },    ];    const work: WorkLogEntry[] = [      {        id: "history-work",        turnId: historyTurnId,        createdAt: time(1),        label: "Ran command",        tone: "tool",        command: "vp test",        toolCallId: "history-tool",        toolLifecycleStatus: "failed",        sourceActivityKind: "tool.completed",      },      {        id: "live-work",        turnId,        createdAt: time(6),        label: "Read file",        tone: "tool",        changedFiles: ["/repo/src/index.ts"],        toolCallId: "live-tool",        toolLifecycleStatus: "completed",        sourceActivityKind: "tool.completed",      },    ];    const timeline = deriveTimelineEntriesWithState(messages, [], work);    const input = {      timelineEntries: timeline.entries,      latestTurn: { turnId, state: "running", startedAt: time(5), completedAt: null },      runningTurnId: turnId,      isWorking: true,      activeTurnStartedAt: time(5),      turnDiffSummaries: [],      supportsConversationRollback: false,    } satisfies Parameters<typeof deriveMessagesTimelineRows>[0];    return { messages, work, timeline, input, time, turnId, historyTurnId };  }   it.each([    ["", "Now visible"],    [" \n", "Now visible"],    ["Visible", ""],    ["", " \t\n"],    ["Visible", "★ Insight\nKeep these line breaks"],  ])("keeps row parity and history identity when text changes from %j to %j", (before, after) => {    const initial = fixture(before);    const previous = deriveMessagesTimelineRowsWithState(initial.input);    Object.freeze(previous.rows);    for (const row of previous.rows) Object.freeze(row);    const last = initial.messages.at(-1)!;    const messages = [      ...initial.messages.slice(0, -1),      { ...last, text: after, updatedAt: initial.time(8) },    ];    const timeline = deriveTimelineEntriesWithState(messages, [], initial.work, initial.timeline);    const input = { ...initial.input, timelineEntries: timeline.entries };    const next = deriveMessagesTimelineRowsWithState(input, previous);     expect(next.rows).toEqual(      deriveMessagesTimelineRows({        ...input,        timelineEntries: deriveTimelineEntries(messages, [], initial.work),      }),    );    for (const [index, row] of previous.rows.entries()) {      if ((row.kind === "message" || row.kind === "assistant-meta") && row.message === last) {        expect(next.rows[index]).toMatchObject({ message: { text: after } });        expect(row.message.text).toBe(before);      } else {        expect(next.rows[index]).toBe(row);      }    }  });   it("keeps earlier emitted rows unchanged across two projection branches", () => {    const initial = fixture();    const previous = deriveMessagesTimelineRowsWithState(initial.input);    const last = initial.messages.at(-1)!;    const branch = (text: string) => {      const messages = [...initial.messages.slice(0, -1), { ...last, text }];      const timeline = deriveTimelineEntriesWithState(messages, [], initial.work, initial.timeline);      const input = { ...initial.input, timelineEntries: timeline.entries };      const projection = deriveMessagesTimelineRowsWithState(input, previous);      expect(projection.rows).toEqual(deriveMessagesTimelineRows(input));      return projection;    };    const first = branch("First branch");    const savedFirst = structuredClone(first.rows);    branch("Second branch");    expect(first.rows).toEqual(savedFirst);    expect(previous.rows).toEqual(deriveMessagesTimelineRows(initial.input));  });   it.each(["file", "image", "handoff", "streaming-image"] as const)(    "preserves stream reuse through the real %s preview materializer",    (kind) => {      const initial = fixture("Partial");      const image = {        type: "image" as const,        id: "image",        name: "image.png",        mimeType: "image/png",        sizeBytes: 42,      };      const file = {        type: "file" as const,        id: "file",        name: "file.txt",        mimeType: "text/plain",        sizeBytes: 8,      };      const source = initial.messages.map((message, index) =>        index === 0 || (kind === "streaming-image" && index === initial.messages.length - 1)          ? { ...message, attachments: kind === "file" ? [file] : [image, file] }          : message,      );      const server = createMessageAttachmentPreviewProjector();      const handoff = createMessageAttachmentPreviewProjector();      const materialize = (messages: ReadonlyArray<ChatMessage>, url: string) => {        const urls = new Map(kind === "handoff" ? [] : [[image.id, url]]);        return messages.map((message) => {          const displayed = server(message, (attachment) => urls.get(attachment.id));          return kind === "handoff" && message.role === "user"            ? handoff(displayed, (attachment) =>                isImageAttachment(attachment) ? "blob:handoff" : undefined,              )            : displayed;        });      };      const firstMessages = materialize(source, "https://first.test/image");      const firstTimeline = deriveTimelineEntriesWithState(firstMessages, [], initial.work);      const input = { ...initial.input, timelineEntries: firstTimeline.entries };      const previous = deriveMessagesTimelineRowsWithState(input);      const saved = structuredClone(previous.rows);      const nextSource = [...source.slice(0, -1), { ...source.at(-1)!, text: "Next token" }];      const nextMessages = materialize(nextSource, "https://first.test/image");      const nextTimeline = deriveTimelineEntriesWithState(        nextMessages,        [],        initial.work,        firstTimeline,      );      const nextInput = { ...input, timelineEntries: nextTimeline.entries };      const next = deriveMessagesTimelineRowsWithState(nextInput, previous);       expect(nextMessages[0]).toBe(firstMessages[0]);      expect(next.rows).toEqual(deriveMessagesTimelineRows(nextInput));      for (const [index, row] of previous.rows.entries()) {        if (          (row.kind === "message" || row.kind === "assistant-meta") &&          row.message.id === source.at(-1)?.id        )          continue;        expect(next.rows[index]).toBe(row);      }      expect(previous.rows).toEqual(saved);       if (kind === "image" || kind === "streaming-image") {        const renewed = materialize(nextSource, "https://renewed.test/image");        const renewedTimeline = deriveTimelineEntriesWithState(          renewed,          [],          initial.work,          nextTimeline,        );        const renewedInput = { ...input, timelineEntries: renewedTimeline.entries };        const renewedRows = deriveMessagesTimelineRowsWithState(renewedInput, next);        expect(renewedRows.rows).toEqual(deriveMessagesTimelineRows(renewedInput));        expect(renewed[0]?.attachments?.[0]).toMatchObject({          previewUrl: "https://renewed.test/image",        });        expect(firstMessages[0]?.attachments?.[0]).toMatchObject({          previewUrl: "https://first.test/image",        });      }    },  );   it("owns checkpoint lookups across streaming and equal source snapshots", () => {    const initial = fixture("Partial");    let checkpointLookupReads = 0;    const summary: TurnDiffSummary = {      turnId: initial.historyTurnId,      get checkpointTurnCount() {        checkpointLookupReads += 1;        return 1;      },      checkpointRef: CheckpointRef.make("refs/t3/checkpoints/history-turn"),      status: "ready",      files: [],      get assistantMessageId() {        checkpointLookupReads += 1;        return MessageId.make("history-assistant");      },      get completedAt() {        checkpointLookupReads += 1;        return initial.time(4);      },    };    const input = {      ...initial.input,      turnDiffSummaries: [summary],      supportsConversationRollback: true,      expandedTurnIds: new Set([initial.historyTurnId]),      expandedWorkGroupIds: new Set<string>(),    };    const previous = deriveMessagesTimelineRowsWithState(input);    expect(checkpointLookupReads).toBeGreaterThan(0);    expect(previous.rows.some((row) => row.kind === "message" && row.revertTurnCount === 0)).toBe(      true,    );    const last = initial.messages.at(-1)!;    const messages = [...initial.messages.slice(0, -1), { ...last, text: "Partial token" }];    const timeline = deriveTimelineEntriesWithState(messages, [], initial.work, initial.timeline);    const nextInput = {      ...input,      timelineEntries: timeline.entries,      turnDiffSummaries: [...input.turnDiffSummaries],      latestTurn: { ...input.latestTurn },      expandedTurnIds: new Set(input.expandedTurnIds),      expandedWorkGroupIds: new Set(input.expandedWorkGroupIds),    };    checkpointLookupReads = 0;    const next = deriveMessagesTimelineRowsWithState(nextInput, previous);    expect(checkpointLookupReads).toBe(0);    expect(next.rows).toEqual(deriveMessagesTimelineRows(nextInput));    for (const [index, row] of previous.rows.entries()) {      if ((row.kind === "message" || row.kind === "assistant-meta") && row.message === last) {        expect(next.rows[index]).toMatchObject({ message: { text: "Partial token" } });      } else {        expect(next.rows[index]).toBe(row);      }    }     const changed = deriveMessagesTimelineRowsWithState(      { ...nextInput, turnDiffSummaries: [{ ...summary, checkpointTurnCount: 3 }] },      next,    );    expect(      changed.rows.find((row) => row.kind === "message" && row.message.id === messages[0]?.id),    ).toMatchObject({ revertTurnCount: 2 });    const unsupported = deriveMessagesTimelineRowsWithState(      { ...changed.input, supportsConversationRollback: false },      changed,    );    expect(      unsupported.rows.find((row) => row.kind === "message" && row.message.id === messages[0]?.id),    ).toMatchObject({ revertTurnCount: undefined });    expect(      previous.rows.find((row) => row.kind === "message" && row.message.id === messages[0]?.id),    ).toMatchObject({ revertTurnCount: 0 });  });   it("reuses long-thread rows through detail events, selectors, and attachment previews", () => {    const initial = fixture("Partial");    let checkpointLookupReads = 0;    const history = Array.from({ length: 250 }, (_, index) => {      const turnId = TurnId.make(`older-turn-${index}`);      const user = {        ...initial.messages[0]!,        id: MessageId.make(`older-user-${index}`),        createdAt: new Date(Date.UTC(2026, 8, 3, 0, 0, index * 5)).toISOString(),        attachments: [          {            type: "image" as const,            id: "image",            name: "image.png",            mimeType: "image/png",            sizeBytes: 42,          },        ],      };      const assistant = {        ...initial.messages[1]!,        id: MessageId.make(`older-assistant-${index}`),        turnId,        createdAt: new Date(Date.UTC(2026, 8, 3, 0, 0, index * 5 + 3)).toISOString(),      };      const checkpoint: TurnDiffSummary = {        turnId,        get checkpointTurnCount() {          checkpointLookupReads += 1;          return index + 1;        },        checkpointRef: CheckpointRef.make(`refs/t3/checkpoints/older-${index}`),        status: "ready",        files: [],        get assistantMessageId() {          checkpointLookupReads += 1;          return assistant.id;        },        get completedAt() {          checkpointLookupReads += 1;          return assistant.createdAt;        },      };      return { user, assistant, checkpoint };    });    const liveMessage = initial.messages.at(-1)!;    let thread: OrchestrationThread = {      id: ThreadId.make("streaming-thread"),      projectId: ProjectId.make("project"),      title: "Long thread",      modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },      runtimeMode: "full-access",      interactionMode: "default",      branch: null,      pullRequests: [],      worktreePath: null,      latestTurn: {        ...initial.input.latestTurn,        requestedAt: initial.time(5),        assistantMessageId: liveMessage.id,      },      createdAt: initial.time(0),      updatedAt: initial.time(7),      archivedAt: null,      settledOverride: null,      settledAt: null,      deletedAt: null,      messages: [        ...history.flatMap(({ user, assistant }) => [user, assistant]),        ...initial.messages,      ],      proposedPlans: [],      activities: [],      checkpoints: history.map(({ checkpoint }) => checkpoint),      session: null,    };    const state = Atom.make(      AsyncResult.success({ ...EMPTY_ENVIRONMENT_THREAD_STATE, data: Option.some(thread) }),    );    const details = createEnvironmentThreadDetailAtoms(() => state);    const ref = { environmentId: EnvironmentId.make("local"), threadId: thread.id };    const registry = AtomRegistry.make();    const unmount = registry.mount(details.detailAtom(ref));    const preview = createMessageAttachmentPreviewProjector();    let imageUrl = "https://first.test/image";    let timeline: TimelineEntriesProjection | null = null;    let projection: MessagesTimelineRowsProjection | null = null;    const project = () => {      const selected = registry.get(details.detailAtom(ref));      if (selected === null) throw new Error("Missing thread detail");      const messages = selected.messages.map((message) => preview(message, () => imageUrl));      timeline = deriveTimelineEntriesWithState(        messages,        selected.proposedPlans,        initial.work,        timeline,      );      projection = deriveMessagesTimelineRowsWithState(        {          timelineEntries: timeline.entries,          latestTurn: selected.latestTurn,          runningTurnId:            selected.latestTurn?.state === "running" ? selected.latestTurn.turnId : null,          isWorking: selected.latestTurn?.state === "running",          activeTurnStartedAt: selected.latestTurn?.startedAt ?? null,          turnDiffSummaries: selected.checkpoints,          supportsConversationRollback: true,        },        projection,      );      return projection;    };    const send = (text: string, sequence: number, streaming = true) => {      const result = applyThreadDetailEvent(thread, {        eventId: EventId.make(`delta-${sequence}`),        sequence,        commandId: null,        causationEventId: null,        correlationId: null,        metadata: {},        occurredAt: initial.time(8 + sequence),        aggregateKind: "thread",        aggregateId: thread.id,        type: "thread.message-sent",        payload: {          threadId: thread.id,          messageId: liveMessage.id,          role: "assistant",          text,          turnId: initial.turnId,          streaming,          createdAt: liveMessage.createdAt,          updatedAt: initial.time(8 + sequence),        },      });      if (result.kind !== "updated") throw new Error("Message event did not update the thread");      thread = result.thread;      registry.set(        state,        AsyncResult.success({ ...EMPTY_ENVIRONMENT_THREAD_STATE, data: Option.some(thread) }),      );      return project();    };     try {      const first = project();      const saved = structuredClone(first.rows);      expect(checkpointLookupReads).toBeGreaterThan(0);      checkpointLookupReads = 0;      for (let index = 0; index < 10; index += 1) {        const next = send(` ${index}`, index + 1);        for (const [rowIndex, row] of first.rows.entries()) {          if (            (row.kind === "message" || row.kind === "assistant-meta") &&            row.message.id === liveMessage.id          )            continue;          expect(next.rows[rowIndex]).toBe(row);        }      }      expect(checkpointLookupReads).toBe(0);      const streamed = project();

Lines 1–500 of 3071.