fix: contain sort order persistence failures

This commit is contained in:
Neil
2026-07-10 18:05:28 -07:00
parent f1d09275b7
commit 9c915ae152
3 changed files with 122 additions and 19 deletions
@@ -202,14 +202,12 @@ import {
pruneWorktreeSelection,
updateWorktreeSelection
} from './worktree-multi-selection'
import { callRuntimeRpc } from '@/runtime/runtime-rpc-client'
import { splitWorktreeSortOrderByHost } from '@/lib/worktree-sort-order-host-split'
import { persistWorktreeSortOrderByHost } from '@/lib/worktree-sort-order-persistence'
import {
ALL_EXECUTION_HOSTS_SCOPE,
getRepoExecutionHostId,
getSettingsFocusedExecutionHostId,
type ExecutionHostId,
parseExecutionHostId
type ExecutionHostId
} from '../../../../shared/execution-host'
import { getRepoHeaderCreateState } from './repo-header-create-state'
import type { PendingSidebarRowReveal, PendingSidebarWorktreeReveal } from '@/store/slices/ui'
@@ -5520,21 +5518,7 @@ const WorktreeList = React.memo(function WorktreeList({
// Why: sortOrder is persisted in each host's worktreeMeta and enriched from
// the owner host, so persist each host's ids on that host.
const state = useAppStore.getState()
for (const group of splitWorktreeSortOrderByHost(state, sortedIds)) {
const parsed = parseExecutionHostId(group.hostId)
const target =
parsed?.kind === 'runtime'
? ({ kind: 'environment', environmentId: parsed.environmentId } as const)
: ({ kind: 'local' } as const)
void (target.kind === 'environment'
? callRuntimeRpc(
target,
'worktree.persistSortOrder',
{ orderedIds: group.orderedIds },
{ timeoutMs: 15_000 }
)
: window.api.worktrees.persistSortOrder({ orderedIds: group.orderedIds }))
}
persistWorktreeSortOrderByHost(state, sortedIds)
}, [sortedIds, sortBy])
// Flatten, filter, and apply stable sort order via the shared utility so
@@ -0,0 +1,84 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { callRuntimeRpc } from '@/runtime/runtime-rpc-client'
import type { WorktreeRuntimeOwnerState } from './worktree-runtime-owner'
import { persistWorktreeSortOrderByHost } from './worktree-sort-order-persistence'
vi.mock('@/runtime/runtime-rpc-client', () => ({
callRuntimeRpc: vi.fn()
}))
const localPersistSortOrder = vi.fn()
const runtimeCall = vi.mocked(callRuntimeRpc)
const UNHANDLED_REJECTION_SETTLE_MS = 20
const state: WorktreeRuntimeOwnerState = {
settings: { activeRuntimeEnvironmentId: 'focused-env' },
repos: [
{ id: 'local-repo', connectionId: null, executionHostId: 'local' },
{ id: 'runtime-repo', connectionId: null, executionHostId: 'runtime:env-1' }
],
worktreesByRepo: {
'local-repo': [{ id: 'local-repo::wt-a', repoId: 'local-repo' }],
'runtime-repo': [{ id: 'runtime-repo::wt-b', repoId: 'runtime-repo' }]
}
}
async function collectUnhandledRejections(run: () => void): Promise<unknown[]> {
const reasons: unknown[] = []
const onUnhandledRejection = (reason: unknown): void => {
reasons.push(reason)
}
process.on('unhandledRejection', onUnhandledRejection)
try {
run()
await new Promise((resolve) => setTimeout(resolve, UNHANDLED_REJECTION_SETTLE_MS))
} finally {
process.off('unhandledRejection', onUnhandledRejection)
}
return reasons
}
beforeEach(() => {
runtimeCall.mockReset()
localPersistSortOrder.mockReset()
runtimeCall.mockResolvedValue(undefined)
localPersistSortOrder.mockResolvedValue(undefined)
vi.stubGlobal('window', {
api: {
worktrees: {
persistSortOrder: localPersistSortOrder
}
}
})
})
afterEach(() => {
vi.unstubAllGlobals()
})
describe('persistWorktreeSortOrderByHost', () => {
it('persists each owner host through the matching transport', () => {
persistWorktreeSortOrderByHost(state, ['runtime-repo::wt-b', 'local-repo::wt-a'])
expect(runtimeCall).toHaveBeenCalledWith(
{ kind: 'environment', environmentId: 'env-1' },
'worktree.persistSortOrder',
{ orderedIds: ['runtime-repo::wt-b'] },
{ timeoutMs: 15_000 }
)
expect(localPersistSortOrder).toHaveBeenCalledWith({ orderedIds: ['local-repo::wt-a'] })
})
it('handles best-effort persistence rejections from disconnected hosts', async () => {
runtimeCall.mockRejectedValueOnce(new Error('SSH disconnected'))
localPersistSortOrder.mockRejectedValueOnce(new Error('local store unavailable'))
const unhandledRejections = await collectUnhandledRejections(() => {
persistWorktreeSortOrderByHost(state, ['runtime-repo::wt-b', 'local-repo::wt-a'])
})
expect(unhandledRejections).toEqual([])
})
})
@@ -0,0 +1,35 @@
import { callRuntimeRpc } from '@/runtime/runtime-rpc-client'
import { parseExecutionHostId } from '../../../shared/execution-host'
import type { WorktreeRuntimeOwnerState } from './worktree-runtime-owner'
import { splitWorktreeSortOrderByHost } from './worktree-sort-order-host-split'
function ignoreSortOrderPersistenceFailure(promise: Promise<unknown>): void {
void promise.catch(() => {
// Why: sort-order restore is best-effort; SSH disconnects during smart sort
// must not surface as unhandled rejections that pollute crash diagnostics.
})
}
export function persistWorktreeSortOrderByHost(
state: WorktreeRuntimeOwnerState,
orderedIds: readonly string[]
): void {
for (const group of splitWorktreeSortOrderByHost(state, orderedIds)) {
const parsed = parseExecutionHostId(group.hostId)
if (parsed?.kind === 'runtime') {
ignoreSortOrderPersistenceFailure(
callRuntimeRpc(
{ kind: 'environment', environmentId: parsed.environmentId },
'worktree.persistSortOrder',
{ orderedIds: group.orderedIds },
{ timeoutMs: 15_000 }
)
)
continue
}
ignoreSortOrderPersistenceFailure(
window.api.worktrees.persistSortOrder({ orderedIds: group.orderedIds })
)
}
}