Files
orca/src/main/runtime/agent-detection-host-resolution.test.ts
T
Neil 121eb3bc84 fix(runtime): resolve folder workspaces wherever a command can be given one (#35223)
* fix(runtime): resolve workspaces, not only Git worktrees, wherever a folder id can arrive

The Git-only selector resolver never matches project-group folder
workspaces (no repo row), so worktree show/sleep, terminal list by
workspace key, the active-terminal fallback, scrollback reads, Kitty file
reads, Linear --current, browser commands, plugin context and the session
tab fallbacks failed with selector_not_found for them. Route those
callers through resolveWorkspaceSelector and rename the Git-only resolver
to resolveGitWorktreeSelector so it reads as the repo-backed catalog only.

Refs STA-12562

* test(runtime): follow the workspace resolver rename in host fakes

Refs STA-12562

* test(runtime): keep partial browser host fakes and mail-routing stubs on the renamed resolver

Refs STA-12562

* test(runtime): drop an accidental copy of the folder selector suite

Refs STA-12562

* fix(runtime): keep folder workspaces off the skill worktree fence and the Linear meta backfill

Skill installs fence a worktreeId by repo rows, so a folder id there must
come through folderWorkspaceId, where its own host fence applies. Linear
--current answers a folder's legacy link without writing a worktreeMeta
row keyed by the folder id.

Refs STA-12562
2026-10-28 10:52:37 -07:00

257 lines
9.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const electronMocks = vi.hoisted(() => {
const ipcMain = {
on: vi.fn(() => ipcMain),
removeListener: vi.fn(() => ipcMain),
emit: vi.fn(() => true)
}
return {
BrowserWindow: { fromId: vi.fn((): unknown => null) },
webContents: { fromId: vi.fn((): unknown => null) },
ipcMain,
app: { getPath: vi.fn(() => '/tmp'), isPackaged: false }
}
})
vi.mock('electron', () => electronMocks)
import { OrcaRuntimeService } from './orca-runtime'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants'
import type { Repo } from '../../shared/repo-types'
import type { Project } from '../../shared/project-types'
import type { ResolvedWorktree } from './runtime-worktree-path-identity'
import { partialRuntimeStore } from './runtime-store-partial-fixture'
const originalPlatform = process.platform
function makeRuntime(repos: Repo[], projects: Project[] = []) {
const runtime = new OrcaRuntimeService(
partialRuntimeStore({
getRepo: (id: string) => repos.find((repo) => repo.id === id),
getRepos: () => repos,
getProjects: () => projects,
getSettings: () => ({ localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' } })
})
)
const selector = vi.fn<(selector: string) => Promise<ResolvedWorktree>>()
runtime['resolveGitWorktreeSelector'] = selector
return { runtime, selector }
}
function worktree(fields: Partial<ResolvedWorktree>): ResolvedWorktree {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the method reads only repoId, path and hostId.
return fields as ResolvedWorktree
}
describe('OrcaRuntimeService.resolveAgentDetectionHost', () => {
beforeEach(() => {
Object.defineProperty(process, 'platform', { value: 'win32' })
})
afterEach(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform })
})
const local: Repo = {
id: 'r1',
path: 'C:\\src\\r1',
displayName: 'r1',
badgeColor: 'blue',
addedAt: 1,
executionHostId: 'local'
}
it('keeps the host default for no, floating, or unknown workspaces', async () => {
const { runtime, selector } = makeRuntime([local])
selector.mockRejectedValue(new Error('selector_not_found'))
expect(await runtime.resolveAgentDetectionHost(undefined)).toEqual({ kind: 'local' })
expect(await runtime.resolveAgentDetectionHost(FLOATING_TERMINAL_WORKTREE_ID)).toEqual({
kind: 'local'
})
expect(await runtime.resolveAgentDetectionHost('r1::C:\\gone')).toEqual({ kind: 'local' })
expect(selector).toHaveBeenCalledTimes(1)
})
it('refuses a workspace id two hosts share instead of probing this one', async () => {
const { runtime, selector } = makeRuntime([local])
selector.mockRejectedValue(new Error('selector_ambiguous'))
await expect(runtime.resolveAgentDetectionHost('r1::/srv/r1')).rejects.toThrow(
'selector_ambiguous'
)
})
it('resolves the selected repo before a workspace exists (#20060)', async () => {
const wslRepo: Repo = { ...local, path: '\\\\wsl.localhost\\Debian\\home\\neil\\repo' }
const { runtime, selector } = makeRuntime([wslRepo])
expect(await runtime.resolveAgentDetectionHost(undefined, local.id)).toEqual({
kind: 'local',
context: {
projectRuntime: {
status: 'resolved',
runtime: expect.objectContaining({ kind: 'wsl', distro: 'Debian' })
}
}
})
expect(selector).not.toHaveBeenCalled()
})
it('refuses a repository target without a store while retaining the untargeted default', async () => {
const runtime = new OrcaRuntimeService(null)
await expect(runtime.resolveAgentDetectionHost(undefined, local.id)).rejects.toThrow(
'repo_not_found'
)
expect(await runtime.resolveAgentDetectionHost(undefined)).toEqual({ kind: 'local' })
expect(
await runtime.resolveAgentDetectionHost(FLOATING_TERMINAL_WORKTREE_ID, local.id)
).toEqual({
kind: 'local'
})
})
it.each(['removed', 'path', 'owner', 'connection', 'duplicate'] as const)(
'refuses a selected repository %s change across the selector await',
async (change) => {
const selected = { ...local, path: '\\\\wsl$\\Old\\home\\repo' }
const repos: Repo[] = [selected]
const { runtime } = makeRuntime(repos)
const pending = runtime.resolveAgentDetectionHost(undefined, local.id)
repos.splice(
0,
1,
...(change === 'removed'
? []
: change === 'duplicate'
? [selected, { ...selected }]
: [
{
...selected,
...(change === 'path' ? { path: '\\\\wsl$\\Current\\home\\repo' } : {}),
...(change === 'owner' ? { executionHostId: 'ssh:build-host' as const } : {}),
...(change === 'connection' ? { connectionId: 'build-host' } : {})
}
])
)
await expect(pending).rejects.toThrow('repo_target_changed')
}
)
it('keeps repo detection on its configured runtime for a Windows path', async () => {
const { runtime } = makeRuntime([local])
expect(await runtime.resolveAgentDetectionHost(undefined, local.id)).toMatchObject({
kind: 'local',
context: { projectRuntime: { runtime: { kind: 'wsl', distro: 'Ubuntu' } } }
})
})
it.each(['darwin', 'linux', 'win32'])(
'matches existing workspace runtime resolution on %s',
async (platform) => {
Object.defineProperty(process, 'platform', { value: platform })
const selected = { ...local, path: platform === 'win32' ? local.path : '/src/r1' }
const { runtime, selector } = makeRuntime([selected])
selector.mockResolvedValue(
worktree({ repoId: selected.id, path: selected.path, hostId: 'local' })
)
const detected = await runtime.resolveAgentDetectionHost(undefined, selected.id)
expect(detected).toEqual(
await runtime.resolveAgentDetectionHost(`${selected.id}::${selected.path}`)
)
expect(detected).toMatchObject({
kind: 'local',
context: {
projectRuntime: { runtime: { kind: platform === 'win32' ? 'wsl' : 'local-host' } }
}
})
}
)
it('keeps project overrides above a selected folder repo WSL path', async () => {
const folderRepo: Repo = {
...local,
kind: 'folder',
path: '\\\\wsl$\\Debian\\home\\neil\\notes'
}
const project: Project = {
id: 'project-1',
displayName: 'Notes',
badgeColor: 'blue',
sourceRepoIds: [local.id],
localWindowsRuntimePreference: { kind: 'windows-host' },
createdAt: 1,
updatedAt: 1
}
const { runtime } = makeRuntime([folderRepo], [project])
expect(await runtime.resolveAgentDetectionHost(undefined, local.id)).toMatchObject({
kind: 'local',
context: { projectRuntime: { runtime: { kind: 'windows-host', reason: 'project-override' } } }
})
})
it.each([{ connectionId: 'build-host' }, { executionHostId: 'ssh:build-host' }] as const)(
'preserves selected repo SSH ownership: %j',
async (ownership) => {
const { runtime } = makeRuntime([
{ ...local, executionHostId: undefined, ...ownership, path: '/srv/r1' }
])
expect(await runtime.resolveAgentDetectionHost(undefined, local.id)).toEqual({
kind: 'ssh',
connectionId: 'build-host'
})
}
)
it('keeps an explicit workspace authoritative when a repo target is also sent', async () => {
const { runtime, selector } = makeRuntime([local])
selector.mockResolvedValue(worktree({ repoId: local.id, path: local.path, hostId: 'local' }))
const workspaceId = `${local.id}::${local.path}`
expect(await runtime.resolveAgentDetectionHost(workspaceId, 'missing')).toEqual(
await runtime.resolveAgentDetectionHost(workspaceId)
)
expect(
await runtime.resolveAgentDetectionHost(FLOATING_TERMINAL_WORKTREE_ID, 'missing')
).toEqual({ kind: 'local' })
})
it('refuses an unknown or ambiguous repo instead of probing the host default', async () => {
const { runtime } = makeRuntime([local, { ...local, executionHostId: 'runtime:other' }])
await expect(runtime.resolveAgentDetectionHost(undefined, local.id)).rejects.toThrow(
'selector_ambiguous'
)
await expect(runtime.resolveAgentDetectionHost(undefined, 'missing')).rejects.toThrow(
'repo_not_found'
)
})
it.each(['runtime:%broken', 'ssh:%broken'] as const)(
'refuses a selected repository with malformed owner %s',
async (executionHostId) => {
const { runtime } = makeRuntime([{ ...local, executionHostId }])
await expect(runtime.resolveAgentDetectionHost(undefined, local.id)).rejects.toThrow(
'worktree_execution_host_unresolved'
)
}
)
it('refuses a repo owned by another runtime', async () => {
const { runtime } = makeRuntime([{ ...local, executionHostId: 'runtime:other' }])
await expect(runtime.resolveAgentDetectionHost(undefined, local.id)).rejects.toThrow(
'workspace_on_other_runtime'
)
})
it("routes by the host's own worktree record", async () => {
const ssh: Repo = { ...local, path: '/srv/r1', executionHostId: 'ssh:build-host' }
const { runtime, selector } = makeRuntime([ssh, local])
selector.mockResolvedValueOnce(worktree({ repoId: 'r1', path: 'C:\\src\\r1', hostId: 'local' }))
expect(await runtime.resolveAgentDetectionHost('r1::C:\\src\\r1')).toEqual({
kind: 'local',
context: {
projectRuntime: {
status: 'resolved',
runtime: expect.objectContaining({ kind: 'wsl', distro: 'Ubuntu' })
}
}
})
expect(selector).toHaveBeenCalledWith('id:r1::C:\\src\\r1')
})
})