mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 19:17:28 +00:00
* fix(projects): refuse a folder-to-Git type change that would hide folder workspaces (STA-10638) A folder project's extra workspaces exist only as metadata rows that the folder listing shows. Changing the project to Git switches it to `git worktree list`, so those workspaces drop out of the next refresh and their saved tabs are purged as if they had been deleted. Only the automatic upgrade checked for this; `orca project setup-update --kind git`, repo updates and clone-into-folder did not. The check now lives in the one repo write every type change goes through, so any caller gets the same refusal and nothing is written. The automatic upgrade and both clone paths use the same check and keep the project as a folder project. * test(repos): give the repo handler store mock the metadata read the clone path now uses * fix(projects): a manual folder-to-Git change proves the folder is the Git root first (STA-10638) Only the automatic upgrade checked that a folder project's path is the top of a Git repository and recorded Git's spelling of that root. A manual change (orca project setup-update --kind git, repo updates, clone into a folder project) flipped the type without either, so a project inside another repo became a broken Git project, and a project whose path Git spells differently lost its main workspace's id and tabs on the next refresh. All of them now run the same proof and write the proven root with the type change. SSH projects keep the unverified change because their Git runs on a host this process can't probe. * Revalidate folder-to-Git conversion targets after Git proof * Explain when a project changes during its Git check * Fix clone conversion for replaced folder project * test(runtime): create real Git root for clone upgrade fixture * test: normalize runtime clone root stamp expectation * test: check clone registration identity without unknown access * chore: restore main's pnpm-lock.yaml that the main merge reverted
88 lines
3.6 KiB
TypeScript
88 lines
3.6 KiB
TypeScript
// 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(),
|
|
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', async () => {
|
|
const { controller, notifyReposChanged, invalidateResolvedWorktrees, retired } =
|
|
makeController()
|
|
await 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',
|
|
async (updates) => {
|
|
const { controller, notifyReposChanged, retired } = makeController()
|
|
await 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)
|
|
})
|
|
})
|