Files
orca/src/shared/execution-host.test.ts
T
Neil b241a68ae4 Fix worktree identity collisions across hosts (#16691)
* fix(workspaces): add collision-safe worktree identity

* fix(workspaces): read worktree metadata per host and repair ambiguous identities

The canonical identity store landed write-only: getWorktreeMetaForHost had no
production callers while setWorktreeMetaForHost kept the legacy projection only
for the first known owner, so a second host's edits persisted and were never
read back. Wire the listing paths through host-qualified reads.

An ambiguous alias was also unrecoverable — reads returned undefined and writes
threw forever, and the throw escaped the detected-worktree loop, emptying the
whole repo's sidebar. Fail open onto the most recently active instance instead.

- collapse ambiguous aliases deterministically and persist the repair
- reclaim identity rows in the metadata GC so they cannot outlive their locator
  or resurrect onto a worktree recreated at the same path
- drop every host's rows when a locator is removed outright, not just the owner's
- honour an explicit instanceId so the stale-lineage rotation guard still works
- scope a rename to the moving host; other hosts keep their own locator
- prefer the project host setup matching the repo's own execution host, so a
  repoId registered on two hosts no longer stamps the wrong one durably
- reject an unencoded `|` in a host id, the invariant the alias delimiter needs
- drop the never-populated hostGeneration from the canonical key

* fix(workspaces): close remaining identity review gaps

* fix(workspaces): close remaining review gaps

* fix(workspaces): address review and CI regressions

* test(workspaces): update host-qualified metadata expectations

* fix(workspaces): preserve ambiguous identity records

* fix(workspaces): snapshot metadata during listing

* test(workspaces): mirror listing metadata snapshot in windows fixture

* fix(workspaces): preserve identity routing for metadata writes

* fix(workspaces): scope stale metadata cleanup by host

* fix(workspaces): rekey identities on SSH readoption

* fix(workspaces): fail closed for ambiguous board ids

* perf(workspaces): snapshot metadata across catalog listing

* fix(workspaces): retain neighboring manual order updates

* test(workspaces): cover ambiguous board id index

* fix(persistence): harden host-qualified worktree metadata

* refactor(shared): split project host setup lookup

* refactor(workspaces): simplify host-qualified metadata
2026-08-27 15:08:40 -07:00

136 lines
5.7 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
import {
ALL_EXECUTION_HOSTS_SCOPE,
LOCAL_EXECUTION_HOST_ID,
getLocalExecutionHostLabel,
getRepoExecutionHostId,
getSettingsFocusedExecutionHostId,
getWorktreeExecutionHostId,
normalizeExecutionHostOrder,
normalizeExecutionHostScope,
normalizeVisibleExecutionHostIds,
parseExecutionHostId,
requestedExecutionHostScope,
toRuntimeExecutionHostId,
toSshExecutionHostId
} from './execution-host'
describe('execution host identity', () => {
// Why: the navigator cases below replace globalThis.navigator; restore it after
// each test so the stub can't bleed into the rest of the suite.
afterEach(() => {
vi.unstubAllGlobals()
})
it('normalizes local, SSH, and runtime host ids', () => {
expect(parseExecutionHostId('local')).toEqual({ kind: 'local', id: 'local' })
expect(parseExecutionHostId(toSshExecutionHostId('win vm'))).toEqual({
kind: 'ssh',
id: 'ssh:win%20vm',
targetId: 'win vm'
})
expect(parseExecutionHostId(toRuntimeExecutionHostId('prod/server'))).toEqual({
kind: 'runtime',
id: 'runtime:prod%2Fserver',
environmentId: 'prod/server'
})
})
it('labels the local host by platform and by navigator detection', () => {
expect(getLocalExecutionHostLabel('darwin')).toBe('Local Mac')
expect(getLocalExecutionHostLabel('win32')).toBe('Local Windows')
expect(getLocalExecutionHostLabel('linux')).toBe('Local Linux')
expect(getLocalExecutionHostLabel('freebsd')).toBe('This computer')
// With no explicit platform, the label is derived from navigator.userAgent
// (the path the live host-selector dialog uses).
vi.stubGlobal('navigator', { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' })
expect(getLocalExecutionHostLabel()).toBe('Local Windows')
vi.stubGlobal('navigator', { userAgent: 'Mozilla/5.0 (X11; Linux x86_64)' })
expect(getLocalExecutionHostLabel()).toBe('Local Linux')
vi.stubGlobal('navigator', { userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)' })
expect(getLocalExecutionHostLabel()).toBe('Local Mac')
// Non-matching userAgent falls through to process.platform; compare against the
// explicit-platform label so the assertion is deterministic on any CI OS.
vi.stubGlobal('navigator', { userAgent: 'totally-unknown-agent' })
expect(getLocalExecutionHostLabel()).toBe(getLocalExecutionHostLabel(process.platform))
})
it('falls back invalid scopes to all hosts', () => {
expect(normalizeExecutionHostScope(null)).toBe(ALL_EXECUTION_HOSTS_SCOPE)
expect(normalizeExecutionHostScope('')).toBe(ALL_EXECUTION_HOSTS_SCOPE)
expect(normalizeExecutionHostScope('bogus')).toBe(ALL_EXECUTION_HOSTS_SCOPE)
expect(normalizeExecutionHostScope('ssh:')).toBe(ALL_EXECUTION_HOSTS_SCOPE)
expect(normalizeExecutionHostScope('all')).toBe(ALL_EXECUTION_HOSTS_SCOPE)
})
it('defaults an omitted scope request to this host, not a fan-out', () => {
expect(requestedExecutionHostScope(undefined)).toBe(LOCAL_EXECUTION_HOST_ID)
expect(requestedExecutionHostScope(null)).toBe(LOCAL_EXECUTION_HOST_ID)
// An empty or unrecognized scope is a real value, so it still fans out.
expect(requestedExecutionHostScope('')).toBe(ALL_EXECUTION_HOSTS_SCOPE)
expect(requestedExecutionHostScope('bogus')).toBe(ALL_EXECUTION_HOSTS_SCOPE)
expect(requestedExecutionHostScope('all')).toBe(ALL_EXECUTION_HOSTS_SCOPE)
expect(requestedExecutionHostScope('ssh:dev%20box')).toBe('ssh:dev%20box')
})
it('normalizes visible host id arrays', () => {
expect(normalizeVisibleExecutionHostIds(null)).toBeNull()
expect(normalizeVisibleExecutionHostIds([])).toBeNull()
expect(normalizeVisibleExecutionHostIds(['local', 'bogus', 'ssh:win%20vm', 'local'])).toEqual([
'local',
'ssh:win%20vm'
])
})
it('normalizes host order arrays', () => {
expect(normalizeExecutionHostOrder(null)).toEqual([])
expect(normalizeExecutionHostOrder([])).toEqual([])
expect(normalizeExecutionHostOrder(['ssh:win%20vm', 'bogus', 'local', 'ssh:win%20vm'])).toEqual(
['ssh:win%20vm', 'local']
)
})
it('derives repo ownership from SSH connection ids', () => {
expect(getRepoExecutionHostId({ connectionId: null })).toBe(LOCAL_EXECUTION_HOST_ID)
expect(getRepoExecutionHostId({ connectionId: 'ssh-target-1' })).toBe('ssh:ssh-target-1')
})
it('prefers explicit worktree ownership before repo and focused-host fallbacks', () => {
expect(
getWorktreeExecutionHostId(
{ hostId: 'runtime:workspace-owner' },
{ connectionId: 'repo-owner' },
'runtime:focused-host'
)
).toBe('runtime:workspace-owner')
expect(
getWorktreeExecutionHostId({}, { connectionId: 'repo-owner' }, 'runtime:focused-host')
).toBe('ssh:repo-owner')
expect(getWorktreeExecutionHostId({}, {}, 'runtime:focused-host')).toBe('runtime:focused-host')
})
it('derives focused host compatibility from active runtime settings', () => {
expect(getSettingsFocusedExecutionHostId(null)).toBe(LOCAL_EXECUTION_HOST_ID)
expect(getSettingsFocusedExecutionHostId({ activeRuntimeEnvironmentId: 'runtime-1' })).toBe(
'runtime:runtime-1'
)
})
})
describe('execution host id delimiter invariant', () => {
it('rejects an unencoded pipe so a crafted id cannot rebind a worktree identity alias', () => {
// composeWorktreeHostIdentity splits at the first `|`, so `ssh:a|b` would resolve as `ssh:a`.
expect(parseExecutionHostId('ssh:a|b')).toBeNull()
expect(parseExecutionHostId('runtime:a|b')).toBeNull()
expect(parseExecutionHostId(toSshExecutionHostId('a|b'))).toEqual({
kind: 'ssh',
id: 'ssh:a%7Cb',
targetId: 'a|b'
})
})
})