1139 lines · 33.1 KB · TypeScript
RawDownload
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";import {  createThreadJumpHintVisibilityController,  getSidebarThreadIdsToPrewarm,  getVisibleSidebarThreadIds,  resolveAdjacentThreadId,  getFallbackThreadIdAfterDelete,  getVisibleThreadsForProject,  getProjectSortTimestamp,  hasUnseenCompletion,  isContextMenuPointerDown,  isTrailingDoubleClick,  orderItemsByPreferredIds,  resolveProjectStatusIndicator,  resolveSidebarNewThreadSeedContext,  resolveSidebarNewThreadEnvMode,  resolveSidebarStageBadgeLabel,  resolveThreadRowClassName,  resolveThreadStatusPill,  shouldClearThreadSelectionOnMouseDown,  sortProjectsForSidebar,  sortScopedProjectsForSidebar,  THREAD_JUMP_HINT_SHOW_DELAY_MS,} from "./Sidebar.logic";import {  EnvironmentId,  OrchestrationLatestTurn,  ProjectId,  ProviderInstanceId,  ThreadId,} from "@t3tools/contracts";import {  DEFAULT_INTERACTION_MODE,  DEFAULT_RUNTIME_MODE,  type Project,  type Thread,} from "../types"; const localEnvironmentId = EnvironmentId.make("environment-local"); describe("resolveSidebarStageBadgeLabel", () => {  it("returns Nightly for nightly primary server versions", () => {    expect(      resolveSidebarStageBadgeLabel({        primaryServerVersion: "0.0.28-nightly.20260616.12",        fallbackStageLabel: "Alpha",      }),    ).toBe("Nightly");  });   it("returns the fallback label for stable primary server versions", () => {    expect(      resolveSidebarStageBadgeLabel({        primaryServerVersion: "0.0.27",        fallbackStageLabel: "Alpha",      }),    ).toBe("Alpha");  });   it("returns the fallback label when the primary server version is missing", () => {    expect(      resolveSidebarStageBadgeLabel({        primaryServerVersion: null,        fallbackStageLabel: "Dev",      }),    ).toBe("Dev");  });   it("returns the fallback label for malformed nightly prerelease versions", () => {    expect(      resolveSidebarStageBadgeLabel({        primaryServerVersion: "0.0.28-nightly.20260616",        fallbackStageLabel: "Alpha",      }),    ).toBe("Alpha");  });}); function makeLatestTurn(overrides?: {  completedAt?: string | null;  startedAt?: string | null;}): OrchestrationLatestTurn {  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({        hasActionableProposedPlan: false,        hasPendingApprovals: false,        hasPendingUserInput: false,        interactionMode: "default",        latestTurn: makeLatestTurn(),        lastVisitedAt: "2026-03-09T10:04:00.000Z",        session: null,      }),    ).toBe(true);  });   it("treats a missing client visit marker as read", () => {    expect(      hasUnseenCompletion({        hasActionableProposedPlan: false,        hasPendingApprovals: false,        hasPendingUserInput: false,        interactionMode: "default",        latestTurn: makeLatestTurn(),        lastVisitedAt: undefined,        session: null,      }),    ).toBe(false);  });}); describe("createThreadJumpHintVisibilityController", () => {  beforeEach(() => {    vi.useFakeTimers();  });   afterEach(() => {    vi.useRealTimers();  });   it("delays showing jump hints until the configured delay elapses", () => {    const visibilityChanges: boolean[] = [];    const controller = createThreadJumpHintVisibilityController({      delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS,      onVisibilityChange: (visible) => {        visibilityChanges.push(visible);      },    });     controller.sync(true);    vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS - 1);     expect(visibilityChanges).toEqual([]);     vi.advanceTimersByTime(1);     expect(visibilityChanges).toEqual([true]);  });   it("hides immediately when the modifiers are released", () => {    const visibilityChanges: boolean[] = [];    const controller = createThreadJumpHintVisibilityController({      delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS,      onVisibilityChange: (visible) => {        visibilityChanges.push(visible);      },    });     controller.sync(true);    vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS);    controller.sync(false);     expect(visibilityChanges).toEqual([true, false]);  });   it("cancels a pending reveal when the modifier is released early", () => {    const visibilityChanges: boolean[] = [];    const controller = createThreadJumpHintVisibilityController({      delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS,      onVisibilityChange: (visible) => {        visibilityChanges.push(visible);      },    });     controller.sync(true);    vi.advanceTimersByTime(Math.floor(THREAD_JUMP_HINT_SHOW_DELAY_MS / 2));    controller.sync(false);    vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS);     expect(visibilityChanges).toEqual([]);  });}); describe("getSidebarThreadIdsToPrewarm", () => {  it("returns only the first visible thread ids up to the prewarm limit", () => {    expect(getSidebarThreadIdsToPrewarm(["t1", "t2", "t3"], 2)).toEqual(["t1", "t2"]);  });   it("returns all visible thread ids when they fit within the limit", () => {    expect(getSidebarThreadIdsToPrewarm(["t1", "t2"], 10)).toEqual(["t1", "t2"]);  });   it("returns no thread ids when the limit is zero", () => {    expect(getSidebarThreadIdsToPrewarm(["t1", "t2"], 0)).toEqual([]);  });}); 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("isTrailingDoubleClick", () => {  it("treats a single click as a normal activation", () => {    expect(isTrailingDoubleClick(1)).toBe(false);  });   it("treats synthetic/keyboard activations (detail 0) as a normal activation", () => {    expect(isTrailingDoubleClick(0)).toBe(false);  });   it("ignores the second click of a double-click so it does not navigate", () => {    expect(isTrailingDoubleClick(2)).toBe(true);  });   it("ignores further clicks of a triple-click", () => {    expect(isTrailingDoubleClick(3)).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("resolveSidebarNewThreadSeedContext", () => {  it("prefers the default worktree mode over active thread context", () => {    expect(      resolveSidebarNewThreadSeedContext({        projectId: "project-1",        defaultEnvMode: "worktree",        activeThread: {          projectId: "project-1",          branch: "feature/existing",          worktreePath: "/repo/.t3/worktrees/existing",        },        activeDraftThread: {          projectId: "project-1",          branch: "feature/draft",          worktreePath: "/repo/.t3/worktrees/draft",          envMode: "worktree",          startFromOrigin: true,        },      }),    ).toEqual({      envMode: "worktree",    });  });   it("inherits the active server thread context when creating a new thread in the same project", () => {    expect(      resolveSidebarNewThreadSeedContext({        projectId: "project-1",        defaultEnvMode: "local",        activeThread: {          projectId: "project-1",          branch: "effect-atom",          worktreePath: null,        },        activeDraftThread: null,      }),    ).toEqual({      branch: "effect-atom",      worktreePath: null,      envMode: "local",    });  });   it("prefers the active draft thread context when it matches the target project", () => {    expect(      resolveSidebarNewThreadSeedContext({        projectId: "project-1",        defaultEnvMode: "local",        activeThread: {          projectId: "project-1",          branch: "effect-atom",          worktreePath: null,        },        activeDraftThread: {          projectId: "project-1",          branch: "feature/new-draft",          worktreePath: "/repo/worktree",          envMode: "worktree",          startFromOrigin: true,        },      }),    ).toEqual({      branch: "feature/new-draft",      worktreePath: "/repo/worktree",      envMode: "worktree",      startFromOrigin: true,    });  });   it("falls back to the default env mode when there is no matching active thread context", () => {    expect(      resolveSidebarNewThreadSeedContext({        projectId: "project-2",        defaultEnvMode: "worktree",        activeThread: {          projectId: "project-1",          branch: "effect-atom",          worktreePath: null,        },        activeDraftThread: null,      }),    ).toEqual({      envMode: "worktree",    });  });}); describe("orderItemsByPreferredIds", () => {  it("keeps preferred ids first, skips stale ids, and preserves the relative order of remaining items", () => {    const ordered = orderItemsByPreferredIds({      items: [        { id: ProjectId.make("project-1"), name: "One" },        { id: ProjectId.make("project-2"), name: "Two" },        { id: ProjectId.make("project-3"), name: "Three" },      ],      preferredIds: [        ProjectId.make("project-3"),        ProjectId.make("project-missing"),        ProjectId.make("project-1"),      ],      getId: (project) => project.id,    });     expect(ordered.map((project) => project.id)).toEqual([      ProjectId.make("project-3"),      ProjectId.make("project-1"),      ProjectId.make("project-2"),    ]);  });   it("does not duplicate items when preferred ids repeat", () => {    const ordered = orderItemsByPreferredIds({      items: [        { id: ProjectId.make("project-1"), name: "One" },        { id: ProjectId.make("project-2"), name: "Two" },      ],      preferredIds: [        ProjectId.make("project-2"),        ProjectId.make("project-1"),        ProjectId.make("project-2"),      ],      getId: (project) => project.id,    });     expect(ordered.map((project) => project.id)).toEqual([      ProjectId.make("project-2"),      ProjectId.make("project-1"),    ]);  });   it("honors projectOrder physical keys via getProjectOrderKey", async () => {    // Regression guard for #1904 / the regression introduced by #2055:    // `projectOrder` is populated with physical keys (envId + cwd-derived)    // by the store and by drag-end handlers. Readers must identify projects    // with the same key format, or manual sort silently snaps back.    const { getProjectOrderKey } = await import("../logicalProject");    const projects = [      {        environmentId: EnvironmentId.make("environment-local"),        id: ProjectId.make("id-alpha"),        workspaceRoot: "/work/alpha",      },      {        environmentId: EnvironmentId.make("environment-local"),        id: ProjectId.make("id-beta"),        workspaceRoot: "/work/beta",      },      {        environmentId: EnvironmentId.make("environment-local"),        id: ProjectId.make("id-gamma"),        workspaceRoot: "/work/gamma",      },    ];    const ordered = orderItemsByPreferredIds({      items: projects,      preferredIds: [getProjectOrderKey(projects[2]!), getProjectOrderKey(projects[0]!)],      getId: getProjectOrderKey,    });     expect(ordered.map((project) => project.workspaceRoot)).toEqual([      "/work/gamma",      "/work/alpha",      "/work/beta",    ]);  });   it("resolves legacy preference aliases without materializing project state", () => {    const ordered = orderItemsByPreferredIds({      items: [        { id: "physical-a", cwd: "/work/a" },        { id: "physical-b", cwd: "/work/b" },        { id: "physical-c", cwd: "/work/c" },      ],      preferredIds: ["legacy:/work/c", "legacy:/work/a"],      getId: (project) => project.id,      getPreferenceIds: (project) => [project.id, `legacy:${project.cwd}`],    });     expect(ordered.map((project) => project.id)).toEqual([      "physical-c",      "physical-a",      "physical-b",    ]);  });}); describe("resolveAdjacentThreadId", () => {  it("resolves adjacent thread ids in ordered sidebar traversal", () => {    const threads = [      ThreadId.make("thread-1"),      ThreadId.make("thread-2"),      ThreadId.make("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([        {

Lines 1–500 of 1139.