Files
orca/src/renderer/src/components/sidebar/workspace-kanban-worktree-groups.test.ts
T
OrcaWin 3bb6f917e4 @ (#8683)
fix(sidebar): guard kanban worktree sort against undefined displayName (crash 99657ab1)

A worktree reached the sidebar with an undefined displayName, so the
Manual-sort comparator ran `a.displayName.localeCompare(...)` and threw
`TypeError: Cannot read properties of undefined (reading localeCompare)`,
unmounting the sidebar.worktrees error boundary.

Route both kanban comparators through a null-safe compareDisplayName so a
missing name only affects tie-break order instead of crashing. Behavior is
unchanged when displayName is present.

Adds regression tests for both the manual and recent sort paths.
@
2026-07-13 23:30:48 -07:00

148 lines
3.9 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { Worktree } from '../../../../shared/types'
import { groupWorkspaceKanbanWorktrees } from './workspace-kanban-worktree-groups'
function worktree({
id,
displayName,
...overrides
}: Partial<Worktree> & Pick<Worktree, 'id' | 'displayName'>): Worktree {
return {
repoId: 'repo',
path: `/tmp/${id}`,
head: 'head',
branch: displayName,
isBare: false,
isMainWorktree: false,
id,
displayName,
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
...overrides
} as Worktree
}
const statuses = [
{ id: 'todo', label: 'Todo' },
{ id: 'doing', label: 'Doing' }
]
describe('groupWorkspaceKanbanWorktrees', () => {
it('uses manualOrder inside lanes when Manual sort is active', () => {
const grouped = groupWorkspaceKanbanWorktrees({
worktrees: [
worktree({
id: 'a',
displayName: 'A',
workspaceStatus: 'doing',
manualOrder: 100,
lastActivityAt: 10
}),
worktree({
id: 'b',
displayName: 'B',
workspaceStatus: 'doing',
manualOrder: 300,
lastActivityAt: 1
}),
worktree({
id: 'c',
displayName: 'C',
workspaceStatus: 'doing',
manualOrder: 200,
lastActivityAt: 50
})
],
visibleWorktreeIds: new Set(['a', 'b', 'c']),
workspaceStatuses: statuses,
sortBy: 'manual'
})
expect(grouped.get('doing')?.map((item) => item.id)).toEqual(['b', 'c', 'a'])
})
it('does not crash when a worktree is missing its displayName under Manual sort', () => {
// Repro for crash 99657ab1: a worktree reached the sidebar with an
// undefined displayName, so `a.displayName.localeCompare(...)` threw
// `Cannot read properties of undefined (reading 'localeCompare')`.
expect(() =>
groupWorkspaceKanbanWorktrees({
worktrees: [
worktree({
id: 'a',
displayName: undefined as unknown as string,
workspaceStatus: 'doing',
manualOrder: 100
}),
worktree({
id: 'b',
displayName: undefined as unknown as string,
workspaceStatus: 'doing',
manualOrder: 100
})
],
visibleWorktreeIds: new Set(['a', 'b']),
workspaceStatuses: statuses,
sortBy: 'manual'
})
).not.toThrow()
})
it('does not crash when a worktree is missing its displayName outside Manual sort', () => {
expect(() =>
groupWorkspaceKanbanWorktrees({
worktrees: [
worktree({
id: 'a',
displayName: undefined as unknown as string,
workspaceStatus: 'doing',
lastActivityAt: 10
}),
worktree({
id: 'b',
displayName: undefined as unknown as string,
workspaceStatus: 'doing',
lastActivityAt: 10
})
],
visibleWorktreeIds: new Set(['a', 'b']),
workspaceStatuses: statuses,
sortBy: 'recent'
})
).not.toThrow()
})
it('keeps pinned then recent ordering outside Manual sort', () => {
const grouped = groupWorkspaceKanbanWorktrees({
worktrees: [
worktree({
id: 'a',
displayName: 'A',
workspaceStatus: 'doing',
isPinned: false,
lastActivityAt: 50
}),
worktree({
id: 'b',
displayName: 'B',
workspaceStatus: 'doing',
isPinned: true,
lastActivityAt: 1
})
],
visibleWorktreeIds: new Set(['a', 'b']),
workspaceStatuses: statuses,
sortBy: 'recent'
})
expect(grouped.get('doing')?.map((item) => item.id)).toEqual(['b', 'a'])
})
})