From ba024d52bffe151a0fcb2203a166e69952cf3aec Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:32:13 -0700 Subject: [PATCH] test(worktree): cover retirement runtime contracts --- .../worktree/use-retired-worktree-names.ts | 3 +-- src/main/ipc/worktrees-windows.test.ts | 10 ++++++- src/main/ipc/worktrees.test.ts | 9 ++++++- src/main/runtime/orca-runtime.test.ts | 13 +++++++--- src/main/runtime/orca-runtime.ts | 16 ++++++++++++ src/main/runtime/rpc/methods/worktree.test.ts | 17 ++++++++++++ src/main/runtime/rpc/methods/worktree.ts | 26 +++++++++---------- src/main/runtime/runtime-rpc.ts | 1 + src/main/worktree-name-retirement.ts | 9 ++++++- 9 files changed, 82 insertions(+), 22 deletions(-) diff --git a/mobile/src/worktree/use-retired-worktree-names.ts b/mobile/src/worktree/use-retired-worktree-names.ts index b3cc666f8a5..c875f2ca0c1 100644 --- a/mobile/src/worktree/use-retired-worktree-names.ts +++ b/mobile/src/worktree/use-retired-worktree-names.ts @@ -25,8 +25,7 @@ export function useRetiredWorktreeNames( } let cancelled = false void client - // limit 1 because only the retirement map is wanted; the rows are incidental. - .sendRequest('worktree.list', { repo: `id:${repoId}`, limit: 1 }) + .sendRequest('worktree.listRetiredNames', { repo: `id:${repoId}` }) .then((response) => { if (cancelled) { return diff --git a/src/main/ipc/worktrees-windows.test.ts b/src/main/ipc/worktrees-windows.test.ts index ee9540048cd..a594e182cd5 100644 --- a/src/main/ipc/worktrees-windows.test.ts +++ b/src/main/ipc/worktrees-windows.test.ts @@ -158,7 +158,10 @@ describe('registerWorktreeHandlers – Windows path handling', () => { getSettings: vi.fn(), getWorktreeMeta: vi.fn(), setWorktreeMeta: vi.fn(), - removeWorktreeMeta: vi.fn() + removeWorktreeMeta: vi.fn(), + addRetiredWorktreeName: vi.fn(), + getRetiredWorktreeNames: vi.fn(), + mergeRetiredWorktreeNames: vi.fn() } beforeEach(() => { @@ -200,6 +203,9 @@ describe('registerWorktreeHandlers – Windows path handling', () => { store.getWorktreeMeta.mockReset() store.setWorktreeMeta.mockReset() store.removeWorktreeMeta.mockReset() + store.addRetiredWorktreeName.mockReset() + store.getRetiredWorktreeNames.mockReset() + store.mergeRetiredWorktreeNames.mockReset() for (const key of Object.keys(handlers)) { delete handlers[key] @@ -236,6 +242,7 @@ describe('registerWorktreeHandlers – Windows path handling', () => { }) resolveSetupRunnerShellMock.mockReturnValue(undefined) store.getWorktreeMeta.mockReturnValue(undefined) + store.getRetiredWorktreeNames.mockReturnValue([]) store.setWorktreeMeta.mockReturnValue({}) resolveLocalGitUsernameMock.mockResolvedValue('') getDefaultBaseRefMock.mockReturnValue('origin/main') @@ -302,6 +309,7 @@ describe('registerWorktreeHandlers – Windows path handling', () => { false ) expect(resolveLocalGitUsernameMock).not.toHaveBeenCalled() + expect(store.addRetiredWorktreeName).toHaveBeenCalledWith('repo-1', 'improve-dashboard') expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::C:/workspaces/improve-dashboard', expect.objectContaining({ diff --git a/src/main/ipc/worktrees.test.ts b/src/main/ipc/worktrees.test.ts index 02f23bc094a..ea52426cb4d 100644 --- a/src/main/ipc/worktrees.test.ts +++ b/src/main/ipc/worktrees.test.ts @@ -316,7 +316,10 @@ describe('registerWorktreeHandlers', () => { removeWorktreeLineage: vi.fn(), getAllWorkspaceLineage: vi.fn(), getFolderWorkspaces: vi.fn(), - getProjectGroups: vi.fn() + getProjectGroups: vi.fn(), + addRetiredWorktreeName: vi.fn(), + getRetiredWorktreeNames: vi.fn(), + mergeRetiredWorktreeNames: vi.fn() } let runtimeStub: { resolveRemoteTrackingBase: ReturnType @@ -400,6 +403,9 @@ describe('registerWorktreeHandlers', () => { store.getAllWorkspaceLineage, store.getFolderWorkspaces, store.getProjectGroups, + store.addRetiredWorktreeName, + store.getRetiredWorktreeNames, + store.mergeRetiredWorktreeNames, killAllProcessesForWorktreeMock, clearProviderPtyStateMock, getLocalPtyProviderMock, @@ -448,6 +454,7 @@ describe('registerWorktreeHandlers', () => { }) store.getWorktreeMeta.mockReturnValue(undefined) store.getAllWorktreeMeta.mockReturnValue({}) + store.getRetiredWorktreeNames.mockReturnValue([]) store.setWorktreeMeta.mockReturnValue({}) store.getProjectHostSetups.mockReturnValue([ { diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index ae31995374c..af8a19b5a8c 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -1331,6 +1331,9 @@ const store = { addedAt: 1 } ], + addRetiredWorktreeName: () => {}, + getRetiredWorktreeNames: () => [], + mergeRetiredWorktreeNames: () => false, addRepo: () => {}, updateRepo: (id: string, updates: Record) => ({ @@ -4048,12 +4051,13 @@ describe('OrcaRuntimeService', () => { }) it('does not reuse stale in-flight worktree scans after creating a worktree', async () => { - const runtime = new OrcaRuntimeService(store) + const addRetiredWorktreeName = vi.fn() + const runtime = new OrcaRuntimeService({ ...store, addRetiredWorktreeName }) const staleScan = deferred() const createdWorktree = { - path: '/tmp/workspaces/cache-race', + path: '/tmp/workspaces/nautilus', head: 'def', - branch: 'cache-race', + branch: 'nautilus', isBare: false, isMainWorktree: false } @@ -4067,7 +4071,7 @@ describe('OrcaRuntimeService', () => { const staleLookup = runtime.showManagedWorktree(TEST_WORKTREE_ID) const result = await runtime.createManagedWorktree({ repoSelector: 'id:repo-1', - name: 'cache-race' + name: 'nautilus' }) const freshLookup = runtime.showManagedWorktree(result.worktree.id) @@ -4083,6 +4087,7 @@ describe('OrcaRuntimeService', () => { worktrees: expect.arrayContaining([expect.objectContaining({ path: createdWorktree.path })]) } ) + expect(addRetiredWorktreeName).toHaveBeenCalledWith(TEST_REPO_ID, 'nautilus') }) it('creates additional workspace metadata for folder-mode repos through runtime create', async () => { diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index f927be3d650..c375a1ea60f 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -21091,6 +21091,22 @@ export class OrcaRuntimeService { } } + async listRetiredWorktreeNames(repoSelector: string): Promise> { + const store = this.store + if (!store?.getRetiredWorktreeNames) { + return {} + } + const repo = await this.resolveRepoSelector(repoSelector) + if (store.mergeRetiredWorktreeNames) { + try { + await ensureRetiredWorktreeNamesBackfilled(store, repo, store.getSettings()) + } catch (error) { + console.warn(`[runtime] retirement backfill failed for repo ${repo.id}:`, error) + } + } + return this.collectRetiredNamesForRepos(new Set([repo.id])) + } + /** Why: scoped to the repos in the page plus any explicitly requested one, so the payload cannot * grow with the number of repos on the host; a client only suggests names for repos it sees. */ private collectRetiredNamesForRepos(repoIds: Set): Record { diff --git a/src/main/runtime/rpc/methods/worktree.test.ts b/src/main/runtime/rpc/methods/worktree.test.ts index 0b5be4da384..d70e5941a0c 100644 --- a/src/main/runtime/rpc/methods/worktree.test.ts +++ b/src/main/runtime/rpc/methods/worktree.test.ts @@ -25,6 +25,23 @@ const passthroughDedupe = (_repo: string, _id: string | undefined, run: () => run() describe('worktree RPC methods', () => { + it('returns retired names without exposing the full worktree listing', async () => { + const runtime = { + getRuntimeId: () => 'test-runtime', + listRetiredWorktreeNames: vi.fn().mockResolvedValue({ 'repo-1': ['nautilus'] }) + } as unknown as OrcaRuntimeService + + const response = await new RpcDispatcher({ runtime, methods: WORKTREE_METHODS }).dispatch( + makeRequest('worktree.listRetiredNames', { repo: 'id:repo-1' }) + ) + + expect(runtime.listRetiredWorktreeNames).toHaveBeenCalledWith('id:repo-1') + expect(response).toMatchObject({ + ok: true, + result: { retiredNamesByRepo: { 'repo-1': ['nautilus'] } } + }) + }) + it('routes mobile session-only activation without notifying desktop clients', async () => { const runtime = { getRuntimeId: () => 'test-runtime', diff --git a/src/main/runtime/rpc/methods/worktree.ts b/src/main/runtime/rpc/methods/worktree.ts index 3d2a05b523f..ead8306f72c 100644 --- a/src/main/runtime/rpc/methods/worktree.ts +++ b/src/main/runtime/rpc/methods/worktree.ts @@ -43,6 +43,13 @@ export const WORKTREE_METHODS: RpcMethod[] = [ params: WorktreeListParams, handler: async (params, { runtime }) => runtime.listManagedWorktrees(params.repo, params.limit) }), + defineMethod({ + name: 'worktree.listRetiredNames', + params: WorktreeDetectedListParams, + handler: async (params, { runtime }) => ({ + retiredNamesByRepo: await runtime.listRetiredWorktreeNames(params.repo) + }) + }), defineMethod({ name: 'worktree.detectedList', params: WorktreeDetectedListParams, @@ -289,18 +296,11 @@ export const WORKTREE_METHODS: RpcMethod[] = [ defineMethod({ name: 'worktree.forceDeleteBranch', params: WorktreeForceDeleteBranch, - handler: async (params, { runtime }) => - params.hostId - ? runtime.forceDeletePreservedBranch( - params.worktree, - params.branchName, - params.expectedHead, - params.hostId - ) - : runtime.forceDeletePreservedBranch( - params.worktree, - params.branchName, - params.expectedHead - ) + handler: async (params, { runtime }) => { + const deletionArgs = [params.worktree, params.branchName, params.expectedHead] as const + return params.hostId + ? runtime.forceDeletePreservedBranch(...deletionArgs, params.hostId) + : runtime.forceDeletePreservedBranch(...deletionArgs) + } }) ] diff --git a/src/main/runtime/runtime-rpc.ts b/src/main/runtime/runtime-rpc.ts index 326801c6fa9..1b8c39f317d 100644 --- a/src/main/runtime/runtime-rpc.ts +++ b/src/main/runtime/runtime-rpc.ts @@ -429,6 +429,7 @@ const MOBILE_RPC_METHOD_ALLOWLIST = new Set([ 'worktree.activate', 'worktree.create', 'worktree.forceDeleteBranch', + 'worktree.listRetiredNames', 'worktree.prefetchCreateBase', 'worktree.ps', 'worktree.show', diff --git a/src/main/worktree-name-retirement.ts b/src/main/worktree-name-retirement.ts index 3e022e58482..04ee3d253ea 100644 --- a/src/main/worktree-name-retirement.ts +++ b/src/main/worktree-name-retirement.ts @@ -137,16 +137,23 @@ export function getRetiredWorktreeNamesByRepo( if (isFolderRepo(candidate)) { continue } + const retiredNames = getRetiredNames(candidate.id) + if (retiredNames.length === 0) { + continue + } const collisionKey = getRetirementCollisionKey(candidate, settings) let names = namesByCollisionKey.get(collisionKey) if (!names) { names = new Set() namesByCollisionKey.set(collisionKey, names) } - for (const name of getRetiredNames(candidate.id)) { + for (const name of retiredNames) { names.add(name) } } + if (namesByCollisionKey.size === 0) { + return {} + } const requestedRepoIds = new Set(repoIds) const byRepo: Record = {} for (const repo of repos) {