mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 16:02:03 +00:00
* fix(repos): remove a paired computer's deleted projects from every connected device A project deleted on a paired Orca host stayed in every connected client's sidebar and could not be removed there. Two independent defects: 1. Host-local repo IPC mutations only sent `repos:changed` to the host's own renderer (src/main/ipc/repos.ts:2711). The runtime client-event stream was fed only by mutations arriving over runtime RPC, and clients refetch a remote catalog only on a `reposChanged` event -- there is no polling on desktop -- so the deleted rows persisted indefinitely. The shared `notifyReposChanged` helper now also calls the new `OrcaRuntimeService.notifyReposChangedForRemoteClients()` (src/main/runtime/orca-runtime.ts:5175), mirroring the existing `notifyWorktreesChangedForRemoteClients` precedent. This covers every repo, project-group and folder-workspace IPC mutation, so renames, colors, reorders and adds propagate too. 2. Deleting the ghost row on the client routed `repo.rm` to the owner, which answered `repo_not_found`. `removeProject` wrapped its whole body in one try/catch, so the rejection aborted the local purge before the `set()` (src/renderer/src/store/slices/repos.ts:3466) and the delete button appeared to do nothing. Only `repo_not_found` is now tolerated; any other failure still keeps the row, and an opt-in `errorFeedback: 'toast'` makes it visible at the three single-project user-initiated entry points. Bulk and background callers keep today's silence plus their own aggregate reporting. Closes #11994 Co-authored-by: Orca <help@stably.ai> * fix(repos): revert inert RepositoryPane removeProject arg The settings pane's only render site drops the argument; the toast is already delivered by removeSettingsProjectFromAllHosts. Co-authored-by: Orca <help@stably.ai> * fix(repos): scope duplicate-repo-id deletes to the owning execution host Cover the cross-host collisions #11994's broadcast now fans out to every paired device. Same-name projects on different hosts were already isolated (per-host UUIDs, host-scoped catalog merge and purge) and are pinned by regression tests. Two same-repo-id paths were not: `repo.rm` with a `path:`/`name:` selector and `deleteProjectHostSetup` both resolved one row and then deleted by bare id, taking the sibling host's registration with it. Co-authored-by: Orca <help@stably.ai> * test(mobile): align the poll-interval rationale with the new reposChanged emission Co-authored-by: Orca <help@stably.ai> * fix(repos): resolve deleteProjectHostSetup's repo row only on the setup's own host The sibling-host fallback could only ever pick a row on a host the caller did not name; with no exact match the setup is stale and the existing path already drops just the setup. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
92 lines
3.0 KiB
TypeScript
92 lines
3.0 KiB
TypeScript
/**
|
|
* The same repo id is allowed on two execution hosts (persistence.ts `removeProjectForHost`).
|
|
* `repo.rm` resolves a *row*, so the deletion it performs must be scoped to that row's host:
|
|
* `Store.removeProject` is id-only and would take the sibling host's registration with it.
|
|
* A `path:`/`name:` selector resolves unambiguously even when the id is duplicated, so this
|
|
* is reachable — and since #11994 every paired device now learns about it immediately.
|
|
*/
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import type { Repo } from '../../shared/types'
|
|
import { OrcaRuntimeService } from './orca-runtime'
|
|
|
|
function makeRepos(): Repo[] {
|
|
return [
|
|
{
|
|
id: 'dup',
|
|
path: '/laptop/dup',
|
|
displayName: 'Dup Local',
|
|
badgeColor: '#000',
|
|
addedAt: 1,
|
|
executionHostId: 'local'
|
|
} as Repo,
|
|
{
|
|
id: 'dup',
|
|
path: '/remote/dup',
|
|
displayName: 'Dup Remote',
|
|
badgeColor: '#000',
|
|
addedAt: 2,
|
|
connectionId: 'ssh-1'
|
|
} as Repo
|
|
]
|
|
}
|
|
|
|
function createRuntime() {
|
|
const repos = makeRepos()
|
|
const removeProject = vi.fn((id: string) => {
|
|
for (let index = repos.length - 1; index >= 0; index -= 1) {
|
|
if (repos[index].id === id) {
|
|
repos.splice(index, 1)
|
|
}
|
|
}
|
|
})
|
|
const removeProjectForHost = vi.fn((id: string, hostId: string) => {
|
|
for (let index = repos.length - 1; index >= 0; index -= 1) {
|
|
const repo = repos[index]
|
|
const repoHostId =
|
|
repo.executionHostId ?? (repo.connectionId ? `ssh:${repo.connectionId}` : 'local')
|
|
if (repo.id === id && repoHostId === hostId) {
|
|
repos.splice(index, 1)
|
|
}
|
|
}
|
|
})
|
|
const runtime = new OrcaRuntimeService({
|
|
getRepos: () => [...repos],
|
|
getRepo: (id: string) => repos.find((repo) => repo.id === id) ?? null,
|
|
getAllWorktreeMeta: () => ({}),
|
|
getWorktreeMeta: () => null,
|
|
setWorktreeMeta: vi.fn(),
|
|
removeWorktreeMeta: vi.fn(),
|
|
getGitHubCache: () => null,
|
|
removeProject,
|
|
removeProjectForHost
|
|
} as never)
|
|
return { runtime, repos, removeProject, removeProjectForHost }
|
|
}
|
|
|
|
describe('repo.rm with the same repo id on two execution hosts', () => {
|
|
it('removes only the resolved row when a path selector picks the SSH host copy', async () => {
|
|
const { runtime, repos } = createRuntime()
|
|
|
|
await runtime.removeProject('path:/remote/dup')
|
|
|
|
expect(repos.map((repo) => repo.path)).toEqual(['/laptop/dup'])
|
|
})
|
|
|
|
it('removes only the resolved row when a name selector picks the local copy', async () => {
|
|
const { runtime, repos } = createRuntime()
|
|
|
|
await runtime.removeProject('name:Dup Local')
|
|
|
|
expect(repos.map((repo) => repo.path)).toEqual(['/remote/dup'])
|
|
})
|
|
|
|
it('refuses a bare duplicated id rather than guessing a host', async () => {
|
|
const { runtime, repos, removeProject, removeProjectForHost } = createRuntime()
|
|
|
|
await expect(runtime.removeProject('dup')).rejects.toThrow('selector_ambiguous')
|
|
expect(removeProject).not.toHaveBeenCalled()
|
|
expect(removeProjectForHost).not.toHaveBeenCalled()
|
|
expect(repos).toHaveLength(2)
|
|
})
|
|
})
|