1035 lines · 30.8 KB · TypeScript
RawDownload
import {  EnvironmentId,  MessageId,  ProjectId,  ProviderInstanceId,  ThreadId,  TurnId,} from "@t3tools/contracts";import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import type { Thread, ThreadShell } from "../types";import type { CodexArtifactTemplate } from "@t3tools/client-runtime/codex-artifact-templates";import {  MAX_HIDDEN_MOUNTED_PREVIEW_THREADS,  MAX_HIDDEN_MOUNTED_TERMINAL_THREADS,  branchMismatchKey,  buildExpiredTerminalContextToastCopy,  buildLoadingThreadFromShell,  buildThreadTurnInterruptInput,  createLocalDispatchSnapshot,  deriveComposerSendState,  dismissBranchMismatchForSession,  ENVIRONMENT_RECONNECT_WARNING_GRACE_MS,  getStartedThreadModelChangeBlockReason,  loadVideoPreviewUrl,  isVideoPreviewRequestCurrent,  hasEnvironmentReconnectWarningGraceElapsed,  hasServerAcknowledgedLocalDispatch,  isBranchMismatchDismissedForSession,  reconcileMountedTerminalThreadIds,  reconcileRetainedMountedThreadIds,  resolveBackgroundDraftWorkspaceOptions,  resolveDraftPromotionNavigationTarget,  resolveThreadMetadataUpdateForNextTurn,  resolveSendEnvMode,  resolveDraftHeroState,  scheduleEnvironmentReconnectWarning,  startNewThreadForProject,  codexArtifactTemplatePromptToAppend,  shouldDockDraftHeroForSubmission,  shouldReleaseTimelineAnchorForToolActivity,  shouldShowBranchMismatchBanner,  shouldShowPlanFollowUpPrompt,  shouldWriteThreadErrorToCurrentServerThread,} from "./ChatView.logic"; describe("loadVideoPreviewUrl", () => {  it("loads video bytes into an object URL", async () => {    const objectUrl = await loadVideoPreviewUrl("data:video/mp4;base64,AA==");    expect(objectUrl).toMatch(/^blob:/);    URL.revokeObjectURL(objectUrl);  });   it("stops loading when the preview request is cancelled", async () => {    const controller = new AbortController();    controller.abort();     await expect(      loadVideoPreviewUrl("data:video/mp4;base64,AA==", controller.signal),    ).rejects.toMatchObject({ name: "AbortError" });  });}); describe("isVideoPreviewRequestCurrent", () => {  it("rejects changed threads and replaced previews", () => {    expect(isVideoPreviewRequestCurrent("thread-1", "thread-2", 1, 1)).toBe(false);    expect(isVideoPreviewRequestCurrent("thread-1", "thread-1", 1, 2)).toBe(false);    expect(isVideoPreviewRequestCurrent("thread-1", "thread-1", 2, 2)).toBe(true);  });}); 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",              createdAt: now,              turnId: activeTurnId,              label: "Provider error",              tone: "error",            },          },        ],      }),    ).toBe(false);  });   it("does nothing without an anchor or running turn", () => {    const input = {      anchorMessageId,      liveFollowEnabled: true,      runningTurnId: activeTurnId,      timelineEntries: [activeToolEntry],    };     expect(shouldReleaseTimelineAnchorForToolActivity({ ...input, anchorMessageId: null })).toBe(      false,    );    expect(shouldReleaseTimelineAnchorForToolActivity({ ...input, runningTurnId: null })).toBe(      false,    );  });}); describe("environment reconnect warning grace", () => {  afterEach(() => vi.useRealTimers());   it("shows a persistent reconnect after the grace period", () => {    vi.useFakeTimers();    const showWarning = vi.fn();     scheduleEnvironmentReconnectWarning(showWarning);    vi.advanceTimersByTime(ENVIRONMENT_RECONNECT_WARNING_GRACE_MS - 1);    expect(showWarning).not.toHaveBeenCalled();     vi.advanceTimersByTime(1);    expect(showWarning).toHaveBeenCalledOnce();  });   it("cancels the warning when the connection recovers during the grace period", () => {    vi.useFakeTimers();    const showWarning = vi.fn();     const cancel = scheduleEnvironmentReconnectWarning(showWarning);    cancel();    vi.advanceTimersByTime(ENVIRONMENT_RECONNECT_WARNING_GRACE_MS);     expect(showWarning).not.toHaveBeenCalled();  });   it("does not reuse elapsed grace from another environment", () => {    const anotherEnvironmentId = EnvironmentId.make("environment-remote");     expect(hasEnvironmentReconnectWarningGraceElapsed(environmentId, environmentId)).toBe(true);    expect(hasEnvironmentReconnectWarningGraceElapsed(anotherEnvironmentId, environmentId)).toBe(      false,    );  });}); function makeThread(overrides: Partial<Thread> = {}): Thread {  return {    id: threadId,    environmentId,    projectId,    title: "Thread",    modelSelection: {      instanceId: ProviderInstanceId.make("codex"),      model: "gpt-5.4",    },    runtimeMode: "full-access",    interactionMode: "default",    session: null,    messages: [],    proposedPlans: [],    activities: [],    checkpoints: [],    createdAt: now,    updatedAt: now,    archivedAt: null,    settledOverride: null,    settledAt: null,    deletedAt: null,    latestTurn: null,    branch: null,    worktreePath: null,    ...overrides,  };} const completedTurn = {  turnId: TurnId.make("turn-1"),  state: "completed" as const,  requestedAt: now,  startedAt: "2026-03-29T00:00:01.000Z",  completedAt: "2026-03-29T00:00:10.000Z",  assistantMessageId: null,}; const readySession = {  threadId,  status: "ready" as const,  providerName: "codex",  providerInstanceId: ProviderInstanceId.make("codex"),  runtimeMode: "full-access" as const,  activeTurnId: null,  lastError: null,  updatedAt: "2026-03-29T00:00:10.000Z",}; describe("draft promotion during worktree setup", () => {  const serverThreadRef = { environmentId, threadId };   it.each([null, "idle", "starting", "ready"] as const)(    "keeps the draft mounted while the first turn waits with session %s",    (status) => {      const serverThread = makeThread({        messages: [          {            id: MessageId.make("submitted-message"),            role: "user",            text: "Start in a new worktree",            turnId: null,            createdAt: now,            updatedAt: now,            streaming: false,          },        ],        session: status ? { ...readySession, status } : null,      });       expect(        resolveDraftPromotionNavigationTarget({          serverThreadRef,          serverThread,          backgroundSubmissionPending: false,        }),      ).toBeNull();    },  );   it("promotes when the provider starts the first turn", () => {    const latestTurn = { ...completedTurn, state: "running" as const, completedAt: null };     expect(      resolveDraftPromotionNavigationTarget({        serverThreadRef,        serverThread: makeThread({          latestTurn,          session: { ...readySession, status: "running", activeTurnId: latestTurn.turnId },        }),        backgroundSubmissionPending: false,      }),    ).toEqual(serverThreadRef);  });   it.each(["error", "stopped", "interrupted"] as const)(    "promotes a startup that ends as %s before a turn starts",    (status) => {      expect(        resolveDraftPromotionNavigationTarget({          serverThreadRef,          serverThread: makeThread({ session: { ...readySession, status } }),          backgroundSubmissionPending: false,        }),      ).toEqual(serverThreadRef);    },  );}); describe("buildLoadingThreadFromShell", () => {  it("preserves shell metadata and supplies empty detail collections", () => {    const shell = {      environmentId,      id: threadId,      projectId,      title: "Loading thread",      modelSelection: {        instanceId: ProviderInstanceId.make("codex"),        model: "gpt-5.4",      },      runtimeMode: "full-access",      interactionMode: "default",      branch: "main",      worktreePath: null,      latestTurn: null,      createdAt: now,      updatedAt: now,      archivedAt: null,      settledOverride: null,      settledAt: null,      snoozedUntil: null,      snoozedAt: null,      session: null,      latestUserMessageAt: now,      hasPendingApprovals: false,      hasPendingUserInput: false,      hasActionableProposedPlan: false,    } satisfies ThreadShell;     expect(buildLoadingThreadFromShell(shell)).toMatchObject({      environmentId,      id: threadId,      projectId,      title: "Loading thread",      branch: "main",      deletedAt: null,      messages: [],      proposedPlans: [],      activities: [],      checkpoints: [],    });  });}); describe("resolveThreadMetadataUpdateForNextTurn", () => {  const modelSelection = {    instanceId: ProviderInstanceId.make("codex"),    model: "gpt-5.4",  };   it("updates a stale local thread branch to the active checkout", () => {    expect(      resolveThreadMetadataUpdateForNextTurn({        currentModelSelection: modelSelection,        currentBranch: "feature/thread",        nextBranch: "feature/checkout",      }),    ).toEqual({ branch: "feature/checkout", worktreePath: null });  });   it("does not write metadata when the model and branch are unchanged", () => {    expect(      resolveThreadMetadataUpdateForNextTurn({        currentModelSelection: modelSelection,        nextModelSelection: modelSelection,        currentBranch: "feature/current",        nextBranch: "feature/current",      }),    ).toBeNull();  });}); describe("buildThreadTurnInterruptInput", () => {  it("targets the session's active running turn", () => {    const activeTurnId = TurnId.make("turn-running");     expect(      buildThreadTurnInterruptInput(        makeThread({          session: {            ...readySession,            status: "running",            activeTurnId,          },        }),      ),    ).toEqual({ threadId, turnId: activeTurnId });  });   it("omits a turn id when the session is not running", () => {    expect(buildThreadTurnInterruptInput(makeThread({ session: readySession }))).toEqual({      threadId,    });  });}); describe("deriveComposerSendState", () => {  it("treats expired terminal pills as non-sendable content", () => {    const state = deriveComposerSendState({      prompt: "\uFFFC",      imageCount: 0,      terminalContexts: [        {          id: "ctx-expired",          threadId,          terminalId: "default",          terminalLabel: "Terminal 1",          lineStart: 4,          lineEnd: 4,          text: "",          createdAt: now,        },      ],    });     expect(state.trimmedPrompt).toBe("");    expect(state.sendableTerminalContexts).toEqual([]);    expect(state.expiredTerminalContextCount).toBe(1);    expect(state.hasSendableContent).toBe(false);  }); 

Lines 1–500 of 1035.