test(worktree): cover retirement runtime contracts

This commit is contained in:
Brennan Benson
2026-08-13 15:32:13 -07:00
parent 21a117aa34
commit ba024d52bf
9 changed files with 82 additions and 22 deletions
@@ -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
+9 -1
View File
@@ -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({
+8 -1
View File
@@ -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<typeof vi.fn>
@@ -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([
{
+9 -4
View File
@@ -1331,6 +1331,9 @@ const store = {
addedAt: 1
}
],
addRetiredWorktreeName: () => {},
getRetiredWorktreeNames: () => [],
mergeRetiredWorktreeNames: () => false,
addRepo: () => {},
updateRepo: (id: string, updates: Record<string, unknown>) =>
({
@@ -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<typeof MOCK_GIT_WORKTREES>()
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 () => {
+16
View File
@@ -21091,6 +21091,22 @@ export class OrcaRuntimeService {
}
}
async listRetiredWorktreeNames(repoSelector: string): Promise<Record<string, string[]>> {
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<string>): Record<string, string[]> {
@@ -25,6 +25,23 @@ const passthroughDedupe = <T>(_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',
+13 -13
View File
@@ -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)
}
})
]
+1
View File
@@ -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',
+8 -1
View File
@@ -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<string, string[]> = {}
for (const repo of repos) {