fix: don't mint worktreeMeta from a sidebar sort-order snapshot (#9342)

`worktrees:persistSortOrder` (ipc/worktrees.ts) and `persistManagedWorktreeSortOrder`
(orca-runtime.ts) call `store.setWorktreeMeta(id, { sortOrder })` for every id the
renderer sends. `setWorktreeMeta` has no repo-existence check, so a stale id the
client still lists — e.g. a removed repo's `${repoId}::${path}` that lingers in the
renderer's order — gets a brand-new `worktreeMeta` entry minted on every sidebar
snapshot, resurrecting an orphan/duplicate workspace on the next launch.

A sort-order snapshot must only reorder worktrees that already exist, never create
one. Guard both call sites with `getWorktreeMeta` so absent ids are skipped.

Co-authored-by: fanyunqian.1 <fanyunqian.1@bytedance.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yunqian Fan 2026-07-24 15:25:39 +08:00 committed by GitHub
parent dd642cb3e3
commit d8499fae16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 30 additions and 0 deletions

View File

@ -540,6 +540,22 @@ describe('registerWorktreeHandlers', () => {
expect(handlers['worktrees:getBranchRenameFailureOutput']).toBeDefined()
})
it('persistSortOrder only reorders existing worktrees and never mints meta for a stale id', () => {
const liveId = 'repo-1::/workspace/repo'
const staleId = 'removed-repo::/workspace/gone'
// Only the live worktree has meta; the stale id (e.g. a removed repo the
// renderer still lists) has none and must be skipped, not created.
store.getWorktreeMeta.mockImplementation((id: string) =>
id === liveId ? ({ instanceId: 'x' } as never) : undefined
)
handlers['worktrees:persistSortOrder'](null, { orderedIds: [liveId, staleId] })
const orderedTargets = store.setWorktreeMeta.mock.calls.map((call) => call[0])
expect(orderedTargets).toContain(liveId)
expect(orderedTargets).not.toContain(staleId)
})
it('prefetches the local default create base through the runtime refresh cache', async () => {
const repo = {
id: 'repo-1',

View File

@ -2067,6 +2067,14 @@ export function registerWorktreeHandlers(
}
const now = Date.now()
for (let i = 0; i < args.orderedIds.length; i++) {
// Why: a sidebar-order snapshot must only reorder worktrees that already
// exist — it must never create one. Without this guard a stale id the
// renderer still lists (e.g. a removed repo's `${repoId}::${path}`) gets a
// fresh worktreeMeta entry minted here, resurrecting an orphan/duplicate
// workspace on the next launch. setWorktreeMeta has no repo-existence check.
if (!store.getWorktreeMeta(args.orderedIds[i])) {
continue
}
// Descending timestamps: first item gets highest sortOrder so b - a sorts first-wins on cold start.
store.setWorktreeMeta(args.orderedIds[i], { sortOrder: now - i * 1000 })
}

View File

@ -19973,6 +19973,12 @@ export class OrcaRuntimeService {
const now = Date.now()
let updated = 0
for (let i = 0; i < orderedIds.length; i++) {
// Why: a sort-order snapshot must only reorder existing worktrees, never
// mint new meta — a stale id would otherwise resurrect an orphan workspace
// (setWorktreeMeta has no repo-existence check).
if (!this.store.getWorktreeMeta(orderedIds[i])) {
continue
}
this.store.setWorktreeMeta(orderedIds[i], { sortOrder: now - i * 1000 })
updated++
}