860 lines · 24.9 KB · TypeScript
RawDownload
import { describe, expect, it } from "vitest"; import {  getVisibleSidebarThreadIds,  resolveAdjacentThreadId,  getFallbackThreadIdAfterDelete,  getVisibleThreadsForProject,  getProjectSortTimestamp,  hasUnseenCompletion,  isContextMenuPointerDown,  resolveProjectStatusIndicator,  resolveSidebarNewThreadEnvMode,  resolveThreadRowClassName,  resolveThreadStatusPill,  shouldClearThreadSelectionOnMouseDown,  sortProjectsForSidebar,  sortThreadsForSidebar,} from "./Sidebar.logic";import { ProjectId, ThreadId } from "@t3tools/contracts";import {  DEFAULT_INTERACTION_MODE,  DEFAULT_RUNTIME_MODE,  type Project,  type Thread,} from "../types"; function makeLatestTurn(overrides?: {  completedAt?: string | null;  startedAt?: string | null;}): Parameters<typeof hasUnseenCompletion>[0]["latestTurn"] {  return {    turnId: "turn-1" as never,    state: "completed",    assistantMessageId: null,    requestedAt: "2026-03-09T10:00:00.000Z",    startedAt: overrides?.startedAt ?? "2026-03-09T10:00:00.000Z",    completedAt: overrides?.completedAt ?? "2026-03-09T10:05:00.000Z",  };} describe("hasUnseenCompletion", () => {  it("returns true when a thread completed after its last visit", () => {    expect(      hasUnseenCompletion({        interactionMode: "default",        latestTurn: makeLatestTurn(),        lastVisitedAt: "2026-03-09T10:04:00.000Z",        proposedPlans: [],        session: null,      }),    ).toBe(true);  });}); describe("shouldClearThreadSelectionOnMouseDown", () => {  it("preserves selection for thread items", () => {    const child = {      closest: (selector: string) =>        selector.includes("[data-thread-item]") ? ({} as Element) : null,    } as unknown as HTMLElement;     expect(shouldClearThreadSelectionOnMouseDown(child)).toBe(false);  });   it("preserves selection for thread list toggle controls", () => {    const selectionSafe = {      closest: (selector: string) =>        selector.includes("[data-thread-selection-safe]") ? ({} as Element) : null,    } as unknown as HTMLElement;     expect(shouldClearThreadSelectionOnMouseDown(selectionSafe)).toBe(false);  });   it("clears selection for unrelated sidebar clicks", () => {    const unrelated = {      closest: () => null,    } as unknown as HTMLElement;     expect(shouldClearThreadSelectionOnMouseDown(unrelated)).toBe(true);  });}); describe("resolveSidebarNewThreadEnvMode", () => {  it("uses the app default when the caller does not request a specific mode", () => {    expect(      resolveSidebarNewThreadEnvMode({        defaultEnvMode: "worktree",      }),    ).toBe("worktree");  });   it("preserves an explicit requested mode over the app default", () => {    expect(      resolveSidebarNewThreadEnvMode({        requestedEnvMode: "local",        defaultEnvMode: "worktree",      }),    ).toBe("local");  });}); describe("resolveAdjacentThreadId", () => {  it("resolves adjacent thread ids in ordered sidebar traversal", () => {    const threads = [      ThreadId.makeUnsafe("thread-1"),      ThreadId.makeUnsafe("thread-2"),      ThreadId.makeUnsafe("thread-3"),    ];     expect(      resolveAdjacentThreadId({        threadIds: threads,        currentThreadId: threads[1] ?? null,        direction: "previous",      }),    ).toBe(threads[0]);    expect(      resolveAdjacentThreadId({        threadIds: threads,        currentThreadId: threads[1] ?? null,        direction: "next",      }),    ).toBe(threads[2]);    expect(      resolveAdjacentThreadId({        threadIds: threads,        currentThreadId: null,        direction: "next",      }),    ).toBe(threads[0]);    expect(      resolveAdjacentThreadId({        threadIds: threads,        currentThreadId: null,        direction: "previous",      }),    ).toBe(threads[2]);    expect(      resolveAdjacentThreadId({        threadIds: threads,        currentThreadId: threads[0] ?? null,        direction: "previous",      }),    ).toBeNull();  });}); describe("getVisibleSidebarThreadIds", () => {  it("returns only the rendered visible thread order across projects", () => {    expect(      getVisibleSidebarThreadIds([        {          renderedThreads: [            { id: ThreadId.makeUnsafe("thread-12") },            { id: ThreadId.makeUnsafe("thread-11") },            { id: ThreadId.makeUnsafe("thread-10") },          ],        },        {          renderedThreads: [            { id: ThreadId.makeUnsafe("thread-8") },            { id: ThreadId.makeUnsafe("thread-6") },          ],        },      ]),    ).toEqual([      ThreadId.makeUnsafe("thread-12"),      ThreadId.makeUnsafe("thread-11"),      ThreadId.makeUnsafe("thread-10"),      ThreadId.makeUnsafe("thread-8"),      ThreadId.makeUnsafe("thread-6"),    ]);  });}); describe("isContextMenuPointerDown", () => {  it("treats secondary-button presses as context menu gestures on all platforms", () => {    expect(      isContextMenuPointerDown({        button: 2,        ctrlKey: false,        isMac: false,      }),    ).toBe(true);  });   it("treats ctrl+primary-click as a context menu gesture on macOS", () => {    expect(      isContextMenuPointerDown({        button: 0,        ctrlKey: true,        isMac: true,      }),    ).toBe(true);  });   it("does not treat ctrl+primary-click as a context menu gesture off macOS", () => {    expect(      isContextMenuPointerDown({        button: 0,        ctrlKey: true,        isMac: false,      }),    ).toBe(false);  });}); describe("resolveThreadStatusPill", () => {  const baseThread = {    interactionMode: "plan" as const,    latestTurn: null,    lastVisitedAt: undefined,    proposedPlans: [],    session: {      provider: "codex" as const,      status: "running" as const,      createdAt: "2026-03-09T10:00:00.000Z",      updatedAt: "2026-03-09T10:00:00.000Z",      orchestrationStatus: "running" as const,    },  };   it("shows pending approval before all other statuses", () => {    expect(      resolveThreadStatusPill({        thread: baseThread,        hasPendingApprovals: true,        hasPendingUserInput: true,      }),    ).toMatchObject({ label: "Pending Approval", pulse: false });  });   it("shows awaiting input when plan mode is blocked on user answers", () => {    expect(      resolveThreadStatusPill({        thread: baseThread,        hasPendingApprovals: false,        hasPendingUserInput: true,      }),    ).toMatchObject({ label: "Awaiting Input", pulse: false });  });   it("falls back to working when the thread is actively running without blockers", () => {    expect(      resolveThreadStatusPill({        thread: baseThread,        hasPendingApprovals: false,        hasPendingUserInput: false,      }),    ).toMatchObject({ label: "Working", pulse: true });  });   it("shows plan ready when a settled plan turn has a proposed plan ready for follow-up", () => {    expect(      resolveThreadStatusPill({        thread: {          ...baseThread,          latestTurn: makeLatestTurn(),          proposedPlans: [            {              id: "plan-1" as never,              turnId: "turn-1" as never,              createdAt: "2026-03-09T10:00:00.000Z",              updatedAt: "2026-03-09T10:05:00.000Z",              planMarkdown: "# Plan",              implementedAt: null,              implementationThreadId: null,            },          ],          session: {            ...baseThread.session,            status: "ready",            orchestrationStatus: "ready",          },        },        hasPendingApprovals: false,        hasPendingUserInput: false,      }),    ).toMatchObject({ label: "Plan Ready", pulse: false });  });   it("does not show plan ready after the proposed plan was implemented elsewhere", () => {    expect(      resolveThreadStatusPill({        thread: {          ...baseThread,          latestTurn: makeLatestTurn(),          proposedPlans: [            {              id: "plan-1" as never,              turnId: "turn-1" as never,              createdAt: "2026-03-09T10:00:00.000Z",              updatedAt: "2026-03-09T10:05:00.000Z",              planMarkdown: "# Plan",              implementedAt: "2026-03-09T10:06:00.000Z",              implementationThreadId: "thread-implement" as never,            },          ],          session: {            ...baseThread.session,            status: "ready",            orchestrationStatus: "ready",          },        },        hasPendingApprovals: false,        hasPendingUserInput: false,      }),    ).toMatchObject({ label: "Completed", pulse: false });  });   it("shows completed when there is an unseen completion and no active blocker", () => {    expect(      resolveThreadStatusPill({        thread: {          ...baseThread,          interactionMode: "default",          latestTurn: makeLatestTurn(),          lastVisitedAt: "2026-03-09T10:04:00.000Z",          session: {            ...baseThread.session,            status: "ready",            orchestrationStatus: "ready",          },        },        hasPendingApprovals: false,        hasPendingUserInput: false,      }),    ).toMatchObject({ label: "Completed", pulse: false });  });}); describe("resolveThreadRowClassName", () => {  it("uses the darker selected palette when a thread is both selected and active", () => {    const className = resolveThreadRowClassName({ isActive: true, isSelected: true });    expect(className).toContain("bg-primary/22");    expect(className).toContain("hover:bg-primary/26");    expect(className).toContain("dark:bg-primary/30");    expect(className).not.toContain("bg-accent/85");  });   it("uses selected hover colors for selected threads", () => {    const className = resolveThreadRowClassName({ isActive: false, isSelected: true });    expect(className).toContain("bg-primary/15");    expect(className).toContain("hover:bg-primary/19");    expect(className).toContain("dark:bg-primary/22");    expect(className).not.toContain("hover:bg-accent");  });   it("keeps the accent palette for active-only threads", () => {    const className = resolveThreadRowClassName({ isActive: true, isSelected: false });    expect(className).toContain("bg-accent/85");    expect(className).toContain("hover:bg-accent");  });}); describe("resolveProjectStatusIndicator", () => {  it("returns null when no threads have a notable status", () => {    expect(resolveProjectStatusIndicator([null, null])).toBeNull();  });   it("surfaces the highest-priority actionable state across project threads", () => {    expect(      resolveProjectStatusIndicator([        {          label: "Completed",          colorClass: "text-emerald-600",          dotClass: "bg-emerald-500",          pulse: false,        },        {          label: "Pending Approval",          colorClass: "text-amber-600",          dotClass: "bg-amber-500",          pulse: false,        },        {          label: "Working",          colorClass: "text-sky-600",          dotClass: "bg-sky-500",          pulse: true,        },      ]),    ).toMatchObject({ label: "Pending Approval", dotClass: "bg-amber-500" });  });   it("prefers plan-ready over completed when no stronger action is needed", () => {    expect(      resolveProjectStatusIndicator([        {          label: "Completed",          colorClass: "text-emerald-600",          dotClass: "bg-emerald-500",          pulse: false,        },        {          label: "Plan Ready",          colorClass: "text-violet-600",          dotClass: "bg-violet-500",          pulse: false,        },      ]),    ).toMatchObject({ label: "Plan Ready", dotClass: "bg-violet-500" });  });}); describe("getVisibleThreadsForProject", () => {  it("includes the active thread even when it falls below the folded preview", () => {    const threads = Array.from({ length: 8 }, (_, index) =>      makeThread({        id: ThreadId.makeUnsafe(`thread-${index + 1}`),        title: `Thread ${index + 1}`,      }),    );     const result = getVisibleThreadsForProject({      threads,      activeThreadId: ThreadId.makeUnsafe("thread-8"),      isThreadListExpanded: false,      previewLimit: 6,    });     expect(result.hasHiddenThreads).toBe(true);    expect(result.visibleThreads.map((thread) => thread.id)).toEqual([      ThreadId.makeUnsafe("thread-1"),      ThreadId.makeUnsafe("thread-2"),      ThreadId.makeUnsafe("thread-3"),      ThreadId.makeUnsafe("thread-4"),      ThreadId.makeUnsafe("thread-5"),      ThreadId.makeUnsafe("thread-6"),      ThreadId.makeUnsafe("thread-8"),    ]);  });   it("returns all threads when the list is expanded", () => {    const threads = Array.from({ length: 8 }, (_, index) =>      makeThread({        id: ThreadId.makeUnsafe(`thread-${index + 1}`),      }),    );     const result = getVisibleThreadsForProject({      threads,      activeThreadId: ThreadId.makeUnsafe("thread-8"),      isThreadListExpanded: true,      previewLimit: 6,    });     expect(result.hasHiddenThreads).toBe(true);    expect(result.visibleThreads.map((thread) => thread.id)).toEqual(      threads.map((thread) => thread.id),    );  });}); function makeProject(overrides: Partial<Project> = {}): Project {  const { defaultModelSelection, ...rest } = overrides;  return {    id: ProjectId.makeUnsafe("project-1"),    name: "Project",    cwd: "/tmp/project",    workspaceState: "available",    defaultModelSelection: {      provider: "codex",      model: "gpt-5.4",      ...defaultModelSelection,    },    expanded: true,    createdAt: "2026-03-09T10:00:00.000Z",    updatedAt: "2026-03-09T10:00:00.000Z",    scripts: [],    ...rest,  };} function makeThread(overrides: Partial<Thread> = {}): Thread {  return {    id: ThreadId.makeUnsafe("thread-1"),    codexThreadId: null,    projectId: ProjectId.makeUnsafe("project-1"),    title: "Thread",    modelSelection: {      provider: "codex",      model: "gpt-5.4",      ...overrides?.modelSelection,    },    runtimeMode: DEFAULT_RUNTIME_MODE,    interactionMode: DEFAULT_INTERACTION_MODE,    session: null,    messages: [],    proposedPlans: [],    error: null,    createdAt: "2026-03-09T10:00:00.000Z",    archivedAt: null,    updatedAt: "2026-03-09T10:00:00.000Z",    latestTurn: null,    branch: null,    worktreePath: null,    effectiveCwd: "/tmp/project",    effectiveCwdSource: "project",    effectiveCwdState: "available",

Lines 1–500 of 860.