test: remove low-value sidebar tests (#38525)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-05 04:18:17 -08:00
committed by GitHub
co-authored by OrcaWin
parent a0384aa58f
commit bdcae110ab
22 changed files with 11 additions and 7821 deletions
-1
View File
@@ -535,7 +535,6 @@
1 test-no-wall-clock:src/renderer/src/components/native-chat/NativeChatMessageList.unsent-message.test.tsx
1 test-no-wall-clock:src/renderer/src/components/settings/accounts/providers/GrokAccountsSection.test.tsx
1 test-no-wall-clock:src/renderer/src/components/sidebar/worktree-list/listing/WorktreeList.lineage-agent-expansion-coupling.test.tsx
1 test-no-wall-clock:src/renderer/src/components/sidebar/worktree-list/listing/WorktreeList.lineage-child-real-card.test.tsx
1 test-no-wall-clock:src/renderer/src/components/tab-bar/open-tab-search.test.ts
1 test-no-wall-clock:src/renderer/src/components/task-page/jira/task-page-jira-saved-filter-load-failure.test.tsx
1 test-no-wall-clock:src/renderer/src/components/terminal-pane/cache-timer-seeding.test.ts
@@ -7829,18 +7829,13 @@
"src/renderer/src/components/sidebar/workspace-card/surface/WorktreeCard.single-reference-details.test.tsx": 2032,
"src/renderer/src/components/sidebar/workspace-actions/context-menu/WorktreeContextMenu.closed-menu-work.test.tsx": 1753,
"src/renderer/src/components/sidebar/workspace-actions/context-menu/WorktreeContextMenu.test.ts": 299,
"src/renderer/src/components/sidebar/worktree-list/listing/WorktreeList.card-memo-stability.test.tsx": 2053,
"src/renderer/src/components/sidebar/worktree-list/listing/WorktreeList.folder-workspace-rows.test.ts": 8880,
"src/renderer/src/components/sidebar/worktree-list/listing/WorktreeList.lineage-agent-expansion-coupling.test.tsx": 5388,
"src/renderer/src/components/sidebar/worktree-list/listing/WorktreeList.lineage-child-real-card.test.tsx": 4426,
"src/renderer/src/components/sidebar/worktree-list/listing/WorktreeList.status-lane-lineage-drop.test.tsx": 3576,
"src/renderer/src/components/sidebar/workspace-card/metadata/WorktreeMetaDialog.host-scope.test.tsx": 2215,
"src/renderer/src/components/sidebar/workspace-card/metadata/WorktreeMetaDialog.test.tsx": 7694,
"src/renderer/src/components/sidebar/workspace-card/identity/WorktreeTitleInlineRename.editor-lifecycle.test.tsx": 407,
"src/renderer/src/components/sidebar/workspace-visibility/dialog/WorktreeVisibilityDialog.test.tsx": 2602,
"src/renderer/src/components/sidebar/host-admission/ssh/add-remote-host-ssh-actions.test.ts": 232,
"src/renderer/src/components/sidebar/project-admission/completion/add-repo-skip-finalization.test.ts": 59,
"src/renderer/src/components/sidebar/worktree-list/listing/agent-send-picker-collapse.test.tsx": 2284,
"src/renderer/src/components/sidebar/chat-markdown-math-typeset.test.ts": 1336,
"src/renderer/src/components/sidebar/worktree-list/grouping/default-branch-folder-project-root.test.ts": 227,
"src/renderer/src/components/sidebar/worktree-list/grouping/default-branch-visible-under-hide-sleeping.test.ts": 261,
@@ -7854,9 +7849,7 @@
"src/renderer/src/components/sidebar/workspace-creation/folder/folder-workspace-composer-submit-host-platform.test.ts": 2264,
"src/renderer/src/components/sidebar/workspace-creation/folder/folder-workspace-composer-submit.test.ts": 2665,
"src/renderer/src/components/sidebar/worktree-list/drag/host-header-drag.test.tsx": 281,
"src/renderer/src/components/sidebar/worktree-list/grouping/host-section-project-headers.test.ts": 308,
"src/renderer/src/components/sidebar/worktree-list/grouping/host-section-reveal.test.tsx": 2294,
"src/renderer/src/components/sidebar/worktree-list/grouping/host-section-rows.test.ts": 83,
"src/renderer/src/components/sidebar/linear-agent-skill-setup-reminders.test.ts": 52,
"src/renderer/src/components/sidebar/worktree-list/listing/manual-sort-switch-toast.test.ts": 1753,
"src/renderer/src/components/sidebar/markdown-top-level-blocks.test.ts": 543,
@@ -7905,15 +7898,10 @@
"src/renderer/src/components/sidebar/worktree-list/navigation/worktree-keyboard-cycle.test.ts": 131,
"src/renderer/src/components/sidebar/worktree-list/drag/worktree-lineage-drag-drop.test.ts": 200,
"src/renderer/src/components/sidebar/worktree-list/grouping/worktree-lineage-projection.test.ts": 51,
"src/renderer/src/components/sidebar/worktree-list/grouping/worktree-list-groups-imported-worktrees.test.ts": 251,
"src/renderer/src/components/sidebar/worktree-list/grouping/worktree-list-groups-lineage-nesting.test.ts": 241,
"src/renderer/src/components/sidebar/worktree-list/grouping/worktree-list-groups-nested-project-groups.test.ts": 248,
"src/renderer/src/components/sidebar/worktree-list/grouping/worktree-list-groups-project-groups.test.ts": 239,
"src/renderer/src/components/sidebar/worktree-list/grouping/worktree-list-groups-project-host-setups.test.ts": 280,
"src/renderer/src/components/sidebar/worktree-list/grouping/worktree-list-groups-project-order.test.ts": 1861,
"src/renderer/src/components/sidebar/worktree-list/drag/pointer-flush.test.ts": 517,
"src/renderer/src/components/sidebar/worktree-list/grouping/build-rows.folder-workspace-lanes.test.ts": 271,
"src/renderer/src/components/sidebar/worktree-list/grouping/build-rows.test.ts": 234,
"src/renderer/src/components/sidebar/worktree-list/grouping/cross-project-lineage.test.ts": 248,
"src/renderer/src/components/sidebar/worktree-list/grouping/imported-rows.test.ts": 59,
"src/renderer/src/components/sidebar/worktree-list/grouping/recent-project-rank-scan-budget.test.ts": 183,
@@ -7944,7 +7932,6 @@
"src/renderer/src/components/sidebar/worktree-list/drag/worktree-manual-order-store-write.test.ts": 2144,
"src/renderer/src/components/sidebar/worktree-list/drag/worktree-manual-order.test.ts": 332,
"src/renderer/src/components/sidebar/workspace-actions/parenting/worktree-parent-eligibility.test.ts": 68,
"src/renderer/src/components/sidebar/worktree-list/drag/worktree-sidebar-drop-preview.test.ts": 80,
"src/renderer/src/components/sidebar/worktree-list/listing/worktree-sort-label-ordering.test.ts": 114,
"src/renderer/src/components/sidebar/wsl-host-offer-move.test.ts": 227,
"src/renderer/src/components/sidebar/wsl-host-offer.test.ts": 1765,
@@ -1,334 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
computeWorktreeSidebarDropPreview,
resolveWorktreeSidebarStatusDropCommitTarget
} from './worktree-sidebar-drop-preview'
const rects = [
{ worktreeId: 'done-a', groupIndex: 0, top: 80, bottom: 120 },
{ worktreeId: 'done-b', groupIndex: 1, top: 132, bottom: 172 }
]
describe('computeWorktreeSidebarDropPreview', () => {
it('computes an insertion line for a target group', () => {
expect(
computeWorktreeSidebarDropPreview({
pointerY: 151,
containerTop: 100,
scrollTop: 100,
rects,
groupIds: ['done-a', 'done-b'],
draggedIds: ['in-progress-a']
})
).toMatchObject({
dropIndex: 1,
dropIndicatorY: 129
})
})
it('returns null outside the group boundary', () => {
expect(
computeWorktreeSidebarDropPreview({
pointerY: -20,
containerTop: 100,
scrollTop: 100,
rects,
groupIds: ['done-a', 'done-b'],
draggedIds: ['in-progress-a']
})
).toBeNull()
})
it('collapses lineage child rects into the parent drag unit for preview offsets', () => {
const preview = computeWorktreeSidebarDropPreview({
pointerY: 430,
containerTop: 0,
scrollTop: 0,
rects: [
{ worktreeId: 'parent', groupIndex: 0, top: 0, bottom: 90 },
{ worktreeId: 'child-a', groupIndex: 1, top: 96, bottom: 186 },
{ worktreeId: 'child-b', groupIndex: 2, top: 192, bottom: 282 },
{ worktreeId: 'sibling', groupIndex: 3, top: 288, bottom: 388 }
],
groupIds: ['parent', 'sibling'],
draggedIds: ['parent']
})
// The parent lands after the sibling at 106px; mark 103px rather than its old bottom at 391px.
expect(preview).toMatchObject({
dropIndex: 2,
dropIndicatorY: 103
})
expect(Array.from(preview?.previewOffsetsByWorktreeId ?? [])).toEqual([['sibling', -288]])
})
it('uses one card-height placeholder for multi-select reorder previews', () => {
const preview = computeWorktreeSidebarDropPreview({
pointerY: 280,
containerTop: 0,
scrollTop: 0,
rects: [
{ worktreeId: 'a', groupIndex: 0, top: 0, bottom: 50 },
{ worktreeId: 'b', groupIndex: 1, top: 56, bottom: 106 },
{ worktreeId: 'c', groupIndex: 2, top: 112, bottom: 162 },
{ worktreeId: 'd', groupIndex: 3, top: 168, bottom: 218 },
{ worktreeId: 'e', groupIndex: 4, top: 224, bottom: 274 }
],
groupIds: ['a', 'b', 'c', 'd', 'e'],
draggedIds: ['b', 'c', 'd'],
draggingWorktreeId: 'b'
})
// Rows slide upward to open a slot at 224px; mark 221px rather than the old 277px slot.
expect(preview).toMatchObject({
dropIndex: 5,
dropIndicatorY: 221
})
expect(Array.from(preview?.previewOffsetsByWorktreeId ?? [])).toEqual([
['c', -56],
['d', -56],
['e', -56]
])
})
it('uses the grabbed selected card as the multi-select preview placeholder', () => {
const preview = computeWorktreeSidebarDropPreview({
pointerY: 280,
containerTop: 0,
scrollTop: 0,
rects: [
{ worktreeId: 'a', groupIndex: 0, top: 0, bottom: 50 },
{ worktreeId: 'b', groupIndex: 1, top: 56, bottom: 106 },
{ worktreeId: 'c', groupIndex: 2, top: 112, bottom: 162 },
{ worktreeId: 'd', groupIndex: 3, top: 168, bottom: 218 },
{ worktreeId: 'e', groupIndex: 4, top: 224, bottom: 274 }
],
groupIds: ['a', 'b', 'c', 'd', 'e'],
draggedIds: ['b', 'c', 'd'],
draggingWorktreeId: 'd'
})
expect(preview).toMatchObject({
dropIndex: 5,
dropIndicatorY: 221
})
expect(Array.from(preview?.previewOffsetsByWorktreeId ?? [])).toEqual([['e', -56]])
})
it('marks the gap the row previews open, not the displaced card top', () => {
// An expanded 404px neighbor exposes card-height errors in insertion-line placement.
const rects = [
{ worktreeId: 'a', groupIndex: 0, top: 0, bottom: 116 },
{ worktreeId: 'b', groupIndex: 1, top: 122, bottom: 238 },
{ worktreeId: 'expanded', groupIndex: 2, top: 244, bottom: 648 },
{ worktreeId: 'd', groupIndex: 3, top: 654, bottom: 770 }
]
const groupIds = ['a', 'b', 'expanded', 'd']
const preview = computeWorktreeSidebarDropPreview({
pointerY: 700,
containerTop: 0,
scrollTop: 0,
rects,
groupIds,
draggedIds: ['a'],
draggingWorktreeId: 'a',
grab: { offsetY: 58, height: 116 }
})!
// 'a' lands last, so every other card slides up by its 116px + 6px gap.
const offsets = preview.previewOffsetsByWorktreeId
expect(offsets.get('b')).toBe(-122)
expect(offsets.get('expanded')).toBe(-122)
expect(offsets.get('d')).toBe(-122)
// d ends at 532..648, so a's slot opens at 654 and the line marks 651.
expect(preview.dropIndicatorY).toBe(651)
})
it('resolves the same slot wherever a tall card is grabbed', () => {
const rects = [
{ worktreeId: 'a', groupIndex: 0, top: 0, bottom: 116 },
{ worktreeId: 'b', groupIndex: 1, top: 122, bottom: 238 },
{ worktreeId: 'expanded', groupIndex: 2, top: 244, bottom: 648 }
]
const groupIds = ['a', 'b', 'expanded']
const height = 404
const dropIndexes = [0.05, 0.5, 0.95].map((fraction) => {
const offsetY = height * fraction
return computeWorktreeSidebarDropPreview({
pointerY: 122 + offsetY,
containerTop: 0,
scrollTop: 0,
rects,
groupIds,
draggedIds: ['expanded'],
draggingWorktreeId: 'expanded',
grab: { offsetY, height }
})!.dropIndex
})
expect(new Set(dropIndexes).size).toBe(1)
})
it('keeps a downward end drop stable when leading rows are virtualized', () => {
const groupIds = ['a', 'b', 'c', 'd', 'e', 'f']
const mountedRects = [
{ worktreeId: 'd', groupIndex: 3, top: 168, bottom: 218 },
{ worktreeId: 'e', groupIndex: 4, top: 224, bottom: 274 },
{ worktreeId: 'f', groupIndex: 5, top: 280, bottom: 330 }
]
const input = {
pointerY: 320,
containerTop: 0,
scrollTop: 0,
rects: mountedRects,
groupIds,
draggedIds: ['a'],
draggingWorktreeId: 'a',
grab: { offsetY: 25, height: 50 }
}
const first = computeWorktreeSidebarDropPreview(input)!
const held = computeWorktreeSidebarDropPreview({
...input,
anchor: { beforeWorktreeId: first.dropAnchorId, pointerY: 320, scrollTop: 0 }
})!
for (const preview of [first, held]) {
expect(preview).toMatchObject({
dropIndex: 6,
dropIndicatorY: 277,
dropAnchorId: null
})
expect(Array.from(preview.previewOffsetsByWorktreeId)).toEqual([
['d', -56],
['e', -56],
['f', -56]
])
}
})
it('uses the full group index when the dragged row is outside the mounted window', () => {
const preview = computeWorktreeSidebarDropPreview({
pointerY: 193,
containerTop: 0,
scrollTop: 0,
rects: [
{ worktreeId: 'd', groupIndex: 3, top: 168, bottom: 218 },
{ worktreeId: 'e', groupIndex: 4, top: 224, bottom: 274 },
{ worktreeId: 'f', groupIndex: 5, top: 280, bottom: 330 }
],
groupIds: ['a', 'b', 'c', 'd', 'e', 'f'],
draggedIds: ['e'],
draggingWorktreeId: 'e',
grab: { offsetY: 25, height: 50 }
})
expect(preview?.dropIndex).toBe(3)
})
it('preserves the virtual row gap when only one row remains mounted', () => {
const preview = computeWorktreeSidebarDropPreview({
pointerY: 305,
containerTop: 0,
scrollTop: 0,
rects: [{ worktreeId: 'f', groupIndex: 5, top: 280, bottom: 330 }],
groupIds: ['a', 'b', 'c', 'd', 'e', 'f'],
draggedIds: ['a'],
draggingWorktreeId: 'a',
fallbackGap: 6,
grab: { offsetY: 25, height: 50 }
})
expect(preview).toMatchObject({ dropIndex: 6, dropIndicatorY: 277 })
expect(Array.from(preview?.previewOffsetsByWorktreeId ?? [])).toEqual([['f', -56]])
})
})
describe('resolveWorktreeSidebarStatusDropCommitTarget', () => {
const preview = {
dropIndex: 1,
dropIndicatorY: 129,
previewOffsetsByWorktreeId: new Map<string, number>(),
dropAnchorId: null
}
it('uses the current status target when pointerup hit-testing succeeds', () => {
expect(
resolveWorktreeSidebarStatusDropCommitTarget({
currentTarget: { status: 'completed', isPinDrop: false, lineageParentId: null },
currentPreview: preview,
latestTrackedTarget: {
target: { status: 'in-progress', isPinDrop: false, lineageParentId: null },
preview: null,
x: 100,
y: 100
},
x: 100,
y: 100
})
).toEqual({
target: { status: 'completed', isPinDrop: false, lineageParentId: null },
preview
})
})
it('reuses the latest status target when pointerup hit-testing blanks at the same point', () => {
expect(
resolveWorktreeSidebarStatusDropCommitTarget({
currentTarget: { status: null, isPinDrop: false, lineageParentId: null },
currentPreview: null,
latestTrackedTarget: {
target: { status: 'completed', isPinDrop: false, lineageParentId: null },
preview,
x: 100,
y: 100
},
x: 102,
y: 101
})
).toEqual({
target: { status: 'completed', isPinDrop: false, lineageParentId: null },
preview
})
})
it('reuses the latest lineage target when pointerup hit-testing blanks at the same point', () => {
expect(
resolveWorktreeSidebarStatusDropCommitTarget({
currentTarget: { status: null, isPinDrop: false, lineageParentId: null },
currentPreview: null,
latestTrackedTarget: {
target: { status: null, isPinDrop: false, lineageParentId: 'parent-worktree' },
preview: null,
x: 100,
y: 100
},
x: 102,
y: 101
})
).toEqual({
target: { status: null, isPinDrop: false, lineageParentId: 'parent-worktree' },
preview: null
})
})
it('does not reuse a stale status target after the pointer has moved away', () => {
expect(
resolveWorktreeSidebarStatusDropCommitTarget({
currentTarget: { status: null, isPinDrop: false, lineageParentId: null },
currentPreview: null,
latestTrackedTarget: {
target: { status: 'completed', isPinDrop: false, lineageParentId: null },
preview,
x: 100,
y: 100
},
x: 140,
y: 100
})
).toEqual({
target: { status: null, isPinDrop: false, lineageParentId: null },
preview: null
})
})
})
@@ -1,258 +0,0 @@
import { describe, expect, it } from 'vitest'
import { buildRows } from './build-rows.test-fixture'
import { getFolderWorkspaceLaneKey } from '../../../../../../shared/sidebar-list/folder-workspace-lanes'
import { getPRLaneKey } from '../../../../../../shared/sidebar-list/group-keys'
import { getPRGroupKey } from './pr-group-key'
import { getRenderRowKey } from '../listing/render-row'
import type { Row, WorktreeGroupBy } from '../../../../../../shared/sidebar-list/row-types'
import { repo, worktree } from './worktree-list-groups-test-fixtures'
import type { FolderWorkspace } from '../../../../../../shared/folder-workspaces/folder-workspace-types'
import type { ProjectGroup } from '../../../../../../shared/project-group-types'
import type { Repo } from '../../../../../../shared/repo-types'
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
const GROUP: ProjectGroup = {
id: 'group-1',
name: 'Multi-repo Group',
parentPath: '/tmp/parent',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
function makeFolderWorkspace(overrides: Partial<FolderWorkspace> = {}): FolderWorkspace {
return {
id: 'fw-1',
projectGroupId: GROUP.id,
name: 'Folder workspace',
folderPath: '/tmp/parent',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 1,
lastActivityAt: 1,
createdAt: 1,
updatedAt: 1,
workspaceStatus: 'in-progress',
...overrides
}
}
const GROUPED_REPO: Repo = { ...repo, projectGroupId: GROUP.id }
function buildSidebarRows(options: {
groupBy: WorktreeGroupBy
folderWorkspaces?: readonly FolderWorkspace[]
projectGroups?: readonly ProjectGroup[]
worktrees?: (typeof worktree)[]
collapsedGroups?: Set<string>
}): Row[] {
const worktrees = options.worktrees ?? [worktree]
return buildRows(
options.groupBy,
worktrees,
new Map([[GROUPED_REPO.id, GROUPED_REPO]]),
null,
options.collapsedGroups ?? new Set<string>(),
undefined,
undefined,
'manual',
{},
new Map(worktrees.map((entry) => [entry.id, entry])),
false,
undefined,
options.projectGroups ?? [GROUP],
new Set(),
new Map(),
new Map(),
[],
undefined,
options.folderWorkspaces ?? [makeFolderWorkspace()]
)
}
function folderRows(rows: Row[]): Extract<Row, { type: 'folder-workspace' }>[] {
return rows.filter(
(row): row is Extract<Row, { type: 'folder-workspace' }> => row.type === 'folder-workspace'
)
}
const ALL_GROUP_BY: WorktreeGroupBy[] = ['repo', 'workspace-status', 'pr-status', 'none']
describe('folder workspaces render under every Group by mode', () => {
// Non-repo modes exercise the fix; repo mode guards existing behavior.
for (const groupBy of ALL_GROUP_BY) {
it(`emits the folder-workspace row when groupBy is ${groupBy}`, () => {
expect(folderRows(buildSidebarRows({ groupBy }))).toHaveLength(1)
})
}
it('does not duplicate the row under repo grouping', () => {
const rows = buildSidebarRows({ groupBy: 'repo' })
expect(folderRows(rows).map((row) => row.key)).toEqual(['folder-workspace:local|folder:fw-1'])
})
it('keys the same folder id on two hosts apart, as the desktop draws them', () => {
const folders = [
makeFolderWorkspace(),
makeFolderWorkspace({ executionHostId: 'runtime:env-1', connectionId: null }),
makeFolderWorkspace({ connectionId: 'gpu-vm' })
]
const rows = folderRows(buildSidebarRows({ groupBy: 'none', folderWorkspaces: folders }))
expect(new Set(rows.map((row) => row.key))).toEqual(
new Set([
'folder-workspace:local|folder:fw-1',
'folder-workspace:runtime:env-1|folder:fw-1',
'folder-workspace:ssh:gpu-vm|folder:fw-1'
])
)
expect(rows.map(getRenderRowKey)).toEqual(rows.map((row) => row.key))
})
})
describe('a folder workspace can be the only member of a lane', () => {
it('creates its status lane with no worktrees present', () => {
const rows = buildSidebarRows({ groupBy: 'workspace-status', worktrees: [] })
expect(folderRows(rows)).toHaveLength(1)
const header = rows.find((row) => row.type === 'header')
expect(header).toBeDefined()
expect(header && 'count' in header ? header.count : null).toBe(1)
})
it('renders in flat mode with no worktrees present', () => {
const rows = buildSidebarRows({ groupBy: 'none', worktrees: [] })
expect(folderRows(rows)).toHaveLength(1)
})
})
describe('lane assignment', () => {
it('routes to the same PR lane as a worktree with no PR', () => {
const laneKey = getFolderWorkspaceLaneKey(
{ folderWorkspace: makeFolderWorkspace(), projectGroup: GROUP },
'pr-status',
[]
)
const noPrWorktreeLane = getPRLaneKey(
getPRGroupKey(worktree, new Map([[GROUPED_REPO.id, GROUPED_REPO]]), null)
)
expect(laneKey).toBe(noPrWorktreeLane)
})
})
describe('ordering', () => {
const ordered = [
makeFolderWorkspace({ id: 'fw-low', name: 'Low', sortOrder: 1 }),
makeFolderWorkspace({ id: 'fw-high', name: 'High', sortOrder: 9 })
]
// Both emitters are covered: grouped lanes and the separate flat-mode path.
for (const groupBy of ['workspace-status', 'none'] as WorktreeGroupBy[]) {
it(`orders by manualOrder then sortOrder under ${groupBy}`, () => {
const rows = buildSidebarRows({ groupBy, folderWorkspaces: ordered })
expect(folderRows(rows).map((row) => row.folderWorkspace.id)).toEqual(['fw-high', 'fw-low'])
})
it(`prefers manualOrder over sortOrder under ${groupBy}`, () => {
const rows = buildSidebarRows({
groupBy,
folderWorkspaces: [
makeFolderWorkspace({ id: 'fw-low', name: 'Low', sortOrder: 9, manualOrder: 1 }),
makeFolderWorkspace({ id: 'fw-high', name: 'High', sortOrder: 1, manualOrder: 9 })
]
})
expect(folderRows(rows).map((row) => row.folderWorkspace.id)).toEqual(['fw-high', 'fw-low'])
})
}
})
describe('membership is decided once, not per mode', () => {
// Pair this negative case with a positive non-repo case so missing folder rows cannot pass vacuously.
it('renders nothing when the owning project group is not visible', () => {
const rows = buildSidebarRows({ groupBy: 'workspace-status', projectGroups: [] })
expect(folderRows(rows)).toHaveLength(0)
})
it('renders under a non-repo mode when the owning group is visible', () => {
const rows = buildSidebarRows({ groupBy: 'workspace-status' })
expect(folderRows(rows)).toHaveLength(1)
})
it('keeps archived folder workspaces behaving identically in every mode', () => {
const archived = [makeFolderWorkspace({ isArchived: true })]
const counts = ALL_GROUP_BY.map(
(groupBy) => folderRows(buildSidebarRows({ groupBy, folderWorkspaces: archived })).length
)
// Keep archive handling consistent across modes; none currently filters folder workspaces by isArchived.
expect(counts).toEqual([1, 1, 1, 1])
})
})
describe('host bookkeeping for lanes containing folder workspaces', () => {
const SSH_HOST = 'ssh:target-1' as ExecutionHostId
it('scopes a collapsed folder-only lane header to its host', () => {
const sshGroup: ProjectGroup = { ...GROUP, connectionId: 'target-1' }
const rows = buildRows(
'workspace-status',
[],
new Map(),
null,
new Set(['workspace-status:in-progress']),
undefined,
undefined,
'manual',
{},
new Map(),
false,
undefined,
[sshGroup],
new Set(),
new Map(),
new Map(),
[],
undefined,
[makeFolderWorkspace()]
)
const header = rows.find((row) => row.type === 'header')
expect(header).toBeDefined()
const counts = header && 'hostWorktreeCounts' in header ? header.hostWorktreeCounts : undefined
expect(counts).toBeDefined()
expect(counts?.get(SSH_HOST)).toBe(1)
})
it('gives a folder-only host an explicit empty id array', () => {
const sshGroup: ProjectGroup = { ...GROUP, connectionId: 'target-1' }
const rows = buildRows(
'workspace-status',
[],
new Map(),
null,
new Set(),
undefined,
undefined,
'manual',
{},
new Map(),
false,
undefined,
[sshGroup],
new Set(),
new Map(),
new Map(),
[],
undefined,
[makeFolderWorkspace()]
)
const header = rows.find((row) => row.type === 'header')
const ids = header && 'hostWorktreeIds' in header ? header.hostWorktreeIds : undefined
// Publish the empty host key or host-section fallback will leak the global worktree IDs.
expect(ids?.has(SSH_HOST)).toBe(true)
expect(ids?.get(SSH_HOST)).toEqual([])
})
})
@@ -1,357 +0,0 @@
import { describe, expect, it } from 'vitest'
import { buildRows } from './build-rows.test-fixture'
import { PINNED_GROUP_KEY } from '../../../../../../shared/sidebar-list/group-keys'
import { getPRGroupKey } from './pr-group-key'
import { getGroupKeyForWorktree } from '../../../../../../shared/sidebar-list/worktree-group-keys'
import { repo, worktree, repoMap } from './worktree-list-groups-test-fixtures'
import type { Repo } from '../../../../../../shared/repo-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
describe('getPRGroupKey', () => {
it('puts merged PRs in the done group', () => {
const prCache = {
'repo-1::feature/super-critical': {
data: { state: 'merged' }
}
}
expect(getPRGroupKey(worktree, repoMap, prCache)).toBe('done')
})
it('treats a matching suppressed PR as in progress', () => {
const prCache = {
'repo-1::feature/super-critical': {
data: { number: 42, state: 'merged' }
}
}
expect(
getPRGroupKey({ ...worktree, linkedPR: null, suppressedGitHubPR: 42 }, repoMap, prCache)
).toBe('in-progress')
})
it('keeps a different PR in its review-status group', () => {
const prCache = {
'repo-1::feature/super-critical': {
data: { number: 43, state: 'merged' }
}
}
expect(
getPRGroupKey({ ...worktree, linkedPR: null, suppressedGitHubPR: 42 }, repoMap, prCache)
).toBe('done')
})
it('prefers repo-scoped PR status over stale legacy path-scoped status', () => {
const prCache = {
'/tmp/orca::feature/super-critical': {
data: { state: 'closed' }
},
'repo-1::feature/super-critical': {
data: { state: 'merged' }
}
}
expect(getPRGroupKey(worktree, repoMap, prCache)).toBe('done')
})
it('falls back to legacy path-scoped PR status when no repo-scoped entry exists', () => {
const prCache = {
'/tmp/orca::feature/super-critical': {
data: { state: 'closed' }
}
}
expect(getPRGroupKey(worktree, repoMap, prCache)).toBe('closed')
})
it('uses SSH-scoped PR cache entries instead of local entries for SSH repos', () => {
const sshRepo = { ...repo, connectionId: 'ssh-1' }
const sshRepoMap = new Map([[sshRepo.id, sshRepo]])
const prCache = {
'repo-1::feature/super-critical': {
data: { state: 'merged' }
},
'ssh:ssh-1::repo-1::feature/super-critical': {
data: { state: 'closed' }
}
}
expect(getPRGroupKey(worktree, sshRepoMap, prCache)).toBe('closed')
})
})
describe('getGroupKeyForWorktree', () => {
it('returns the all group key for the ungrouped mode', () => {
expect(getGroupKeyForWorktree('none', worktree, repoMap, null)).toBe('all')
})
it('returns a workspace-status key only in status grouping mode', () => {
expect(getGroupKeyForWorktree('workspace-status', worktree, repoMap, null)).toBe(
'workspace-status:in-progress'
)
})
})
describe('buildRows with pinned worktrees', () => {
const pinned = { ...worktree, id: 'wt-pinned', isPinned: true, displayName: 'pinned-feature' }
const unpinned1 = { ...worktree, id: 'wt-1', displayName: 'alpha' }
const unpinned2 = { ...worktree, id: 'wt-2', displayName: 'beta' }
it('emits Pinned and All headers in groupBy none', () => {
const rows = buildRows('none', [unpinned1, pinned, unpinned2], repoMap, null, new Set())
expect(rows[0]).toMatchObject({ type: 'header', key: 'pinned', icon: { kind: 'pinned' } })
expect(rows[1]).toMatchObject({ type: 'item', worktree: { id: 'wt-pinned' } })
expect(rows[2]).toMatchObject({ type: 'header', key: 'all', icon: { kind: 'all' }, count: 2 })
expect(rows[2]).toMatchObject({ type: 'header', icon: { kind: 'all' } })
})
it('uses worktree host ownership for pinned header host counts', () => {
const runtimePinned = {
...pinned,
hostId: 'runtime:03ef704c-b180-4b10-998d-e28fbd5de9a3' as const
}
const rows = buildRows(
'none',
[runtimePinned, unpinned1],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([
[runtimePinned.id, runtimePinned],
[unpinned1.id, unpinned1]
])
)
const pinnedHeader = rows[0]
expect(pinnedHeader).toMatchObject({ type: 'header', key: 'pinned' })
expect(pinnedHeader.type === 'header' ? pinnedHeader.hostWorktreeCounts : undefined).toEqual(
new Map([['runtime:03ef704c-b180-4b10-998d-e28fbd5de9a3', 1]])
)
})
it('groups all worktrees under All in groupBy none', () => {
const rows = buildRows('none', [unpinned1, unpinned2], repoMap, null, new Set())
expect(rows).toMatchObject([
{ type: 'header', key: 'all', icon: { kind: 'all' } },
{ type: 'item', worktree: { id: 'wt-1' } },
{ type: 'item', worktree: { id: 'wt-2' } }
])
})
it('moves pinned worktrees out of the All group', () => {
const rows = buildRows('none', [unpinned1, pinned, unpinned2], repoMap, null, new Set())
expect(rows).toMatchObject([
{ type: 'header', key: 'pinned' },
{ type: 'item', worktree: { id: 'wt-pinned' } },
{ type: 'header', key: 'all', count: 2 },
{ type: 'item', worktree: { id: 'wt-1' } },
{ type: 'item', worktree: { id: 'wt-2' } }
])
})
it('duplicates pinned worktrees into All when the policy allows it', () => {
const rows = buildRows(
'none',
[unpinned1, pinned, unpinned2],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{},
undefined,
false,
{ showPinnedWorktreesInGroups: true } as never
)
expect(rows).toMatchObject([
{ type: 'header', key: 'pinned' },
{ type: 'item', sectionKey: PINNED_GROUP_KEY, worktree: { id: 'wt-pinned' } },
{ type: 'header', key: 'all', count: 3 },
{ type: 'item', sectionKey: 'all', worktree: { id: 'wt-1' } },
{ type: 'item', sectionKey: 'all', worktree: { id: 'wt-pinned' } },
{ type: 'item', sectionKey: 'all', worktree: { id: 'wt-2' } }
])
})
it('collapses the All group in groupBy none', () => {
const rows = buildRows('none', [unpinned1, pinned, unpinned2], repoMap, null, new Set(['all']))
expect(rows).toMatchObject([
{ type: 'header', key: 'pinned' },
{ type: 'item', worktree: { id: 'wt-pinned' } },
{ type: 'header', key: 'all', count: 2 }
])
})
it('emits status headers for unpinned matching worktrees in groupBy workspace-status', () => {
const rows = buildRows(
'workspace-status',
[unpinned1, pinned, unpinned2],
repoMap,
null,
new Set()
)
expect(rows[2]).toMatchObject({
type: 'header',
key: 'workspace-status:in-progress',
label: 'In progress',
count: 2
})
expect(rows[3]).toMatchObject({ type: 'item', worktree: { id: 'wt-1' } })
expect(rows[4]).toMatchObject({ type: 'item', worktree: { id: 'wt-2' } })
})
it('duplicates pinned worktrees into status groups when the policy allows it', () => {
const rows = buildRows(
'workspace-status',
[unpinned1, pinned],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{},
undefined,
false,
{ showPinnedWorktreesInGroups: true } as never
)
expect(rows).toMatchObject([
{ type: 'header', key: 'pinned', count: 1 },
{ type: 'item', sectionKey: PINNED_GROUP_KEY, worktree: { id: 'wt-pinned' } },
{ type: 'header', key: 'workspace-status:in-progress', count: 2 },
{ type: 'item', sectionKey: 'workspace-status:in-progress', worktree: { id: 'wt-1' } },
{ type: 'item', sectionKey: 'workspace-status:in-progress', worktree: { id: 'wt-pinned' } }
])
})
it('moves pinned items out of regular groups in pr-status mode', () => {
const rows = buildRows('pr-status', [unpinned1, pinned], repoMap, null, new Set())
const pinnedHeader = rows.find((r) => r.type === 'header' && r.key === 'pinned')
expect(pinnedHeader).toBeDefined()
const prGroup = rows.filter((r) => r.type === 'header' && r.key.startsWith('pr:'))
for (const header of prGroup) {
if (header.type === 'header') {
expect(header.count).toBe(1)
}
}
})
it('omits empty pinned sections in groupBy workspace-status', () => {
const rows = buildRows('workspace-status', [unpinned1, unpinned2], repoMap, null, new Set())
expect(rows[0]).toMatchObject({
type: 'header',
key: 'workspace-status:in-progress',
label: 'In progress'
})
expect(rows[1]).toMatchObject({ type: 'item', worktree: { id: 'wt-1' } })
expect(rows[2]).toMatchObject({ type: 'item', worktree: { id: 'wt-2' } })
})
it('collapses pinned group when in collapsedGroups', () => {
const rows = buildRows(
'workspace-status',
[pinned, unpinned1],
repoMap,
null,
new Set(['pinned'])
)
expect(rows[0]).toMatchObject({ type: 'header', key: 'pinned' })
expect(rows[1]).toMatchObject({ type: 'header', key: 'workspace-status:in-progress' })
expect(rows[2]).toMatchObject({ type: 'item', worktree: { id: 'wt-1' } })
})
it('omits status sections when all matching worktrees are pinned', () => {
const allPinned = { ...unpinned1, isPinned: true }
const rows = buildRows('workspace-status', [pinned, allPinned], repoMap, null, new Set())
expect(rows.filter((r) => r.type === 'header')).toHaveLength(1)
expect(rows[0]).toMatchObject({ type: 'header', key: 'pinned', count: 2 })
})
it('preserves repo display casing in group labels', () => {
const lowercaseRepo = { ...repo, displayName: 'c15t' }
const rows = buildRows('repo', [worktree], new Map([[repo.id, lowercaseRepo]]), null, new Set())
expect(rows[0]).toMatchObject({ type: 'header', label: 'c15t' })
})
it('groups folder-mode workspaces under their folder name', () => {
const folderRepo: Repo = {
...repo,
id: 'folder-1',
path: '/tmp/design-assets',
displayName: 'design-assets',
kind: 'folder'
}
const folderWorktree: Worktree = {
...worktree,
id: 'folder-1::/tmp/design-assets',
repoId: folderRepo.id,
path: folderRepo.path,
branch: '',
displayName: folderRepo.displayName,
isMainWorktree: true
}
const rows = buildRows(
'repo',
[folderWorktree],
new Map([[folderRepo.id, folderRepo]]),
null,
new Set()
)
expect(rows[0]).toMatchObject({
type: 'header',
key: 'repo:folder-1',
label: 'design-assets',
repo: folderRepo
})
expect(rows[1]).toMatchObject({ type: 'item', worktree: { id: folderWorktree.id } })
})
it('emits assigned workspace statuses as sections in groupBy workspace-status', () => {
const review = { ...worktree, id: 'wt-review', workspaceStatus: 'in-review' as const }
const rows = buildRows('workspace-status', [review], repoMap, null, new Set())
expect(
rows.filter((r) => r.type === 'header').map((r) => ({ key: r.key, label: r.label }))
).toEqual([{ key: 'workspace-status:in-review', label: 'In review' }])
})
it('uses customized workspace status labels and order', () => {
const customStatuses = [
{ id: 'blocked', label: 'Blocked' },
{ id: 'todo', label: 'Ready' },
{ id: 'in-progress', label: 'Doing' }
]
const blocked = { ...worktree, id: 'wt-blocked', workspaceStatus: 'blocked' }
const doing = { ...worktree, id: 'wt-doing', workspaceStatus: 'in-progress' }
const rows = buildRows(
'workspace-status',
[doing, blocked],
repoMap,
null,
new Set(),
undefined,
customStatuses
)
expect(
rows.filter((r) => r.type === 'header').map((r) => ({ key: r.key, label: r.label }))
).toEqual([
{ key: 'workspace-status:blocked', label: 'Blocked' },
{ key: 'workspace-status:in-progress', label: 'Doing' }
])
})
})
@@ -1,238 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { Repo } from '../../../../../../shared/repo-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
import {
addHostSectionRows,
type HostSectionOption,
type HostSectionRow
} from '../../../../../../shared/sidebar-list/host-section-rows'
import { planSidebarRows } from '../../../../../../shared/sidebar-list/sidebar-row-plan'
import { getProjectHeaderHostRepos } from '../../../../../../shared/sidebar-list/host-labels'
import { createHostScopedRepoOf } from '../../../../../../shared/sidebar-list/sidebar-feed-index'
import {
getRepoExecutionHostId,
type ExecutionHostId
} from '../../../../../../shared/execution-host'
import { cloneDefaultWorkspaceStatuses } from '../../../../../../shared/workspace-statuses'
import {
repo,
remoteRepo,
worktree,
remoteWorktree,
project,
projectHostSetups
} from './worktree-list-groups-test-fixtures'
const hosts: HostSectionOption[] = [
{ id: 'local', kind: 'local', label: 'Local Mac', detail: '', health: 'local' },
{ id: 'ssh:gpu-vm', kind: 'ssh', label: 'Builder', detail: '', health: 'available' }
]
const otherRepo: Repo = { ...repo, id: 'repo-other', path: '/tmp/other', displayName: 'other' }
const otherWorktree: Worktree = {
...worktree,
id: 'wt-other',
repoId: otherRepo.id,
path: '/tmp/other-feature'
}
function hostSectionRows(collapsedGroups: Set<string>): HostSectionRow[] {
const worktrees = [worktree, otherWorktree, remoteWorktree]
const rows = planSidebarRows({
groupBy: 'repo',
sortBy: 'smart',
worktrees,
repoMap: new Map([repo, otherRepo, remoteRepo].map((entry) => [entry.id, entry])),
prGroupOf: () => 'in-progress',
workspaceStatuses: cloneDefaultWorkspaceStatuses(),
projectOrderBy: 'manual',
repoOrder: new Map([
[repo.id, 0],
[otherRepo.id, 1],
[remoteRepo.id, 2]
]),
lineageById: {},
worktreeMap: new Map(worktrees.map((row) => [row.id, row])),
nestLineage: false,
projectGroups: [],
placeholderRepoIds: new Set(),
importedWorktreesByRepo: new Map(),
newExternalWorktreesInboxByRepo: new Map(),
pendingCreations: [],
projectGrouping: { projects: [project], projectHostSetups },
folderWorkspaces: [],
hostLabelById: new Map(hosts.map((host) => [host.id, host.label])),
defaultHostId: 'local',
pinnedDisplayPolicy: 'single-location',
filterRepoIds: []
}).render(collapsedGroups)
return addHostSectionRows({
rows,
hostOptions: hosts,
workspaceHostScope: 'all',
visibleWorkspaceHostIds: hosts.map((host) => host.id),
defaultHostId: 'local',
localHostLabel: 'Local Mac',
preferProjectGrouping: true
})
}
function describeRow(row: HostSectionRow): string {
switch (row.type) {
case 'host-header':
return `${row.key} (${row.count})`
case 'header':
return `${row.key} @${row.repo?.id} (${row.count})`
case 'item':
return row.worktree.id
case 'folder-workspace':
case 'imported-worktrees-card':
case 'new-external-worktrees-inbox':
case 'pending-creation':
return row.type
}
}
describe('a project checked out on two hosts, in host sections', () => {
it("heads each host's rows with that host's own project header", () => {
expect(hostSectionRows(new Set()).map(describeRow)).toEqual([
'host:local (2)',
`project:${project.id} @${repo.id} (1)`,
worktree.id,
`repo:${otherRepo.id} @${otherRepo.id} (1)`,
otherWorktree.id,
'host:ssh:gpu-vm (1)',
`project:${project.id} @${remoteRepo.id} (1)`,
remoteWorktree.id
])
})
it('keeps a collapsed copy in each host section with that host’s count', () => {
const rows = hostSectionRows(new Set([`project:${project.id}`]))
expect(rows.map(describeRow)).toEqual([
'host:local (2)',
`project:${project.id} @${repo.id} (1)`,
`repo:${otherRepo.id} @${otherRepo.id} (1)`,
otherWorktree.id,
'host:ssh:gpu-vm (1)',
`project:${project.id} @${remoteRepo.id} (1)`
])
expect(rows.flatMap((row) => (row.type === 'header' && row.repo ? [row.hostId] : []))).toEqual([
'local',
'local',
'ssh:gpu-vm'
])
})
})
describe('one repo id registered on two hosts, in host sections', () => {
const localTwin: Repo = { ...repo, id: 'repo-twin' }
const sshTwin: Repo = { ...localTwin, path: '/home/alice/twin', connectionId: 'gpu-vm' }
const runtimeHost: HostSectionOption = {
id: 'runtime:env',
kind: 'runtime',
label: 'Server',
detail: '',
health: 'available'
}
const runtimeRepo: Repo = { ...repo, id: 'repo-runtime', executionHostId: 'runtime:env' }
function twinSectionRows(
worktrees: Worktree[],
repoMapOrder: Repo[],
visibleWorkspaceHostIds: ExecutionHostId[]
): HostSectionRow[] {
const repoMap = new Map(repoMapOrder.map((entry) => [entry.id, entry]))
const rows = planSidebarRows({
groupBy: 'repo',
sortBy: 'smart',
worktrees,
repoMap,
repoOfWorktree: createHostScopedRepoOf([localTwin, sshTwin, runtimeRepo], repoMap),
prGroupOf: () => 'in-progress',
workspaceStatuses: cloneDefaultWorkspaceStatuses(),
projectOrderBy: 'manual',
lineageById: {},
worktreeMap: new Map(worktrees.map((row) => [row.id, row])),
nestLineage: false,
projectGroups: [],
placeholderRepoIds: new Set(),
importedWorktreesByRepo: new Map(),
newExternalWorktreesInboxByRepo: new Map(),
pendingCreations: [],
folderWorkspaces: [],
defaultHostId: 'local',
pinnedDisplayPolicy: 'single-location',
filterRepoIds: []
}).render(new Set())
return addHostSectionRows({
rows,
hostOptions: [...hosts, runtimeHost],
workspaceHostScope: 'all',
visibleWorkspaceHostIds,
defaultHostId: 'local',
localHostLabel: 'Local Mac',
preferProjectGrouping: true
})
}
function headerHosts(rows: HostSectionRow[]): [string | undefined, string | undefined][] {
return rows.flatMap((row) =>
row.type === 'header' && row.repo ? [[row.hostId, getRepoExecutionHostId(row.repo)]] : []
)
}
const sshWorktree: Worktree = {
...worktree,
id: 'wt-twin-ssh',
repoId: sshTwin.id,
hostId: 'ssh:gpu-vm'
}
const runtimeWorktree: Worktree = { ...worktree, id: 'wt-runtime', repoId: runtimeRepo.id }
it.each<[string, ExecutionHostId[]]>([
['an empty second host', ['ssh:gpu-vm', 'runtime:env']],
['one visible host', ['ssh:gpu-vm']]
])('names the shown host’s registration when %s leaves no host sections', (_, visible) => {
const rows = twinSectionRows([sshWorktree], [localTwin, runtimeRepo], visible)
expect(rows.some((row) => row.type === 'host-header')).toBe(false)
expect(headerHosts(rows)).toEqual([['ssh:gpu-vm', 'ssh:gpu-vm']])
})
it('does not open a section for a hidden host that only anchors the header', () => {
const rows = twinSectionRows(
[sshWorktree, runtimeWorktree],
[localTwin, runtimeRepo],
['ssh:gpu-vm', 'runtime:env']
)
expect(rows.filter((row) => row.type === 'host-header').map((row) => row.hostId)).toEqual([
'ssh:gpu-vm',
'runtime:env'
])
expect(headerHosts(rows)).toEqual([
['ssh:gpu-vm', 'ssh:gpu-vm'],
['runtime:env', 'runtime:env']
])
})
})
describe('a lineage child from another project on another host', () => {
it("never heads that host's copy with the child's project", () => {
const childProject: Repo = { ...remoteRepo, id: 'repo-child-project' }
const child: Worktree = { ...remoteWorktree, id: 'wt-child', repoId: childProject.id }
expect(
getProjectHeaderHostRepos({
group: { label: 'orca', items: [worktree, child], repo, repoIds: new Set([repo.id]) },
repoMap: new Map([repo, childProject].map((entry) => [entry.id, entry])),
repoOf: (row) => (row.id === child.id ? childProject : repo),
noticeRepoOf: () => [],
hasPendingCreation: () => false,
defaultHostId: 'local'
})
).toEqual(new Map([['local', repo]]))
})
})
@@ -1,866 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { FolderWorkspace } from '../../../../../../shared/folder-workspaces/folder-workspace-types'
import type { ProjectGroup } from '../../../../../../shared/project-group-types'
import type { Repo } from '../../../../../../shared/repo-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
import { PINNED_GROUP_KEY } from '../../../../../../shared/sidebar-list/group-keys'
import type { Row } from '../../../../../../shared/sidebar-list/row-types'
import {
addHostSectionRows,
type HostSectionRow
} from '../../../../../../shared/sidebar-list/host-section-rows'
function sectionRows(
args: Omit<Parameters<typeof addHostSectionRows>[0], 'localHostLabel'>
): HostSectionRow[] {
return addHostSectionRows({ ...args, localHostLabel: 'Local Mac' })
}
function repo(id: string, connectionId?: string | null): Repo {
return {
id,
path: `/${id}`,
displayName: id,
badgeColor: '#000000',
addedAt: 0,
connectionId
}
}
function worktree(id: string, repoId: string): Worktree {
return {
id,
repoId,
path: `/${repoId}/${id}`,
branch: `refs/heads/${id}`,
head: 'abc123',
isBare: false,
isMainWorktree: false,
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
comment: '',
isUnread: false,
isPinned: false,
displayName: id,
sortOrder: 0,
lastActivityAt: 0
}
}
function header(key: string, label = key): Extract<Row, { type: 'header' }> {
return {
type: 'header',
key,
label,
icon: { kind: 'project' },
count: 1
}
}
function pinnedHeader(
counts: ReadonlyMap<'local' | 'ssh:ssh-1', number>,
idsByHost?: ReadonlyMap<'local' | 'ssh:ssh-1', readonly string[]>
): Extract<Row, { type: 'header' }> {
return {
...header(PINNED_GROUP_KEY, 'Pinned'),
count: Array.from(counts.values()).reduce((sum, count) => sum + count, 0),
hostWorktreeCounts: counts,
hostWorktreeIds: idsByHost,
worktreeIds: idsByHost ? Array.from(idsByHost.values()).flat() : undefined
}
}
function repoHeader(project: Repo): Extract<Row, { type: 'header' }> {
return {
...header(`repo:${project.id}`, project.displayName),
repo: project
}
}
function item(id: string, project: Repo): Extract<Row, { type: 'item' }> {
const sectionKey = `repo:${project.id}`
return {
type: 'item',
rowKey: `${sectionKey}:${id}`,
sectionKey,
worktree: worktree(id, project.id),
repo: project,
depth: 0,
groupDepth: 0,
isLastLineageChild: true,
lineageChildCount: 0
}
}
function pinnedItem(id: string, project: Repo, sectionKey: string): Extract<Row, { type: 'item' }> {
const row = item(id, project)
row.worktree.isPinned = true
row.rowKey = `${sectionKey}:${id}`
row.sectionKey = sectionKey
return row
}
function folderWorkspaceRow(
connectionId: string | null
): Extract<Row, { type: 'folder-workspace' }> {
const projectGroup: ProjectGroup = {
id: 'group-1',
name: 'Remote folder',
parentPath: '/srv/project',
connectionId,
parentGroupId: null,
createdFrom: 'manual',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const folderWorkspace: FolderWorkspace = {
id: 'folder-1',
projectGroupId: projectGroup.id,
name: 'Folder workspace',
folderPath: '/srv/project',
connectionId,
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 1,
createdAt: 1,
updatedAt: 1
}
return {
type: 'folder-workspace',
key: 'folder-workspace:folder-1',
folderWorkspace,
projectGroup,
depth: 0,
groupDepth: 0
}
}
function rowKey(row: HostSectionRow): string {
return row.type === 'item' ? row.worktree.id : row.key
}
describe('addHostSectionRows', () => {
it('does not add host headers for a specific host scope', () => {
const local = repo('local')
const rows = [repoHeader(local), item('local-wt', local)]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{ id: 'ssh:ssh-1', kind: 'ssh', label: 'Builder', detail: 'SSH', health: 'available' }
],
workspaceHostScope: 'local',
defaultHostId: 'local'
})
expect(sectioned).toHaveLength(2)
expect(sectioned).toEqual(rows)
})
it('does not add host headers when only the local host exists', () => {
const local = repo('local')
const rows = [repoHeader(local), item('local-wt', local)]
expect(
sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
}
],
workspaceHostScope: 'all',
defaultHostId: 'local'
})
).toEqual(rows)
})
it('groups rows under host headers in all-host scope', () => {
const local = repo('local')
const ssh = repo('ssh', 'ssh-1')
const rows = [repoHeader(local), item('local-wt', local), repoHeader(ssh), item('ssh-wt', ssh)]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{ id: 'ssh:ssh-1', kind: 'ssh', label: 'Builder', detail: 'SSH', health: 'available' }
],
workspaceHostScope: 'all',
defaultHostId: 'local'
})
expect(sectioned.map(rowKey)).toEqual([
'host:local',
'repo:local',
'local-wt',
'host:ssh:ssh-1',
'repo:ssh',
'ssh-wt'
])
expect(sectioned.filter((row) => row.type === 'host-header')).toMatchObject([
{ label: 'Local Mac', count: 1 },
{ label: 'Builder', count: 1 }
])
})
it('keeps project grouping outermost in the default Projects view', () => {
const local = repo('local')
const ssh = repo('ssh', 'ssh-1')
const rows = [repoHeader(local), item('local-wt', local), repoHeader(ssh), item('ssh-wt', ssh)]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{ id: 'ssh:ssh-1', kind: 'ssh', label: 'Builder', detail: 'SSH', health: 'available' }
],
workspaceHostScope: 'all',
defaultHostId: 'local',
preferProjectGrouping: true
})
expect(sectioned).toEqual(rows)
})
it('keeps host headers for a custom multi-host visibility filter', () => {
const local = repo('local')
const ssh = repo('ssh', 'ssh-1')
const rows = [repoHeader(local), item('local-wt', local), repoHeader(ssh), item('ssh-wt', ssh)]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{ id: 'ssh:ssh-1', kind: 'ssh', label: 'Builder', detail: 'SSH', health: 'available' }
],
workspaceHostScope: 'all',
visibleWorkspaceHostIds: ['local', 'ssh:ssh-1'],
defaultHostId: 'local',
preferProjectGrouping: true
})
expect(sectioned.map(rowKey)).toEqual([
'host:local',
'repo:local',
'local-wt',
'host:ssh:ssh-1',
'repo:ssh',
'ssh-wt'
])
})
it('keeps non-repo group headers with the following host-owned rows', () => {
const local = repo('local')
const ssh = repo('ssh', 'ssh-1')
const rows = [header('all'), item('local-wt', local), header('done'), item('ssh-wt', ssh)]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{ id: 'ssh:ssh-1', kind: 'ssh', label: 'Builder', detail: 'SSH', health: 'available' }
],
workspaceHostScope: 'all',
defaultHostId: 'local'
})
expect(sectioned.map(rowKey)).toEqual([
'host:local',
'all',
'local-wt',
'host:ssh:ssh-1',
'done',
'ssh-wt'
])
})
it('copies global pinned and all headers into each mixed-host section without duplicating pins', () => {
const local = repo('local')
const ssh = repo('ssh', 'ssh-1')
const rows = [
pinnedHeader(
new Map([
['local', 1],
['ssh:ssh-1', 1]
])
),
pinnedItem('local-pinned', local, PINNED_GROUP_KEY),
pinnedItem('ssh-pinned', ssh, PINNED_GROUP_KEY),
header('all', 'All'),
item('local-normal', local),
item('ssh-normal', ssh)
]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{ id: 'ssh:ssh-1', kind: 'ssh', label: 'Builder', detail: 'SSH', health: 'available' }
],
workspaceHostScope: 'all',
defaultHostId: 'local'
})
expect(sectioned.map((row) => (row.type === 'item' ? row.rowKey : row.key))).toEqual([
'host:local',
'pinned',
'pinned:local-pinned',
'all',
'repo:local:local-normal',
'host:ssh:ssh-1',
'pinned',
'pinned:ssh-pinned',
'all',
'repo:ssh:ssh-normal'
])
expect(sectioned.filter((row) => row.type === 'host-header')).toMatchObject([
{ label: 'Local Mac', count: 2 },
{ label: 'Builder', count: 2 }
])
expect(
sectioned.filter((row) => row.type === 'header' && row.key === PINNED_GROUP_KEY)
).toEqual(
expect.arrayContaining([
expect.objectContaining({ count: 1, hostId: 'local' }),
expect.objectContaining({ count: 1, hostId: 'ssh:ssh-1' })
])
)
})
it('keeps collapsed pinned rows attributable to their owning hosts', () => {
const rows = [
pinnedHeader(
new Map([
['local', 1],
['ssh:ssh-1', 2]
])
)
]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{ id: 'ssh:ssh-1', kind: 'ssh', label: 'Builder', detail: 'SSH', health: 'available' }
],
workspaceHostScope: 'all',
defaultHostId: 'local',
collapsedHostKeys: new Set(['host:ssh:ssh-1'])
})
expect(sectioned.map(rowKey)).toEqual(['host:local', 'pinned', 'host:ssh:ssh-1'])
expect(sectioned.filter((row) => row.type === 'host-header')).toMatchObject([
{ hostId: 'local', collapsed: false, count: 1 },
{ hostId: 'ssh:ssh-1', collapsed: true, count: 2 }
])
expect(sectioned.filter((row) => row.type === 'header')).toMatchObject([
{ key: PINNED_GROUP_KEY, count: 1, hostId: 'local' }
])
})
it('keeps a pinned row with its explicit worktree host', () => {
const local = repo('local')
const pinned = pinnedItem('ssh-pinned', local, PINNED_GROUP_KEY)
pinned.worktree.hostId = 'ssh:ssh-1'
const rows = [
pinnedHeader(new Map([['ssh:ssh-1', 1]]), new Map([['ssh:ssh-1', ['ssh-pinned']]])),
pinned,
repoHeader(local),
item('local-wt', local)
]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{ id: 'ssh:ssh-1', kind: 'ssh', label: 'Builder', detail: 'SSH', health: 'available' }
],
workspaceHostScope: 'all',
defaultHostId: 'local'
})
expect(sectioned.map(rowKey)).toEqual([
'host:local',
'repo:local',
'local-wt',
'host:ssh:ssh-1',
PINNED_GROUP_KEY,
'ssh-pinned'
])
expect(sectioned.filter((row) => row.type === 'host-header')).toMatchObject([
{ hostId: 'local', count: 1 },
{ hostId: 'ssh:ssh-1', count: 1 }
])
})
it('does not double-count a collapsed pinned header and its natural duplicate row', () => {
const local = repo('local')
const ssh = repo('ssh', 'ssh-1')
const rows = [
pinnedHeader(new Map([['local', 1]]), new Map([['local', ['wt-1']]])),
{ ...header('all', 'All'), count: 1, worktreeIds: ['wt-1'] },
item('wt-1', local),
repoHeader(ssh),
item('ssh-wt', ssh)
]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{ id: 'ssh:ssh-1', kind: 'ssh', label: 'Builder', detail: 'SSH', health: 'available' }
],
workspaceHostScope: 'all',
defaultHostId: 'local'
})
expect(
sectioned.find((row) => row.type === 'host-header' && row.hostId === 'local')
).toMatchObject({ count: 1 })
})
it('localizes collapsed natural headers even when the hidden rows are owned by one host', () => {
const ssh = repo('ssh', 'ssh-1')
const localHostId = 'local' as const
const rows = [
{
...header('all', 'All'),
count: 1,
hostWorktreeCounts: new Map([[localHostId, 1]]),
hostWorktreeIds: new Map([[localHostId, ['local-wt']]]),
worktreeIds: ['local-wt']
},
repoHeader(ssh),
item('ssh-wt', ssh)
]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{ id: 'ssh:ssh-1', kind: 'ssh', label: 'Builder', detail: 'SSH', health: 'available' }
],
workspaceHostScope: 'all',
defaultHostId: 'local'
})
expect(sectioned.map(rowKey)).toEqual([
'host:local',
'all',
'host:ssh:ssh-1',
'repo:ssh',
'ssh-wt'
])
expect(sectioned.filter((row) => row.type === 'host-header')).toMatchObject([
{ hostId: 'local', count: 1 },
{ hostId: 'ssh:ssh-1', count: 1 }
])
expect(sectioned.filter((row) => row.type === 'header' && row.key === 'all')).toMatchObject([
{ hostId: 'local', count: 1, worktreeIds: ['local-wt'] }
])
})
it('groups explicitly runtime-owned repos under their owner host, not the focused host', () => {
const localOwned: Repo = { ...repo('local-project'), executionHostId: 'local' }
const runtimeOwned: Repo = { ...repo('remote-project'), executionHostId: 'runtime:env-2' }
const rows = [
repoHeader(localOwned),
item('local-wt', localOwned),
repoHeader(runtimeOwned),
item('remote-wt', runtimeOwned)
]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{
id: 'runtime:env-1',
kind: 'runtime',
label: 'env-1',
detail: 'Orca server',
health: 'available'
},
{
id: 'runtime:env-2',
kind: 'runtime',
label: 'env-2',
detail: 'Orca server',
health: 'available'
}
],
workspaceHostScope: 'all',
defaultHostId: 'runtime:env-1'
})
expect(sectioned.map(rowKey)).toEqual([
'host:local',
'repo:local-project',
'local-wt',
'host:runtime:env-2',
'repo:remote-project',
'remote-wt'
])
})
it('groups SSH folder workspace rows under their connection host', () => {
const local = repo('local')
const rows: Row[] = [repoHeader(local), item('local-wt', local), folderWorkspaceRow('ssh-1')]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{ id: 'ssh:ssh-1', kind: 'ssh', label: 'Builder', detail: 'SSH', health: 'available' }
],
workspaceHostScope: 'all',
defaultHostId: 'local'
})
expect(sectioned.map(rowKey)).toEqual([
'host:local',
'repo:local',
'local-wt',
'host:ssh:ssh-1',
'folder-workspace:folder-1'
])
})
it('carries the SSH connection status through to the host header row', () => {
const local = repo('local')
const ssh = repo('ssh', 'ssh-1')
const rows = [repoHeader(local), item('local-wt', local), repoHeader(ssh), item('ssh-wt', ssh)]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{
id: 'ssh:ssh-1',
kind: 'ssh',
label: 'Builder',
detail: 'SSH',
health: 'error',
connectionStatus: 'auth-failed'
}
],
workspaceHostScope: 'all',
defaultHostId: 'local'
})
expect(
sectioned.find((row) => row.type === 'host-header' && row.hostId === 'ssh:ssh-1')
).toMatchObject({
health: 'error',
connectionStatus: 'auth-failed',
collapsed: false
})
})
it('uses the focused runtime as the owner for non-SSH repos', () => {
const localOwned: Repo = { ...repo('local-project'), executionHostId: 'local' }
const project = repo('runtime-project')
const rows = [
repoHeader(localOwned),
item('local-wt', localOwned),
repoHeader(project),
item('runtime-wt', project)
]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{
id: 'runtime:env-1',
kind: 'runtime',
label: 'env-1',
detail: 'Orca server',
health: 'available'
}
],
workspaceHostScope: 'all',
defaultHostId: 'runtime:env-1'
})
expect(
sectioned.find((row) => row.type === 'host-header' && row.hostId === 'runtime:env-1')
).toMatchObject({
key: 'host:runtime:env-1',
label: 'env-1'
})
})
it('passes host kind, blocked compatibility and reason through to the header row', () => {
const localOwned: Repo = { ...repo('local-project'), executionHostId: 'local' }
const project = repo('runtime-project')
const rows = [
repoHeader(localOwned),
item('local-wt', localOwned),
repoHeader(project),
item('runtime-wt', project)
]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{
id: 'runtime:env-1',
kind: 'runtime',
label: 'env-1',
detail: 'Orca server',
health: 'blocked',
compatibility: {
kind: 'blocked',
reason: 'server-too-old',
clientProtocolVersion: 5,
serverProtocolVersion: 1,
requiredServerProtocolVersion: 4
},
blockedReason: 'incompatible'
}
],
workspaceHostScope: 'all',
defaultHostId: 'runtime:env-1'
})
expect(
sectioned.find((row) => row.type === 'host-header' && row.hostId === 'runtime:env-1')
).toMatchObject({
kind: 'runtime',
health: 'blocked',
compatibility: { kind: 'blocked', reason: 'server-too-old' },
blockedReason: 'incompatible'
})
})
it('suppresses host headers when only one host has visible workspaces', () => {
const local = repo('local')
const rows = [repoHeader(local), item('local-wt', local)]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{ id: 'ssh:ssh-1', kind: 'ssh', label: 'Builder', detail: 'SSH', health: 'disconnected' },
{
id: 'runtime:env-1',
kind: 'runtime',
label: 'env-1',
detail: 'Orca server',
health: 'available'
}
],
workspaceHostScope: 'all',
defaultHostId: 'local'
})
expect(sectioned).toEqual(rows)
})
it('counts a collapsed repo group via its header count instead of zero', () => {
const local = repo('local')
const ssh = repo('ssh', 'ssh-1')
const collapsedSshHeader = { ...repoHeader(ssh), count: 9 }
const rows = [repoHeader(local), item('local-wt', local), collapsedSshHeader]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{ id: 'ssh:ssh-1', kind: 'ssh', label: 'Builder', detail: 'SSH', health: 'available' }
],
workspaceHostScope: 'all',
defaultHostId: 'local'
})
expect(
sectioned.find((row) => row.type === 'host-header' && row.hostId === 'ssh:ssh-1')
).toMatchObject({ count: 9 })
expect(
sectioned.find((row) => row.type === 'host-header' && row.hostId === 'local')
).toMatchObject({ count: 1 })
})
it('keeps a collapsed host header but hides its rows', () => {
const local = repo('local')
const ssh = repo('ssh', 'ssh-1')
const rows = [repoHeader(local), item('local-wt', local), repoHeader(ssh), item('ssh-wt', ssh)]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{ id: 'ssh:ssh-1', kind: 'ssh', label: 'Builder', detail: 'SSH', health: 'available' }
],
workspaceHostScope: 'all',
defaultHostId: 'local',
collapsedHostKeys: new Set(['host:ssh:ssh-1'])
})
expect(sectioned.map(rowKey)).toEqual([
'host:local',
'repo:local',
'local-wt',
'host:ssh:ssh-1'
])
expect(sectioned.filter((row) => row.type === 'host-header')).toMatchObject([
{ hostId: 'local', collapsed: false },
{ hostId: 'ssh:ssh-1', collapsed: true, count: 1 }
])
})
it('can temporarily collapse every host without mutating persisted collapse keys', () => {
const local = repo('local')
const ssh = repo('ssh', 'ssh-1')
const rows = [repoHeader(local), item('local-wt', local), repoHeader(ssh), item('ssh-wt', ssh)]
const sectioned = sectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{ id: 'ssh:ssh-1', kind: 'ssh', label: 'Builder', detail: 'SSH', health: 'available' }
],
workspaceHostScope: 'all',
defaultHostId: 'local',
collapsedHostKeys: new Set(),
forceCollapseHosts: true
})
expect(sectioned.map(rowKey)).toEqual(['host:local', 'host:ssh:ssh-1'])
expect(sectioned.filter((row) => row.type === 'host-header')).toMatchObject([
{ hostId: 'local', collapsed: true, count: 1 },
{ hostId: 'ssh:ssh-1', collapsed: true, count: 1 }
])
})
})
@@ -1,527 +0,0 @@
import { describe, expect, it } from 'vitest'
import { buildRows } from './build-rows.test-fixture'
import { PINNED_GROUP_KEY } from '../../../../../../shared/sidebar-list/group-keys'
import { repo, worktree, repoMap, makeDetectedWorktree } from './worktree-list-groups-test-fixtures'
import type { Repo } from '../../../../../../shared/repo-types'
describe('buildRows with pinned worktrees', () => {
it('emits an imported worktrees card at the top of repo-group rows', () => {
const hidden = [
makeDetectedWorktree({ id: 'hidden-1', displayName: 'payments-refactor' }),
makeDetectedWorktree({ id: 'hidden-2', displayName: 'auth-cache-debug' }),
makeDetectedWorktree({ id: 'hidden-3', displayName: 'legacy-oauth-fix' })
]
const rows = buildRows(
'repo',
[worktree],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([[worktree.id, worktree]]),
false,
undefined,
[],
new Set(),
new Map([[repo.id, { repo, hiddenWorktrees: hidden }]])
)
expect(rows).toMatchObject([
{ type: 'header', key: 'repo:repo-1' },
{
type: 'imported-worktrees-card',
key: 'imported-worktrees-card:repo-group:repo-1',
placement: 'repo-group',
repo: { id: 'repo-1' },
hiddenWorktrees: [{ id: 'hidden-1' }, { id: 'hidden-2' }, { id: 'hidden-3' }]
},
{ type: 'item', worktree: { id: 'wt-1' } }
])
})
it('suppresses the repo-group imported worktrees card when the repo group is collapsed', () => {
const rows = buildRows(
'repo',
[worktree],
repoMap,
null,
new Set(['repo:repo-1']),
undefined,
undefined,
undefined,
{},
new Map([[worktree.id, worktree]]),
false,
undefined,
[],
new Set(),
new Map([[repo.id, { repo, hiddenWorktrees: [makeDetectedWorktree()] }]])
)
expect(rows).toMatchObject([{ type: 'header', key: 'repo:repo-1' }])
})
it('emits a repo header and imported worktrees card when no visible worktree rows remain', () => {
const rows = buildRows(
'repo',
[],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map(),
false,
undefined,
[],
new Set(),
new Map([[repo.id, { repo, hiddenWorktrees: [makeDetectedWorktree()] }]])
)
expect(rows).toMatchObject([
{ type: 'header', key: 'repo:repo-1' },
{
type: 'imported-worktrees-card',
key: 'imported-worktrees-card:repo-group:repo-1',
placement: 'repo-group'
}
])
})
it('emits an empty ungrouped repo placeholder before imported cards are merged', () => {
const rows = buildRows(
'repo',
[],
repoMap,
null,
new Set(),
new Map([[repo.id, 0]]),
undefined,
'manual',
{},
new Map(),
false,
undefined,
[],
new Set([repo.id]),
new Map([[repo.id, { repo, hiddenWorktrees: [makeDetectedWorktree()] }]])
)
expect(rows).toMatchObject([
{ type: 'header', key: 'repo:repo-1', label: 'orca' },
{
type: 'imported-worktrees-card',
key: 'imported-worktrees-card:repo-group:repo-1',
placement: 'repo-group'
}
])
})
it('skips stale empty placeholder repo ids that are absent from repoMap', () => {
const rows = buildRows(
'repo',
[],
new Map(),
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map(),
false,
undefined,
[],
new Set([repo.id])
)
expect(rows).toEqual([])
})
it('shows one line for every project at the end of a list not grouped by project', () => {
const repoTwo: Repo = { ...repo, id: 'repo-2', displayName: 'auth-service' }
const pinned = { ...worktree, id: 'repo-1-pinned', isPinned: true }
const review = {
...worktree,
id: 'repo-2-review',
repoId: repoTwo.id,
workspaceStatus: 'in-review' as const
}
const hiddenOne = makeDetectedWorktree({ id: 'hidden-one' })
const hiddenTwo = makeDetectedWorktree({ id: 'hidden-two', repoId: repoTwo.id })
for (const [groupBy, collapsed] of [
['workspace-status', new Set<string>()],
['workspace-status', new Set([PINNED_GROUP_KEY])],
['none', new Set(['all'])]
] as const) {
const rows = buildRows(
groupBy,
[pinned, review],
new Map([
[repo.id, repo],
[repoTwo.id, repoTwo]
]),
null,
new Set(collapsed),
undefined,
undefined,
undefined,
{},
new Map([
[pinned.id, pinned],
[review.id, review]
]),
false,
undefined,
[],
new Set(),
new Map([
[repo.id, { repo, hiddenWorktrees: [hiddenOne] }],
[repoTwo.id, { repo: repoTwo, hiddenWorktrees: [hiddenTwo] }]
])
)
expect(rows.filter((row) => row.type === 'imported-worktrees-card')).toHaveLength(1)
expect(rows.at(-1)).toMatchObject({
type: 'imported-worktrees-card',
placement: 'all-projects',
projects: [{ hiddenWorktrees: [hiddenOne] }, { hiddenWorktrees: [hiddenTwo] }]
})
expect(rows.at(-1)).not.toHaveProperty('hostContextLabel')
}
})
it('emits a new external worktrees inbox row before repo worktree rows', () => {
const inboxWorktrees = [
makeDetectedWorktree({ id: 'inbox-1', displayName: 'payments-refactor' }),
makeDetectedWorktree({ id: 'inbox-2', displayName: 'auth-cache-debug' })
]
const rows = buildRows(
'repo',
[worktree],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([[worktree.id, worktree]]),
false,
undefined,
[],
new Set(),
new Map(),
new Map([[repo.id, { repo, inboxWorktrees }]])
)
expect(rows).toMatchObject([
{ type: 'header', key: 'repo:repo-1' },
{
type: 'new-external-worktrees-inbox',
key: 'new-external-worktrees-inbox:repo-1',
repo: { id: 'repo-1' },
inboxWorktrees: [{ id: 'inbox-1' }, { id: 'inbox-2' }]
},
{ type: 'item', worktree: { id: 'wt-1' } }
])
})
it('suppresses the new external worktrees inbox row when the repo group is collapsed', () => {
const rows = buildRows(
'repo',
[worktree],
repoMap,
null,
new Set(['repo:repo-1']),
undefined,
undefined,
undefined,
{},
new Map([[worktree.id, worktree]]),
false,
undefined,
[],
new Set(),
new Map(),
new Map([[repo.id, { repo, inboxWorktrees: [makeDetectedWorktree()] }]])
)
expect(rows).toMatchObject([{ type: 'header', key: 'repo:repo-1' }])
})
it('emits a repo header and inbox row when no visible worktree rows remain', () => {
const rows = buildRows(
'repo',
[],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map(),
false,
undefined,
[],
new Set(),
new Map(),
new Map([[repo.id, { repo, inboxWorktrees: [makeDetectedWorktree()] }]])
)
expect(rows).toMatchObject([
{ type: 'header', key: 'repo:repo-1' },
{
type: 'new-external-worktrees-inbox',
key: 'new-external-worktrees-inbox:repo-1'
}
])
})
it('keeps the inbox group when the repo only has a pinned worktree', () => {
const pinnedWorktree = { ...worktree, id: 'wt-pinned', isPinned: true }
const rows = buildRows(
'repo',
[pinnedWorktree],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([[pinnedWorktree.id, pinnedWorktree]]),
false,
undefined,
[],
new Set(),
new Map(),
new Map([[repo.id, { repo, inboxWorktrees: [makeDetectedWorktree()] }]])
)
expect(rows).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'item',
sectionKey: PINNED_GROUP_KEY,
worktree: expect.objectContaining({ id: pinnedWorktree.id })
}),
expect.objectContaining({ type: 'header', key: `repo:${repo.id}` }),
expect.objectContaining({
type: 'new-external-worktrees-inbox',
key: `new-external-worktrees-inbox:${repo.id}`
})
])
)
expect(
rows.some(
(row) =>
row.type === 'item' &&
row.sectionKey === `repo:${repo.id}` &&
row.worktree.id === pinnedWorktree.id
)
).toBe(false)
})
it('does not emit new external worktrees inbox rows outside repo grouping', () => {
const rows = buildRows(
'workspace-status',
[worktree],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([[worktree.id, worktree]]),
false,
undefined,
[],
new Set(),
new Map(),
new Map([[repo.id, { repo, inboxWorktrees: [makeDetectedWorktree()] }]])
)
expect(rows.some((row) => row.type === 'new-external-worktrees-inbox')).toBe(false)
})
it('emits imported worktree cards in repo groups when visible rows are pinned', () => {
const repoTwo: Repo = { ...repo, id: 'repo-2', displayName: 'auth-service' }
const pinnedOneA = { ...worktree, id: 'repo-1-pinned-a', isPinned: true }
const pinnedTwo = {
...worktree,
id: 'repo-2-pinned',
repoId: repoTwo.id,
isPinned: true,
displayName: 'auth-main'
}
const pinnedOneB = { ...worktree, id: 'repo-1-pinned-b', isPinned: true }
const rows = buildRows(
'repo',
[pinnedOneA, pinnedTwo, pinnedOneB],
new Map([
[repo.id, repo],
[repoTwo.id, repoTwo]
]),
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([
[pinnedOneA.id, pinnedOneA],
[pinnedTwo.id, pinnedTwo],
[pinnedOneB.id, pinnedOneB]
]),
false,
undefined,
[],
new Set(),
new Map([
[repo.id, { repo, hiddenWorktrees: [makeDetectedWorktree({ id: 'hidden-one' })] }],
[
repoTwo.id,
{
repo: repoTwo,
hiddenWorktrees: [makeDetectedWorktree({ id: 'hidden-two', repoId: repoTwo.id })]
}
]
])
)
expect(rows.filter((row) => row.type === 'imported-worktrees-card')).toMatchObject([
{
key: 'imported-worktrees-card:repo-group:repo-1',
placement: 'repo-group'
},
{
key: 'imported-worktrees-card:repo-group:repo-2',
placement: 'repo-group'
}
])
expect(
rows.filter((row) => row.type === 'item' && row.sectionKey !== PINNED_GROUP_KEY)
).toEqual([])
expect(rows).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'item',
sectionKey: PINNED_GROUP_KEY,
worktree: expect.objectContaining({ id: 'repo-1-pinned-a' })
}),
expect.objectContaining({
type: 'item',
sectionKey: PINNED_GROUP_KEY,
worktree: expect.objectContaining({ id: 'repo-1-pinned-b' })
}),
expect.objectContaining({
type: 'item',
sectionKey: PINNED_GROUP_KEY,
worktree: expect.objectContaining({ id: 'repo-2-pinned' })
})
])
)
})
it('duplicates pinned worktrees into repo groups when the policy allows it', () => {
const pinnedWorktree = { ...worktree, id: 'wt-pinned', isPinned: true }
const rows = buildRows(
'repo',
[pinnedWorktree],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([[pinnedWorktree.id, pinnedWorktree]]),
false,
{ showPinnedWorktreesInGroups: true } as never,
[],
new Set(),
new Map([[repo.id, { repo, hiddenWorktrees: [makeDetectedWorktree()] }]])
)
expect(rows).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'item',
sectionKey: PINNED_GROUP_KEY,
worktree: expect.objectContaining({ id: 'wt-pinned' })
}),
expect.objectContaining({
type: 'item',
sectionKey: `repo:${repo.id}`,
worktree: expect.objectContaining({ id: 'wt-pinned' })
})
])
)
})
it('suppresses pinned imported worktree fallback when the repo has visible unpinned rows', () => {
const pinnedWorktree = { ...worktree, id: 'wt-pinned', isPinned: true }
const rows = buildRows(
'repo',
[pinnedWorktree, worktree],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([
[pinnedWorktree.id, pinnedWorktree],
[worktree.id, worktree]
]),
false,
undefined,
[],
new Set(),
new Map([[repo.id, { repo, hiddenWorktrees: [makeDetectedWorktree()] }]])
)
expect(rows.filter((row) => row.type === 'imported-worktrees-card')).toMatchObject([
{ placement: 'repo-group' }
])
})
it('keeps repo imported worktree cards visible when Pinned is collapsed', () => {
const pinnedWorktree = { ...worktree, id: 'wt-pinned', isPinned: true }
const rows = buildRows(
'repo',
[pinnedWorktree],
repoMap,
null,
new Set(['pinned']),
undefined,
undefined,
undefined,
{},
new Map([[pinnedWorktree.id, pinnedWorktree]]),
false,
undefined,
[],
new Set(),
new Map([[repo.id, { repo, hiddenWorktrees: [makeDetectedWorktree()] }]])
)
expect(rows).toMatchObject([
{ type: 'header', key: 'pinned' },
{ type: 'header', key: 'repo:repo-1' },
{ type: 'imported-worktrees-card', placement: 'repo-group' }
])
expect(
rows.filter((row) => row.type === 'item' && row.sectionKey !== PINNED_GROUP_KEY)
).toEqual([])
})
})
@@ -1,13 +1,7 @@
import { describe, expect, it } from 'vitest'
import { buildRows } from './build-rows.test-fixture'
import {
getLineageGroupKey,
getWorktreeLineageGroupKey
} from '../../../../../../shared/sidebar-list/group-keys'
import {
getLineageRenderInfo,
getWorktreeLineageAncestors
} from '../../../../../../shared/sidebar-list/worktree-lineage-projection'
import { getWorktreeLineageGroupKey } from '../../../../../../shared/sidebar-list/group-keys'
import { worktree, repoMap } from './worktree-list-groups-test-fixtures'
import type { WorktreeLineage } from '../../../../../../shared/worktree/lineage-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
@@ -31,12 +25,7 @@ describe('buildRows workspace lineage nesting', () => {
instanceId: 'child-instance',
displayName: 'worker'
}
const grandchild: Worktree = {
...worktree,
id: 'wt-grandchild',
instanceId: 'grandchild-instance',
displayName: 'nested-worker'
}
const lineage: WorktreeLineage = {
worktreeId: child.id,
worktreeInstanceId: 'child-instance',
@@ -46,68 +35,6 @@ describe('buildRows workspace lineage nesting', () => {
capture: { source: 'terminal-context', confidence: 'inferred' },
createdAt: 1
}
const grandchildLineage: WorktreeLineage = {
worktreeId: grandchild.id,
worktreeInstanceId: 'grandchild-instance',
parentWorktreeId: child.id,
parentWorktreeInstanceId: 'child-instance',
origin: 'cli',
capture: { source: 'terminal-context', confidence: 'inferred' },
createdAt: 1
}
it('keeps lineage flat when nesting is off', () => {
const rows = buildRows(
'none',
[child, parent],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{ [child.id]: lineage },
new Map([
[parent.id, parent],
[child.id, child]
])
)
const items = rows.filter((row) => row.type === 'item')
expect(items[0]).toMatchObject({ type: 'item', worktree: { id: child.id } })
expect(items[0]).not.toHaveProperty('parentLabel')
expect(items[1]).toMatchObject({
type: 'item',
worktree: { id: parent.id }
})
})
it('places children directly under their parent when nesting is on', () => {
const rows = buildRows(
'none',
[child, parent],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{ [child.id]: lineage },
new Map([
[parent.id, parent],
[child.id, child]
]),
true
)
const items = rows.filter((row) => row.type === 'item')
expect(items[0]).toMatchObject({ type: 'item', worktree: { id: parent.id } })
expect(items[1]).toMatchObject({
type: 'item',
worktree: { id: child.id },
depth: 1
})
})
it('keeps same-id parent and child lineages partitioned by host', () => {
const parentA = { ...parent, hostId: 'local' as const }
@@ -156,216 +83,6 @@ describe('buildRows workspace lineage nesting', () => {
])
})
it('nests stable-update resolved legacy lineage when generalized lineage is absent', () => {
const parentId =
'32a0226d-9f33-42e8-8b7b-24867dea06d4::/Users/jinwoo/orca/workspaces/orca/assigned-issues'
const childId =
'32a0226d-9f33-42e8-8b7b-24867dea06d4::/Users/jinwoo/orca/workspaces/orca/issue-9276-nested-ssh-runtime-routing'
const secondChildId =
'32a0226d-9f33-42e8-8b7b-24867dea06d4::/Users/jinwoo/orca/workspaces/orca/issue-9744-terminal-close-lifecycle'
const resolvedParent: ResolvedLineageWorktree = {
...parent,
id: parentId,
instanceId: 'b0ffd635-91cd-424f-b804-80d4bb277a4c',
lineage: null,
workspaceLineage: null
}
const resolvedLineage: WorktreeLineage = {
...lineage,
worktreeId: childId,
worktreeInstanceId: '1ceb9823-aa98-4f79-8eaa-af0b3a3d551b',
parentWorktreeId: parentId,
parentWorktreeInstanceId: 'b0ffd635-91cd-424f-b804-80d4bb277a4c',
capture: { source: 'explicit-cli-flag', confidence: 'explicit' }
}
const resolvedChild: ResolvedLineageWorktree = {
...child,
id: childId,
instanceId: '1ceb9823-aa98-4f79-8eaa-af0b3a3d551b',
lineage: resolvedLineage,
workspaceLineage: null
}
const secondResolvedLineage: WorktreeLineage = {
...resolvedLineage,
worktreeId: secondChildId,
worktreeInstanceId: '87e2ef9a-99d3-48e3-9a53-3d1a979b5417'
}
const secondResolvedChild: ResolvedLineageWorktree = {
...child,
id: secondChildId,
instanceId: '87e2ef9a-99d3-48e3-9a53-3d1a979b5417',
lineage: secondResolvedLineage,
workspaceLineage: null
}
const rows = buildRows(
'none',
[secondResolvedChild, resolvedChild, resolvedParent],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([
[resolvedParent.id, resolvedParent],
[resolvedChild.id, resolvedChild],
[secondResolvedChild.id, secondResolvedChild]
]),
true
)
const items = rows.filter((row) => row.type === 'item')
expect(items.map((row) => [row.worktree.id, row.depth])).toEqual([
[parentId, 0],
[secondChildId, 1],
[childId, 1]
])
expect(items[0]).toMatchObject({
lineageChildCount: 2,
lineageChildWorktreeIds: [secondChildId, childId],
lineageCollapsed: false
})
})
it('rejects stale resolved lineage after a parent instance is replaced', () => {
const resolvedChild: ResolvedLineageWorktree = {
...child,
lineage: { ...lineage, parentWorktreeInstanceId: 'replaced-parent-instance' }
}
const rows = buildRows(
'none',
[resolvedChild, parent],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([
[parent.id, parent],
[resolvedChild.id, resolvedChild]
]),
true
)
expect(rows.filter((row) => row.type === 'item').map((row) => row.depth)).toEqual([0, 0])
})
it('keeps mixed cyclic lineage participants visible as roots', () => {
const parentLineage: WorktreeLineage = {
...lineage,
worktreeId: parent.id,
worktreeInstanceId: parent.instanceId!,
parentWorktreeId: child.id,
parentWorktreeInstanceId: child.instanceId!
}
const rows = buildRows(
'none',
[grandchild, child, parent],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{ [child.id]: lineage, [parent.id]: parentLineage },
new Map([
[parent.id, parent],
[child.id, child],
[grandchild.id, grandchild]
]),
true
)
expect(
rows.filter((row) => row.type === 'item').map((row) => [row.worktree.id, row.depth])
).toEqual([
[grandchild.id, 0],
[child.id, 0],
[parent.id, 0]
])
})
it('resolves inline-only ancestor chains for reveal and temporary picker expansion', () => {
const resolvedChild: ResolvedLineageWorktree = { ...child, lineage }
const resolvedGrandchild: ResolvedLineageWorktree = {
...grandchild,
lineage: grandchildLineage
}
const worktreeMap = new Map<string, Worktree>([
[parent.id, parent],
[resolvedChild.id, resolvedChild],
[resolvedGrandchild.id, resolvedGrandchild]
])
expect(
getWorktreeLineageAncestors(resolvedGrandchild, {}, worktreeMap).map(
(worktree) => worktree.id
)
).toEqual([child.id, parent.id])
})
it('keeps a resolved child at the root when its parent is missing', () => {
const resolvedChild: ResolvedLineageWorktree = { ...child, lineage }
const rows = buildRows(
'none',
[resolvedChild],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([[child.id, resolvedChild]]),
true
)
expect(rows.find((row) => row.type === 'item')).toMatchObject({ depth: 0 })
})
it.each([
['repo', { repoId: 'other-repo' }, [0, 1]],
['host', { hostId: 'ssh:other-host' as const }, [0, 0]],
['project', { projectId: 'github:other/project' }, [0, 1]]
] as const)('applies the known %s lineage boundary', (_label, boundary, depths) => {
const boundedParent = {
...parent,
repoId: 'repo-1',
hostId: 'local' as const,
projectId: 'github:stablyai/orca',
...boundary
}
const boundedChild: ResolvedLineageWorktree = {
...child,
repoId: 'repo-1',
hostId: 'local' as const,
projectId: 'github:stablyai/orca',
lineage
}
const rows = buildRows(
'none',
[boundedChild, boundedParent],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map<string, Worktree>([
[boundedParent.id, boundedParent],
[boundedChild.id, boundedChild]
]),
true
)
expect(rows.filter((row) => row.type === 'item').map((row) => row.depth)).toEqual(depths)
})
it('keeps the hydrated lineage side-map authoritative when inline metadata disagrees', () => {
const otherParent = {
...parent,
@@ -408,261 +125,4 @@ describe('buildRows workspace lineage nesting', () => {
[child.id, 1]
])
})
it('supports nested lineage chains beyond one level', () => {
const rows = buildRows(
'none',
[grandchild, child, parent],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{ [child.id]: lineage, [grandchild.id]: grandchildLineage },
new Map([
[parent.id, parent],
[child.id, child],
[grandchild.id, grandchild]
]),
true
)
const items = rows.filter((row) => row.type === 'item')
expect(items.map((row) => row.worktree.id)).toEqual([parent.id, child.id, grandchild.id])
expect(items[0]).toMatchObject({
type: 'item',
depth: 0,
lineageChildCount: 1,
lineageCollapsed: false
})
expect(items[1]).toMatchObject({
type: 'item',
worktree: { id: child.id },
depth: 1,
lineageChildCount: 1
})
expect(items[2]).toMatchObject({
type: 'item',
worktree: { id: grandchild.id },
depth: 2,
lineageChildCount: 0
})
})
it('collapses descendants under lineage parents', () => {
const rows = buildRows(
'none',
[grandchild, child, parent],
repoMap,
null,
new Set([getLineageGroupKey(parent.id)]),
undefined,
undefined,
undefined,
{ [child.id]: lineage, [grandchild.id]: grandchildLineage },
new Map([
[parent.id, parent],
[child.id, child],
[grandchild.id, grandchild]
]),
true
)
const items = rows.filter((row) => row.type === 'item')
expect(items).toHaveLength(1)
expect(items[0]).toMatchObject({
type: 'item',
worktree: { id: parent.id },
lineageChildCount: 1,
lineageChildWorktreeIds: [child.id],
lineageCollapsed: true
})
})
it('does not create a parent group for stale instance links', () => {
const staleLineage = { ...lineage, parentWorktreeInstanceId: 'old-parent-instance' }
const rows = buildRows(
'none',
[child],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{ [child.id]: staleLineage },
new Map([
[parent.id, parent],
[child.id, child]
]),
true
)
const item = rows.find((row) => row.type === 'item')
expect(item).toMatchObject({
type: 'item',
worktree: { id: child.id },
depth: 0
})
})
it('marks stale instance links as missing for shared context-menu validation', () => {
const staleLineage = { ...lineage, parentWorktreeInstanceId: 'old-parent-instance' }
const info = getLineageRenderInfo(
child,
{ [child.id]: staleLineage },
new Map([
[parent.id, parent],
[child.id, child]
]),
new Set()
)
expect(info).toMatchObject({ state: 'missing' })
})
it('nests unpinned children under a pinned parent in Pinned', () => {
const pinnedParent = { ...parent, isPinned: true }
const rows = buildRows(
'none',
[child, pinnedParent],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{ [child.id]: lineage },
new Map([
[pinnedParent.id, pinnedParent],
[child.id, child]
]),
true
)
const items = rows.filter((row) => row.type === 'item')
expect(rows[0]).toMatchObject({ type: 'header', key: 'pinned', count: 2 })
expect(items.map((row) => [row.worktree.id, row.depth, row.sectionKey])).toEqual([
[pinnedParent.id, 0, 'pinned'],
[child.id, 1, 'pinned']
])
expect(rows.some((row) => row.type === 'header' && row.key === 'all')).toBe(false)
})
it('nests grandchildren under a pinned ancestor in Pinned', () => {
const pinnedParent = { ...parent, isPinned: true }
const rows = buildRows(
'none',
[grandchild, child, pinnedParent],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{ [child.id]: lineage, [grandchild.id]: grandchildLineage },
new Map([
[pinnedParent.id, pinnedParent],
[child.id, child],
[grandchild.id, grandchild]
]),
true
)
expect(
rows.filter((row) => row.type === 'item').map((row) => [row.worktree.id, row.depth])
).toEqual([
[pinnedParent.id, 0],
[child.id, 1],
[grandchild.id, 2]
])
})
it('duplicates a pinned parent tree into All when the policy allows it', () => {
const pinnedParent = { ...parent, isPinned: true }
const rows = buildRows(
'none',
[child, pinnedParent],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{ [child.id]: lineage },
new Map([
[pinnedParent.id, pinnedParent],
[child.id, child]
]),
true,
{ showPinnedWorktreesInGroups: true } as never
)
expect(
rows
.filter((row) => row.type === 'item')
.map((row) => [row.sectionKey, row.worktree.id, row.depth])
).toEqual([
['pinned', pinnedParent.id, 0],
['pinned', child.id, 1],
['all', pinnedParent.id, 0],
['all', child.id, 1]
])
})
it('nests a pinned child under its pinned parent in Pinned', () => {
const pinnedParent = { ...parent, isPinned: true }
const pinnedChild = { ...child, isPinned: true }
const rows = buildRows(
'none',
[pinnedChild, pinnedParent],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{ [child.id]: lineage },
new Map([
[pinnedParent.id, pinnedParent],
[pinnedChild.id, pinnedChild]
]),
true
)
expect(
rows.filter((row) => row.type === 'item').map((row) => [row.worktree.id, row.depth])
).toEqual([
[pinnedParent.id, 0],
[pinnedChild.id, 1]
])
})
it('keeps pinned children in Pinned without a parent badge', () => {
const pinnedChild = { ...child, isPinned: true }
const rows = buildRows(
'none',
[parent, pinnedChild],
repoMap,
null,
new Set(),
undefined,
undefined,
undefined,
{ [child.id]: lineage },
new Map([
[parent.id, parent],
[child.id, pinnedChild]
]),
true
)
expect(rows[0]).toMatchObject({ type: 'header', key: 'pinned' })
expect(rows[1]).toMatchObject({
type: 'item',
worktree: { id: child.id }
})
expect(rows[1]).not.toHaveProperty('parentLabel')
})
})
@@ -1,418 +0,0 @@
import { describe, expect, it } from 'vitest'
import { buildRows } from './build-rows.test-fixture'
import { getGroupKeysForWorktree } from '../../../../../../shared/sidebar-list/worktree-group-keys'
import { repo, worktree, repoMap } from './worktree-list-groups-test-fixtures'
import type { FolderWorkspace } from '../../../../../../shared/folder-workspaces/folder-workspace-types'
import type { ProjectGroup } from '../../../../../../shared/project-group-types'
import type { Repo } from '../../../../../../shared/repo-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
describe('project groups', () => {
it('renders nested Project Groups before repos assigned to their leaf group', () => {
const rootGroup: ProjectGroup = {
id: 'group-root',
name: 'Services',
parentPath: '/monorepo',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const childGroup: ProjectGroup = {
...rootGroup,
id: 'group-payments',
name: 'payments',
parentPath: '/monorepo/services/payments',
parentGroupId: rootGroup.id,
tabOrder: 1
}
const groupedRepo: Repo = {
...repo,
id: 'repo-payments-api',
displayName: 'api',
projectGroupId: childGroup.id,
projectGroupOrder: 0
}
const groupedWorktree: Worktree = {
...worktree,
id: 'wt-payments-api',
repoId: groupedRepo.id
}
const rows = buildRows(
'repo',
[groupedWorktree],
new Map([[groupedRepo.id, groupedRepo]]),
null,
new Set(),
new Map([[groupedRepo.id, 0]]),
undefined,
'manual',
undefined,
undefined,
false,
undefined,
[rootGroup, childGroup]
)
expect(rows.filter((row) => row.type === 'header').map((row) => row.key)).toEqual([
'project-group:group-root',
'project-group:group-payments',
'repo:repo-payments-api'
])
expect(rows.filter((row) => row.type === 'header').map((row) => row.projectGroupDepth)).toEqual(
[0, 1, 2]
)
expect(rows.find((row) => row.type === 'item')).toMatchObject({
type: 'item',
groupDepth: 2
})
})
it('renders folder workspaces under their owning folder-backed Project Group', () => {
const group: ProjectGroup = {
id: 'group-root',
name: 'Platform',
parentPath: '/monorepo',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const folderWorkspace: FolderWorkspace = {
id: 'folder-workspace-1',
projectGroupId: group.id,
name: 'Refund fix',
folderPath: '/monorepo',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 10,
lastActivityAt: 0,
createdAt: 1,
updatedAt: 1
}
const rows = buildRows(
'repo',
[],
new Map(),
null,
new Set(),
undefined,
undefined,
undefined,
undefined,
undefined,
false,
undefined,
[group],
new Set(),
new Map(),
new Map(),
[],
undefined,
[folderWorkspace]
)
expect(rows).toMatchObject([
{
type: 'header',
key: 'project-group:group-root',
count: 1
},
{
type: 'folder-workspace',
folderWorkspace: { id: 'folder-workspace-1' },
projectGroup: { id: 'group-root' },
groupDepth: 1
}
])
})
it('preserves nested Project Group depth for folder workspace rows', () => {
const rootGroup: ProjectGroup = {
id: 'group-root',
name: 'Platform',
parentPath: '/monorepo',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const childGroup: ProjectGroup = {
id: 'group-shared',
name: 'packages/shared',
parentPath: '/monorepo/packages/shared',
parentGroupId: rootGroup.id,
createdFrom: 'folder-scan',
tabOrder: 1,
isCollapsed: false,
color: null,
createdAt: 2,
updatedAt: 2
}
const folderWorkspace: FolderWorkspace = {
id: 'folder-workspace-nested',
projectGroupId: childGroup.id,
name: 'Shared package work',
folderPath: '/monorepo/packages/shared',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 10,
lastActivityAt: 0,
createdAt: 3,
updatedAt: 3
}
const rows = buildRows(
'repo',
[],
new Map(),
null,
new Set(),
undefined,
undefined,
undefined,
undefined,
undefined,
false,
undefined,
[rootGroup, childGroup],
new Set(),
new Map(),
new Map(),
[],
undefined,
[folderWorkspace]
)
expect(rows).toMatchObject([
{
type: 'header',
key: 'project-group:group-root',
projectGroupDepth: 0
},
{
type: 'header',
key: 'project-group:group-shared',
projectGroupDepth: 1
},
{
type: 'folder-workspace',
folderWorkspace: { id: 'folder-workspace-nested' },
groupDepth: 2
}
])
})
it('does not render folder workspaces under non-folder Project Groups', () => {
const group: ProjectGroup = {
id: 'group-manual',
name: 'Manual',
parentPath: null,
parentGroupId: null,
createdFrom: 'manual',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const folderWorkspace: FolderWorkspace = {
id: 'folder-workspace-1',
projectGroupId: group.id,
name: 'Hidden',
folderPath: '/monorepo',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 10,
lastActivityAt: 0,
createdAt: 1,
updatedAt: 1
}
const rows = buildRows(
'repo',
[],
new Map(),
null,
new Set(),
undefined,
undefined,
undefined,
undefined,
undefined,
false,
undefined,
[group],
new Set(),
new Map(),
new Map(),
[],
undefined,
[folderWorkspace]
)
expect(rows).toMatchObject([
{
type: 'header',
key: 'project-group:group-manual',
count: 0
}
])
expect(rows.some((row) => row.type === 'folder-workspace')).toBe(false)
})
it('renders imported repos under nested Project Groups before worktree rows load', () => {
const rootGroup: ProjectGroup = {
id: 'group-root',
name: 'Root',
parentPath: '/monorepo',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const platformGroup: ProjectGroup = {
...rootGroup,
id: 'group-platform',
name: 'Platform',
parentGroupId: rootGroup.id,
tabOrder: 1
}
const servicesGroup: ProjectGroup = {
...rootGroup,
id: 'group-services',
name: 'Services',
parentGroupId: platformGroup.id,
tabOrder: 2
}
const serviceA: Repo = {
...repo,
id: 'repo-service-a',
displayName: 'service-a',
projectGroupId: servicesGroup.id,
projectGroupOrder: 0
}
const serviceB: Repo = {
...repo,
id: 'repo-service-b',
displayName: 'service-b',
projectGroupId: servicesGroup.id,
projectGroupOrder: 1
}
const rows = buildRows(
'repo',
[],
new Map([
[serviceA.id, serviceA],
[serviceB.id, serviceB]
]),
null,
new Set(),
new Map([
[serviceA.id, 0],
[serviceB.id, 1]
]),
undefined,
'manual',
undefined,
undefined,
false,
undefined,
[rootGroup, platformGroup, servicesGroup],
new Set([serviceA.id, serviceB.id])
)
expect(rows.filter((row) => row.type === 'header').map((row) => row.key)).toEqual([
'project-group:group-root',
'project-group:group-platform',
'project-group:group-services',
'repo:repo-service-a',
'repo:repo-service-b'
])
expect(rows.filter((row) => row.type === 'header').map((row) => row.projectGroupDepth)).toEqual(
[0, 1, 2, 3, 3]
)
})
it('returns both parent Project Group and repo keys for grouped repo reveals', () => {
const groupedRepo: Repo = { ...repo, projectGroupId: 'group-1' }
const group: ProjectGroup = {
id: 'group-1',
name: 'Platform',
parentPath: '/platform',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
expect(
getGroupKeysForWorktree(
'repo',
worktree,
new Map([[groupedRepo.id, groupedRepo]]),
null,
undefined,
[group]
)
).toEqual(['project-group:group-1', 'repo:repo-1'])
})
it('returns only the repo key for missing Project Group metadata reveals', () => {
const groupedRepo: Repo = { ...repo, projectGroupId: 'missing-group' }
const loadedGroup: ProjectGroup = {
id: 'loaded-group',
name: 'Loaded',
parentPath: '/loaded',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
expect(
getGroupKeysForWorktree(
'repo',
worktree,
new Map([[groupedRepo.id, groupedRepo]]),
null,
undefined,
[loadedGroup]
)
).toEqual(['repo:repo-1'])
})
it('returns only the repo key for ungrouped repo reveals', () => {
expect(getGroupKeysForWorktree('repo', worktree, repoMap, null)).toEqual(['repo:repo-1'])
})
})
@@ -1,462 +0,0 @@
import { describe, expect, it } from 'vitest'
import { buildRows } from './build-rows.test-fixture'
import { repo, worktree, repoMap } from './worktree-list-groups-test-fixtures'
import type { ProjectGroup } from '../../../../../../shared/project-group-types'
import type { Repo } from '../../../../../../shared/repo-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
describe('project groups', () => {
it('keeps empty project groups visible in project grouping mode', () => {
const group: ProjectGroup = {
id: 'group-1',
name: 'Platform',
parentPath: null,
parentGroupId: null,
createdFrom: 'manual',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const rows = buildRows(
'repo',
[],
new Map(),
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map(),
false,
undefined,
[group]
)
expect(rows).toEqual([
expect.objectContaining({
type: 'header',
key: 'project-group:group-1',
label: 'Platform',
projectGroup: group
})
])
})
it('renders grouped repos before their visible worktrees are loaded', () => {
const group: ProjectGroup = {
id: 'group-1',
name: 'Platform',
parentPath: '/platform',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const groupedRepo: Repo = { ...repo, projectGroupId: group.id }
const rows = buildRows(
'repo',
[],
new Map([[groupedRepo.id, groupedRepo]]),
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map(),
false,
undefined,
[group],
new Set([groupedRepo.id])
)
expect(rows[0]).toMatchObject({
type: 'header',
key: 'project-group:group-1'
})
expect(rows[1]).toMatchObject({
type: 'header',
key: 'repo:repo-1',
projectGroupDepth: 1
})
})
it('does not resurrect filtered repos as empty Project Group headers', () => {
const group: ProjectGroup = {
id: 'group-1',
name: 'Platform',
parentPath: '/platform',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const groupedRepo: Repo = { ...repo, projectGroupId: group.id }
const rows = buildRows(
'repo',
[],
new Map([[groupedRepo.id, groupedRepo]]),
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map(),
false,
undefined,
[group]
)
expect(rows.filter((row) => row.type === 'header').map((row) => row.key)).toEqual([
'project-group:group-1'
])
expect(rows[0]).toMatchObject({ label: 'Platform' })
})
it('keeps sleep-filtered Project Group members as empty project headers', () => {
// Hide sleeping removes cards, but placeholders must retain project headers and accurate group counts.
const group: ProjectGroup = {
id: 'group-1',
name: 'Platform',
parentPath: '/platform',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const sleepingRepo: Repo = {
...repo,
id: 'repo-sleeping',
displayName: 'sleeping-project',
projectGroupId: group.id
}
const awakeRepo: Repo = {
...repo,
id: 'repo-awake',
displayName: 'awake-project',
projectGroupId: group.id
}
const awakeWorktree: Worktree = {
...worktree,
id: 'wt-awake',
repoId: awakeRepo.id,
path: '/tmp/awake'
}
const rows = buildRows(
'repo',
[awakeWorktree],
new Map([
[sleepingRepo.id, sleepingRepo],
[awakeRepo.id, awakeRepo]
]),
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([[awakeWorktree.id, awakeWorktree]]),
false,
undefined,
[group],
new Set([sleepingRepo.id])
)
expect(rows[0]).toMatchObject({
type: 'header',
key: 'project-group:group-1',
count: 2
})
expect(rows.filter((row) => row.type === 'header').map((row) => row.key)).toEqual([
'project-group:group-1',
`repo:${awakeRepo.id}`,
`repo:${sleepingRepo.id}`
])
})
it('renders ungrouped repos as top-level repo rows when Project Groups exist', () => {
const group: ProjectGroup = {
id: 'group-1',
name: 'Platform',
parentPath: '/platform',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const rows = buildRows(
'repo',
[worktree],
repoMap,
null,
new Set(),
new Map([[repo.id, 0]]),
undefined,
'manual',
{},
new Map([[worktree.id, worktree]]),
false,
undefined,
[group]
)
expect(rows.filter((row) => row.type === 'header').map((row) => row.key)).toEqual([
'project-group:group-1',
'repo:repo-1'
])
})
it('renders repos whose Project Group metadata is missing as top-level repo rows', () => {
const group: ProjectGroup = {
id: 'group-1',
name: 'Platform',
parentPath: '/platform',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const repoWithMissingGroup: Repo = { ...repo, projectGroupId: 'missing-group' }
const rows = buildRows(
'repo',
[worktree],
new Map([[repoWithMissingGroup.id, repoWithMissingGroup]]),
null,
new Set(),
new Map([[repoWithMissingGroup.id, 0]]),
undefined,
'manual',
{},
new Map([[worktree.id, worktree]]),
false,
undefined,
[group]
)
expect(rows.filter((row) => row.type === 'header').map((row) => row.key)).toEqual([
'project-group:group-1',
'repo:repo-1'
])
expect(rows.find((row) => row.type === 'header' && row.key === 'repo:repo-1')).toMatchObject({
projectGroupDepth: 0
})
})
it('does not render collapsed child-group repos as missing metadata fallbacks', () => {
const parentGroup: ProjectGroup = {
id: 'parent-group',
name: 'Platform',
parentPath: '/platform',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const childGroup: ProjectGroup = {
...parentGroup,
id: 'child-group',
name: 'Services',
parentPath: '/platform/services',
parentGroupId: parentGroup.id
}
const repoInChildGroup: Repo = { ...repo, projectGroupId: childGroup.id }
const rows = buildRows(
'repo',
[worktree],
new Map([[repoInChildGroup.id, repoInChildGroup]]),
null,
new Set(['project-group:parent-group']),
new Map([[repoInChildGroup.id, 0]]),
undefined,
'manual',
{},
new Map([[worktree.id, worktree]]),
false,
undefined,
[parentGroup, childGroup]
)
expect(rows.filter((row) => row.type === 'header').map((row) => row.key)).toEqual([
'project-group:parent-group'
])
})
it('disambiguates duplicate top-level repo basenames without renaming repos', () => {
const group: ProjectGroup = {
id: 'group-1',
name: 'Platform',
parentPath: '/platform',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const paymentsApi: Repo = {
...repo,
id: 'repo-payments-api',
path: '/workspace/platform/payments/api',
displayName: 'api'
}
const billingApi: Repo = {
...repo,
id: 'repo-billing-api',
path: '/workspace/platform/billing/api',
displayName: 'api'
}
const webRepo: Repo = {
...repo,
id: 'repo-web',
path: '/workspace/platform/web',
displayName: 'web'
}
const repos = new Map([
[paymentsApi.id, paymentsApi],
[billingApi.id, billingApi],
[webRepo.id, webRepo]
])
const worktrees = [
{ ...worktree, id: 'wt-payments-api', repoId: paymentsApi.id },
{ ...worktree, id: 'wt-billing-api', repoId: billingApi.id },
{ ...worktree, id: 'wt-web', repoId: webRepo.id }
]
const rows = buildRows(
'repo',
worktrees,
repos,
null,
new Set(),
new Map([
[paymentsApi.id, 0],
[billingApi.id, 1],
[webRepo.id, 2]
]),
undefined,
'manual',
{},
new Map(worktrees.map((entry) => [entry.id, entry])),
false,
undefined,
[group]
)
expect(rows.filter((row) => row.type === 'header').map((row) => row.label)).toEqual([
'Platform',
'payments/api',
'billing/api',
'web'
])
expect(paymentsApi.displayName).toBe('api')
expect(billingApi.displayName).toBe('api')
})
it('disambiguates duplicate repo basenames inside each Project Group scope', () => {
const group: ProjectGroup = {
id: 'group-1',
name: 'Platform',
parentPath: '/platform',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const paymentsApi: Repo = {
...repo,
id: 'repo-payments-api',
path: '/workspace/platform/payments/api',
displayName: 'api',
projectGroupId: group.id,
projectGroupOrder: 0
}
const billingApi: Repo = {
...repo,
id: 'repo-billing-api',
path: '/workspace/platform/billing/api',
displayName: 'api',
projectGroupId: group.id,
projectGroupOrder: 1
}
const webRepo: Repo = {
...repo,
id: 'repo-web',
path: '/workspace/platform/web',
displayName: 'web',
projectGroupId: group.id,
projectGroupOrder: 2
}
const repos = new Map([
[paymentsApi.id, paymentsApi],
[billingApi.id, billingApi],
[webRepo.id, webRepo]
])
const worktrees = [
{ ...worktree, id: 'wt-payments-api', repoId: paymentsApi.id },
{ ...worktree, id: 'wt-billing-api', repoId: billingApi.id },
{ ...worktree, id: 'wt-web', repoId: webRepo.id }
]
const rows = buildRows(
'repo',
worktrees,
repos,
null,
new Set(),
new Map([
[paymentsApi.id, 0],
[billingApi.id, 1],
[webRepo.id, 2]
]),
undefined,
'manual',
{},
new Map(worktrees.map((entry) => [entry.id, entry])),
false,
undefined,
[group]
)
expect(rows.filter((row) => row.type === 'header').map((row) => row.label)).toEqual([
'Platform',
'payments/api',
'billing/api',
'web'
])
expect(paymentsApi.displayName).toBe('api')
expect(billingApi.displayName).toBe('api')
})
})
@@ -1,691 +1,13 @@
import { describe, expect, it } from 'vitest'
import { projectHostSetupProjectionFromRepos } from '../../../../../../shared/project-host-setup-projection'
import { buildRows } from './build-rows.test-fixture'
import { getGroupKeyForWorktree } from '../../../../../../shared/sidebar-list/worktree-group-keys'
import {
LOCAL_HOST_LABEL,
repo,
worktree,
remoteRepo,
remoteWorktree,
project,
projectHostSetups,
makeDetectedWorktree
} from './worktree-list-groups-test-fixtures'
import type { Project, ProjectHostSetup } from '../../../../../../shared/project-types'
import { repo, worktree, project, projectHostSetups } from './worktree-list-groups-test-fixtures'
import type { ProjectHostSetup } from '../../../../../../shared/project-types'
import type { Repo } from '../../../../../../shared/repo-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
describe('buildRows with pinned worktrees', () => {
it('groups multiple host setups for the same project under one project header', () => {
const rows = buildRows(
'repo',
[worktree, remoteWorktree],
new Map([
[repo.id, repo],
[remoteRepo.id, remoteRepo]
]),
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([
[worktree.id, worktree],
[remoteWorktree.id, remoteWorktree]
]),
false,
undefined,
[],
new Set(),
new Map(),
new Map(),
[],
{ projects: [project], projectHostSetups }
)
expect(rows).toMatchObject([
{ type: 'header', key: 'project:github:stablyai/orca', label: 'Orca', count: 2 },
{ type: 'item', worktree: { id: worktree.id }, hostContextLabel: LOCAL_HOST_LABEL },
{ type: 'item', worktree: { id: remoteWorktree.id }, hostContextLabel: 'gpu-vm' }
])
})
it('keeps the cross-host project header label when another project section renders', () => {
// Filtering other sections must not replace a project’s display name with its anchor checkout’s name.
const otherRepo: Repo = {
...repo,
id: 'repo-other',
path: '/tmp/design-assets',
displayName: 'design-assets'
}
const otherWorktree: Worktree = {
...worktree,
id: 'wt-other',
repoId: otherRepo.id,
path: '/tmp/design-assets-feature',
displayName: 'palette'
}
const buildHeaders = (extraWorktrees: Worktree[], extraRepos: Repo[]) => {
const worktrees = [worktree, remoteWorktree, ...extraWorktrees]
const rows = buildRows(
'repo',
worktrees,
new Map([
[repo.id, repo],
[remoteRepo.id, remoteRepo],
...extraRepos.map((entry): [string, Repo] => [entry.id, entry])
]),
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map(worktrees.map((entry) => [entry.id, entry])),
false,
undefined,
[],
new Set(),
new Map(),
new Map(),
[],
{ projects: [project], projectHostSetups }
)
return rows.filter((row) => row.type === 'header')
}
expect(buildHeaders([], [])).toMatchObject([
{ key: 'project:github:stablyai/orca', label: 'Orca' }
])
expect(buildHeaders([otherWorktree], [otherRepo])).toMatchObject([
{ key: 'project:github:stablyai/orca', label: 'Orca' },
{ key: 'repo:repo-other', label: 'design-assets' }
])
})
it('renders same-project records with git remote identity as one mixed-host project header', () => {
const localRepo: Repo = {
...repo,
id: 'local-sample-app',
path: '/Users/alice/work/sample-app',
displayName: 'sample-app',
gitRemoteIdentity: {
canonicalKey: 'git.company.test/team/sample-app',
remoteName: 'origin',
remoteUrl: 'git@git.company.test:team/sample-app.git'
}
}
const sshRepo: Repo = {
...repo,
id: 'ssh-sample-app',
path: '/home/alice/src/sample-app',
displayName: 'sample-app',
connectionId: 'build server',
gitRemoteIdentity: {
canonicalKey: 'git.company.test/team/sample-app',
remoteName: 'origin',
remoteUrl: 'https://git.company.test/team/sample-app.git'
}
}
const runtimeRepo: Repo = {
...repo,
id: 'runtime-sample-app',
path: '/workspace/sample-app',
displayName: 'sample-app',
executionHostId: 'runtime:dev-container',
gitRemoteIdentity: {
canonicalKey: 'git.company.test/team/sample-app',
remoteName: 'origin',
remoteUrl: 'ssh://git@git.company.test/team/sample-app.git'
}
}
const localWorktree: Worktree = {
...worktree,
id: 'wt-local-sample-app',
repoId: localRepo.id,
path: '/Users/alice/work/sample-app-feature'
}
const sshWorktree: Worktree = {
...worktree,
id: 'wt-ssh-sample-app',
repoId: sshRepo.id,
path: '/home/alice/src/sample-app-feature'
}
const runtimeWorktree: Worktree = {
...worktree,
id: 'wt-runtime-sample-app',
repoId: runtimeRepo.id,
path: '/workspace/sample-app-feature'
}
const projection = projectHostSetupProjectionFromRepos([localRepo, sshRepo, runtimeRepo])
const rows = buildRows(
'repo',
[localWorktree, sshWorktree, runtimeWorktree],
new Map([
[localRepo.id, localRepo],
[sshRepo.id, sshRepo],
[runtimeRepo.id, runtimeRepo]
]),
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([
[localWorktree.id, localWorktree],
[sshWorktree.id, sshWorktree],
[runtimeWorktree.id, runtimeWorktree]
]),
false,
undefined,
[],
new Set(),
new Map(),
new Map(),
[],
{
projects: projection.projects,
projectHostSetups: projection.setups
}
)
expect(rows).toMatchObject([
{
type: 'header',
key: 'project:git:git.company.test/team/sample-app',
label: 'sample-app',
count: 3
},
{ type: 'item', worktree: { id: localWorktree.id }, hostContextLabel: LOCAL_HOST_LABEL },
{ type: 'item', worktree: { id: sshWorktree.id }, hostContextLabel: 'build server' },
{ type: 'item', worktree: { id: runtimeWorktree.id }, hostContextLabel: 'dev-container' }
])
})
it('keeps mixed-host project item order while inserting inbox rows before worktrees', () => {
const localRepo: Repo = {
...repo,
id: 'local-sample-app',
displayName: 'sample-app',
gitRemoteIdentity: {
canonicalKey: 'git.company.test/team/sample-app',
remoteName: 'origin',
remoteUrl: 'https://git.company.test/team/sample-app.git'
}
}
const sshRepo: Repo = {
...repo,
id: 'ssh-sample-app',
path: '/home/alice/src/sample-app',
displayName: 'sample-app',
connectionId: 'build server',
gitRemoteIdentity: {
canonicalKey: 'git.company.test/team/sample-app',
remoteName: 'origin',
remoteUrl: 'https://git.company.test/team/sample-app.git'
}
}
const localFirst: Worktree = {
...worktree,
id: 'wt-local-first',
repoId: localRepo.id,
path: '/Users/alice/work/sample-app-a'
}
const sshWorktree: Worktree = {
...worktree,
id: 'wt-ssh',
repoId: sshRepo.id,
path: '/home/alice/src/sample-app-b'
}
const localSecond: Worktree = {
...worktree,
id: 'wt-local-second',
repoId: localRepo.id,
path: '/Users/alice/work/sample-app-c'
}
const projection = projectHostSetupProjectionFromRepos([localRepo, sshRepo])
const rows = buildRows(
'repo',
[localFirst, sshWorktree, localSecond],
new Map([
[localRepo.id, localRepo],
[sshRepo.id, sshRepo]
]),
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([
[localFirst.id, localFirst],
[sshWorktree.id, sshWorktree],
[localSecond.id, localSecond]
]),
false,
undefined,
[],
new Set(),
new Map(),
new Map([
[
localRepo.id,
{ repo: localRepo, inboxWorktrees: [makeDetectedWorktree({ id: 'local-inbox' })] }
],
[sshRepo.id, { repo: sshRepo, inboxWorktrees: [makeDetectedWorktree({ id: 'ssh-inbox' })] }]
]),
[],
{
projects: projection.projects,
projectHostSetups: projection.setups
}
)
expect(rows).toMatchObject([
{ type: 'header' },
{ type: 'new-external-worktrees-inbox', repo: { id: localRepo.id } },
{ type: 'new-external-worktrees-inbox', repo: { id: sshRepo.id } },
{ type: 'item', worktree: { id: localFirst.id } },
{ type: 'item', worktree: { id: sshWorktree.id } },
{ type: 'item', worktree: { id: localSecond.id } }
])
})
it('orders project identity headers by the manual repo order anchor', () => {
const analyticsProject: Project = {
...project,
id: 'github:stablyai/analytics',
displayName: 'Analytics',
sourceRepoIds: ['repo-analytics']
}
const analyticsRepo: Repo = {
...repo,
id: 'repo-analytics',
path: '/tmp/analytics',
displayName: 'analytics',
upstream: { owner: 'stablyai', repo: 'analytics' }
}
const analyticsWorktree: Worktree = {
...worktree,
id: 'wt-analytics',
repoId: analyticsRepo.id,
displayName: 'analytics'
}
const analyticsSetup: ProjectHostSetup = {
...projectHostSetups[0]!,
id: analyticsRepo.id,
projectId: analyticsProject.id,
repoId: analyticsRepo.id,
path: analyticsRepo.path,
displayName: analyticsRepo.displayName
}
const repoOrder = new Map([
[repo.id, 0],
[remoteRepo.id, 1],
[analyticsRepo.id, 2]
])
const rows = buildRows(
'repo',
[worktree, analyticsWorktree, remoteWorktree],
new Map([
[repo.id, repo],
[remoteRepo.id, remoteRepo],
[analyticsRepo.id, analyticsRepo]
]),
null,
new Set(),
repoOrder,
undefined,
'manual',
{},
new Map([
[worktree.id, worktree],
[remoteWorktree.id, remoteWorktree],
[analyticsWorktree.id, analyticsWorktree]
]),
false,
undefined,
[],
new Set(),
new Map(),
new Map(),
[],
{
projects: [project, analyticsProject],
projectHostSetups: [...projectHostSetups, analyticsSetup]
}
)
const headers = rows.filter((row) => row.type === 'header')
expect(headers.map((row) => row.key)).toEqual([
'project:github:stablyai/orca',
'project:github:stablyai/analytics'
])
expect(headers[0]).toMatchObject({
key: 'project:github:stablyai/orca',
repo: { id: repo.id, badgeColor: repo.badgeColor }
})
})
it('splits same-host checkouts of one project into separate per-setup groups', () => {
const repoB: Repo = { ...repo, id: 'repo-2', path: '/tmp/orca-2', displayName: 'orca-2' }
const worktreeB: Worktree = {
...worktree,
id: 'wt-2',
repoId: repoB.id,
path: '/tmp/orca-2-feature',
displayName: 'feature-b'
}
const localSetupB: ProjectHostSetup = {
...projectHostSetups[0]!,
id: repoB.id,
repoId: repoB.id,
path: repoB.path,
displayName: repoB.displayName
}
const rows = buildRows(
'repo',
[worktree, worktreeB],
new Map([
[repo.id, repo],
[repoB.id, repoB]
]),
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([
[worktree.id, worktree],
[worktreeB.id, worktreeB]
]),
false,
undefined,
[],
new Set(),
new Map(),
new Map(),
[],
{
projects: [{ ...project, sourceRepoIds: [repo.id, repoB.id] }],
projectHostSetups: [projectHostSetups[0]!, localSetupB]
}
)
const headers = rows.filter((row) => row.type === 'header')
expect(headers).toHaveLength(2)
expect(headers).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: 'project:github:stablyai/orca::setup:repo-1',
label: 'orca'
}),
expect.objectContaining({
key: 'project:github:stablyai/orca::setup:repo-2',
label: 'orca-2'
})
])
)
})
it('splits only the surface with duplicate checkouts', () => {
const localRepoB: Repo = {
...repo,
id: 'repo-local-b',
path: '/tmp/orca-b',
displayName: 'orca-b'
}
const localWorktreeB: Worktree = {
...worktree,
id: 'wt-local-b',
repoId: localRepoB.id,
path: '/tmp/orca-b-feature',
displayName: 'feature-b'
}
const localSetupB: ProjectHostSetup = {
...projectHostSetups[0]!,
id: localRepoB.id,
repoId: localRepoB.id,
path: localRepoB.path,
displayName: localRepoB.displayName
}
const rows = buildRows(
'repo',
[worktree, localWorktreeB, remoteWorktree],
new Map([
[repo.id, repo],
[localRepoB.id, localRepoB],
[remoteRepo.id, remoteRepo]
]),
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([
[worktree.id, worktree],
[localWorktreeB.id, localWorktreeB],
[remoteWorktree.id, remoteWorktree]
]),
false,
undefined,
[],
new Set(),
new Map(),
new Map(),
[],
{
projects: [{ ...project, sourceRepoIds: [repo.id, localRepoB.id, remoteRepo.id] }],
projectHostSetups: [projectHostSetups[0]!, localSetupB, projectHostSetups[1]!]
}
)
const headers = rows.filter((row) => row.type === 'header')
expect(headers.map((row) => row.key)).toEqual([
'project:github:stablyai/orca::setup:repo-1',
'project:github:stablyai/orca::setup:repo-local-b',
'project:github:stablyai/orca'
])
})
it('counts projection twins for one directory as one checkout', () => {
const runtimeHostId = 'runtime:m2-air'
const runtimeRepo: Repo = {
...remoteRepo,
id: 'repo-runtime',
path: '/home/alice/orca-runtime',
connectionId: undefined,
executionHostId: runtimeHostId
}
const runtimeWorktree: Worktree = {
...remoteWorktree,
id: 'wt-runtime',
repoId: runtimeRepo.id,
path: '/home/alice/orca-runtime-feature'
}
const runtimeSetup: ProjectHostSetup = {
...projectHostSetups[1]!,
id: runtimeRepo.id,
repoId: runtimeRepo.id,
path: runtimeRepo.path,
hostId: runtimeHostId,
executionHostId: runtimeHostId
}
const derivedSetup: ProjectHostSetup = {
...projectHostSetups[1]!,
hostId: runtimeHostId,
connectionId: 'intel mac',
executionHostId: runtimeHostId
}
const authoritativeSetup: ProjectHostSetup = {
...derivedSetup,
id: 'setup-authoritative',
path: `${derivedSetup.path}/`,
connectionId: null,
executionHostId: 'ssh:intel%20mac'
}
const grouping = {
projects: [{ ...project, sourceRepoIds: [remoteRepo.id, runtimeRepo.id] }],
projectHostSetups: [runtimeSetup, derivedSetup, authoritativeSetup]
}
expect([
getGroupKeyForWorktree(
'repo',
remoteWorktree,
new Map([[remoteRepo.id, remoteRepo]]),
null,
undefined,
grouping
),
getGroupKeyForWorktree(
'repo',
runtimeWorktree,
new Map([[runtimeRepo.id, runtimeRepo]]),
null,
undefined,
grouping
)
]).toEqual(['project:github:stablyai/orca', 'project:github:stablyai/orca'])
})
it('keeps Git hosts grouped when folder setups share the project identity', () => {
const windowsHostId = 'runtime:windows-server'
const windowsRepo: Repo = {
...repo,
id: 'repo-windows',
path: 'C:\\Users\\neil\\orca\\orca',
executionHostId: windowsHostId
}
const folderRepoA: Repo = {
...repo,
id: 'folder-qa-a',
path: '/tmp/pr11751-folder-qa',
displayName: 'pr11751-folder-qa',
kind: 'folder'
}
const folderRepoB: Repo = {
...folderRepoA,
id: 'folder-qa-b',
path: '/tmp/pr11767-runtime-folder',
displayName: 'pr11767-runtime-folder'
}
const setups: ProjectHostSetup[] = [
projectHostSetups[0]!,
{ ...projectHostSetups[0]!, id: 'projection-twin' },
projectHostSetups[1]!,
{
...projectHostSetups[0]!,
id: windowsRepo.id,
repoId: windowsRepo.id,
path: windowsRepo.path,
hostId: windowsHostId,
executionHostId: windowsHostId
},
{
...projectHostSetups[0]!,
id: folderRepoA.id,
repoId: folderRepoA.id,
path: folderRepoA.path,
displayName: folderRepoA.displayName,
kind: 'folder'
},
{
...projectHostSetups[0]!,
id: folderRepoB.id,
repoId: folderRepoB.id,
path: folderRepoB.path,
displayName: folderRepoB.displayName,
kind: 'folder'
}
]
const repos = [repo, remoteRepo, windowsRepo, folderRepoA, folderRepoB]
const grouping = {
projects: [{ ...project, sourceRepoIds: repos.map((entry) => entry.id) }],
projectHostSetups: setups
}
const groupKeys = repos.map((entry) =>
getGroupKeyForWorktree(
'repo',
{ ...worktree, id: `wt-${entry.id}`, repoId: entry.id, path: entry.path },
new Map([[entry.id, entry]]),
null,
undefined,
grouping
)
)
expect(new Set(groupKeys)).toEqual(new Set(['project:github:stablyai/orca']))
})
it('keeps a provisioned runtime copy under the project header alongside a same-host checkout', () => {
const runtimeRepoB: Repo = {
...repo,
id: 'repo-runtime-b',
path: '/tmp/orca-runtime-b',
displayName: 'orca-runtime-b'
}
const runtimeWorktreeB: Worktree = {
...worktree,
id: 'wt-runtime-b',
repoId: runtimeRepoB.id,
path: '/tmp/orca-runtime-b-feature',
displayName: 'feature-runtime-b'
}
// Provisioned copies share project identity and nest rather than split real checkouts into extra headers.
const runtimeSetupB: ProjectHostSetup = {
...projectHostSetups[0]!,
id: runtimeRepoB.id,
repoId: runtimeRepoB.id,
path: runtimeRepoB.path,
displayName: runtimeRepoB.displayName,
setupMethod: 'provisioned'
}
const rows = buildRows(
'repo',
[worktree, runtimeWorktreeB],
new Map([
[repo.id, repo],
[runtimeRepoB.id, runtimeRepoB]
]),
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([
[worktree.id, worktree],
[runtimeWorktreeB.id, runtimeWorktreeB]
]),
false,
undefined,
[],
new Set(),
new Map(),
new Map(),
[],
{
projects: [{ ...project, sourceRepoIds: [repo.id, runtimeRepoB.id] }],
projectHostSetups: [projectHostSetups[0]!, runtimeSetupB]
}
)
const headers = rows.filter((row) => row.type === 'header')
expect(headers).toHaveLength(1)
expect(headers[0]).toMatchObject({
key: 'project:github:stablyai/orca',
label: 'Orca',
count: 2
})
})
it('splits duplicate user checkouts while a provisioned copy nests, on one host', () => {
// Exercise real-checkout splitting and provisioned-copy nesting together on one host.
const localRepoB: Repo = {
@@ -1,146 +1,11 @@
import { describe, expect, it } from 'vitest'
import { buildRows } from './build-rows.test-fixture'
import { repo, worktree } from './worktree-list-groups-test-fixtures'
import type { ProjectGroup } from '../../../../../../shared/project-group-types'
import type { Repo } from '../../../../../../shared/repo-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
describe('buildRows project grouping order', () => {
const repoA: Repo = { ...repo, id: 'repo-a', displayName: 'alpha' }
const repoB: Repo = { ...repo, id: 'repo-b', displayName: 'beta' }
const repoC: Repo = { ...repo, id: 'repo-c', displayName: 'gamma' }
const map = new Map([
[repoA.id, repoA],
[repoB.id, repoB],
[repoC.id, repoC]
])
// A stale sibling precedes A’s fresh child to require max-child activity ranking.
const wA: Worktree = {
...worktree,
id: 'wt-a',
repoId: repoA.id,
displayName: 'a',
lastActivityAt: 200
}
const wAStale: Worktree = {
...worktree,
id: 'wt-a-stale',
repoId: repoA.id,
displayName: 'a2',
lastActivityAt: 50
}
const wB: Worktree = {
...worktree,
id: 'wt-b',
repoId: repoB.id,
displayName: 'b',
lastActivityAt: 100
}
const wC: Worktree = {
...worktree,
id: 'wt-c',
repoId: repoC.id,
displayName: 'c',
lastActivityAt: 300
}
it('orders repo headers by explicit repoOrder, not first-encounter', () => {
const repoOrder = new Map([
[repoB.id, 0],
[repoA.id, 1],
[repoC.id, 2]
])
const rows = buildRows('repo', [wC, wA, wB], map, null, new Set(), repoOrder)
const headerKeys = rows.filter((r) => r.type === 'header').map((r) => r.key)
expect(headerKeys).toEqual(['repo:repo-b', 'repo:repo-a', 'repo:repo-c'])
})
it('places unknown repo ids last and sorts them by label', () => {
const repoOrder = new Map([[repoB.id, 0]])
const rows = buildRows('repo', [wC, wA, wB], map, null, new Set(), repoOrder)
const headerKeys = rows.filter((r) => r.type === 'header').map((r) => r.key)
expect(headerKeys).toEqual(['repo:repo-b', 'repo:repo-a', 'repo:repo-c'])
})
it('orders repo headers by max(lastActivityAt) per repo in Recent mode', () => {
// Pass name order and conflicting manual ranks so Recent must compute activity ranks itself.
const repoOrder = new Map([
[repoB.id, 0],
[repoA.id, 1],
[repoC.id, 2]
])
const rows = buildRows(
'repo',
[wA, wB, wC],
map,
null,
new Set(),
repoOrder,
undefined,
'recent'
)
const headerKeys = rows.filter((r) => r.type === 'header').map((r) => r.key)
expect(headerKeys).toEqual(['repo:repo-c', 'repo:repo-a', 'repo:repo-b'])
})
it("uses each repo's freshest visible child, not its first, in Recent mode", () => {
const rows = buildRows(
'repo',
[wAStale, wA, wB, wC],
map,
null,
new Set(),
undefined,
undefined,
'recent'
)
expect(rows).toMatchObject([
{ type: 'header', key: 'repo:repo-c' },
{ type: 'item', worktree: { id: 'wt-c' } },
{ type: 'header', key: 'repo:repo-a' },
// Child rows keep their input order; only the header rank uses max activity.
{ type: 'item', worktree: { id: 'wt-a-stale' } },
{ type: 'item', worktree: { id: 'wt-a' } },
{ type: 'header', key: 'repo:repo-b' },
{ type: 'item', worktree: { id: 'wt-b' } }
])
})
it('keeps the main workspace first inside its project group in Recent mode', () => {
const main = {
...wA,
id: 'wt-a-main',
displayName: 'main',
isMainWorktree: true,
lastActivityAt: 10
}
const freshChild = {
...wA,
id: 'wt-a-fresh-child',
displayName: 'fresh-child',
isMainWorktree: false,
lastActivityAt: 500
}
const rows = buildRows(
'repo',
[freshChild, wB, main],
map,
null,
new Set(),
undefined,
undefined,
'recent'
)
expect(rows).toMatchObject([
{ type: 'header', key: 'repo:repo-a' },
{ type: 'item', worktree: { id: 'wt-a-main' } },
{ type: 'item', worktree: { id: 'wt-a-fresh-child' } },
{ type: 'header', key: 'repo:repo-b' },
{ type: 'item', worktree: { id: 'wt-b' } }
])
})
it('builds rows for a very large repo-group list', () => {
const count = 130_000
@@ -158,344 +23,3 @@ describe('buildRows project grouping order', () => {
expect(rows.at(-1)).toMatchObject({ type: 'item', worktree: { id: 'wt-129999' } })
})
})
describe('buildRows Recent project order fallbacks', () => {
const active: Repo = { ...repo, id: 'repo-active', displayName: 'active', addedAt: 0 }
const empty: Repo = { ...repo, id: 'repo-empty', displayName: 'empty', addedAt: 999 }
const map = new Map([
[active.id, active],
[empty.id, empty]
])
const activeWorktree: Worktree = {
...worktree,
id: 'wt-active',
repoId: active.id,
displayName: 'active',
lastActivityAt: 100
}
it('sorts placeholder projects after projects with activity', () => {
// Real workspace activity must outrank an empty project’s larger addedAt fallback.
const rows = buildRows(
'repo',
[activeWorktree],
map,
null,
new Set(),
undefined,
undefined,
'recent',
{},
undefined,
false,
undefined,
[],
new Set([empty.id])
)
const headerKeys = rows.filter((r) => r.type === 'header').map((r) => r.key)
expect(headerKeys).toEqual(['repo:repo-active', 'repo:repo-empty'])
})
})
describe('project groups', () => {
it('orders repos inside a Project Group by projectGroupOrder in manual mode', () => {
const group: ProjectGroup = {
id: 'group-1',
name: 'Platform',
parentPath: '/platform',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const repoA: Repo = {
...repo,
id: 'repo-a',
displayName: 'alpha',
projectGroupId: group.id,
projectGroupOrder: 1
}
const repoB: Repo = {
...repo,
id: 'repo-b',
displayName: 'beta',
projectGroupId: group.id,
projectGroupOrder: 0
}
const worktreeA: Worktree = { ...worktree, id: 'wt-a', repoId: repoA.id }
const worktreeB: Worktree = { ...worktree, id: 'wt-b', repoId: repoB.id }
const groupedMap = new Map([
[repoA.id, repoA],
[repoB.id, repoB]
])
const repoOrder = new Map([
[repoA.id, 0],
[repoB.id, 1]
])
const rows = buildRows(
'repo',
[worktreeA, worktreeB],
groupedMap,
null,
new Set(),
repoOrder,
undefined,
'manual',
undefined,
undefined,
false,
undefined,
[group]
)
expect(rows.filter((row) => row.type === 'header').map((row) => row.key)).toEqual([
'project-group:group-1',
'repo:repo-b',
'repo:repo-a'
])
})
it('falls back to repoOrder for grouped repos missing projectGroupOrder in manual mode', () => {
const group: ProjectGroup = {
id: 'group-1',
name: 'Platform',
parentPath: '/platform',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const repoA: Repo = { ...repo, id: 'repo-a', displayName: 'alpha', projectGroupId: group.id }
const repoB: Repo = { ...repo, id: 'repo-b', displayName: 'beta', projectGroupId: group.id }
const repoC: Repo = { ...repo, id: 'repo-c', displayName: 'gamma', projectGroupId: group.id }
const groupedMap = new Map([
[repoA.id, repoA],
[repoB.id, repoB],
[repoC.id, repoC]
])
const repoOrder = new Map([
[repoA.id, 0],
[repoB.id, 1],
[repoC.id, 2]
])
const rows = buildRows(
'repo',
[
{ ...worktree, id: 'wt-a', repoId: repoA.id },
{ ...worktree, id: 'wt-b', repoId: repoB.id },
{ ...worktree, id: 'wt-c', repoId: repoC.id }
],
groupedMap,
null,
new Set(),
repoOrder,
undefined,
'manual',
undefined,
undefined,
false,
undefined,
[group]
)
expect(rows.filter((row) => row.type === 'header').map((row) => row.key)).toEqual([
'project-group:group-1',
'repo:repo-a',
'repo:repo-b',
'repo:repo-c'
])
})
it('sorts a dragged project between repo-order fallbacks inside a group', () => {
const group: ProjectGroup = {
id: 'group-1',
name: 'Platform',
parentPath: '/platform',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const repoA: Repo = { ...repo, id: 'repo-a', displayName: 'alpha', projectGroupId: group.id }
const repoB: Repo = { ...repo, id: 'repo-b', displayName: 'beta', projectGroupId: group.id }
const repoC: Repo = {
...repo,
id: 'repo-c',
displayName: 'gamma',
projectGroupId: group.id,
projectGroupOrder: 500
}
const groupedMap = new Map([
[repoA.id, repoA],
[repoB.id, repoB],
[repoC.id, repoC]
])
const repoOrder = new Map([
[repoA.id, 0],
[repoB.id, 1],
[repoC.id, 2]
])
const rows = buildRows(
'repo',
[
{ ...worktree, id: 'wt-a', repoId: repoA.id },
{ ...worktree, id: 'wt-b', repoId: repoB.id },
{ ...worktree, id: 'wt-c', repoId: repoC.id }
],
groupedMap,
null,
new Set(),
repoOrder,
undefined,
'manual',
undefined,
undefined,
false,
undefined,
[group]
)
expect(rows.filter((row) => row.type === 'header').map((row) => row.key)).toEqual([
'project-group:group-1',
'repo:repo-a',
'repo:repo-c',
'repo:repo-b'
])
})
it('orders repos inside a Project Group by activity in recent mode, keeping tabOrder', () => {
const groupA: ProjectGroup = {
id: 'group-a',
name: 'Platform',
parentPath: '/platform',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 1,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const groupB: ProjectGroup = { ...groupA, id: 'group-b', name: 'Infra', tabOrder: 0 }
// Recent activity must override conflicting projectGroupOrder within the group.
const repoStale: Repo = {
...repo,
id: 'repo-stale',
displayName: 'stale',
projectGroupId: groupA.id,
projectGroupOrder: 0
}
const repoFresh: Repo = {
...repo,
id: 'repo-fresh',
displayName: 'fresh',
projectGroupId: groupA.id,
projectGroupOrder: 1
}
const groupedMap = new Map([
[repoStale.id, repoStale],
[repoFresh.id, repoFresh]
])
const worktrees = [
{ ...worktree, id: 'wt-stale', repoId: repoStale.id, lastActivityAt: 10 },
{ ...worktree, id: 'wt-fresh', repoId: repoFresh.id, lastActivityAt: 500 }
]
const rows = buildRows(
'repo',
worktrees,
groupedMap,
null,
new Set(),
undefined,
undefined,
'recent',
{},
new Map(worktrees.map((entry) => [entry.id, entry])),
false,
undefined,
// Group headers retain tabOrder regardless of projectOrderBy.
[groupA, groupB]
)
expect(rows.filter((row) => row.type === 'header').map((row) => row.key)).toEqual([
'project-group:group-b',
'project-group:group-a',
'repo:repo-fresh',
'repo:repo-stale'
])
})
it('orders Project Group siblings by tabOrder within each parent bucket', () => {
const rootA: ProjectGroup = {
id: 'group-root-a',
name: 'Platform',
parentPath: '/platform',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 20,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const rootB: ProjectGroup = {
...rootA,
id: 'group-root-b',
name: 'Infrastructure',
tabOrder: 10
}
const childLate: ProjectGroup = {
...rootA,
id: 'group-child-late',
name: 'late',
parentGroupId: rootB.id,
tabOrder: 30
}
const childEarly: ProjectGroup = {
...rootA,
id: 'group-child-early',
name: 'early',
parentGroupId: rootB.id,
tabOrder: 5
}
const rows = buildRows(
'repo',
[],
new Map(),
null,
new Set(),
undefined,
undefined,
'recent',
{},
undefined,
false,
undefined,
[rootA, rootB, childLate, childEarly]
)
expect(rows.filter((row) => row.type === 'header').map((row) => row.key)).toEqual([
'project-group:group-root-b',
'project-group:group-child-early',
'project-group:group-child-late',
'project-group:group-root-a'
])
expect(rows.filter((row) => row.type === 'header').map((row) => row.projectGroupDepth)).toEqual(
[0, 1, 1, 0]
)
})
})
@@ -1,327 +0,0 @@
// @vitest-environment happy-dom
vi.mock('@/components/confirmation-dialog-context', () => ({
useConfirmationDialog: () => vi.fn().mockResolvedValue(false)
}))
import { act, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as ProjectHeaderDragContract from '../drag/project-header-drag-contract'
import type { Repo } from '../../../../../../shared/repo-types'
import type { WorktreeCardProperty } from '../../../../../../shared/ui-chrome-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
globalThis.IS_REACT_ACT_ENVIRONMENT = true
const mockStore = vi.hoisted(() => ({
state: {} as Record<string, unknown>,
setVisibleReviewCardWorktreeIds: vi.fn<(ids: readonly string[]) => void>(),
activateWorktreeFromSidebar: vi.fn(),
openModal: vi.fn()
}))
// Count inner renders so React.memo bailouts remain observable across order-preserving epoch changes.
const cardRenderSpy = vi.hoisted(() => vi.fn())
type WorktreeListComponent = React.ComponentType<{
scrollOffsetRef: React.RefObject<number>
scrollAnchorRef: React.RefObject<unknown>
}>
let WorktreeList: WorktreeListComponent
vi.mock('@/store', () => {
const useAppStore = ((selector: (state: Record<string, unknown>) => unknown) =>
selector(mockStore.state)) as ((
selector: (state: Record<string, unknown>) => unknown
) => unknown) & {
getState: () => Record<string, unknown>
}
useAppStore.getState = () => mockStore.state
return { useAppStore }
})
vi.mock('@tanstack/react-virtual', () => ({
defaultRangeExtractor: ({ startIndex, endIndex }: { startIndex: number; endIndex: number }) =>
Array.from({ length: endIndex - startIndex + 1 }, (_, index) => startIndex + index),
measureElement: () => 32,
useVirtualizer: ({ count }: { count: number }) => ({
elementsCache: new Map(),
getTotalSize: () => count * 96,
getVirtualItems: () =>
Array.from({ length: count }, (_, index) => ({
index,
key: `row-${index}`,
start: index * 96
})),
measureElement: vi.fn(),
scrollToIndex: vi.fn()
})
}))
vi.mock('@/hooks/useVirtualizedScrollAnchor', () => ({
VIRTUALIZED_SCROLL_ANCHOR_RECORD_EVENT: 'orca:test-record-scroll-anchor',
useVirtualizedScrollAnchor: vi.fn()
}))
vi.mock('../drag/project-header-drag', () => ({
useRepoHeaderDrag: () => ({
state: { draggingRepoId: null, dropIndicatorY: null },
onHandlePointerDown: vi.fn()
})
}))
vi.mock('../drag/project-header-drag-contract', async (importOriginal) => ({
...(await importOriginal<typeof ProjectHeaderDragContract>()),
isRepoHeaderActionTarget: () => false
}))
vi.mock('@/components/ui/hover-card', () => ({
HoverCard: ({ children }: { children: ReactNode }) => <>{children}</>,
HoverCardContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
HoverCardTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
}))
vi.mock('@/components/ui/tooltip', () => ({
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipContent: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
}))
vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenu: ({ children }: { children: ReactNode }) => <>{children}</>,
DropdownMenuContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuItem: ({ children, onSelect }: { children: ReactNode; onSelect?: () => void }) => (
<button onClick={onSelect}>{children}</button>
),
DropdownMenuSeparator: () => <hr />,
DropdownMenuSub: ({ children }: { children: ReactNode }) => <>{children}</>,
DropdownMenuSubContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuSubTrigger: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
}))
vi.mock('@/lib/sidebar-worktree-activation', () => ({
activateWorktreeFromSidebar: mockStore.activateWorktreeFromSidebar
}))
vi.mock('@/lib/telemetry', () => ({ track: vi.fn() }))
vi.mock('@/lib/worktree-activation', () => ({
activateAndRevealWorktree: vi.fn()
}))
vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: vi.fn() }))
vi.mock('../../workspace-card/activity/WorktreeCardAgents', () => ({
default: () => <div>Agent row</div>,
SUPPRESS_WORKTREE_LIST_SCROLL_ADJUSTMENT_EVENT: 'orca:test-suppress-scroll-adjustment'
}))
vi.mock('../../workspace-card/surface/WorktreeCard', async () => {
const ReactModule = await import('react')
const MockWorktreeCard = ReactModule.memo(function WorktreeCard({
worktree
}: {
worktree: Worktree
}) {
cardRenderSpy(worktree.id)
return <div data-mock-worktree-card={worktree.id} />
})
return { default: MockWorktreeCard }
})
function makeRepo(): Repo {
return {
id: 'repo-1',
path: '/tmp/card-memo-stability',
displayName: 'card-memo-stability',
badgeColor: '#999999',
addedAt: 1
}
}
function makeWorktree(args: { id: string; displayName: string; sortOrder: number }): Worktree {
return {
id: args.id,
instanceId: `${args.id}-instance`,
repoId: 'repo-1',
path: `/tmp/card-memo-stability/${args.id}`,
displayName: args.displayName,
branch: `${args.id}-branch`,
head: 'abc123',
isBare: false,
isMainWorktree: false,
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: args.sortOrder,
lastActivityAt: args.sortOrder
}
}
function makeFolderWorkspacePathStatusState(): Record<string, unknown> {
return {
fetchFolderWorkspacePathStatus: vi.fn(),
folderWorkspacePathStatuses: {},
folderWorkspaces: [],
getFolderWorkspacePathStatusCacheKey: (request: unknown) => JSON.stringify(request),
getFreshFolderWorkspacePathStatus: vi.fn(() => null)
}
}
function setFlatWorktreeState(): void {
const repo = makeRepo()
const worktrees = [
makeWorktree({ id: 'wt-a', displayName: 'alpha', sortOrder: 20 }),
makeWorktree({ id: 'wt-b', displayName: 'beta', sortOrder: 10 })
]
mockStore.state = {
...makeFolderWorkspacePathStatusState(),
activeModal: '',
activeView: 'terminal',
activeWorktreeId: null,
agentStatusByPaneKey: {},
agentStatusEpoch: 0,
browserTabsByWorktree: {},
clearPendingRevealWorktreeId: vi.fn(),
collapsedGroups: new Set<string>(),
deleteStateByWorktreeId: {},
detectedWorktreesByRepo: {},
fetchHostedReviewForBranch: vi.fn(),
fetchIssue: vi.fn(),
fetchLinearIssue: vi.fn(),
filterRepoIds: [],
gitConflictOperationByWorktree: {},
groupBy: 'none',
hideDefaultBranchWorkspace: false,
hostedReviewCache: {},
issueCache: {},
linearIssueCache: {},
linearStatus: null,
migrationUnsupportedByPtyId: {},
openModal: mockStore.openModal,
openSettingsPage: vi.fn(),
openSettingsTarget: null,
openTaskPage: vi.fn(),
pendingRevealWorktree: null,
prCache: {},
projectGroups: [],
ptyIdsByTabId: {},
recordFeatureInteraction: vi.fn(),
remoteBranchConflictByWorktreeId: {},
reorderRepos: vi.fn(),
reportVisibleGitHubPRRefreshCandidates: vi.fn(),
setVisibleReviewCardWorktreeIds: mockStore.setVisibleReviewCardWorktreeIds,
repos: [repo],
retainedAgentsByPaneKey: {},
revealWorktreeInSidebar: vi.fn(),
runtimePaneTitlesByTabId: {},
setFilterRepoIds: vi.fn(),
setHideDefaultBranchWorkspace: vi.fn(),
setRenamingWorktreeId: vi.fn(),
setShowSleepingWorkspaces: vi.fn(),
setSortBy: vi.fn(),
setWorktreesPinnedAndReveal: vi.fn(),
settings: null,
showSleepingWorkspaces: true,
sortBy: 'manual',
sortEpoch: 0,
sshConnectedGeneration: 0,
sshConnectionStates: new Map(),
sshTargetLabels: new Map(),
tabsByWorktree: {},
terminalLayoutsByTabId: {},
toggleCollapsedGroup: vi.fn(),
updateRepo: vi.fn(),
updateWorktreeMeta: vi.fn(),
updateWorktreesMeta: vi.fn(),
workspaceHostScope: 'all',
workspacePortScan: null,
workspaceStatuses: [],
worktreeCardProperties: ['status', 'pr', 'comment'] satisfies WorktreeCardProperty[],
worktreeLineageById: {},
worktreesByRepo: {
[repo.id]: worktrees
}
}
}
const mountedRoots: Root[] = []
async function renderList(root: Root): Promise<void> {
await act(async () => {
// Fresh ref props bypass the list’s own memo to isolate whether individual cards bail out.
root.render(
<WorktreeList scrollOffsetRef={{ current: 0 }} scrollAnchorRef={{ current: null }} />
)
})
}
describe('WorktreeCard memo bail-out across epoch bumps', () => {
beforeAll(async () => {
WorktreeList = (await import('./WorktreeList')).default as WorktreeListComponent
}, 60_000)
beforeEach(() => {
vi.clearAllMocks()
setFlatWorktreeState()
})
afterEach(async () => {
await act(async () => {
for (const root of mountedRoots.splice(0)) {
root.unmount()
}
})
document.body.innerHTML = ''
})
it('does not re-render cards on an order-preserving sortEpoch bump', async () => {
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
mountedRoots.push(root)
await renderList(root)
expect(container.querySelectorAll('[data-mock-worktree-card]')).toHaveLength(2)
const countAfterMount = cardRenderSpy.mock.calls.length
await renderList(root)
expect(cardRenderSpy.mock.calls.length).toBe(countAfterMount)
// Manual sort applies the order-preserving epoch immediately without smart-sort debounce.
mockStore.state = { ...mockStore.state, sortEpoch: 1 }
await renderList(root)
expect(cardRenderSpy.mock.calls.length).toBe(countAfterMount)
})
it('re-renders a card when its own worktree data changes', async () => {
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
mountedRoots.push(root)
await renderList(root)
const countAfterMount = cardRenderSpy.mock.calls.length
const worktrees = (mockStore.state.worktreesByRepo as Record<string, Worktree[]>)['repo-1']!
mockStore.state = {
...mockStore.state,
sortEpoch: 2,
worktreesByRepo: {
'repo-1': [{ ...worktrees[0]!, displayName: 'alpha renamed' }, worktrees[1]!]
}
}
await renderList(root)
expect(cardRenderSpy.mock.calls.length).toBeGreaterThan(countAfterMount)
expect(cardRenderSpy).toHaveBeenCalledWith('wt-a')
})
})
@@ -1,175 +0,0 @@
import { beforeAll, describe, expect, it, vi } from 'vitest'
import type { FolderWorkspace } from '../../../../../../shared/folder-workspaces/folder-workspace-types'
import type { ProjectGroup } from '../../../../../../shared/project-group-types'
import { folderWorkspaceKey } from '../../../../../../shared/workspace-scope'
import {
createAppStoreModuleMock,
createDropdownMenuModuleMock,
createProjectHeaderDragModuleMock,
createReactVirtualModuleMock,
createTooltipModuleMock,
createVirtualizedScrollAnchorModuleMock,
createWorktreeCardAgentsModuleMock,
createWorktreeCardModuleMock,
createWorktreeContextMenuModuleMock,
createWorktreeTitleInlineRenameModuleMock,
loadWorktreeList,
mockStore,
renderWorktreeListMarkup
} from './worktree-list-lineage-card-test-harness'
import {
makeFolderWorkspacePathStatusMockState,
makeFolderWorkspacePathStatusState
} from './worktree-list-lineage-card-test-fixtures'
vi.mock('@/store', () => createAppStoreModuleMock())
vi.mock('@tanstack/react-virtual', () => createReactVirtualModuleMock())
vi.mock('@/hooks/useVirtualizedScrollAnchor', () => createVirtualizedScrollAnchorModuleMock())
vi.mock('../drag/project-header-drag', () => createProjectHeaderDragModuleMock())
vi.mock('../../workspace-card/surface/WorktreeCard', () => createWorktreeCardModuleMock())
vi.mock('../../workspace-card/activity/WorktreeCardAgents', () =>
createWorktreeCardAgentsModuleMock()
)
vi.mock('../../workspace-card/identity/WorktreeTitleInlineRename', () =>
createWorktreeTitleInlineRenameModuleMock()
)
vi.mock('../../workspace-actions/context-menu/WorktreeContextMenu', () =>
createWorktreeContextMenuModuleMock()
)
vi.mock('@/components/ui/tooltip', () => createTooltipModuleMock())
vi.mock('@/components/ui/dropdown-menu', () => createDropdownMenuModuleMock())
function makeFolderWorkspace(
groupId: string,
overrides: Partial<FolderWorkspace> = {}
): FolderWorkspace {
return {
id: 'folder-workspace-1',
projectGroupId: groupId,
name: 'Folder workspace fixture',
folderPath: '/tmp/lineage-order/folder',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 1,
lastActivityAt: 1,
createdAt: 1,
updatedAt: 1,
...overrides
}
}
function setFolderWorkspaceFixtureState() {
const group: ProjectGroup = {
id: 'folder-group-1',
name: 'Folder Group',
parentPath: '/tmp/lineage-order/folder',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const folderWorkspace = makeFolderWorkspace(group.id)
mockStore.state = {
...makeFolderWorkspacePathStatusMockState(),
activeModal: '',
activeView: 'terminal',
activeWorktreeId: folderWorkspaceKey(folderWorkspace.id),
agentStatusEpoch: 0,
agentStatusByPaneKey: {},
browserTabsByWorktree: {},
clearPendingRevealWorktreeId: vi.fn(),
collapsedGroups: new Set<string>(),
deleteStateByWorktreeId: {},
filterRepoIds: [],
...makeFolderWorkspacePathStatusState(),
folderWorkspaces: [folderWorkspace],
groupBy: 'repo',
hideDefaultBranchWorkspace: false,
issueCache: {},
migrationUnsupportedByPtyId: {},
openModal: vi.fn(),
pendingRevealWorktree: null,
prCache: {},
prVisibleRefreshGeneration: 0,
projectGroups: [group],
ptyIdsByTabId: {},
reorderRepos: vi.fn(),
reportVisibleGitHubPRRefreshCandidates: vi.fn(),
retainedAgentsByPaneKey: {},
repos: [],
runtimePaneTitlesByTabId: {},
setFilterRepoIds: vi.fn(),
setHideDefaultBranchWorkspace: vi.fn(),
setRenamingWorktreeId: vi.fn(),
setShowSleepingWorkspaces: vi.fn(),
setSortBy: vi.fn(),
settings: null,
renamingWorktreeId: null,
showSleepingWorkspaces: true,
sortBy: 'manual',
sortEpoch: 0,
sshConnectedGeneration: 0,
sshConnectionStates: new Map(),
sshTargetLabels: new Map(),
tabsByWorktree: {},
terminalLayoutsByTabId: {},
toggleCollapsedGroup: vi.fn(),
updateWorktreeMeta: vi.fn(),
updateWorktreesMeta: vi.fn(),
workspaceHostScope: 'all',
workspaceStatuses: [],
worktreeCardProperties: ['status', 'inline-agents'],
worktreeLineageById: {},
worktreesByRepo: {}
}
return { folderWorkspace, group }
}
describe('WorktreeList lineage child card renderer', () => {
beforeAll(async () => {
await loadWorktreeList()
}, 60_000)
it.each(['runtime:host-a', 'runtime:host-b'] as const)(
'marks only the selected folder host %s as active',
async (selectedHost) => {
const { folderWorkspace, group } = setFolderWorkspaceFixtureState()
const hosts = ['runtime:host-a', 'runtime:host-b'] as const
mockStore.state.folderWorkspaces = hosts.map((executionHostId) => ({
...folderWorkspace,
executionHostId,
projectGroupId: executionHostId,
name: executionHostId
}))
mockStore.state.projectGroups = hosts.map((executionHostId) => ({
...group,
id: executionHostId,
executionHostId
}))
mockStore.state.activeWorkspaceExecutionHostId = selectedHost
const markup = await renderWorktreeListMarkup()
const options = Array.from(markup.matchAll(/<div[^>]*role="option"[^>]*>/g), ([tag]) => tag)
expect(options).toHaveLength(2)
const active = options.filter((tag) => tag.includes('aria-current="page"'))
expect(active).toHaveLength(1)
expect(active[0]).toContain(`data-worktree-host-identity="${selectedHost}|folder:`)
expect(markup.match(/data-worktree-card-active="true"/g)).toHaveLength(1)
}
)
it('points aria-activedescendant at the active folder workspace row', async () => {
setFolderWorkspaceFixtureState()
const markup = await renderWorktreeListMarkup()
expect(markup).toContain(
'aria-activedescendant="worktree-list-option-local%7Cfolder%3Afolder-workspace-1"'
)
})
})
@@ -448,39 +448,6 @@ describe('WorktreeCard agent-list <-> child-worktrees expansion coupling', () =>
clearWorktreeAgentExpansionStateForTests()
})
it('[full mode] both toggles render independently at mount', async () => {
setAgentLineageState({ agentActivityDisplayMode: 'full' })
const { container } = await renderWorktreeList()
expect(container.textContent).toContain('Parent Terminal')
expect(container.textContent).not.toContain('PARENT_AGENT_PROMPT')
// Child agents stay collapsed until the user opens them; child worktrees stay open.
expect(container.textContent).not.toContain('CHILD_AGENT_PROMPT')
expect(childWorktreeChip(container)).not.toBeNull()
expect(agentChildDisclosure(container)).not.toBeNull()
expect(childWorktreeChip(container)!.getAttribute('aria-expanded')).toBe('true')
expect(agentChildDisclosure(container)!.getAttribute('aria-expanded')).toBe('false')
expect(childWorktreeCardPresent(container)).toBe(true)
})
it('[full mode] toggling AGENTS does NOT change the child-worktrees chip (agents -> children uncoupled)', async () => {
setAgentLineageState({ agentActivityDisplayMode: 'full' })
const { container } = await renderWorktreeList()
expect(agentChildDisclosure(container)!.getAttribute('aria-expanded')).toBe('false')
expect(childWorktreeCardPresent(container)).toBe(true)
await click(agentChildDisclosure(container)!)
expect(agentChildDisclosure(container)!.getAttribute('aria-expanded')).toBe('true')
expect(container.textContent).toContain('CHILD_AGENT_PROMPT')
expect(childWorktreeChip(container)!.getAttribute('aria-expanded')).toBe('true')
expect(childWorktreeCardPresent(container)).toBe(true)
expect((mockStore.state.collapsedGroups as Set<string>).size).toBe(0)
})
it('[full mode] toggling CHILD WORKTREES still remounts the card but PRESERVES agent expansion (regression)', async () => {
setAgentLineageState({ agentActivityDisplayMode: 'full' })
const { container, root } = await renderWorktreeList()
@@ -1,627 +0,0 @@
// @vitest-environment happy-dom
vi.mock('@/components/confirmation-dialog-context', () => ({
useConfirmationDialog: () => vi.fn().mockResolvedValue(false)
}))
import { act, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as ProjectHeaderDragContract from '../drag/project-header-drag-contract'
import type { FolderWorkspace } from '../../../../../../shared/folder-workspaces/folder-workspace-types'
import type { HostedReviewInfo } from '../../../../../../shared/source-control/hosted-review/hosted-review-contracts'
import type { ProjectGroup } from '../../../../../../shared/project-group-types'
import type { Repo } from '../../../../../../shared/repo-types'
import type { WorktreeCardProperty } from '../../../../../../shared/ui-chrome-types'
import type { WorktreeLineage } from '../../../../../../shared/worktree/lineage-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
import {
FLUSH_CARD_MIN_CONTENT_INSET,
LINEAGE_CHILDREN_INLINE_OFFSET,
LINEAGE_IMMEDIATE_PARENT_STEP,
LINEAGE_NESTED_ROW_SURFACE_INSET,
WORKTREE_CARD_SURFACE_MARGIN
} from '../rows/indentation'
import { revealMountedWorktreeElement } from '../navigation/mounted-row-reveal'
globalThis.IS_REACT_ACT_ENVIRONMENT = true
const mockStore = vi.hoisted(() => ({
state: {} as Record<string, unknown>,
setVisibleReviewCardWorktreeIds: vi.fn<(ids: readonly string[]) => void>(),
activateWorktreeFromSidebar: vi.fn(),
openModal: vi.fn(),
updateWorktreeMeta: vi.fn(),
fetchHostedReviewForBranch: vi.fn(),
fetchIssue: vi.fn(),
fetchLinearIssue: vi.fn(),
openTaskPage: vi.fn()
}))
type WorktreeListComponent = React.ComponentType<{
scrollOffsetRef: React.RefObject<number>
scrollAnchorRef: React.RefObject<unknown>
}>
let WorktreeList: WorktreeListComponent
function makeFolderWorkspacePathStatusMockState(): Record<string, unknown> {
return {
fetchFolderWorkspacePathStatus: vi.fn(),
folderWorkspaces: [],
folderWorkspacePathStatuses: {},
getFolderWorkspacePathStatusCacheKey: (request: unknown) => JSON.stringify(request),
getFreshFolderWorkspacePathStatus: () => null
}
}
vi.mock('@/store', () => ({
useAppStore: Object.assign(
(selector: (state: Record<string, unknown>) => unknown) => selector(mockStore.state),
{
getState: () => mockStore.state,
subscribe: () => () => {}
}
)
}))
vi.mock('@tanstack/react-virtual', () => ({
defaultRangeExtractor: ({ startIndex, endIndex }: { startIndex: number; endIndex: number }) =>
Array.from({ length: endIndex - startIndex + 1 }, (_, index) => startIndex + index),
measureElement: () => 32,
useVirtualizer: ({ count }: { count: number }) => ({
elementsCache: new Map(),
getTotalSize: () => count * 96,
getVirtualItems: () =>
Array.from({ length: count }, (_, index) => ({
index,
key: `row-${index}`,
start: index * 96
})),
measureElement: vi.fn(),
scrollToIndex: vi.fn()
})
}))
vi.mock('@/hooks/useVirtualizedScrollAnchor', () => ({
VIRTUALIZED_SCROLL_ANCHOR_RECORD_EVENT: 'orca:test-record-scroll-anchor',
useVirtualizedScrollAnchor: vi.fn()
}))
vi.mock('../drag/project-header-drag', () => ({
useRepoHeaderDrag: () => ({
state: { draggingRepoId: null, dropIndicatorY: null },
onHandlePointerDown: vi.fn()
})
}))
vi.mock('../drag/project-header-drag-contract', async (importOriginal) => ({
...(await importOriginal<typeof ProjectHeaderDragContract>()),
isRepoHeaderActionTarget: () => false
}))
vi.mock('@/components/ui/hover-card', () => ({
HoverCard: ({ children }: { children: ReactNode }) => <>{children}</>,
HoverCardContent: ({ children }: { children: ReactNode }) => (
<div data-hover-card-content="">{children}</div>
),
HoverCardTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
}))
vi.mock('@/components/ui/tooltip', () => ({
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipContent: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
}))
vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenu: ({ children }: { children: ReactNode }) => <>{children}</>,
DropdownMenuContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuItem: ({ children, onSelect }: { children: ReactNode; onSelect?: () => void }) => (
<button onClick={onSelect}>{children}</button>
),
DropdownMenuSeparator: () => <hr />,
DropdownMenuSub: ({ children }: { children: ReactNode }) => <>{children}</>,
DropdownMenuSubContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuSubTrigger: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
}))
vi.mock('@/lib/sidebar-worktree-activation', () => ({
activateWorktreeFromSidebar: mockStore.activateWorktreeFromSidebar
}))
vi.mock('@/lib/worktree-activation', () => ({
activateAndRevealWorktree: vi.fn()
}))
vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: vi.fn() }))
vi.mock('../../workspace-card/activity/CacheTimer', () => ({
default: () => null,
usePromptCacheCountdownStartedAt: () => null
}))
vi.mock('../../workspace-card/activity/WorktreeCardAgents', () => ({
default: ({ worktreeId }: { worktreeId: string }) => (
<div data-agent-worktree-id={worktreeId}>Agent row</div>
),
SUPPRESS_WORKTREE_LIST_SCROLL_ADJUSTMENT_EVENT: 'orca:test-suppress-scroll-adjustment'
}))
vi.mock('../../workspace-actions/context-menu/WorktreeContextMenu', () => ({
default: ({ children }: { children: ReactNode }) => (
<div data-worktree-context-menu-scope>{children}</div>
),
WORKTREE_CONTEXT_MENU_SCOPE_ATTR: 'data-orca-context-menu-scope',
WORKTREE_NATIVE_CONTEXT_MENU_ATTR: 'data-worktree-native-context-menu'
}))
function makeRepo(): Repo {
return {
id: 'repo-1',
path: '/tmp/lineage-real-card',
displayName: 'lineage-real-card',
badgeColor: '#999999',
addedAt: 1
}
}
function makeWorktree(args: {
id: string
displayName: string
branch: string
sortOrder: number
instanceId: string
overrides?: Partial<Worktree>
}): Worktree {
return {
id: args.id,
instanceId: args.instanceId,
repoId: 'repo-1',
path: `/tmp/lineage-real-card/${args.id}`,
displayName: args.displayName,
branch: args.branch,
head: 'abc123',
isBare: false,
isMainWorktree: false,
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: args.sortOrder,
lastActivityAt: args.sortOrder,
...args.overrides
}
}
function makeLineage(worktree: Worktree, parent: Worktree): WorktreeLineage {
return {
worktreeId: worktree.id,
worktreeInstanceId: worktree.instanceId!,
parentWorktreeId: parent.id,
parentWorktreeInstanceId: parent.instanceId!,
origin: 'orchestration',
capture: { source: 'orchestration-context', confidence: 'explicit' },
createdAt: 1
}
}
function makeHostedReview(overrides: Partial<HostedReviewInfo> = {}): HostedReviewInfo {
return {
provider: 'gitlab',
number: 42,
title: 'Child GitLab MR',
state: 'open',
url: 'https://gitlab.com/acme/orca/-/merge_requests/42',
status: 'success',
updatedAt: '2026-06-09T00:00:00.000Z',
mergeable: 'MERGEABLE',
...overrides
}
}
function makeFolderWorkspacePathStatusState(): Record<string, unknown> {
return {
fetchFolderWorkspacePathStatus: vi.fn(),
folderWorkspacePathStatuses: {},
folderWorkspaces: [],
getFolderWorkspacePathStatusCacheKey: (request: unknown) => JSON.stringify(request),
getFreshFolderWorkspacePathStatus: vi.fn(() => null)
}
}
function setLineageState(
options: { deletingChild?: boolean; includeGrandchild?: boolean } = {}
): void {
const repo = makeRepo()
const parent = makeWorktree({
id: 'parent',
instanceId: 'parent-instance',
displayName: 'lineage parent',
branch: 'parent-branch',
sortOrder: 20
})
const child = makeWorktree({
id: 'child',
instanceId: 'child-instance',
displayName: 'lineage child',
branch: 'child-branch',
sortOrder: 10,
overrides: {
linkedGitLabMR: 42,
comment: 'Child handoff note'
}
})
const grandchild = makeWorktree({
id: 'grandchild',
instanceId: 'grandchild-instance',
displayName: 'lineage grandchild',
branch: 'grandchild-branch',
sortOrder: 5
})
const worktrees = options.includeGrandchild ? [parent, child, grandchild] : [parent, child]
const worktreeLineageById: Record<string, WorktreeLineage> = {
[child.id]: makeLineage(child, parent)
}
if (options.includeGrandchild) {
worktreeLineageById[grandchild.id] = makeLineage(grandchild, child)
}
mockStore.state = {
...makeFolderWorkspacePathStatusMockState(),
activeModal: '',
activeView: 'terminal',
activeWorktreeId: null,
agentStatusByPaneKey: {},
agentStatusEpoch: 0,
browserTabsByWorktree: {},
clearPendingRevealWorktreeId: vi.fn(),
collapsedGroups: new Set<string>(),
deleteStateByWorktreeId: options.deletingChild
? { [child.id]: { isDeleting: true, error: null, canForceDelete: false } }
: {},
detectedWorktreesByRepo: {},
fetchHostedReviewForBranch: mockStore.fetchHostedReviewForBranch,
fetchIssue: mockStore.fetchIssue,
fetchLinearIssue: mockStore.fetchLinearIssue,
filterRepoIds: [],
...makeFolderWorkspacePathStatusState(),
gitConflictOperationByWorktree: {},
groupBy: 'none',
hideDefaultBranchWorkspace: false,
hostedReviewCache: {
'local::repo-1::child-branch': {
data: makeHostedReview(),
fetchedAt: Date.now(),
linkedReviewHintKey: 'gitlab:42'
}
},
issueCache: {},
linearIssueCache: {},
linearStatus: null,
migrationUnsupportedByPtyId: {},
openModal: mockStore.openModal,
openSettingsPage: vi.fn(),
openSettingsTarget: null,
openTaskPage: mockStore.openTaskPage,
pendingRevealWorktree: null,
prCache: {},
projectGroups: [],
ptyIdsByTabId: {},
recordFeatureInteraction: vi.fn(),
remoteBranchConflictByWorktreeId: {},
reorderRepos: vi.fn(),
reportVisibleGitHubPRRefreshCandidates: vi.fn(),
setVisibleReviewCardWorktreeIds: mockStore.setVisibleReviewCardWorktreeIds,
repos: [repo],
retainedAgentsByPaneKey: {},
revealWorktreeInSidebar: vi.fn(),
runtimePaneTitlesByTabId: {},
setFilterRepoIds: vi.fn(),
setHideDefaultBranchWorkspace: vi.fn(),
setRenamingWorktreeId: vi.fn(),
setShowSleepingWorkspaces: vi.fn(),
setSortBy: vi.fn(),
setWorktreesPinnedAndReveal: vi.fn(),
settings: null,
showSleepingWorkspaces: true,
sortBy: 'manual',
sortEpoch: 0,
sshConnectedGeneration: 0,
sshConnectionStates: new Map(),
sshTargetLabels: new Map(),
tabsByWorktree: {},
unifiedTabsByWorktree: {},
structuredChatSleptAtByPaneKey: {},
terminalLayoutsByTabId: {},
toggleCollapsedGroup: vi.fn(),
updateRepo: vi.fn(),
updateWorktreeMeta: mockStore.updateWorktreeMeta,
updateWorktreesMeta: vi.fn(),
workspaceHostScope: 'all',
workspacePortScan: null,
workspaceStatuses: [],
worktreeCardProperties: [
'status',
'pr',
'comment',
'inline-agents'
] satisfies WorktreeCardProperty[],
worktreeLineageById,
worktreesByRepo: {
[repo.id]: worktrees
}
}
}
const mountedRoots: Root[] = []
async function renderWorktreeList(): Promise<HTMLDivElement> {
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
mountedRoots.push(root)
await act(async () => {
root.render(
<WorktreeList scrollOffsetRef={{ current: 0 }} scrollAnchorRef={{ current: null }} />
)
})
return container
}
function px(value: string): number {
return value === '' ? 0 : Number.parseFloat(value)
}
function expectBoundaryStep(args: {
wrapper: HTMLElement
row: HTMLElement
surface: HTMLElement
}): void {
const effectiveStep =
px(args.wrapper.style.marginLeft) +
px(args.row.style.paddingLeft) +
WORKTREE_CARD_SURFACE_MARGIN +
px(args.surface.style.paddingLeft)
expect(effectiveStep).toBe(LINEAGE_IMMEDIATE_PARENT_STEP)
}
describe('WorktreeList real child WorktreeCard integration', () => {
beforeAll(async () => {
WorktreeList = (await import('./WorktreeList')).default as WorktreeListComponent
}, 60_000)
beforeEach(() => {
vi.clearAllMocks()
setLineageState()
})
afterEach(async () => {
await act(async () => {
for (const root of mountedRoots.splice(0)) {
root.unmount()
}
})
document.body.innerHTML = ''
})
it('renders GitLab MR metadata from a child through the real WorktreeCard path', async () => {
const container = await renderWorktreeList()
const childOption = container.querySelector('[id="worktree-list-option-all%3A%7Cchild"]')
expect(childOption?.textContent).toContain('MR #42')
expect(childOption?.textContent).toContain('Child GitLab MR')
expect(childOption?.textContent).toContain('Child handoff note')
})
it('keeps expanded child cards in the parent title column', async () => {
mockStore.state.settings = { experimentalNewWorktreeCardStyle: true }
const container = await renderWorktreeList()
const childList = container.querySelector<HTMLElement>('[data-worktree-lineage-children]')
expect(childList).not.toBeNull()
expect(childList!.style.marginLeft).toBe(`${LINEAGE_CHILDREN_INLINE_OFFSET}px`)
expect(childList!.style.width).toBe(`calc(100% - ${LINEAGE_CHILDREN_INLINE_OFFSET}px)`)
})
it.each([
{ newCardStyle: false, showStatus: false },
{ newCardStyle: false, showStatus: true },
{ newCardStyle: true, showStatus: false },
{ newCardStyle: true, showStatus: true }
])('keeps lineage outside the status lane (%j)', async ({ newCardStyle, showStatus }) => {
setLineageState({ includeGrandchild: true })
mockStore.state.settings = { experimentalNewWorktreeCardStyle: newCardStyle }
mockStore.state.worktreeCardProperties = showStatus ? ['status'] : []
const container = await renderWorktreeList()
const wrappers = [
...container.querySelectorAll<HTMLElement>('[data-worktree-lineage-children]')
]
const childRow = container.querySelector<HTMLElement>(
'[id="worktree-list-option-all%3A%7Cchild"]'
)
const grandchildRow = container.querySelector<HTMLElement>(
'[id="worktree-list-option-all%3A%7Cgrandchild"]'
)
const childSurface = childRow?.querySelector<HTMLElement>('[data-worktree-card-surface="true"]')
const grandchildSurface = grandchildRow?.querySelector<HTMLElement>(
'[data-worktree-card-surface="true"]'
)
expect(wrappers).toHaveLength(2)
expect(childRow).not.toBeNull()
expect(grandchildRow).not.toBeNull()
expect(childSurface).not.toBeNull()
expect(grandchildSurface).not.toBeNull()
expect(px(childRow!.style.paddingLeft)).toBe(LINEAGE_NESTED_ROW_SURFACE_INSET)
expect(px(grandchildRow!.style.paddingLeft)).toBe(LINEAGE_NESTED_ROW_SURFACE_INSET)
expect(px(childSurface!.style.paddingLeft)).toBe(FLUSH_CARD_MIN_CONTENT_INSET)
expect(px(grandchildSurface!.style.paddingLeft)).toBe(FLUSH_CARD_MIN_CONTENT_INSET)
for (const wrapper of wrappers) {
expect(wrapper.parentElement?.dataset.worktreeCardSurface).toBe('true')
expect(wrapper.closest('[data-worktree-card-parent-content]')).toBeNull()
}
expectBoundaryStep({
wrapper: wrappers[0]!,
row: childRow!,
surface: childSurface!
})
expectBoundaryStep({
wrapper: wrappers[1]!,
row: grandchildRow!,
surface: grandchildSurface!
})
})
it.each([
{ newStyle: false, deleting: false },
{ newStyle: true, deleting: false },
{ newStyle: true, deleting: true }
])('reveals complete real-card segments %j', async ({ newStyle, deleting }) => {
setLineageState({ includeGrandchild: true, deletingChild: deleting })
mockStore.state.settings = { experimentalNewWorktreeCardStyle: newStyle }
const container = await renderWorktreeList()
const parent = container.querySelector<HTMLElement>(
'[role="option"][data-worktree-id="parent"]'
)!
const child = container.querySelector<HTMLElement>('[role="option"][data-worktree-id="child"]')!
const boundaries = container.querySelectorAll<HTMLElement>('[data-worktree-lineage-children]')
Object.defineProperty(container, 'clientHeight', { value: 673 })
container.getBoundingClientRect = () => new DOMRect(0, 0, 300, 673)
parent.getBoundingClientRect = () => new DOMRect(0, 200, 300, 11800)
child.getBoundingClientRect = () => new DOMRect(0, 900, 300, 11100)
boundaries[0]!.getBoundingClientRect = () => new DOMRect(0, 320, 300, 11680)
boundaries[1]!.getBoundingClientRect = () => new DOMRect(0, 1020, 300, 10980)
container.scrollTo = vi.fn()
expect(revealMountedWorktreeElement(container, 'parent', 'auto', parent.id)).toBe(parent)
expect(container.scrollTo).not.toHaveBeenCalled()
expect(revealMountedWorktreeElement(container, 'child', 'auto', child.id)).toBe(child)
expect(container.scrollTo).toHaveBeenCalledWith({ top: 347, behavior: 'auto' })
const leaf = container.querySelector<HTMLElement>('[data-worktree-id="grandchild"]')!
leaf.getBoundingClientRect = () => new DOMRect(0, 1005, 300, 55)
expect(revealMountedWorktreeElement(container, 'grandchild', 'auto', leaf.id)).toBe(leaf)
expect(container.scrollTo).toHaveBeenLastCalledWith({ top: 387, behavior: 'auto' })
expect(parent.getBoundingClientRect().height).toBe(11800)
if (newStyle && !deleting) {
expect(parent.querySelector('[data-worktree-sleeping-dim]')).not.toBeNull()
}
if (deleting) {
expect(child.querySelector('[aria-busy=true]')).not.toBeNull()
}
expect(boundaries[0]!.parentElement?.lastElementChild).toBe(boundaries[0])
})
it('double-clicking a nested child opens edit metadata for the child only', async () => {
const container = await renderWorktreeList()
const childCard = container.querySelector<HTMLElement>(
'[id="worktree-list-option-all%3A%7Cchild"] [data-worktree-card-surface="true"]'
)
expect(childCard).not.toBeNull()
await act(async () => {
childCard!.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
})
expect(mockStore.openModal).toHaveBeenCalledTimes(1)
expect(mockStore.openModal).toHaveBeenCalledWith(
'edit-meta',
expect.objectContaining({
worktreeId: 'child',
currentDisplayName: 'lineage child',
currentComment: 'Child handoff note'
})
)
expect(mockStore.openModal).not.toHaveBeenCalledWith(
'edit-meta',
expect.objectContaining({ worktreeId: 'parent' })
)
})
it.each([
{ pressed: 'option', worktreeId: 'parent' },
{ pressed: 'option', worktreeId: 'child' },
{ pressed: 'option', worktreeId: 'folder:folder-workspace' },
{ pressed: 'card', worktreeId: 'parent' },
{ pressed: 'card', worktreeId: 'child' },
{ pressed: 'card', worktreeId: 'folder:folder-workspace' }
])('activating the $worktreeId $pressed activates it once', async ({ pressed, worktreeId }) => {
mockStore.state.projectGroups = [
{
id: 'folder-group',
name: 'Folder group',
parentPath: '/tmp/lineage-real-card-folder',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 1,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
] satisfies ProjectGroup[]
mockStore.state.folderWorkspaces = [
{
id: 'folder-workspace',
projectGroupId: 'folder-group',
name: 'folder workspace',
folderPath: '/tmp/lineage-real-card-folder',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 1,
lastActivityAt: 1,
createdAt: 1,
updatedAt: 1
}
] satisfies FolderWorkspace[]
const container = await renderWorktreeList()
const option = container.querySelector<HTMLElement>(
`[role="option"][data-worktree-id="${worktreeId}"]`
)
const target =
pressed === 'option'
? option
: option?.querySelector<HTMLElement>('[data-worktree-card-surface="true"]')
expect(target).not.toBeNull()
await act(async () => {
target!.click()
})
if (pressed === 'option' && worktreeId === 'folder:folder-workspace') {
container.getBoundingClientRect = () => new DOMRect(0, 0, 300, 673)
Object.defineProperty(container, 'clientHeight', { value: 673 })
option!.getBoundingClientRect = () => new DOMRect(0, 1005, 300, 55)
container.scrollTo = vi.fn()
expect(revealMountedWorktreeElement(container, worktreeId, 'auto', option!.id)).toBe(option)
expect(container.scrollTo).toHaveBeenCalledWith({ top: 387, behavior: 'auto' })
}
expect(mockStore.activateWorktreeFromSidebar).toHaveBeenCalledTimes(1)
expect(mockStore.activateWorktreeFromSidebar).toHaveBeenCalledWith(worktreeId, 'local')
})
it('does not activate a nested child while it is deleting', async () => {
setLineageState({ deletingChild: true })
const container = await renderWorktreeList()
const childCard = container.querySelector<HTMLElement>(
'[id="worktree-list-option-all%3A%7Cchild"] [data-worktree-card-surface="true"]'
)
expect(childCard?.textContent).toContain('Deleting')
await act(async () => {
childCard!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
expect(mockStore.activateWorktreeFromSidebar).not.toHaveBeenCalled()
})
})
@@ -1,460 +0,0 @@
// @vitest-environment happy-dom
vi.mock('@/components/confirmation-dialog-context', () => ({
useConfirmationDialog: () => vi.fn().mockResolvedValue(false)
}))
import { act, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as ProjectHeaderDragContract from '../drag/project-header-drag-contract'
import type { Repo } from '../../../../../../shared/repo-types'
import type { WorktreeCardProperty } from '../../../../../../shared/ui-chrome-types'
import type { WorktreeLineage } from '../../../../../../shared/worktree/lineage-types'
import type { WorktreeMeta } from '../../../../../../shared/worktree/meta-types'
import type { WorkspaceStatus, Worktree } from '../../../../../../shared/worktree/types'
import type { WorktreeMetaBatchUpdate } from '../../../../store/slices/worktree-helpers'
import { DEFAULT_WORKSPACE_STATUSES } from '../../../../../../shared/workspace-status-defaults'
import {
WORKSPACE_STATUS_DRAG_IDS_TYPE,
WORKSPACE_STATUS_DRAG_TYPE
} from '../../workspace-status/presentation/workspace-status-drag-data'
globalThis.IS_REACT_ACT_ENVIRONMENT = true
const mockStore = vi.hoisted(() => ({
state: {} as Record<string, unknown>,
setVisibleReviewCardWorktreeIds: vi.fn<(ids: readonly string[]) => void>(),
activateWorktreeFromSidebar: vi.fn(),
openModal: vi.fn(),
updateWorktreeMeta: vi.fn(),
updateWorktreesMeta: vi.fn(),
setSortBy: vi.fn(),
fetchHostedReviewForBranch: vi.fn(),
fetchIssue: vi.fn(),
fetchLinearIssue: vi.fn(),
openTaskPage: vi.fn()
}))
type WorktreeListComponent = React.ComponentType<{
scrollOffsetRef: React.RefObject<number>
scrollAnchorRef: React.RefObject<unknown>
}>
let WorktreeList: WorktreeListComponent
function makeFolderWorkspacePathStatusMockState(): Record<string, unknown> {
return {
fetchFolderWorkspacePathStatus: vi.fn(),
folderWorkspaces: [],
folderWorkspacePathStatuses: {},
getFolderWorkspacePathStatusCacheKey: (request: unknown) => JSON.stringify(request),
getFreshFolderWorkspacePathStatus: () => null
}
}
vi.mock('@/store', () => ({
useAppStore: Object.assign(
(selector: (state: Record<string, unknown>) => unknown) => selector(mockStore.state),
{
getState: () => mockStore.state,
subscribe: () => () => {}
}
)
}))
vi.mock('@tanstack/react-virtual', () => ({
defaultRangeExtractor: ({ startIndex, endIndex }: { startIndex: number; endIndex: number }) =>
Array.from({ length: endIndex - startIndex + 1 }, (_, index) => startIndex + index),
measureElement: () => 32,
useVirtualizer: ({ count }: { count: number }) => ({
elementsCache: new Map(),
getTotalSize: () => count * 96,
getVirtualItems: () =>
Array.from({ length: count }, (_, index) => ({
index,
key: `row-${index}`,
start: index * 96
})),
measureElement: vi.fn(),
scrollToIndex: vi.fn()
})
}))
vi.mock('@/hooks/useVirtualizedScrollAnchor', () => ({
VIRTUALIZED_SCROLL_ANCHOR_RECORD_EVENT: 'orca:test-record-scroll-anchor',
useVirtualizedScrollAnchor: vi.fn()
}))
vi.mock('../drag/project-header-drag', () => ({
useRepoHeaderDrag: () => ({
state: { draggingRepoId: null, dropIndicatorY: null },
onHandlePointerDown: vi.fn()
})
}))
vi.mock('../drag/project-header-drag-contract', async (importOriginal) => ({
...(await importOriginal<typeof ProjectHeaderDragContract>()),
isRepoHeaderActionTarget: () => false
}))
vi.mock('@/components/ui/hover-card', () => ({
HoverCard: ({ children }: { children: ReactNode }) => <>{children}</>,
HoverCardContent: ({ children }: { children: ReactNode }) => (
<div data-hover-card-content="">{children}</div>
),
HoverCardTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
}))
vi.mock('@/components/ui/tooltip', () => ({
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipContent: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
}))
vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenu: ({ children }: { children: ReactNode }) => <>{children}</>,
DropdownMenuContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuItem: ({ children, onSelect }: { children: ReactNode; onSelect?: () => void }) => (
<button onClick={onSelect}>{children}</button>
),
DropdownMenuSeparator: () => <hr />,
DropdownMenuSub: ({ children }: { children: ReactNode }) => <>{children}</>,
DropdownMenuSubContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuSubTrigger: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
}))
vi.mock('@/lib/sidebar-worktree-activation', () => ({
activateWorktreeFromSidebar: mockStore.activateWorktreeFromSidebar
}))
vi.mock('@/lib/worktree-activation', () => ({
activateAndRevealWorktree: vi.fn()
}))
vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: vi.fn() }))
vi.mock('../../workspace-card/activity/CacheTimer', () => ({
default: () => null,
usePromptCacheCountdownStartedAt: () => null
}))
vi.mock('../../workspace-card/activity/WorktreeCardAgents', () => ({
default: ({ worktreeId }: { worktreeId: string }) => (
<div data-agent-worktree-id={worktreeId}>Agent row</div>
),
SUPPRESS_WORKTREE_LIST_SCROLL_ADJUSTMENT_EVENT: 'orca:test-suppress-scroll-adjustment'
}))
vi.mock('../../workspace-actions/context-menu/WorktreeContextMenu', () => ({
default: ({ children }: { children: ReactNode }) => <>{children}</>,
WORKTREE_CONTEXT_MENU_SCOPE_ATTR: 'data-orca-context-menu-scope',
WORKTREE_NATIVE_CONTEXT_MENU_ATTR: 'data-worktree-native-context-menu'
}))
function makeRepo(): Repo {
return {
id: 'repo-1',
path: '/tmp/status-lane-lineage',
displayName: 'status-lane-lineage',
badgeColor: '#999999',
addedAt: 1
}
}
function makeWorktree(args: {
id: string
displayName: string
branch: string
sortOrder: number
instanceId: string
workspaceStatus: WorkspaceStatus
}): Worktree {
return {
id: args.id,
instanceId: args.instanceId,
repoId: 'repo-1',
path: `/tmp/status-lane-lineage/${args.id}`,
displayName: args.displayName,
branch: args.branch,
head: 'abc123',
isBare: false,
isMainWorktree: false,
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: args.sortOrder,
lastActivityAt: args.sortOrder,
workspaceStatus: args.workspaceStatus
}
}
function makeLineage(worktree: Worktree, parent: Worktree): WorktreeLineage {
return {
worktreeId: worktree.id,
worktreeInstanceId: worktree.instanceId!,
parentWorktreeId: parent.id,
parentWorktreeInstanceId: parent.instanceId!,
origin: 'orchestration',
capture: { source: 'orchestration-context', confidence: 'explicit' },
createdAt: 1
}
}
function makeFolderWorkspacePathStatusState(): Record<string, unknown> {
return {
fetchFolderWorkspacePathStatus: vi.fn(),
folderWorkspacePathStatuses: {},
folderWorkspaces: [],
getFolderWorkspacePathStatusCacheKey: (request: unknown) => JSON.stringify(request),
getFreshFolderWorkspacePathStatus: vi.fn(() => null)
}
}
function setStatusLaneState(): void {
const repo = makeRepo()
const parent = makeWorktree({
id: 'parent',
instanceId: 'parent-instance',
displayName: 'lineage parent',
branch: 'parent-branch',
sortOrder: 30,
workspaceStatus: 'in-progress'
})
const child = makeWorktree({
id: 'child',
instanceId: 'child-instance',
displayName: 'lineage child',
branch: 'child-branch',
sortOrder: 20,
workspaceStatus: 'in-progress'
})
const bystander = makeWorktree({
id: 'bystander',
instanceId: 'bystander-instance',
displayName: 'bystander',
branch: 'bystander-branch',
sortOrder: 10,
workspaceStatus: 'todo'
})
mockStore.state = {
...makeFolderWorkspacePathStatusMockState(),
activeModal: '',
activeView: 'terminal',
activeWorktreeId: null,
agentStatusByPaneKey: {},
agentStatusEpoch: 0,
browserTabsByWorktree: {},
clearPendingRevealWorktreeId: vi.fn(),
collapsedGroups: new Set<string>(),
deleteStateByWorktreeId: {},
detectedWorktreesByRepo: {},
fetchHostedReviewForBranch: mockStore.fetchHostedReviewForBranch,
fetchIssue: mockStore.fetchIssue,
fetchLinearIssue: mockStore.fetchLinearIssue,
filterRepoIds: [],
...makeFolderWorkspacePathStatusState(),
gitConflictOperationByWorktree: {},
groupBy: 'workspace-status',
hideDefaultBranchWorkspace: false,
hostedReviewCache: {},
issueCache: {},
linearIssueCache: {},
linearStatus: null,
migrationUnsupportedByPtyId: {},
openModal: mockStore.openModal,
openSettingsPage: vi.fn(),
openSettingsTarget: null,
openTaskPage: mockStore.openTaskPage,
pendingRevealWorktree: null,
prCache: {},
projectGroups: [],
ptyIdsByTabId: {},
recordFeatureInteraction: vi.fn(),
remoteBranchConflictByWorktreeId: {},
reorderRepos: vi.fn(),
reportVisibleGitHubPRRefreshCandidates: vi.fn(),
setVisibleReviewCardWorktreeIds: mockStore.setVisibleReviewCardWorktreeIds,
repos: [repo],
retainedAgentsByPaneKey: {},
revealWorktreeInSidebar: vi.fn(),
runtimePaneTitlesByTabId: {},
setFilterRepoIds: vi.fn(),
setHideDefaultBranchWorkspace: vi.fn(),
setRenamingWorktreeId: vi.fn(),
setShowSleepingWorkspaces: vi.fn(),
setSortBy: mockStore.setSortBy,
setWorktreesPinnedAndReveal: vi.fn(),
settings: null,
showSleepingWorkspaces: true,
sortBy: 'manual',
sortEpoch: 0,
sshConnectedGeneration: 0,
sshConnectionStates: new Map(),
sshTargetLabels: new Map(),
tabsByWorktree: {},
unifiedTabsByWorktree: {},
structuredChatSleptAtByPaneKey: {},
terminalLayoutsByTabId: {},
toggleCollapsedGroup: vi.fn(),
updateRepo: vi.fn(),
updateWorktreeMeta: mockStore.updateWorktreeMeta,
updateWorktreesMeta: mockStore.updateWorktreesMeta,
workspaceHostScope: 'all',
workspacePortScan: null,
workspaceStatuses: [...DEFAULT_WORKSPACE_STATUSES],
worktreeCardProperties: [
'status',
'pr',
'comment',
'inline-agents'
] satisfies WorktreeCardProperty[],
worktreeLineageById: { [child.id]: makeLineage(child, parent) },
worktreesByRepo: { [repo.id]: [parent, child, bystander] }
}
}
const mountedRoots: Root[] = []
async function renderWorktreeList(): Promise<HTMLDivElement> {
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
mountedRoots.push(root)
await act(async () => {
root.render(
<WorktreeList scrollOffsetRef={{ current: 0 }} scrollAnchorRef={{ current: null }} />
)
})
return container
}
function makeWorktreeIdDataTransfer(worktreeIds: readonly string[]): DataTransfer {
const [firstId] = worktreeIds
const data: Record<string, string> = {
[WORKSPACE_STATUS_DRAG_TYPE]: firstId ?? '',
[WORKSPACE_STATUS_DRAG_IDS_TYPE]: JSON.stringify(worktreeIds),
'text/plain': firstId ?? ''
}
return {
effectAllowed: 'move',
types: Object.keys(data),
getData: (type: string) => data[type] ?? '',
setData: () => {}
} as unknown as DataTransfer
}
function findStatusLaneHeader(container: HTMLElement, status: WorkspaceStatus): HTMLElement {
const header = container.querySelector<HTMLElement>(
`[data-workspace-status-drop-target][data-workspace-status="${status}"]`
)
if (!header) {
throw new Error(`No status lane header rendered for "${status}"`)
}
return header
}
async function dropWorktreesOnStatusLane(
header: HTMLElement,
worktreeIds: readonly string[],
dataTransfer: DataTransfer = makeWorktreeIdDataTransfer(worktreeIds)
): Promise<void> {
const event = new Event('drop', { bubbles: true, cancelable: true })
Object.defineProperty(event, 'dataTransfer', {
value: dataTransfer
})
await act(async () => {
header.dispatchEvent(event)
})
}
function committedStatusUpdates(): Map<string, Partial<WorktreeMeta>> {
expect(mockStore.updateWorktreesMeta).toHaveBeenCalledTimes(1)
const input = mockStore.updateWorktreesMeta.mock.calls[0]![0] as
| readonly WorktreeMetaBatchUpdate[]
| ReadonlyMap<string, Partial<WorktreeMeta>>
if ('get' in input) {
return new Map(input)
}
return new Map(input.map((entry) => [entry.worktreeId, entry.updates]))
}
describe('WorktreeList status-lane drop carries visible lineage children (#9083)', () => {
beforeAll(async () => {
WorktreeList = (await import('./WorktreeList')).default as WorktreeListComponent
}, 60_000)
beforeEach(() => {
vi.clearAllMocks()
setStatusLaneState()
})
afterEach(async () => {
await act(async () => {
for (const root of mountedRoots.splice(0)) {
root.unmount()
}
})
document.body.innerHTML = ''
})
it('moves a dragged parent and its visible lineage child to the dropped lane', async () => {
const container = await renderWorktreeList()
const todoHeader = findStatusLaneHeader(container, 'todo')
// Native drag carries only the parent ID; the wrapper must add its lineage children.
await dropWorktreesOnStatusLane(todoHeader, ['parent'])
const updates = committedStatusUpdates()
expect(updates.get('parent')).toEqual({ workspaceStatus: 'todo' })
expect(updates.get('child')).toEqual({ workspaceStatus: 'todo' })
})
it('ignores a hybrid file drag with a workspace id in plain text', async () => {
const container = await renderWorktreeList()
const todoHeader = findStatusLaneHeader(container, 'todo')
const hybridTransfer = {
types: ['Files', 'text/plain'],
files: [{ name: 'notes.txt' }],
getData: (type: string) => (type === 'text/plain' ? 'parent' : '')
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The drop reader uses only types and getData from this fixture.
const dataTransfer = hybridTransfer as unknown as DataTransfer
await dropWorktreesOnStatusLane(todoHeader, ['parent'], dataTransfer)
expect(mockStore.updateWorktreeMeta).not.toHaveBeenCalled()
expect(mockStore.updateWorktreesMeta).not.toHaveBeenCalled()
})
it('leaves worktrees in other lanes untouched', async () => {
const container = await renderWorktreeList()
const todoHeader = findStatusLaneHeader(container, 'todo')
await dropWorktreesOnStatusLane(todoHeader, ['parent'])
const updates = committedStatusUpdates()
expect(updates.has('bystander')).toBe(false)
expect(updates.size).toBe(2)
})
it('commits only itself when a childless worktree is dropped onto another lane', async () => {
const container = await renderWorktreeList()
const inProgressHeader = findStatusLaneHeader(container, 'in-progress')
await dropWorktreesOnStatusLane(inProgressHeader, ['bystander'])
const updates = committedStatusUpdates()
expect(updates.get('bystander')).toEqual({
workspaceStatus: 'in-progress'
})
expect(updates.has('parent')).toBe(false)
expect(updates.has('child')).toBe(false)
expect(updates.size).toBe(1)
})
})
@@ -1,238 +0,0 @@
// @vitest-environment happy-dom
import { cleanup, renderHook } from '@testing-library/react'
import { afterEach, expect, it } from 'vitest'
import { useAppStore } from '@/store/index'
import { makeRepo, makeWorktree } from './worktree-list-lineage-card-test-fixtures'
import { useEffectiveCollapsedGroups } from './use-collapsed-groups'
import { useSidebarSectionRows } from './use-section-rows'
import { getWorktreeLineageGroupKey } from '../../../../../../shared/sidebar-list/group-keys'
import type { WorktreeLineage } from '../../../../../../shared/worktree/lineage-types'
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
import type { WorktreeGroupBy } from '../../../../../../shared/sidebar-list/row-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
import { cloneDefaultWorkspaceStatuses } from '../../../../../../shared/workspace-statuses'
afterEach(cleanup)
const projectGrouping = { projects: [], projectHostSetups: [] }
const settings = useAppStore.getState().settings
const workspaceStatuses = cloneDefaultWorkspaceStatuses()
function setup(hostId: ExecutionHostId) {
const localRepo = makeRepo()
const remoteRepo = { ...localRepo, id: 'remote-repo', executionHostId: hostId }
const repoMap = new Map([
[localRepo.id, localRepo],
[remoteRepo.id, remoteRepo]
])
const local = {
...makeWorktree({
id: 'local',
displayName: 'Local',
branch: 'local',
sortOrder: 0,
instanceId: 'local-instance'
}),
hostId: 'local' as const
}
const remote = {
...local,
id: 'remote',
instanceId: 'remote-instance',
repoId: remoteRepo.id,
hostId
}
return { localRepo, remoteRepo, repoMap, local, remote }
}
function sections(
hostId: ExecutionHostId,
groupBy: WorktreeGroupBy,
worktrees: Worktree[],
collapsedGroups: Set<string>,
lineageById: Record<string, WorktreeLineage> = {}
) {
const { localRepo, remoteRepo, repoMap } = setup(hostId)
return renderHook(() =>
useSidebarSectionRows({
groupBy,
sortBy: 'smart',
projectOrderBy: 'manual',
pinnedDisplayPolicy: 'single-location',
defaultHostId: 'local',
worktrees,
repos: [localRepo, remoteRepo],
repoMap,
worktreeMap: new Map(worktrees.map((worktree) => [worktree.id, worktree])),
worktreeLineageById: lineageById,
prCache: null,
settings,
workspaceStatuses,
effectiveCollapsedGroups: collapsedGroups,
projectGrouping,
visibleReposForRows: [localRepo, remoteRepo],
visibleProjectGroupsForRows: [],
visibleFolderWorkspacesForRows: [],
importedWorktreesByRepo: new Map(),
newExternalWorktreesInboxByRepo: new Map(),
filterRepoIds: [],
visibleWorkspaceHostIds: ['local', hostId],
workspaceHostScope: 'all'
})
).result.current.sectionRows
}
for (const hostId of ['ssh:builder', 'runtime:builder'] as const) {
it(`${hostId}: agent picker should expose a target within a collapsed host`, () => {
const { local, remote, repoMap } = setup(hostId)
const worktrees = [local, remote]
const collapsedGroups = new Set([`host:${hostId}`, 'host:local'])
const { result } = renderHook(() =>
useEffectiveCollapsedGroups({
hostScopedGroups: true,
collapsedGroups,
agentSendTargetWorktreeId: remote.id,
groupBy: 'none',
pinnedDisplayPolicy: 'single-location',
worktrees,
visibleWorktrees: worktrees,
repoMap,
worktreeMap: new Map(worktrees.map((worktree) => [worktree.id, worktree])),
worktreeLineageById: {},
prCache: null,
workspaceStatuses,
projectGroups: [],
projectGrouping,
folderWorkspaces: [],
defaultHostId: 'local'
})
)
expect([...collapsedGroups]).toEqual([`host:${hostId}`, 'host:local'])
expect(result.current.has('host:local')).toBe(true)
const rows = sections(hostId, 'none', worktrees, result.current)
expect(rows.some((row) => row.type === 'host-header' && row.hostId === hostId)).toBe(true)
expect(rows.some((row) => row.type === 'item' && row.worktree.id === remote.id)).toBe(true)
})
}
for (const hostId of ['local', 'ssh:builder', 'runtime:builder'] as const) {
it(`${hostId}: agent picker should expose child under a collapsed parent`, () => {
const { local, remote, repoMap } = setup(hostId)
const parent = hostId === 'local' ? local : remote
if (!parent.instanceId) {
throw new Error('Fixture parent instance missing')
}
const child = { ...parent, id: 'child', instanceId: 'child-instance', sortOrder: 1 }
const worktrees = [parent, child]
const lineage: WorktreeLineage = {
worktreeId: child.id,
worktreeInstanceId: child.instanceId,
parentWorktreeId: parent.id,
parentWorktreeInstanceId: parent.instanceId,
origin: 'orchestration',
capture: { source: 'orchestration-context', confidence: 'explicit' },
createdAt: 1
}
const worktreeLineageById = { [child.id]: lineage }
const worktreeMap = new Map(worktrees.map((worktree) => [worktree.id, worktree]))
const collapsedGroups = new Set([getWorktreeLineageGroupKey(parent)])
expect(
sections(hostId, 'none', worktrees, collapsedGroups, worktreeLineageById).some(
(row) => row.type === 'item' && row.worktree.id === child.id
)
).toBe(false)
const { result } = renderHook(() =>
useEffectiveCollapsedGroups({
hostScopedGroups: true,
collapsedGroups,
agentSendTargetWorktreeId: child.id,
groupBy: 'none',
pinnedDisplayPolicy: 'single-location',
worktrees,
visibleWorktrees: worktrees,
repoMap,
worktreeMap,
worktreeLineageById,
prCache: null,
workspaceStatuses,
projectGroups: [],
projectGrouping,
folderWorkspaces: [],
defaultHostId: 'local'
})
)
expect(collapsedGroups.has(getWorktreeLineageGroupKey(parent))).toBe(true)
const after = sections(hostId, 'none', worktrees, result.current, worktreeLineageById)
expect(after.some((row) => row.type === 'item' && row.worktree.id === child.id)).toBe(true)
})
}
it.each([true, false])('reveals the target with pinned ancestors visible=%s', (visible) => {
const { local, remote, repoMap } = setup('ssh:builder')
const root = { ...remote, instanceId: 'root-instance', isPinned: true }
const parent = { ...remote, id: 'parent', instanceId: 'parent-instance', isPinned: false }
const child = { ...remote, id: 'child', instanceId: 'child-instance', isPinned: false }
const worktrees = [local, root, parent, child]
const visibleWorktrees = visible ? worktrees : [local, child]
const lineage = (worktree: typeof child, ancestor: typeof root): WorktreeLineage => ({
worktreeId: worktree.id,
worktreeInstanceId: worktree.instanceId,
parentWorktreeId: ancestor.id,
parentWorktreeInstanceId: ancestor.instanceId,
origin: 'orchestration',
capture: { source: 'orchestration-context', confidence: 'explicit' },
createdAt: 1
})
const worktreeLineageById = {
[parent.id]: lineage(parent, root),
[child.id]: lineage(child, parent)
}
const collapsedGroups = new Set([
'host:ssh:builder',
'lineage:ssh:builder|remote',
'lineage:ssh:builder|parent',
'all:host:ssh:builder',
'all',
'pinned:host:ssh:builder',
'host:local',
'lineage:local|parent'
])
const initialProps: { targetId: string | null } = { targetId: child.id }
const { result, rerender } = renderHook(
({ targetId }: { targetId: string | null }) =>
useEffectiveCollapsedGroups({
hostScopedGroups: true,
collapsedGroups,
agentSendTargetWorktreeId: targetId,
groupBy: 'none',
pinnedDisplayPolicy: 'single-location',
worktrees,
visibleWorktrees,
repoMap,
worktreeMap: new Map(worktrees.map((worktree) => [worktree.id, worktree])),
worktreeLineageById,
prCache: null,
workspaceStatuses,
projectGroups: [],
projectGrouping,
folderWorkspaces: [],
defaultHostId: 'local'
}),
{ initialProps }
)
expect([...result.current].sort()).toEqual(
[
visible ? 'all:host:ssh:builder' : 'pinned:host:ssh:builder',
'all',
'host:local',
'lineage:local|parent',
...(visible ? [] : ['lineage:ssh:builder|remote', 'lineage:ssh:builder|parent'])
].sort()
)
expect(
sections('ssh:builder', 'none', visibleWorktrees, result.current, worktreeLineageById).some(
(row) => row.type === 'item' && row.worktree.id === child.id
)
).toBe(true)
expect(collapsedGroups.size).toBe(8)
rerender({ targetId: null })
expect(result.current).toBe(collapsedGroups)
})
@@ -11,20 +11,18 @@ import {
worktree,
visibilityArgs,
sectionArgs,
sidebarCorpus,
countArrayEntries,
countMapEntries
} from './host-status-test-fixtures'
import { useProjectHeaderHostContexts } from '../viewport/use-project-header-host-contexts'
import { useVisibleSidebarWorktrees } from './use-visible-worktrees'
import { useSidebarSectionRows } from './use-section-rows'
import { useFolderWorkspacePathStatusRows } from './use-folder-path-statuses'
import { createProjectGroup } from '../../../../../../shared/project-groups'
import type * as FolderStatus from '@/store/folder-workspaces/folder-path-status'
import type * as RuntimeRpcClient from '@/runtime/runtime-rpc-client'
import { useSidebarHostVisibleScope } from './use-host-visible-scope'
import { makeFolderWorkspace } from '../../../worktree-jump/worktree-jump-palette-test-fixtures'
import type { ProjectGroup } from '../../../../../../shared/project-group-types'
import type * as SshGate from '@/lib/repo-ssh-connection'
import type * as DeviceVisibility from '../../../../../../shared/workspace-creator-visibility'
import type * as Registry from '../../../../../../shared/execution-host-registry'
@@ -283,351 +281,6 @@ it('does zero device scans while off and preserves equal identities while on, in
expect(counts.devices).toBe(off)
})
it('keeps equal host options and health-only changes out of the workspace row plan', () => {
const args = sectionArgs()
let renders = 0
const { result } = renderHook(() => {
renders++
return useSidebarSectionRows(args)
})
const first = result.current
const before = renders
const plans = counts.plan
for (let flip = 0; flip < 10; flip++) {
act(() => publish('B', flip + 2, { status: { ...status('B'), liveTabCount: flip } }))
}
act(() => publish('A', 2, { status: { ...status('A'), liveTabCount: 99 } }))
expect(renders).toBe(before)
expect(result.current).toBe(first)
expect(counts.plan).toBe(plans)
const builds = counts.registry
act(() => useAppStore.setState({ sshStateByEnvironment: new Map() }))
expect(counts.registry).toBe(builds)
act(() => publish('A', 3, { verification: 'checking', transport: 'connecting' }))
expect(result.current.hostOptions.find((host) => host.id === 'runtime:A')?.health).toBe(
'connecting'
)
expect(counts.plan).toBe(plans)
expect(result.current.occurrencePlan).toBe(first.occurrencePlan)
act(() =>
useAppStore.setState({
runtimeEnvironments: [environment('A'), { ...environment('B'), name: 'Renamed' }]
})
)
expect(result.current.hostOptions.find((host) => host.id === 'runtime:B')?.label).toBe('Renamed')
expect(counts.plan).toBe(plans + 1)
act(() =>
publish('A', 4, {
status: { ...status('A'), capabilities: ['new-capability'], hostPlatform: 'linux' }
})
)
expect(result.current.hostOptions.find((host) => host.id === 'runtime:A')).toMatchObject({
capabilities: ['new-capability'],
platform: 'linux'
})
act(() =>
useAppStore.setState({
runtimeEnvironments: [],
runtimeStatusByEnvironmentId: new Map([
['status-only', { status: status('status-only'), checkedAt: 1 }]
])
})
)
expect(result.current.hostOptions.map((host) => host.id)).toContain('runtime:status-only')
expect(result.current.hostOptions.map((host) => host.id)).not.toContain('runtime:B')
})
it('keeps compatibility, blocked guidance, aliases and diagnostic equality live in the host projection', () => {
const control = {
state: 'ready' as const,
pendingRequestCount: 0,
subscriptionCount: 0,
reconnectAttempt: 0,
lastConnectedAt: 1,
lastClose: null,
lastError: null,
lastRttMs: 1
}
publish('A', 2, { remoteControl: control })
const args = sectionArgs()
let renders = 0
const { result } = renderHook(() => {
renders++
return useSidebarSectionRows(args)
})
const before = renders
const options = result.current.hostOptions
act(() =>
publish('A', 3, {
remoteControl: { ...control, lastRttMs: 99, pendingRequestCount: 10, subscriptionCount: 5 }
})
)
expect(renders).toBe(before)
expect(result.current.hostOptions).toBe(options)
act(() => publish('A', 4, { verification: 'blocked', blockedReason: 'access-revoked' }))
expect(result.current.hostOptions.find((host) => host.id === 'runtime:A')).toMatchObject({
health: 'blocked',
blockedReason: 'access-revoked'
})
act(() => publish('A', 5, { status: { ...status('A'), runtimeProtocolVersion: 0 } }))
expect(
result.current.hostOptions.find((host) => host.id === 'runtime:A')?.compatibility
).toMatchObject({ kind: 'blocked', reason: 'server-too-old' })
act(() =>
useAppStore.setState({
sshTargetLabels: new Map([['managed', 'My machine']]),
runtimeEnvironments: [
{
...environment('A'),
orcadDeployment: {
sshTargetId: 'managed',
sshTargetGeneration: 1,
localPort: 7001,
remotePort: 7002
}
}
]
})
)
expect(result.current.hostOptions.find((host) => host.id === 'runtime:A')).toMatchObject({
label: 'My machine',
aliasHostIds: ['ssh:managed']
})
act(() =>
useAppStore.setState({
sshConnectionStates: new Map([
[
'managed',
{
...ssh('connected', 'managed'),
managedServer: { kind: 'relay', reason: 'orcad_unavailable' }
}
]
])
})
)
expect(result.current.hostOptions.find((host) => host.id === 'ssh:managed')).toMatchObject({
aliasHostIds: ['runtime:A']
})
})
it.each([
{ repos: 10, hosts: 10 },
{ repos: 100, hosts: 10 },
{ repos: 10, hosts: 100 },
{ repos: 100, hosts: 100 }
])(
'bounds one sidebar publication workload with $repos repos, $hosts hosts and three workspaces per repo',
({ repos: repoCount, hosts: hostCount }) => {
const args = sidebarCorpus(repoCount, hostCount)
const dependencyCount = hostCount === 100 ? repoCount : 1
const projectGroups: ProjectGroup[] = Array.from({ length: repoCount }, (_, index) => ({
...createProjectGroup({
name: `Group ${index}`,
parentPath: `/folders/${index}`,
createdFrom: 'folder-scan',
tabOrder: index,
now: 1
}),
id: `group-${index}`,
executionHostId: 'runtime:host-0',
connectionId: `target-${index % dependencyCount}`
}))
projectGroups.push({
...projectGroups[0]!,
id: 'nested',
parentPath: null,
parentGroupId: 'group-0'
})
const folderWorkspaces = Array.from({ length: repoCount }, (_, index) =>
makeFolderWorkspace({
id: `folder-${index}`,
projectGroupId: 'group-0',
executionHostId: 'runtime:host-0',
folderPath: `/folders/0/${index}`
})
)
args.repos[0] = { ...args.repos[0]!, projectGroupId: 'nested' }
args.repoMap.set(args.repos[0]!.id, args.repos[0]!)
useAppStore.setState({ repos: args.repos, projectGroups, folderWorkspaces })
// Keep requests pending: count production discovery/action snapshots without settlement writes.
const callRuntimeRpc = vi.fn(() => new Promise<never>(() => {}))
runtimeRpc.callRuntimeRpc.mockImplementation(callRuntimeRpc)
const fetch = vi.fn(useAppStore.getState().fetchFolderWorkspacePathStatus)
useAppStore.setState({ fetchFolderWorkspacePathStatus: fetch })
const folderArgs = {
allRepoIds: args.repos.map((repo) => repo.id),
repoMap: args.repoMap,
projectGroups,
folderWorkspaces
}
const visibleArgs = {
...visibilityArgs,
repoMap: args.repoMap,
sortedIds: args.worktrees.map((item) => item.id),
filterState: { ...visibilityArgs.filterState, hideWorkspacesFromOtherDevices: true }
}
let renders = 0
const { result } = renderHook(() => {
renders++
const visible = useVisibleSidebarWorktrees(visibleArgs)
const section = useSidebarSectionRows({ ...args, worktrees: visible.visibleWorktrees })
return {
visible,
section,
folder: useFolderWorkspacePathStatusRows(folderArgs),
contexts: useProjectHeaderHostContexts({
rows: section.sectionRows,
hosts: section.hostOptions,
repoMap: args.repoMap
})
}
})
const before = { ...counts }
expect(counts.dependencies).toBe(2 * repoCount)
expect(counts.dependencyEntries).toBeGreaterThan(2 * repoCount * repoCount)
expect(counts.dependencyEntries).toBeLessThanOrEqual(
2 * repoCount * (5 * projectGroups.length + 2 * repoCount)
)
expect(fetch).toHaveBeenCalledTimes(2 * repoCount)
expect(
fetch.mock.calls.every(
([, options]) => options.force && options.runtimeEnvironmentId === 'host-0'
)
).toBe(true)
const first = result.current
const settledRenders = renders
const fleet = hostCount + 1
expect(first.visible.visibleWorktrees).toHaveLength(3 * repoCount)
expect(first.section.sectionRows).toHaveLength(repoCount)
expect(first.contexts.size).toBe(repoCount)
for (let sequence = 2; sequence < 12; sequence++) {
act(() => publish('B', sequence, { status: { ...status('B'), liveTabCount: sequence } }))
}
expect(renders).toBe(settledRenders)
expect(result.current).toBe(first)
// Expensive verdicts must not grow with targets owned by unchanged hosts.
expect(counts.gates - before.gates).toBe(0)
expect(counts.devices - before.devices).toBe(10)
expect(counts.deviceEntries - before.deviceEntries).toBe(10 * fleet)
expect(counts.registry - before.registry).toBe(10)
expect(counts.registryEntries - before.registryEntries).toBe(10 * (3 * fleet + 2 * repoCount))
expect(counts.contexts).toBe(before.contexts)
expect(counts.contextEntries).toBe(before.contextEntries)
expect(counts.plan).toBe(before.plan)
expect(counts.visibility).toBe(before.visibility)
const snapshots = { ...counts }
function statusMap(value: 'connected' | 'disconnected') {
const map = new Map([['outside', ssh(value, 'outside')]])
const get = map.get.bind(map)
map.get = (key) => {
if (key.startsWith('target-')) {
counts.statusReads++
}
return get(key)
}
return map
}
for (let sequence = 0; sequence < 10; sequence++) {
act(() =>
useAppStore.setState({
sshConnectionStates: statusMap(sequence % 2 ? 'connected' : 'disconnected')
})
)
}
expect(renders).toBe(settledRenders)
expect(result.current).toBe(first)
expect(counts.gates - snapshots.gates).toBe(0)
expect(counts.contexts - snapshots.contexts).toBe(10)
expect(counts.contextEntries - snapshots.contextEntries).toBe(
10 * (2 * fleet + 1 + 2 * repoCount)
)
expect(counts.registry - snapshots.registry).toBe(10)
expect(counts.registryEntries - snapshots.registryEntries).toBe(
10 * (3 * fleet + 2 * repoCount)
)
expect(counts.devices).toBe(snapshots.devices)
expect(counts.plan).toBe(before.plan)
expect(counts.dependencies).toBe(before.dependencies)
expect(counts.dependencyEntries).toBe(before.dependencyEntries)
expect(counts.snapshots).toBe(before.snapshots)
expect(fetch).toHaveBeenCalledTimes(2 * repoCount)
expect(counts.statusReads - snapshots.statusReads).toBe(10 * dependencyCount)
const localSsh = { ...counts }
for (let sequence = 0; sequence < 10; sequence++) {
act(() =>
useAppStore
.getState()
.setEnvironmentSshConnectionState(
'B',
'outside',
ssh(sequence % 2 ? 'connected' : 'disconnected', 'outside')
)
)
}
expect(renders).toBe(settledRenders)
expect(result.current).toBe(first)
expect(counts.gates - localSsh.gates).toBe(0)
expect(counts.registry).toBe(localSsh.registry)
expect(counts.devices).toBe(localSsh.devices)
expect(counts.contexts).toBe(localSsh.contexts)
expect(counts.dependencies).toBe(before.dependencies)
expect(counts.snapshots).toBe(before.snapshots)
const equal = { ...counts }
act(() => publish('host-0', 2, { status: { ...status('host-0'), liveTabCount: 99 } }))
expect(renders).toBe(settledRenders)
expect(result.current).toBe(first)
expect(counts.gates - equal.gates).toBe(0)
expect(counts.deviceEntries - equal.deviceEntries).toBe(fleet)
act(() =>
useAppStore
.getState()
.setEnvironmentSshConnectionState('host-0', 'target-0', ssh('disconnected', 'target-0'))
)
expect(renders).toBe(settledRenders)
expect(result.current).toBe(first)
act(() =>
useAppStore.setState({
sshConnectionStates: new Map([['target-0', ssh('connected', 'target-0')]])
})
)
expect(fetch).toHaveBeenCalledTimes(4 * repoCount)
expect(counts.dependencies).toBe(before.dependencies)
expect(counts.snapshots - before.snapshots).toBe(2 * repoCount)
expect(renders).toBeGreaterThan(settledRenders)
const catalogBuilds = counts.dependencies
act(() =>
useAppStore.setState((state) => ({
repos: state.repos.map((repo, index) =>
index === 0 ? { ...repo, path: '/moved/repo' } : repo
)
}))
)
expect(counts.dependencies - catalogBuilds).toBe(2 * repoCount)
expect(fetch).toHaveBeenCalledTimes(6 * repoCount)
// Requests remain accepted when their snapshot becomes null; route/force policy is unchanged.
act(() => useAppStore.setState({ projectGroups: [], folderWorkspaces: [] }))
expect(fetch).toHaveBeenCalledTimes(8 * repoCount)
expect(
fetch.mock.calls.every(
([, options]) => options.force && options.runtimeEnvironmentId === 'host-0'
)
).toBe(true)
const healthPlans = counts.plan
const occurrencePlan = result.current.section.occurrencePlan
act(() => publish('host-0', 3, { verification: 'checking', transport: 'connecting' }))
expect(
result.current.section.hostOptions.find((host) => host.id === 'runtime:host-0')?.health
).toBe('connecting')
expect(counts.plan).toBe(healthPlans)
expect(result.current.section.occurrencePlan).toBe(occurrencePlan)
act(() => publish('host-0', 4, { status: { ...status('host-0'), pairedDeviceId: 'other' } }))
expect(result.current.visible.visibleWorktrees).toHaveLength(
3 * (repoCount - Math.ceil(repoCount / hostCount))
)
}
)
it('keeps real collapsed same-name header contexts reactive through parent host options and row scope', () => {
const fixture = collapsedSameNameProjectHeaders()
useAppStore.setState({
@@ -7,11 +7,9 @@ import {
setVisibleWorktreeShortcutTargets
} from './visible-worktrees'
import type { AppState } from '@/store/types'
import type { FolderWorkspace } from '../../../../../../shared/folder-workspaces/folder-workspace-types'
import type { ProjectGroup } from '../../../../../../shared/project-group-types'
import type { Repo } from '../../../../../../shared/repo-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
import { folderWorkspaceKey } from '../../../../../../shared/workspace-scope'
const initialState = useAppStore.getInitialState()
@@ -38,10 +36,6 @@ function makeWorktree(id: string, overrides: Partial<Worktree> = {}): Worktree {
} as Worktree
}
function makeMainWorktree(id: string, overrides: Partial<Worktree> = {}): Worktree {
return makeWorktree(id, { isMainWorktree: true, branch: 'refs/heads/main', ...overrides })
}
function makeRepo(id = 'repo1', overrides: Partial<Repo> = {}): Repo {
return {
id,
@@ -53,42 +47,6 @@ function makeRepo(id = 'repo1', overrides: Partial<Repo> = {}): Repo {
} as unknown as Repo
}
function makeProjectGroup(id: string, overrides: Partial<ProjectGroup> = {}): ProjectGroup {
return {
id,
name: id,
parentPath: `/tmp/${id}`,
connectionId: null,
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1,
...overrides
} as ProjectGroup
}
function makeFolderWorkspace(id: string, projectGroupId: string): FolderWorkspace {
return {
id,
projectGroupId,
name: id,
folderPath: `/tmp/${id}`,
connectionId: null,
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
createdAt: 1,
updatedAt: 1
}
}
/** Clear the published order so the closed-sidebar fallback must recompute it from store fields. */
function seedStore(worktrees: Worktree[], overrides: Partial<AppState> = {}): void {
useAppStore.setState(
@@ -116,170 +74,6 @@ describe('closed-sidebar Cmd+1-9 ordering (#9497)', () => {
useAppStore.setState(initialState, true)
})
it('hoists the repo main worktree first for automatic sorting, matching the rendered sidebar', () => {
// Arrange flat sort to disagree with grouping so the main-worktree hoist is observable.
const feature = makeWorktree('wt-feature', { lastActivityAt: 1 })
const main = makeMainWorktree('wt-main', { lastActivityAt: 0 })
seedStore([feature, main], { sortBy: 'recent' })
const order = getVisibleWorktreeIds()
expect(order).toEqual(['wt-main', 'wt-feature'])
expect(order).not.toEqual(['wt-feature', 'wt-main'])
})
it('ignores the agent-send collapse override, which only applies to a mounted list', () => {
// The send-collapse override requires a mounted list, which publishes its own order and bypasses this fallback.
const main = makeMainWorktree('wt-main')
const feature = makeWorktree('wt-feature')
seedStore([main, feature], {
collapsedGroups: new Set(['project:repo:repo1']),
agentSendPopoverTargetMode: {
worktreeId: 'wt-feature',
id: 'send-1',
instanceId: 'inst-1'
} as AppState['agentSendPopoverTargetMode']
})
expect(getVisibleWorktreeIds()).toEqual([])
setVisibleWorktreeIds(['wt-feature', 'wt-main'])
expect(getVisibleWorktreeIds()).toEqual(['wt-feature', 'wt-main'])
})
it('places a pinned workspace per the pinned section under both display policies', () => {
const main = makeMainWorktree('wt-main')
const plain = makeWorktree('wt-plain')
const pinned = makeWorktree('wt-pinned', { isPinned: true })
for (const showPinnedWorktreesInGroups of [false, true]) {
seedStore([main, plain, pinned], {
settings: { showPinnedWorktreesInGroups } as AppState['settings']
})
const order = getVisibleWorktreeIds()
expect(order).toEqual([...new Set(order)])
expect([...order].sort()).toEqual(['wt-main', 'wt-pinned', 'wt-plain'])
expect(order[0]).toBe(showPinnedWorktreesInGroups ? 'wt-main' : 'wt-pinned')
}
})
it('elides members of a collapsed group, which render no card to number', () => {
const main = makeMainWorktree('wt-main')
const feature = makeWorktree('wt-feature')
seedStore([main, feature], { collapsedGroups: new Set(['project:repo:repo1']) })
expect(getVisibleWorktreeIds()).toEqual([])
})
it('numbers folder workspaces, which the flat fallback omitted entirely', () => {
const group = makeProjectGroup('group-1')
const folderWorkspace = makeFolderWorkspace('fw-1', group.id)
seedStore([makeMainWorktree('wt-main')], {
projectGroups: [group],
folderWorkspaces: [folderWorkspace]
})
expect(getVisibleWorktreeIds()).toContain(folderWorkspaceKey(folderWorkspace.id))
})
it('numbers a workspace created while nothing was published', () => {
// A retained cache cannot discover newly created IDs; this path must recompute membership.
seedStore([makeMainWorktree('wt-main')])
expect(getVisibleWorktreeIds()).toEqual(['wt-main'])
seedStore([makeMainWorktree('wt-main'), makeWorktree('wt-created-while-closed')])
expect(getVisibleWorktreeIds()).toEqual(['wt-created-while-closed', 'wt-main'])
})
it('treats a published order as authoritative, including an explicitly empty one', () => {
seedStore([makeMainWorktree('wt-main'), makeWorktree('wt-feature')])
setVisibleWorktreeIds(['x'])
expect(getVisibleWorktreeIds()).toEqual(['x'])
setVisibleWorktreeIds([])
expect(getVisibleWorktreeIds()).toEqual([])
setVisibleWorktreeIds(null)
expect(getVisibleWorktreeIds()).toEqual(['wt-feature', 'wt-main'])
})
it('honors an explicit host filter and keeps the all-hosts default unfiltered', () => {
const localRepo = makeRepo('repo1')
const sshRepo = makeRepo('repo-ssh', { connectionId: 'my-target' })
// Base sort interleaves the hosts, so any host grouping has to reorder them.
const localMain = makeMainWorktree('wt-local-main', { sortOrder: 3 })
const remoteMain = makeMainWorktree('wt-remote-main', { repoId: 'repo-ssh', sortOrder: 2 })
const localFeature = makeWorktree('wt-local-feature', { sortOrder: 1 })
const remoteFeature = makeWorktree('wt-remote-feature', { repoId: 'repo-ssh', sortOrder: 0 })
const storeOverrides = {
repos: [localRepo, sshRepo],
worktreesByRepo: {
repo1: [localMain, localFeature],
'repo-ssh': [remoteMain, remoteFeature]
},
sshTargetLabels: new Map([['my-target', 'My Target']])
} as Partial<AppState>
seedStore([], storeOverrides)
expect(getVisibleWorktreeIds()).toEqual([
'wt-local-main',
'wt-local-feature',
'wt-remote-main',
'wt-remote-feature'
])
seedStore([], { ...storeOverrides, visibleWorkspaceHostIds: ['local'] })
expect(getVisibleWorktreeIds()).toEqual(['wt-local-main', 'wt-local-feature'])
seedStore([], {
...storeOverrides,
visibleWorkspaceHostIds: ['ssh:my-target', 'local']
})
// Host registry order, rather than filter argument order, keeps each host’s workspaces contiguous.
expect(getVisibleWorktreeIds()).toEqual([
'wt-local-main',
'wt-local-feature',
'wt-remote-main',
'wt-remote-feature'
])
})
it("orders a card in a paired host's own column by that column, not the default", () => {
const today = makeWorktree('wt-today', {
hostId: 'runtime:env-1',
workspaceStatus: 'status-6',
sortOrder: 0
})
const todo = makeWorktree('wt-todo', { workspaceStatus: 'todo', sortOrder: 1 })
const doing = makeWorktree('wt-doing', { workspaceStatus: 'in-progress', sortOrder: 2 })
seedStore([today, todo, doing], {
groupBy: 'workspace-status',
workspaceStatuses: [
{ id: 'in-progress', label: 'In progress' },
{ id: 'todo', label: 'Todo' }
],
hostWorkspaceStatuses: { 'env-1': [{ id: 'status-6', label: '🔥 今天收尾' }] }
})
expect(getVisibleWorktreeIds()).toEqual(['wt-doing', 'wt-todo', 'wt-today'])
})
it('keeps the sort layer intact for smart and comparator sort modes', () => {
const main = makeMainWorktree('wt-main')
const older = makeWorktree('wt-older', { displayName: 'b-older', lastActivityAt: 1 })
const newer = makeWorktree('wt-newer', { displayName: 'a-newer', lastActivityAt: 999 })
seedStore([older, newer, main], { sortBy: 'smart' })
expect(getVisibleWorktreeIds()).toEqual(['wt-main', 'wt-newer', 'wt-older'])
seedStore([older, newer, main], { sortBy: 'name' })
expect(getVisibleWorktreeIds()).toEqual(['wt-main', 'wt-newer', 'wt-older'])
})
it('keeps same-id hosts as separate closed-sidebar shortcut positions', () => {
seedStore([
makeWorktree('repo1::/same', { hostId: 'local' }),
@@ -106,51 +106,6 @@ describe('useSidebarWorktreeSortOrder', () => {
useAppStore.setState(initialState, true)
})
it('renders once per sortEpoch bump in Manual (no hook-initiated re-render)', () => {
seed(
[makeWorktree('a', 'A', { manualOrder: 2 }), makeWorktree('b', 'B', { manualOrder: 1 })],
'manual'
)
const { renders } = renderSortOrder()
const before = renders.count
for (let i = 0; i < 10; i++) {
act(() => bumpSortEpoch())
}
expect(renders.count - before).toBe(10)
})
it('applies a Manual reorder in the same commit as the bump', () => {
seed(
[makeWorktree('a', 'A', { manualOrder: 2 }), makeWorktree('b', 'B', { manualOrder: 1 })],
'manual'
)
const { result } = renderSortOrder({ strict: true })
expect(result.current).toEqual(keys('a', 'b'))
act(() =>
replaceWorktrees([
makeWorktree('a', 'A', { manualOrder: 2 }),
makeWorktree('b', 'B', { manualOrder: 3 })
])
)
expect(result.current).toEqual(keys('b', 'a'))
})
it('survives a burst of synchronous store bumps in Manual', () => {
seed(
[makeWorktree('a', 'A', { manualOrder: 2 }), makeWorktree('b', 'B', { manualOrder: 1 })],
'manual'
)
const view = renderSortOrderOutsideAct('manual')
try {
for (let i = 0; i < 80; i++) {
flushSync(() => bumpSortEpoch())
}
expect(view.errors).toEqual([])
} finally {
view.unmount()
}
})
it('survives 80 synchronous bumps in Recent while worktrees are added, then settles in order', () => {
vi.useFakeTimers()
let worktrees = [makeWorktree('w0', 'W0', { lastActivityAt: 0 })]
@@ -185,190 +140,6 @@ describe('useSidebarWorktreeSortOrder', () => {
}
})
it('debounces Recent re-sorts until the settle window passes', () => {
vi.useFakeTimers()
seed(
[
makeWorktree('a', 'A', { lastActivityAt: 2_000 }),
makeWorktree('b', 'B', { lastActivityAt: 1_000 })
],
'recent'
)
const { result } = renderSortOrder()
expect(result.current).toEqual(keys('a', 'b'))
act(() =>
replaceWorktrees([
makeWorktree('a', 'A', { lastActivityAt: 2_000 }),
makeWorktree('b', 'B', { lastActivityAt: 3_000 })
])
)
expect(result.current).toEqual(keys('a', 'b'))
act(() => vi.advanceTimersByTime(3_000))
expect(result.current).toEqual(keys('b', 'a'))
})
it('applies an added worktree immediately in debounced modes', () => {
vi.useFakeTimers()
seed([makeWorktree('a', 'A')], 'name')
const { result } = renderSortOrder()
act(() => replaceWorktrees([makeWorktree('a', 'A'), makeWorktree('0', '0 first')]))
expect(result.current).toEqual(keys('0', 'a'))
})
it('re-sorts on Manual -> Recent and stays put at settle', () => {
vi.useFakeTimers()
seed(
[
makeWorktree('a', 'A', { manualOrder: 2, lastActivityAt: 1_000 }),
makeWorktree('b', 'B', { manualOrder: 1, lastActivityAt: 2_000 })
],
'manual'
)
const { result } = renderSortOrder()
for (let i = 0; i < 5; i++) {
act(() => bumpSortEpoch())
}
expect(result.current).toEqual(keys('a', 'b'))
act(() => useAppStore.getState().setSortBy('recent'))
expect(result.current).toEqual(keys('b', 'a'))
const afterSwitch = result.current
act(() => vi.advanceTimersByTime(3_000))
expect(result.current).toBe(afterSwitch)
})
it('restarts the settle window on every bump in a burst', () => {
vi.useFakeTimers()
seed(
[
makeWorktree('a', 'A', { lastActivityAt: 2_000 }),
makeWorktree('b', 'B', { lastActivityAt: 1_000 })
],
'recent'
)
const { result } = renderSortOrder()
act(() =>
replaceWorktrees([
makeWorktree('a', 'A', { lastActivityAt: 2_000 }),
makeWorktree('b', 'B', { lastActivityAt: 3_000 })
])
)
act(() => vi.advanceTimersByTime(2_000))
act(() => bumpSortEpoch())
act(() => vi.advanceTimersByTime(2_000))
expect(result.current).toEqual(keys('a', 'b'))
act(() => vi.advanceTimersByTime(1_000))
expect(result.current).toEqual(keys('b', 'a'))
})
it('applies a removed worktree immediately in debounced modes', () => {
vi.useFakeTimers()
seed(
[
makeWorktree('a', 'A', { lastActivityAt: 3_000 }),
makeWorktree('b', 'B', { lastActivityAt: 2_000 }),
makeWorktree('c', 'C', { lastActivityAt: 1_000 })
],
'recent'
)
const { result } = renderSortOrder()
act(() =>
replaceWorktrees([
makeWorktree('b', 'B', { lastActivityAt: 2_000 }),
makeWorktree('c', 'C', { lastActivityAt: 4_000 })
])
)
expect(result.current).toEqual(keys('c', 'b'))
})
it('does not re-sort when rows change without a sortEpoch bump', () => {
vi.useFakeTimers()
seed(
[
makeWorktree('a', 'A', { lastActivityAt: 2_000 }),
makeWorktree('b', 'B', { lastActivityAt: 1_000 })
],
'recent'
)
const { result } = renderSortOrder()
const before = result.current
// Same shape as a stale-host purge that drops/changes rows without bumping sortEpoch.
act(() =>
useAppStore.setState({
worktreesByRepo: {
[repo.id]: [
makeWorktree('a', 'A', { lastActivityAt: 2_000 }),
makeWorktree('b', 'B', { lastActivityAt: 3_000 }),
makeWorktree('c', 'C', { lastActivityAt: 4_000 })
]
}
})
)
act(() => vi.advanceTimersByTime(3_000))
expect(result.current).toBe(before)
expect(vi.getTimerCount()).toBe(0)
})
it('switching sort mode cancels a pending settle and re-sorts right away', () => {
vi.useFakeTimers()
seed(
[
makeWorktree('a', 'Zeta', { lastActivityAt: 2_000 }),
makeWorktree('b', 'Alpha', { lastActivityAt: 1_000 })
],
'recent'
)
const { result } = renderSortOrder()
act(() =>
replaceWorktrees([
makeWorktree('a', 'Zeta', { lastActivityAt: 2_000 }),
makeWorktree('b', 'Alpha', { lastActivityAt: 3_000 })
])
)
expect(vi.getTimerCount()).toBe(1)
act(() => useAppStore.getState().setSortBy('name'))
expect(result.current).toEqual(keys('b', 'a'))
expect(useAppStore.getState().settledSortEpoch).toBe(useAppStore.getState().sortEpoch)
expect(vi.getTimerCount()).toBe(0)
})
it.each(['smart', 'manual'] as const)(
'follows another client order save while %s ranks by persisted sortOrder',
(sortBy) => {
seed(
[makeWorktree('a', 'A', { sortOrder: 2 }), makeWorktree('b', 'B', { sortOrder: 1 })],
sortBy
)
const { result } = renderSortOrder()
expect(result.current).toEqual(keys('a', 'b'))
act(() => publishSortOrders({ a: 1, b: 2 }))
expect(result.current).toEqual(keys('b', 'a'))
expect(useAppStore.getState().sortEpoch).toBe(1)
}
)
it('keeps live Smart order steady when only persisted sortOrder changes', () => {
const persistSortOrder = vi.fn(async () => ({ updated: 0 }))
vi.stubGlobal('api', { worktrees: { persistSortOrder } })
try {
seed(
[makeWorktree('a', 'A', { sortOrder: 2 }), makeWorktree('b', 'B', { sortOrder: 1 })],
'smart'
)
useAppStore.setState({
tabsByWorktree: { a: [makeTerminalTab({ id: 'tab-a', worktreeId: 'a' })] },
ptyIdsByTabId: { 'tab-a': ['pty-a'] }
})
const { result } = renderSortOrder()
const before = result.current
const savesBefore = persistSortOrder.mock.calls.length
act(() => publishSortOrders({ a: 1, b: 2 }))
expect(result.current).toBe(before)
expect(persistSortOrder.mock.calls.length).toBe(savesBefore)
} finally {
vi.unstubAllGlobals()
}
})
it('keeps Smart latched after its live signal ends, until the list unmounts', () => {
vi.stubGlobal('api', { worktrees: { persistSortOrder: vi.fn(async () => ({ updated: 0 })) } })
try {