import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";import { archiveSelectedThreadEntries, buildMultiSelectThreadContextMenuItems, createThreadJumpHintVisibilityController, getSidebarThreadIdsToPrewarm, getVisibleSidebarThreadIds, resolveAdjacentThreadId, getFallbackThreadIdAfterDelete, getVisibleThreadsForProject, getProjectSortTimestamp, hasUnseenCompletion, isContextMenuPointerDown, isTrailingDoubleClick, orderItemsByPreferredIds, resolveProjectStatusIndicator, resolveSidebarStageBadgeLabel, resolveThreadRowClassName, resolveSidebarV2Status, resolveThreadStatusPill, resolveWorkingStartedAt, formatWorkingDurationLabel, shouldNavigateAfterProjectRemoval, shouldClearThreadSelectionOnMouseDown, sortLogicalProjectsForSidebar, sortSettledThreadsForSidebarV2, sortThreadsForSidebarV2, 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("shouldNavigateAfterProjectRemoval", () => { const projectThreads = [{ environmentId: "environment-local", id: "thread-1" }]; it("navigates away from a draft route owned by the removed project", () => { expect( shouldNavigateAfterProjectRemoval({ routeTarget: { kind: "draft", draftId: "draft-1" as never }, projectThreads, projectDraftId: "draft-1", }), ).toBe(true); }); it("does not navigate away from a different draft route", () => { expect( shouldNavigateAfterProjectRemoval({ routeTarget: { kind: "draft", draftId: "draft-2" as never }, projectThreads, projectDraftId: "draft-1", }), ).toBe(false); }); it("navigates away from a server thread owned by the removed project", () => { expect( shouldNavigateAfterProjectRemoval({ routeTarget: { kind: "server", threadRef: { environmentId: EnvironmentId.make("environment-local"), threadId: ThreadId.make("thread-1"), }, }, projectThreads, projectDraftId: null, }), ).toBe(true); }); it("does not navigate from an unrelated route", () => { expect( shouldNavigateAfterProjectRemoval({ routeTarget: null, projectThreads, projectDraftId: null, }), ).toBe(false); });}); describe("archiveSelectedThreadEntries", () => { const entries = [{ threadKey: "one" }, { threadKey: "two" }, { threadKey: "three" }] as const; const success = { _tag: "Success" } as const; const failure = { _tag: "Failure" } as const; it("records every entry after full success", async () => { const outcome = await archiveSelectedThreadEntries({ entries, archive: async (_entry, onArchived) => { onArchived(); return success; }, }); expect(outcome).toEqual({ archivedThreadKeys: ["one", "two", "three"], mutationFailure: null, followupFailures: [], }); }); it("stops at a mutation failure and retains prior successes", async () => { const archive = vi.fn(async (entry: (typeof entries)[number], onArchived: () => void) => { if (entry.threadKey === "two") return failure; onArchived(); return success; }); const outcome = await archiveSelectedThreadEntries({ entries, archive }); expect(archive).toHaveBeenCalledTimes(2); expect(outcome).toEqual({ archivedThreadKeys: ["one"], mutationFailure: failure, followupFailures: [], }); }); it("continues after a post-archive failure", async () => { const archive = vi.fn(async (entry: (typeof entries)[number], onArchived: () => void) => { onArchived(); return entry.threadKey === "two" ? failure : success; }); const outcome = await archiveSelectedThreadEntries({ entries, archive }); expect(archive).toHaveBeenCalledTimes(3); expect(outcome).toEqual({ archivedThreadKeys: ["one", "two", "three"], mutationFailure: null, followupFailures: [failure], }); });}); describe("buildMultiSelectThreadContextMenuItems", () => { it("offers bulk archive with the selected count", () => { expect( buildMultiSelectThreadContextMenuItems({ count: 3, hasRunningThread: false }), ).toContainEqual({ id: "archive", label: "Archive (3)", disabled: false }); }); it("disables bulk archive when a selected thread is running", () => { expect( buildMultiSelectThreadContextMenuItems({ count: 2, hasRunningThread: true }), ).toContainEqual({ id: "archive", label: "Archive (2)", disabled: true }); });}); 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 !== undefined ? overrides.startedAt : "2026-03-09T10:00:00.000Z", completedAt: overrides?.completedAt !== undefined ? 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("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(Lines 1–500 of 1426. Load more
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; import { archiveSelectedThreadEntries, buildMultiSelectThreadContextMenuItems, createThreadJumpHintVisibilityController, getSidebarThreadIdsToPrewarm, getVisibleSidebarThreadIds, resolveAdjacentThreadId, getFallbackThreadIdAfterDelete, getVisibleThreadsForProject, getProjectSortTimestamp, hasUnseenCompletion, isContextMenuPointerDown, isTrailingDoubleClick, orderItemsByPreferredIds, resolveProjectStatusIndicator, resolveSidebarStageBadgeLabel, resolveThreadRowClassName, resolveSidebarV2Status, resolveThreadStatusPill, resolveWorkingStartedAt, formatWorkingDurationLabel, shouldNavigateAfterProjectRemoval, shouldClearThreadSelectionOnMouseDown, sortLogicalProjectsForSidebar, sortSettledThreadsForSidebarV2, sortThreadsForSidebarV2, 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("shouldNavigateAfterProjectRemoval", () => { const projectThreads = [{ environmentId: "environment-local", id: "thread-1" }]; it("navigates away from a draft route owned by the removed project", () => { expect( shouldNavigateAfterProjectRemoval({ routeTarget: { kind: "draft", draftId: "draft-1" as never }, projectThreads, projectDraftId: "draft-1", }), ).toBe(true); }); it("does not navigate away from a different draft route", () => { expect( shouldNavigateAfterProjectRemoval({ routeTarget: { kind: "draft", draftId: "draft-2" as never }, projectThreads, projectDraftId: "draft-1", }), ).toBe(false); }); it("navigates away from a server thread owned by the removed project", () => { expect( shouldNavigateAfterProjectRemoval({ routeTarget: { kind: "server", threadRef: { environmentId: EnvironmentId.make("environment-local"), threadId: ThreadId.make("thread-1"), }, }, projectThreads, projectDraftId: null, }), ).toBe(true); }); it("does not navigate from an unrelated route", () => { expect( shouldNavigateAfterProjectRemoval({ routeTarget: null, projectThreads, projectDraftId: null, }), ).toBe(false); }); }); describe("archiveSelectedThreadEntries", () => { const entries = [{ threadKey: "one" }, { threadKey: "two" }, { threadKey: "three" }] as const; const success = { _tag: "Success" } as const; const failure = { _tag: "Failure" } as const; it("records every entry after full success", async () => { const outcome = await archiveSelectedThreadEntries({ entries, archive: async (_entry, onArchived) => { onArchived(); return success; }, }); expect(outcome).toEqual({ archivedThreadKeys: ["one", "two", "three"], mutationFailure: null, followupFailures: [], }); }); it("stops at a mutation failure and retains prior successes", async () => { const archive = vi.fn(async (entry: (typeof entries)[number], onArchived: () => void) => { if (entry.threadKey === "two") return failure; onArchived(); return success; }); const outcome = await archiveSelectedThreadEntries({ entries, archive }); expect(archive).toHaveBeenCalledTimes(2); expect(outcome).toEqual({ archivedThreadKeys: ["one"], mutationFailure: failure, followupFailures: [], }); }); it("continues after a post-archive failure", async () => { const archive = vi.fn(async (entry: (typeof entries)[number], onArchived: () => void) => { onArchived(); return entry.threadKey === "two" ? failure : success; }); const outcome = await archiveSelectedThreadEntries({ entries, archive }); expect(archive).toHaveBeenCalledTimes(3); expect(outcome).toEqual({ archivedThreadKeys: ["one", "two", "three"], mutationFailure: null, followupFailures: [failure], }); }); }); describe("buildMultiSelectThreadContextMenuItems", () => { it("offers bulk archive with the selected count", () => { expect( buildMultiSelectThreadContextMenuItems({ count: 3, hasRunningThread: false }), ).toContainEqual({ id: "archive", label: "Archive (3)", disabled: false }); }); it("disables bulk archive when a selected thread is running", () => { expect( buildMultiSelectThreadContextMenuItems({ count: 2, hasRunningThread: true }), ).toContainEqual({ id: "archive", label: "Archive (2)", disabled: true }); }); }); 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 !== undefined ? overrides.startedAt : "2026-03-09T10:00:00.000Z", completedAt: overrides?.completedAt !== undefined ? 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("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(