mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
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. @
148 lines
3.9 KiB
TypeScript
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'])
|
|
})
|
|
})
|