From 864cb0b4f1265d3affede06e109b0f5de56f8929 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:54:57 -0700 Subject: [PATCH] fix: fail closed on stale qualified removal owner --- src/main/persistence.test.ts | 8 ++++++++ src/main/runtime/orca-runtime.test.ts | 27 +++++++++++++++++++++++++++ src/main/runtime/orca-runtime.ts | 12 ++++++++++-- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts index e414d3969d8..20be3f342fa 100644 --- a/src/main/persistence.test.ts +++ b/src/main/persistence.test.ts @@ -13112,12 +13112,20 @@ describe('Store host-partitioned workspace sessions', () => { store.setWorkspaceSession(session, 'local') store.setWorkspaceSession(session, 'runtime:env-b') store.setWorktreeMeta(worktreeId, { hostId: 'local' }) + const worktreeLineage = makeWorktreeLineage({ worktreeId }) + const workspaceLineage = makeWorkspaceLineage({ + childWorkspaceKey: worktreeWorkspaceKey(worktreeId) + }) + store.setWorktreeLineage(worktreeId, worktreeLineage) + store.setWorkspaceLineage(workspaceLineage) // The confirmed removal target is env-b. Bare metadata belongs to the same-id // local owner and must not redirect the post-delete purge back to local. store.removeWorktreeMeta(worktreeId, 'runtime:env-b') expect(store.getWorktreeMeta(worktreeId)?.hostId).toBe('local') + expect(store.getWorktreeLineage(worktreeId)).toEqual(worktreeLineage) + expect(store.getWorkspaceLineage(workspaceLineage.childWorkspaceKey)).toEqual(workspaceLineage) expect(store.getWorkspaceSession('local').tabsByWorktree[worktreeId]).toHaveLength(1) expect(store.getWorkspaceSession('runtime:env-b').tabsByWorktree[worktreeId]).toBeUndefined() }) diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 8726697a6d2..97f166bfa6f 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -2392,6 +2392,33 @@ describe('OrcaRuntimeService', () => { expect(internals.mobileSessionTabsByWorktree.get(TEST_WORKTREE_ID)).toBe(localSession) }) + it('rejects a qualified removal when only another host has persisted ownership', async () => { + const runtimeStore = { + ...store, + getWorktreeMeta: () => ({ ...store.getWorktreeMeta(TEST_WORKTREE_ID), hostId: 'local' }) + } + const runtime = new OrcaRuntimeService(runtimeStore as never) + const internals = runtime as unknown as { + listResolvedWorktrees: () => Promise + resolveWorktreeRemovalTarget: ( + worktreeSelector: string, + requiredHostId?: string + ) => Promise + } + internals.listResolvedWorktrees = vi.fn().mockResolvedValue([ + { + id: TEST_WORKTREE_ID, + repoId: TEST_REPO_ID, + path: TEST_WORKTREE_PATH, + hostId: 'local' + } + ]) + + await expect( + internals.resolveWorktreeRemovalTarget(TEST_WORKTREE_ID, 'runtime:env-b') + ).rejects.toThrow('selector_not_found') + }) + it('claims the first window as authoritative and ignores later windows', () => { const runtime = createRuntime() diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 0dc5337375c..63b0222e175 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -24619,7 +24619,11 @@ export class OrcaRuntimeService { } const removalTarget = parseExactWorktreeIdSelector(worktreeSelector) const meta = removalTarget ? this.store?.getWorktreeMeta(removalTarget.id) : undefined - if (!removalTarget || !meta) { + if ( + !removalTarget || + !meta || + (requiredHostId !== undefined && meta.hostId !== requiredHostId) + ) { throw error } // Why: delete requests can arrive after Git no longer lists the worktree. @@ -24894,7 +24898,11 @@ export class OrcaRuntimeService { .catch(() => {}) } this.clearOptimisticReconcileToken(removalTarget.id) - this.removeWorktreeMetadataAndHistory(store, removalTarget.id, cleanupHostId) + this.removeWorktreeMetadataAndHistory( + store, + removalTarget.id, + cleanupHostId ?? orphanHost?.id + ) this.preservedBranchCleanupByScope.delete(cleanupScopeKey) this.invalidateResolvedWorktreeCache() this.invalidateWorktreeScanCacheForRepo(removalTarget.repoId)