3387 lines · 110.9 KB · TypeScript
RawDownload
import { derivePendingRequests } from "@t3tools/client-runtime/pending-requests";import { describe, expect, it } from "vite-plus/test"; import {  ApprovalRequestId,  EventId,  MessageId,  ProjectId,  ProviderInstanceId,  ThreadId,  TurnId,  type OrchestrationThread,  type OrchestrationThreadActivity,} from "@t3tools/contracts"; import {  agentSpawnSummary,  buildPendingUserInputAnswers,  buildThreadFeed,  deriveThreadFeedPresentation,  isPendingUserInputOptionSelected,  setPendingUserInputCustomAnswer,  togglePendingUserInputOptionSelection,  workEntryRowLabel,  type ThreadFeedActivity,  type ThreadFeedEntry,  type WorkLogEntry,} from "./threadActivity"; const singleSelectQuestion = {  id: "runtime",  header: "Runtime",  question: "Which runtime should be used?",  options: [    { label: "Go", description: "One binary" },    { label: "Node.js", description: "Reuse TypeScript" },  ],  multiSelect: false,} as const; const multiSelectQuestion = {  id: "scope",  header: "Scope",  question: "Which data should be collected?",  options: [    { label: "Orders", description: "Receipts" },    { label: "Listings", description: "Inventory" },  ],  multiSelect: true,} as const; const nativeQuestion = {  id: "choice",  header: "File",  question: "Which file should be used?",  options: [    { label: "Use this", description: "First file", value: " choice " },    { label: "Use this", description: "Second file", value: "choice" },  ],  multiSelect: false,  allowCustomAnswer: false,} as const; describe("pending user input answers", () => {  it("accepts free-text answers to async questions without options", () => {    const question = {      id: "0",      header: "Question",      question: "What should it be named?",      options: [],      allowCustomAnswer: true,      multiSelect: false,    };    const requested = makeActivity({      id: EventId.make("async-question"),      kind: "user-input.requested",      summary: "User input requested",      createdAt: "2026-09-03T00:00:00.000Z",      payload: { requestId: "async-1", responseMode: "message", questions: [question] },    });    const questions = derivePendingRequests([requested]).userInputs[0]?.questions;    expect(questions).toEqual([question]);    expect(buildPendingUserInputAnswers(questions!, { "0": { customAnswer: "Example" } })).toEqual({      "0": "Example",    });  });   it("preserves native choice values and custom-answer rules from activities", () => {    const requested = makeActivity({      id: EventId.make("native-question"),      kind: "user-input.requested",      summary: "User input requested",      createdAt: "2026-09-02T00:00:00.000Z",      payload: {        requestId: "interaction_1",        questions: [nativeQuestion, singleSelectQuestion],      },    });     expect(derivePendingRequests([requested]).userInputs).toEqual([      {        requestId: "interaction_1",        createdAt: requested.createdAt,        dismissible: false,        questions: [nativeQuestion, singleSelectQuestion],      },    ]);  });   it("replaces single-select options and toggles multi-select options", () => {    expect(      togglePendingUserInputOptionSelection(        singleSelectQuestion,        { selectedOptionValues: ["Go"] },        "Node.js",      ),    ).toEqual({ customAnswer: "", selectedOptionValues: ["Node.js"] });     const orders = togglePendingUserInputOptionSelection(multiSelectQuestion, undefined, "Orders");    const ordersAndListings = togglePendingUserInputOptionSelection(      multiSelectQuestion,      orders,      "Listings",    );    expect(ordersAndListings).toEqual({      customAnswer: "",      selectedOptionValues: ["Orders", "Listings"],    });    expect(      togglePendingUserInputOptionSelection(multiSelectQuestion, ordersAndListings, "Orders"),    ).toEqual({ customAnswer: "", selectedOptionValues: ["Listings"] });     const paddedOrders = togglePendingUserInputOptionSelection(      multiSelectQuestion,      undefined,      "  Orders  ",    );    expect(paddedOrders).toEqual({ customAnswer: "", selectedOptionValues: ["Orders"] });    expect(      togglePendingUserInputOptionSelection(multiSelectQuestion, paddedOrders, "  Orders  "),    ).toEqual({ customAnswer: "" });  });   it("builds array answers for multi-select questions", () => {    expect(      buildPendingUserInputAnswers([singleSelectQuestion, multiSelectQuestion], {        runtime: { selectedOptionValues: ["Go"] },        scope: { selectedOptionValues: ["Orders", "Listings"] },      }),    ).toEqual({      runtime: "Go",      scope: ["Orders", "Listings"],    });  });   it("clears selected options while a custom answer is active", () => {    expect(      setPendingUserInputCustomAnswer(        multiSelectQuestion,        { selectedOptionValues: ["Orders", "Listings"] },        "Orders first",      ),    ).toEqual({ customAnswer: "Orders first" });  });   it("matches selected options against normalized legacy labels", () => {    expect(      isPendingUserInputOptionSelected(        multiSelectQuestion,        { selectedOptionValues: ["Orders"] },        "  Orders  ",      ),    ).toBe(true);    expect(      isPendingUserInputOptionSelected(        multiSelectQuestion,        { selectedOptionValues: ["Orders"], customAnswer: "Orders first" },        "  Orders  ",      ),    ).toBe(false);  });   it("keeps custom answers enabled for legacy questions", () => {    expect(      buildPendingUserInputAnswers([singleSelectQuestion], {        runtime: { selectedOptionValues: ["Go"], customAnswer: "  Use Bun  " },      }),    ).toEqual({ runtime: "Use Bun" });  });   it("keeps duplicate labels and whitespace-sensitive native values separate", () => {    const first = togglePendingUserInputOptionSelection(nativeQuestion, undefined, " choice ");    expect(isPendingUserInputOptionSelected(nativeQuestion, first, " choice ")).toBe(true);    expect(isPendingUserInputOptionSelected(nativeQuestion, first, "choice")).toBe(false);    expect(buildPendingUserInputAnswers([nativeQuestion], { choice: first })).toEqual({      choice: " choice ",    });     const second = togglePendingUserInputOptionSelection(nativeQuestion, first, "choice");    expect(isPendingUserInputOptionSelected(nativeQuestion, second, " choice ")).toBe(false);    expect(isPendingUserInputOptionSelected(nativeQuestion, second, "choice")).toBe(true);    expect(buildPendingUserInputAnswers([nativeQuestion], { choice: second })).toEqual({      choice: "choice",    });  });   it("keeps exact native values in multi-select answers", () => {    const question = { ...nativeQuestion, multiSelect: true };    const first = togglePendingUserInputOptionSelection(question, undefined, " choice ");    const both = togglePendingUserInputOptionSelection(question, first, "choice");    expect(buildPendingUserInputAnswers([question], { choice: both })).toEqual({      choice: [" choice ", "choice"],    });     const second = togglePendingUserInputOptionSelection(question, both, " choice ");    expect(buildPendingUserInputAnswers([question], { choice: second })).toEqual({      choice: ["choice"],    });  });   it("ignores custom answers when a question only accepts choices", () => {    const draft = { selectedOptionValues: [" choice "], customAnswer: "Other" };    expect(setPendingUserInputCustomAnswer(nativeQuestion, draft, "Custom text")).toBe(draft);    expect(isPendingUserInputOptionSelected(nativeQuestion, draft, " choice ")).toBe(true);    expect(buildPendingUserInputAnswers([nativeQuestion], { choice: draft })).toEqual({      choice: " choice ",    });  });   it.each([    { customAnswer: "Other" },    { selectedOptionValues: ["Use this"] },    { selectedOptionValues: ["not offered"] },    { selectedOptionValues: ["  choice  "] },  ])("requires an offered value for a choice-only question: %j", (draft) => {    expect(buildPendingUserInputAnswers([nativeQuestion], { choice: draft })).toBeNull();  });}); function makeActivity(  input: Partial<OrchestrationThreadActivity> &    Pick<OrchestrationThreadActivity, "id" | "kind" | "summary" | "createdAt">,): OrchestrationThreadActivity {  return {    tone: "info",    payload: {},    turnId: null,    ...input,  };} function makeThread(  input: Partial<OrchestrationThread> & Pick<OrchestrationThread, "id" | "projectId" | "title">,): OrchestrationThread {  return {    modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },    runtimeMode: "full-access",    interactionMode: "default",    branch: null,    worktreePath: null,    pullRequests: [],    latestTurn: null,    createdAt: "2026-04-01T00:00:00.000Z",    updatedAt: "2026-04-01T00:00:00.000Z",    archivedAt: null,    deletedAt: null,    messages: [],    proposedPlans: [],    activities: [],    checkpoints: [],    session: null,    ...input,    settledOverride: input.settledOverride ?? null,    settledAt: input.settledAt ?? null,  };} describe("buildThreadFeed", () => {  it("reuses unchanged feed and presentation rows during an assistant text update", () => {    const completedTurnId = TurnId.make("completed-turn");    const activeTurnId = TurnId.make("active-turn");    const thread = makeThread({      id: ThreadId.make("feed-reuse"),      projectId: ProjectId.make("project-1"),      title: "Feed reuse",      messages: [        {          id: MessageId.make("completed-message"),          role: "assistant",          text: "Completed response",          turnId: completedTurnId,          streaming: false,          createdAt: "2026-04-01T00:00:01.000Z",          updatedAt: "2026-04-01T00:00:01.000Z",        },        {          id: MessageId.make("streaming-message"),          role: "assistant",          text: "Current response",          turnId: activeTurnId,          streaming: true,          createdAt: "2026-04-01T00:00:04.000Z",          updatedAt: "2026-04-01T00:00:04.000Z",        },      ],      activities: [        makeActivity({          id: EventId.make("completed-tool"),          kind: "tool.completed",          summary: "Read files",          createdAt: "2026-04-01T00:00:02.000Z",          turnId: completedTurnId,          payload: { itemType: "file_read", status: "completed" },        }),        makeActivity({          id: EventId.make("active-tool"),          kind: "tool.updated",          summary: "Run checks",          createdAt: "2026-04-01T00:00:03.000Z",          turnId: activeTurnId,          payload: { itemType: "command_execution", command: "vp test", status: "inProgress" },        }),      ],    });    const latestTurn = {      turnId: activeTurnId,      state: "running" as const,      startedAt: "2026-04-01T00:00:03.000Z",      completedAt: null,    };    const expandedTurns = new Set([completedTurnId]);    const expandedGroups = new Set(["work-group:completed-tool", "work-group:active-tool"]);    const previousFeed = buildThreadFeed(thread);    const previousRows = deriveThreadFeedPresentation(      previousFeed,      latestTurn,      expandedTurns,      expandedGroups,      latestTurn.startedAt,    );    const updatedMessage = {      ...thread.messages[1]!,      text: "Current response with more text",      updatedAt: "2026-04-01T00:00:05.000Z",    };    const nextFeed = buildThreadFeed({      ...thread,      messages: [thread.messages[0]!, updatedMessage],    });    const nextRows = deriveThreadFeedPresentation(      nextFeed,      latestTurn,      expandedTurns,      expandedGroups,      latestTurn.startedAt,    );     expect(nextFeed).toHaveLength(previousFeed.length);    expect(nextRows).toHaveLength(previousRows.length);    for (const [before, after] of [      [previousFeed, nextFeed],      [previousRows, nextRows],    ] as const) {      for (const [index, row] of after.entries()) {        if (row.id === updatedMessage.id) {          expect(row).not.toBe(before[index]);          expect(row).toMatchObject({ message: updatedMessage });        } else {          expect(row).toBe(before[index]);        }      }    }    expect(nextRows.some((row) => row.type === "turn-fold")).toBe(true);    expect(nextRows.some((row) => row.type === "activity-group")).toBe(true);  });   it("regroups cached activities for message changes and pagination", () => {    const messages = [2, 4].map((second) => ({      id: MessageId.make(`message-${second}`),      role: "assistant" as const,      text: second === 2 ? "" : "Response",      streaming: false,      turnId: null,      createdAt: `2026-04-01T00:00:0${second}.000Z`,      updatedAt: `2026-04-01T00:00:0${second}.000Z`,    }));    const thread = makeThread({      id: ThreadId.make("feed-regroup"),      projectId: ProjectId.make("project-1"),      title: "Feed grouping",      messages,      activities: [1, 3, 5].map((second) =>        makeActivity({          id: EventId.make(`work-${second}`),          kind: "runtime.warning",          summary: `Notice ${second}`,          createdAt: `2026-04-01T00:00:0${second}.000Z`,        }),      ),    });    const initial = buildThreadFeed(thread);    expect(initial.map((row) => row.id)).toEqual(["work-1", "message-4", "work-5"]);    const split = buildThreadFeed({      ...thread,      messages: [{ ...messages[0]!, text: "Now visible" }, messages[1]!],    });    expect(split.map((row) => row.id)).toEqual([      "work-1",      "message-2",      "work-3",      "message-4",      "work-5",    ]);    expect(split[0]).not.toBe(initial[0]);    expect(split.at(-1)).toBe(initial.at(-1));    expect(initial[0]).toMatchObject({ activities: [{ id: "work-1" }, { id: "work-3" }] });     const reordered = buildThreadFeed({      ...thread,      messages: [messages[0]!, { ...messages[1]!, createdAt: "2026-04-01T00:00:06.000Z" }],    });    expect(reordered.map((row) => row.id)).toEqual(["work-1", "message-4"]);    expect(reordered[0]).toMatchObject({      activities: [{ id: "work-1" }, { id: "work-3" }, { id: "work-5" }],    });    const olderMessage = {      ...messages[1]!,      id: MessageId.make("older-message"),      createdAt: "2026-04-01T00:00:00.000Z",    };    const page = buildThreadFeed(thread, {      loadedMessages: [messages[1]!],      localMessages: [olderMessage],    });    expect(page.map((row) => row.id)).toEqual(["older-message", "message-4", "work-5"]);    const prepended = buildThreadFeed(thread, { loadedMessages: [olderMessage, ...messages] });    expect(prepended.map((row) => row.id)).toEqual([      "older-message",      "work-1",      "message-4",      "work-5",    ]);    expect(prepended.at(-1)).toBe(page.at(-1));  });   it("keeps context compaction as a standalone timeline row", () => {    const thread = makeThread({      id: ThreadId.make("thread-context-compaction"),      projectId: ProjectId.make("project-1"),      title: "Context compaction",      activities: [        makeActivity({          id: EventId.make("context-compaction"),          kind: "context-compaction",          tone: "info",          summary: "Compacted context 899K → 19K tokens",          createdAt: "2026-09-01T00:00:00.000Z",          turnId: TurnId.make("turn-context-compaction"),        }),      ],    });     const presented = deriveThreadFeedPresentation(buildThreadFeed(thread), null, new Set());    expect(presented).toMatchObject([      {        type: "activity-group",        id: "context-compaction",        activities: [{ summary: "Compacted context 899K → 19K tokens" }],      },    ]);  });   it("keeps long Claude commands expandable without repeating them in full detail", () => {    const command = `printf 'first line\nsecond line'\n&& printf done`;    const thread = makeThread({      id: ThreadId.make("thread-long-command"),      projectId: ProjectId.make("project-1"),      title: "Long command",      activities: [        makeActivity({          id: EventId.make("long-command"),          kind: "tool.completed",          tone: "tool",          summary: "Command run",          createdAt: "2026-09-01T00:00:00.000Z",          payload: {            itemType: "command_execution",            title: "Command run",            detail: `Bash: ${command}`,            data: { toolName: "Bash", command },          },        }),      ],    });     const [group] = buildThreadFeed(thread);    expect(group?.type).toBe("activity-group");    if (group?.type !== "activity-group") return;    const [row] = group.activities;    expect(row).toMatchObject({ detail: command, canExpand: true });

Lines 1–500 of 3387.