Files
orca/src/main/runtime/runtime-remove-project-host-scope.spec.ts
T
Neil baf123eda8 fix(persistence): repo-row store writes take the row, not the id; ratchet the rest (R4-2a) (#30756)
* fix(persistence): repo-row store writes take the row, not the id; ratchet the rest (R4-2a)

* fix(persistence): host-filter the runtime nested import; widen the id-only write ratchet (review)

* fix(runtime): nested import keeps matching runtime-stamped native rows on a paired host

* fix(persistence): class-scope the ratchet param exemptions; update a new main test to the target shape

* fix(ipc): fence a project-group move by the resolved twin host, so an SSH twin is not WSL-fenced

* test(ci): pin the id-only store write ratchet step to the static phase
2026-10-19 10:14:07 -07:00

146 lines
5.7 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,
* never the sibling host's registration.
* 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/repo-types'
import { OrcaRuntimeService } from './orca-runtime'
import { findRepoRowForHostScopedWrite } from '../persistence/tracking-repos/repo-update-operations'
import { getRepoExecutionHostId } from '../../shared/execution-host'
import type { RepoHostTarget } from '../../shared/repo-host-identity'
export function registerRuntimeRemoveProjectHostScopeTests(): void {
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 removeProjectForHost = vi.fn((target: RepoHostTarget) => {
const id = target.id
const hostId = getRepoExecutionHostId(target)
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)
}
}
})
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the store stub covers every read and write repo.rm and repo.update drive.
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,
// Same row lookup the real store's host-scoped write uses.
updateRepo: (target: RepoHostTarget, updates: Partial<Repo>) => {
const row = findRepoRowForHostScopedWrite(repos, target.id, getRepoExecutionHostId(target))
return row ? Object.assign(row, updates) : null
},
moveProjectToGroup: (target: RepoHostTarget, groupId: string | null) => {
const row = findRepoRowForHostScopedWrite(repos, target.id, getRepoExecutionHostId(target))
return row ? Object.assign(row, { projectGroupId: groupId }) : null
},
removeProjectForHost
} as never)
return { runtime, repos, 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, removeProjectForHost } = createRuntime()
await expect(runtime.removeProject('dup')).rejects.toThrow('selector_ambiguous')
expect(removeProjectForHost).not.toHaveBeenCalled()
expect(repos).toHaveLength(2)
})
})
describe('projectGroup.moveProject with the same repo id on two execution hosts', () => {
it('moves the SSH row a path selector picked, not its local sibling', async () => {
const { runtime, repos } = createRuntime()
await runtime.moveProjectToGroup('path:/remote/dup', 'group-1')
expect(repos.map((repo) => repo.projectGroupId)).toEqual([undefined, 'group-1'])
})
it('moves the local row a name selector picked, not its SSH sibling', async () => {
const { runtime, repos } = createRuntime()
const moved = await runtime.moveProjectToGroup('name:Dup Local', 'group-1')
expect(moved).toMatchObject({ path: '/laptop/dup', projectGroupId: 'group-1' })
expect(repos.map((repo) => repo.projectGroupId)).toEqual(['group-1', undefined])
})
it('refuses a bare duplicated id rather than guessing a host', async () => {
const { runtime, repos } = createRuntime()
await expect(runtime.moveProjectToGroup('dup', 'group-1')).rejects.toThrow(
'selector_ambiguous'
)
expect(repos.map((repo) => repo.projectGroupId)).toEqual([undefined, undefined])
})
})
describe('repo.update with the same repo id on two execution hosts', () => {
it('writes the SSH row a path selector picked, not its local sibling', async () => {
const { runtime, repos } = createRuntime()
await runtime.updateRepo('path:/remote/dup', { displayName: 'Renamed Remote' })
expect(repos.map((repo) => repo.displayName)).toEqual(['Dup Local', 'Renamed Remote'])
})
it('sets the base ref on the SSH row a path selector picked', async () => {
const { runtime, repos } = createRuntime()
await runtime.setRepoBaseRef('path:/remote/dup', 'origin/dev')
expect(repos.map((repo) => repo.worktreeBaseRef)).toEqual([undefined, 'origin/dev'])
})
})
}