mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(runtime): publish project setup changes and retire stale worktree scans (#26941)
Runtime setup create/update/delete changed the store without telling open clients, and a path/kind change or deletion left cached worktree scans answering for the old registration. Publish reposChanged after each successful mutation and run the shared worktree-change invalidators on path/kind changes and deletes. Fixes #26562 Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
import { getProjectIdForProviderIdentity } from '../../shared/project-host-setup-projection'
|
||||
import { getProjectHostSetupForRepo } from '../../shared/project-host-setup-lookup'
|
||||
import { invalidateAuthorizedRootsCache } from '../ipc/filesystem-auth'
|
||||
import { runWorktreeChangeInvalidators } from '../ipc/worktree-change-invalidators'
|
||||
import { prepareLocalWorktreeRootForRepo } from '../worktree-root-preparation'
|
||||
import type { RuntimeStore } from './runtime-store-contract'
|
||||
import { readAllWorktreeMetaForRepo } from '../persistence/host-qualified-worktree-meta'
|
||||
@@ -95,6 +96,7 @@ export class RuntimeProjectHostSetupController {
|
||||
if (!result) {
|
||||
throw new Error(`Project not found: ${args.projectId}`)
|
||||
}
|
||||
this.deps.notifyReposChanged()
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -158,6 +160,11 @@ export class RuntimeProjectHostSetupController {
|
||||
void prepareLocalWorktreeRootForRepo(store, result.repo)
|
||||
invalidateAuthorizedRootsCache()
|
||||
}
|
||||
if (result.repo && ('path' in args.updates || 'kind' in args.updates)) {
|
||||
this.retireRepoCatalog(result.repo.id)
|
||||
}
|
||||
this.deps.invalidateResolvedWorktrees()
|
||||
this.deps.notifyReposChanged()
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -173,9 +180,20 @@ export class RuntimeProjectHostSetupController {
|
||||
if (!result) {
|
||||
throw new Error(`Project host setup not found: ${args.setupId}`)
|
||||
}
|
||||
if (result.repo) {
|
||||
this.retireRepoCatalog(result.repo.id)
|
||||
}
|
||||
this.deps.invalidateResolvedWorktrees()
|
||||
this.deps.notifyReposChanged()
|
||||
return result
|
||||
}
|
||||
|
||||
// Why the shared invalidators: desktop and runtime scan caches key on repo id, not path or kind.
|
||||
private retireRepoCatalog(repoId: string): void {
|
||||
runWorktreeChangeInvalidators(repoId)
|
||||
invalidateAuthorizedRootsCache()
|
||||
}
|
||||
|
||||
// Why only for an explicit `force: false`: older clients omit it and must keep their delete.
|
||||
private assertSetupRemovalIsUnused(store: RuntimeStore, setupId: string): void {
|
||||
const setup = this.listSetups().find((entry) => entry.id === setupId)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Runtime RPC setup edits used to change the store silently: open clients kept the old catalog
|
||||
// and cached worktree scans kept answering for the old path or kind (#26562).
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { RuntimeProjectHostSetupController } from './runtime-project-host-setup-controller'
|
||||
import { registerWorktreeChangeInvalidator } from '../ipc/worktree-change-invalidators'
|
||||
import { projectHostSetupProjectionFromRepos } from '../../shared/project-host-setup-projection'
|
||||
import type { Repo } from '../../shared/repo-types'
|
||||
import type { RuntimeStore } from './runtime-store-contract'
|
||||
|
||||
const repo: Repo = {
|
||||
id: 'repo-1',
|
||||
path: '/srv/app',
|
||||
displayName: 'app',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1,
|
||||
kind: 'git'
|
||||
}
|
||||
const projection = projectHostSetupProjectionFromRepos([repo])
|
||||
const project = projection.projects[0]
|
||||
const setup = projection.setups[0]
|
||||
|
||||
let unregister: (() => void) | undefined
|
||||
afterEach(() => unregister?.())
|
||||
|
||||
function makeController(): {
|
||||
controller: RuntimeProjectHostSetupController
|
||||
notifyReposChanged: ReturnType<typeof vi.fn>
|
||||
invalidateResolvedWorktrees: ReturnType<typeof vi.fn>
|
||||
retired: string[]
|
||||
} {
|
||||
const store = {
|
||||
createProjectHostSetup: () => ({ project, setup }),
|
||||
updateProjectHostSetup: () => ({ project, setup, repo }),
|
||||
deleteProjectHostSetup: () => ({ project, setup, repo }),
|
||||
getProjectHostSetups: () => [setup]
|
||||
}
|
||||
const retired: string[] = []
|
||||
unregister = registerWorktreeChangeInvalidator((repoId) => retired.push(repoId))
|
||||
const notifyReposChanged = vi.fn()
|
||||
const invalidateResolvedWorktrees = vi.fn()
|
||||
const controller = new RuntimeProjectHostSetupController({
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the controller only calls the setup methods stubbed above.
|
||||
getStore: () => store as unknown as RuntimeStore,
|
||||
listRepos: () => [repo],
|
||||
addRepo: vi.fn(),
|
||||
addRemoteRepo: vi.fn(),
|
||||
cloneRepo: vi.fn(),
|
||||
invalidateResolvedWorktrees,
|
||||
invalidateWorktreeScan: vi.fn(),
|
||||
notifyReposChanged,
|
||||
countLiveTerminalsForRepo: () => 0
|
||||
})
|
||||
return { controller, notifyReposChanged, invalidateResolvedWorktrees, retired }
|
||||
}
|
||||
|
||||
describe('RuntimeProjectHostSetupController publication', () => {
|
||||
it('publishes a created setup to open clients', () => {
|
||||
const { controller, notifyReposChanged } = makeController()
|
||||
controller.createSetup({ projectId: project.id, hostId: 'local', path: '/srv/app' })
|
||||
expect(notifyReposChanged).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('publishes a rename without retiring cached worktree scans', () => {
|
||||
const { controller, notifyReposChanged, invalidateResolvedWorktrees, retired } =
|
||||
makeController()
|
||||
controller.updateSetup({ setupId: setup.id, updates: { displayName: 'renamed' } })
|
||||
expect(notifyReposChanged).toHaveBeenCalledTimes(1)
|
||||
expect(invalidateResolvedWorktrees).toHaveBeenCalled()
|
||||
expect(retired).toEqual([])
|
||||
})
|
||||
|
||||
it.each([{ kind: 'folder' as const }, { path: '/srv/moved' }])(
|
||||
'retires cached worktree scans when a setup changes %o',
|
||||
(updates) => {
|
||||
const { controller, notifyReposChanged, retired } = makeController()
|
||||
controller.updateSetup({ setupId: setup.id, updates })
|
||||
expect(retired).toEqual(['repo-1'])
|
||||
expect(notifyReposChanged).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
)
|
||||
|
||||
it('retires cached worktree scans and publishes a deleted setup', () => {
|
||||
const { controller, notifyReposChanged, retired } = makeController()
|
||||
controller.deleteSetup({ setupId: setup.id })
|
||||
expect(retired).toEqual(['repo-1'])
|
||||
expect(notifyReposChanged).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user