1847 lines · 56.6 KB · TypeScript
RawDownload
import {  ANTIGRAVITY_DEFAULT_MODEL,  CheckpointRef,  EnvironmentId,  MessageId,  ProjectId,  ProviderDriverKind,  ProviderInstanceId,  type ServerProvider,  ThreadId,  TurnId,} from "@t3tools/contracts";import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; import type { Thread, ThreadShell, TurnDiffSummary } from "../types";import type { TimelineEntry } from "../session-logic";import { deriveProviderInstanceEntries, NO_PROVIDER_MODEL_SELECTION } from "../providerInstances";import type { CodexArtifactTemplate } from "@t3tools/client-runtime/codex-artifact-templates";import type { RightPanelSurface } from "../rightPanelStore";import {  MAX_HIDDEN_MOUNTED_PREVIEW_THREADS,  MAX_HIDDEN_MOUNTED_TERMINAL_THREADS,  agentControlledBrowserCloseConfirmation,  branchMismatchKey,  buildExpiredTerminalContextToastCopy,  buildLoadingThreadFromShell,  buildRevertTurnCountByUserMessageId,  buildThreadTurnInterruptInput,  createLocalDispatchSnapshot,  deriveComposerSendState,  deriveLockedProvider,  dismissBranchMismatchForSession,  ENVIRONMENT_RECONNECT_WARNING_GRACE_MS,  getAntigravitySendBlockReason,  getStartedThreadModelChangeBlockReason,  hasEnvironmentReconnectWarningGraceElapsed,  hasServerAcknowledgedLocalDispatch,  isBranchMismatchDismissedForSession,  reconcileMountedTerminalThreadIds,  reconcileRetainedMountedThreadIds,  resolveBackgroundDraftWorkspaceOptions,  resolveComposerInteractionMode,  resolveComposerProviderSelection,  resolveDraftPromotionNavigationTarget,  resolveProactiveTurnDiffAction,  resolveThreadMetadataUpdateForNextTurn,  resolveSendEnvMode,  resolveDraftHeroState,  scheduleEnvironmentReconnectWarning,  startNewThreadForProject,  codexArtifactTemplatePromptToAppend,  shouldDockDraftHeroForSubmission,  shouldReleaseTimelineAnchorForToolActivity,  shouldOpenProactivePullRequest,  shouldOpenProactiveTurnDiff,  shouldRenderPreviewMiniPlayer,  shouldShowBranchMismatchBanner,  shouldShowPlanFollowUpPrompt,  shouldWriteThreadErrorToCurrentServerThread,  toolGroupConsumesUpwardNavigation,} from "./ChatView.logic"; describe("agent browser close confirmation", () => {  const surfaces = [    { id: "browser:one", kind: "preview", resourceId: "tab-1" },    { id: "browser:two", kind: "preview", resourceId: "tab-2" },    { id: "diff", kind: "diff" },  ] satisfies RightPanelSurface[];   it("only warns for browsers under active agent control", () => {    expect(      agentControlledBrowserCloseConfirmation(surfaces, {        "tab-1": { controller: "none" },        "tab-2": { controller: "human" },      }),    ).toBeNull();     expect(      agentControlledBrowserCloseConfirmation([surfaces[0]!], {        "tab-1": { controller: "agent" },      }),    ).toBe(      [        "Close browser while the agent is using it?",        "The agent is actively controlling this browser. Closing it may interrupt the current browser action.",      ].join("\n"),    );  });   it("counts every agent-controlled browser in a bulk close", () => {    expect(      agentControlledBrowserCloseConfirmation(surfaces, {        "tab-1": { controller: "agent" },        "tab-2": { controller: "agent" },      }),    ).toContain("Close 2 browsers");  });}); describe("floating browser preview", () => {  it("only hides the duplicate while the same browser is rendered in the panel", () => {    expect(shouldRenderPreviewMiniPlayer(null, null)).toBe(false);    expect(      shouldRenderPreviewMiniPlayer("tab-1", {        id: "browser:one",        kind: "preview",        resourceId: "tab-1",      }),    ).toBe(false);    expect(      shouldRenderPreviewMiniPlayer("tab-1", {        id: "browser:two",        kind: "preview",        resourceId: "tab-2",      }),    ).toBe(true);    expect(shouldRenderPreviewMiniPlayer("tab-1", { id: "diff", kind: "diff" })).toBe(true);  });}); describe("proactive panels", () => {  it("opens a pull request only after a newly observed link appears", () => {    expect(shouldOpenProactivePullRequest(undefined, "project:repo:42")).toBe(false);    expect(shouldOpenProactivePullRequest(null, "project:repo:42")).toBe(true);    expect(shouldOpenProactivePullRequest("project:repo:42", "project:repo:42")).toBe(false);    expect(shouldOpenProactivePullRequest("project:repo:42", null)).toBe(false);  });   it("opens the diff only when the observed running turn settles", () => {    const turnId = TurnId.make("turn-1");    expect(      shouldOpenProactiveTurnDiff({        previousRunningTurnId: undefined,        runningTurnId: null,        settledTurnId: turnId,        turnCompleted: true,      }),    ).toBe(false);    expect(      shouldOpenProactiveTurnDiff({        previousRunningTurnId: turnId,        runningTurnId: null,        settledTurnId: turnId,        turnCompleted: true,      }),    ).toBe(true);    expect(      shouldOpenProactiveTurnDiff({        previousRunningTurnId: turnId,        runningTurnId: TurnId.make("turn-2"),        settledTurnId: turnId,        turnCompleted: true,      }),    ).toBe(false);    expect(      shouldOpenProactiveTurnDiff({        previousRunningTurnId: turnId,        runningTurnId: null,        settledTurnId: turnId,        turnCompleted: false,      }),    ).toBe(false);  });   it("opens a completed turn diff only for changed files", () => {    const changedCheckpoint = {      status: "ready",      files: [{ path: "src/app.ts", kind: "modified", additions: 1, deletions: 0 }],    } satisfies Pick<TurnDiffSummary, "status" | "files">;    const unchangedCheckpoint = {      status: "ready",      files: [],    } satisfies Pick<TurnDiffSummary, "status" | "files">;     expect(      resolveProactiveTurnDiffAction({        checkpoint: changedCheckpoint,        isGitRepo: true,        activeSurfaceKind: null,      }),    ).toBe("open");    expect(      resolveProactiveTurnDiffAction({        checkpoint: unchangedCheckpoint,        isGitRepo: true,        activeSurfaceKind: null,      }),    ).toBe("ignore");  });   it("waits for definitive checkpoint and repository state", () => {    const missingCheckpoint = {      status: "missing",      files: [],    } satisfies Pick<TurnDiffSummary, "status" | "files">;    const changedCheckpoint = {      status: "ready",      files: [{ path: "src/app.ts", kind: "modified", additions: 1, deletions: 0 }],    } satisfies Pick<TurnDiffSummary, "status" | "files">;     expect(      resolveProactiveTurnDiffAction({        checkpoint: undefined,        isGitRepo: true,        activeSurfaceKind: null,      }),    ).toBe("defer");    expect(      resolveProactiveTurnDiffAction({        checkpoint: missingCheckpoint,        isGitRepo: true,        activeSurfaceKind: null,      }),    ).toBe("defer");    expect(      resolveProactiveTurnDiffAction({        checkpoint: changedCheckpoint,        isGitRepo: undefined,        activeSurfaceKind: null,      }),    ).toBe("defer");  });   it("keeps an active pull request above a completed turn diff", () => {    const changedCheckpoint = {      status: "ready",      files: [{ path: "src/app.ts", kind: "modified", additions: 1, deletions: 0 }],    } satisfies Pick<TurnDiffSummary, "status" | "files">;     expect(      resolveProactiveTurnDiffAction({        checkpoint: changedCheckpoint,        isGitRepo: true,        activeSurfaceKind: "pull-request",      }),    ).toBe("ignore");  });}); describe("toolGroupConsumesUpwardNavigation", () => {  class ScrollElement extends EventTarget {    scrollTop = 0;    scrollHeight = 100;    clientHeight = 100;    overflowY = "visible";     constructor(      readonly parentElement: ScrollElement | null = null,      readonly isToolGroup = false,    ) {      super();    }     closest(selector: string): ScrollElement | null {      if (selector !== "[data-tool-group-scroll]") return null;      return this.isToolGroup ? this : (this.parentElement?.closest(selector) ?? null);    }  }   beforeEach(() => {    vi.stubGlobal("Element", ScrollElement);    vi.stubGlobal("getComputedStyle", (element: ScrollElement) => ({      overflowY: element.overflowY,    }));  });  afterEach(() => vi.unstubAllGlobals());   it("releases upward navigation when an overflowing group is at the top", () => {    const group = Object.assign(new ScrollElement(null, true), {      overflowY: "auto",      scrollHeight: 300,    });     expect(toolGroupConsumesUpwardNavigation(new ScrollElement(group))).toBe(false);  });   it.each([    { overflowY: "auto", scrollTop: 1 },    { overflowY: "auto", scrollTop: 0.25 },    { overflowY: "scroll", scrollTop: 80 },  ])("consumes upward navigation within a scrolled group: %j", (scroll) => {    const group = Object.assign(new ScrollElement(null, true), {      scrollHeight: 300,      ...scroll,    });     expect(toolGroupConsumesUpwardNavigation(group)).toBe(true);  });   it.each([100, 300])(    "consumes scrolling in a nested result with a group content height of %i",    (scrollHeight) => {      const group = Object.assign(new ScrollElement(null, true), {        overflowY: "auto",        scrollHeight,      });      const result = Object.assign(new ScrollElement(group), {        overflowY: "auto",        scrollHeight: 300,        scrollTop: 0.25,      });       expect(toolGroupConsumesUpwardNavigation(new ScrollElement(result))).toBe(true);    },  );   it("releases upward navigation when the group and nested result are both at the top", () => {    const group = Object.assign(new ScrollElement(null, true), {      overflowY: "auto",      scrollHeight: 300,    });    const result = Object.assign(new ScrollElement(group), {      overflowY: "scroll",      scrollHeight: 300,    });     expect(toolGroupConsumesUpwardNavigation(new ScrollElement(result))).toBe(false);  });   it("ignores targets outside a tool group and non-element targets", () => {    const outside = Object.assign(new ScrollElement(), {      overflowY: "auto",      scrollHeight: 300,      scrollTop: 40,    });     expect(toolGroupConsumesUpwardNavigation(outside)).toBe(false);    expect(toolGroupConsumesUpwardNavigation(new EventTarget())).toBe(false);    expect(toolGroupConsumesUpwardNavigation(null)).toBe(false);  });   it("does not consume scrolling from an ancestor beyond the tool group", () => {    const timeline = Object.assign(new ScrollElement(), {      overflowY: "auto",      scrollHeight: 300,      scrollTop: 40,    });    const group = new ScrollElement(timeline, true);     expect(toolGroupConsumesUpwardNavigation(new ScrollElement(group))).toBe(false);  });   it.each(["hidden", "clip", "visible"])(    "ignores a non-scrollable child with overflow-y %s",    (overflowY) => {      const group = new ScrollElement(null, true);      const result = Object.assign(new ScrollElement(group), {        overflowY,        scrollHeight: 300,        scrollTop: 40,      });       expect(toolGroupConsumesUpwardNavigation(new ScrollElement(result))).toBe(false);    },  );   it("does not consume programmatic scrolling on an overflow-hidden group", () => {    const group = Object.assign(new ScrollElement(null, true), {      overflowY: "hidden",      scrollHeight: 300,      scrollTop: 40,    });     expect(toolGroupConsumesUpwardNavigation(group)).toBe(false);  });}); const environmentId = EnvironmentId.make("environment-local");const projectId = ProjectId.make("project-1");const threadId = ThreadId.make("thread-1");const now = "2026-03-29T00:00:00.000Z";const helloWorldTemplate: CodexArtifactTemplate = {  artifactKind: "document",  displayName: "Hello World",  skillDirectory: "/Users/test/.codex/skills/artifact-template-hello-world",  skillName: "artifact-template-hello-world",}; describe("artifact template composer insertion", () => {  it("does not insert an already-present prompt", () => {    const prompt = "Create a document using this $artifact-template-hello-world about…";     expect(codexArtifactTemplatePromptToAppend(prompt, helloWorldTemplate)).toBeNull();  });}); describe("draft hero submission transition", () => {  it("does not dock the composer before a background submission", () => {    expect(      shouldDockDraftHeroForSubmission({        isDraftHeroState: true,        activeThreadKey: "environment-local:thread-1",        submissionIntent: "background",      }),    ).toBe(false);  });   it("keeps the composer in the hero layout until navigation after server promotion", () => {    expect(      resolveDraftHeroState({        isLocalDraftThread: false,        hasTimelineEntries: true,        isWorking: true,        draftHeroDockRequested: false,        backgroundSubmissionPending: true,      }),    ).toBe(true);  });   it("does not auto-navigate a background submission after server promotion", () => {    expect(      resolveDraftPromotionNavigationTarget({        serverThreadRef: { environmentId, threadId },        serverThread: makeThread({ latestTurn: completedTurn }),        backgroundSubmissionPending: true,      }),    ).toBeNull();  });}); describe("shouldReleaseTimelineAnchorForToolActivity", () => {  const activeTurnId = TurnId.make("active-turn");  const anchorMessageId = MessageId.make("anchored-message");  const activeToolEntry = {    id: "tool-entry",    kind: "work" as const,    createdAt: now,    entry: {      id: "active-tool",      createdAt: now,      turnId: activeTurnId,      label: "Run command",      tone: "tool" as const,      command: "git status",    },  };   it("releases the send anchor for tool activity in the active turn", () => {    expect(      shouldReleaseTimelineAnchorForToolActivity({        anchorMessageId,        liveFollowEnabled: true,        runningTurnId: activeTurnId,        timelineEntries: [activeToolEntry],      }),    ).toBe(true);  });   it("keeps the anchor while the user reads history", () => {    expect(      shouldReleaseTimelineAnchorForToolActivity({        anchorMessageId,        liveFollowEnabled: false,        runningTurnId: activeTurnId,        timelineEntries: [activeToolEntry],      }),    ).toBe(false);  });   it("ignores tool activity from earlier turns", () => {    expect(      shouldReleaseTimelineAnchorForToolActivity({        anchorMessageId,        liveFollowEnabled: true,        runningTurnId: activeTurnId,        timelineEntries: [          {            ...activeToolEntry,            entry: {              ...activeToolEntry.entry,              turnId: TurnId.make("previous-turn"),            },          },        ],      }),    ).toBe(false);  });   it("ignores thinking and error rows without tool activity", () => {    expect(      shouldReleaseTimelineAnchorForToolActivity({        anchorMessageId,        liveFollowEnabled: true,        runningTurnId: activeTurnId,        timelineEntries: [          {            ...activeToolEntry,            entry: {              id: "thinking-entry",              createdAt: now,              turnId: activeTurnId,              label: "Thinking",              tone: "thinking",            },          },          {            ...activeToolEntry,            id: "error-entry",            entry: {              id: "error-entry",

Lines 1–500 of 1847.