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
This commit is contained in:
Neil
2026-10-28 10:52:37 -07:00
committed by GitHub
parent e03057de40
commit 121eb3bc84
75 changed files with 356 additions and 139 deletions
+2 -2
View File
@@ -273,9 +273,9 @@ describe('launch facts outside the tables', () => {
const paneKey = makePaneKey(pane.tabId, pane.leafId)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: exposes the existing selector; empty-inventory Sleep needs only this local workspace identity.
const selecting = lanes.runtime as unknown as {
resolveWorktreeSelector: () => Promise<{ id: string }>
resolveGitWorktreeSelector: () => Promise<{ id: string }>
}
vi.spyOn(selecting, 'resolveWorktreeSelector').mockResolvedValue({ id: pane.worktreeId })
vi.spyOn(selecting, 'resolveGitWorktreeSelector').mockResolvedValue({ id: pane.worktreeId })
const events: string[] = []
const releases: ReturnType<typeof vi.fn<() => void>>[] = []
const finishes: ReturnType<typeof vi.fn<() => void>>[] = []
@@ -70,7 +70,7 @@ type SidecarRequest = { method: string; params: Record<string, unknown>; authTok
const host: RuntimeBrowserCommandHost = {
getAgentBrowserBridge: () => null,
// Distinct IDs prove registry fencing uses resolved workspaces, not caller selectors.
resolveWorktreeSelector: async (selector) => ({ id: `id-${selector}` }),
resolveWorkspaceSelector: async (selector) => ({ id: `id-${selector}` }),
resolveBrowserWorkspace: async (selector) => ({ id: `id-${selector}` }),
resolveBrowserNetworkExecutionHost: () => {
throw new Error('No browser network execution host')
@@ -448,7 +448,7 @@ describe('ElectronServeBrowserProcess command routing', () => {
it('scopes a folder workspace through the browser-workspace resolver', async () => {
const folderHost: RuntimeBrowserCommandHost = {
...host,
resolveWorktreeSelector: async () => {
resolveWorkspaceSelector: async () => {
throw new Error('selector_not_found')
},
resolveBrowserWorkspace: async () => ({ id: 'folder:docs' })
@@ -32,7 +32,7 @@ describe('ExternalChromiumBrowserProcess integration', () => {
}
const commands = provider.factory({
getAgentBrowserBridge: () => null,
resolveWorktreeSelector: async (selector) => ({ id: selector }),
resolveWorkspaceSelector: async (selector) => ({ id: selector }),
resolveBrowserWorkspace: async (selector) => ({ id: selector }),
resolveBrowserNetworkExecutionHost: () => {
throw new Error('No browser network execution host')
@@ -18,14 +18,14 @@ function fakeSession(): ExternalChromiumBrowserSession {
describe('external Chromium tab registry', () => {
it('scopes a folder workspace through the browser-workspace resolver', async () => {
const resolveWorktreeSelector = vi.fn(async () => {
const resolveWorkspaceSelector = vi.fn(async () => {
throw new Error('selector_not_found')
})
const host: Pick<
RuntimeBrowserCommandHost,
'resolveWorktreeSelector' | 'resolveBrowserWorkspace'
'resolveWorkspaceSelector' | 'resolveBrowserWorkspace'
> = {
resolveWorktreeSelector,
resolveWorkspaceSelector,
resolveBrowserWorkspace: async () => ({ id: 'folder:docs' })
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these tab paths read only the two selector resolvers and optional notifiers.
@@ -40,6 +40,6 @@ describe('external Chromium tab registry', () => {
.parse(await registry.listTabs(commandHost, { worktree: 'id:folder:docs' }))
expect(listed.tabs.map((tab) => tab.browserPageId)).toEqual([created.browserPageId])
expect(resolveWorktreeSelector).not.toHaveBeenCalled()
expect(resolveWorkspaceSelector).not.toHaveBeenCalled()
})
})
@@ -104,7 +104,7 @@ describe('ExternalChromiumBrowserProcess', () => {
await processHandle.start()
const commands = processHandle.createCommands({
getAgentBrowserBridge: () => null,
resolveWorktreeSelector: async (selector) => ({ id: selector }),
resolveWorkspaceSelector: async (selector) => ({ id: selector }),
resolveBrowserWorkspace: async (selector) => ({ id: selector }),
resolveBrowserNetworkExecutionHost: () => {
throw new Error('No browser network execution host')
+1 -1
View File
@@ -115,7 +115,7 @@ function closeHost(pages: RuntimeBrowserPageRegistry): RuntimeBrowserCommandHost
getRuntimeBrowserPageRegistry: () => pages,
getBrowserHostLeaseRegistry: () => ({ getPlacement: () => undefined }),
resolveBrowserWorkspace: async (selector: string) => ({ id: selector.replace(/^id:/, '') }),
resolveWorktreeSelector: async (selector: string) => ({ id: selector.replace(/^id:/, '') }),
resolveWorkspaceSelector: async (selector: string) => ({ id: selector.replace(/^id:/, '') }),
retireRuntimeOwnedBrowserSessionTab: vi.fn(() => true)
})
}
@@ -34,9 +34,7 @@ function makeRuntime(repos: Repo[], projects: Project[] = []) {
})
)
const selector = vi.fn<(selector: string) => Promise<ResolvedWorktree>>()
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: replaces the protected worktree lookup this method awaits; its signature is restated above.
;(runtime as unknown as { resolveWorktreeSelector: typeof selector }).resolveWorktreeSelector =
selector
runtime['resolveGitWorktreeSelector'] = selector
return { runtime, selector }
}
@@ -13,7 +13,7 @@ import { OrcaRuntimeService } from './orca-runtime'
const REMOTE_PATH = '/srv/app-feature'
type RuntimeInternals = {
resolveWorktreeSelector: (selector: string) => Promise<unknown>
resolveGitWorktreeSelector: (selector: string) => Promise<unknown>
buildStartupForAgent: (repo: unknown, agent: unknown, prompt: string) => unknown
createTerminal: (selector: string, opts: unknown) => Promise<unknown>
}
@@ -27,7 +27,7 @@ function makeRuntime(repos: readonly Record<string, unknown>[], hostId?: string)
}
const runtime = new OrcaRuntimeService(store as never)
const internals = runtime as unknown as RuntimeInternals
vi.spyOn(internals, 'resolveWorktreeSelector').mockResolvedValue({
vi.spyOn(internals, 'resolveGitWorktreeSelector').mockResolvedValue({
id: 'repo-shared::/srv/app-feature',
repoId: 'repo-shared',
path: REMOTE_PATH,
@@ -58,7 +58,7 @@ const storeBase = {
type RuntimeInternals = {
offscreenBrowserBackend: unknown
agentBrowserBridge: unknown
resolveWorktreeSelector: (selector: string) => Promise<{ id: string }>
resolveGitWorktreeSelector: (selector: string) => Promise<{ id: string }>
}
function makeSession(): WorkspaceSessionState {
@@ -95,7 +95,7 @@ function createPairedRuntime() {
})
const internals = runtime as unknown as RuntimeInternals
// Stub git selector resolution; this contract needs only the resolved workspace id.
internals.resolveWorktreeSelector = async () => ({ id: WT })
internals.resolveGitWorktreeSelector = async () => ({ id: WT })
internals.offscreenBrowserBackend = {
closeTab: vi.fn(),
createTab: vi.fn(async (options: { browserPageId?: string }) => ({
@@ -25,7 +25,7 @@ const FOLDER = 'folder:folder-1'
function createHost(overrides: Record<string, unknown>): RuntimeBrowserCommandHost {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: test host implements only the members browserTabCreate reads.
return {
resolveWorktreeSelector: vi.fn(async () => {
resolveWorkspaceSelector: vi.fn(async () => {
throw new Error('selector_not_found')
}),
resolveBrowserWorkspace: vi.fn(async () => ({ id: FOLDER })),
@@ -44,8 +44,9 @@ function createCommandHost(
overrides: Partial<RuntimeBrowserCommandHost>
): RuntimeBrowserCommandHost {
const runtimeBrowserPages = new RuntimeBrowserPageRegistry()
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial host fake; these tests reach only the methods it defines.
return {
resolveWorktreeSelector: async (selector: string) => ({ id: selector.replace(/^id:/, '') }),
resolveWorkspaceSelector: async (selector: string) => ({ id: selector.replace(/^id:/, '') }),
resolveBrowserWorkspace: async (selector: string) => ({ id: selector.replace(/^id:/, '') }),
getRuntimeBrowserPageRegistry: () => runtimeBrowserPages,
getAgentBrowserBridge: () =>
@@ -25,7 +25,7 @@ export type ResolvedBrowserPageWebContents = {
export type RuntimeBrowserCommandHost = {
getAgentBrowserBridge(): AgentBrowserBridge | null
resolveWorktreeSelector(selector: string): Promise<{
resolveWorkspaceSelector(selector: string): Promise<{
id: string
repoId?: string
hostId?: ExecutionHostId
@@ -18,7 +18,7 @@ export class RuntimeBrowserCommandTargets {
private readonly host: Pick<
RuntimeBrowserCommandHost,
| 'getAgentBrowserBridge'
| 'resolveWorktreeSelector'
| 'resolveWorkspaceSelector'
| 'getAuthoritativeWindow'
| 'retireRuntimeOwnedBrowserSessionTab'
| 'serveBrowserTabs'
@@ -81,7 +81,7 @@ export class RuntimeBrowserCommandTargets {
return undefined
}
const worktreeId = (await this.host.resolveWorktreeSelector(selector)).id
const worktreeId = (await this.host.resolveWorkspaceSelector(selector)).id
// Explicit selector failures must surface instead of widening scope; activation alone is best-effort.
const bridge = this.host.getAgentBrowserBridge()
if (bridge && !this.hasLiveRegisteredBrowserTab(bridge, worktreeId)) {
@@ -104,7 +104,7 @@ export class RuntimeBrowserCommandTargets {
}
const worktreeId = params.worktree
? (await this.host.resolveWorktreeSelector(params.worktree)).id
? (await this.host.resolveWorkspaceSelector(params.worktree)).id
: undefined
const bridge = this.host.getAgentBrowserBridge()
if (bridge && !this.hasLiveRegisteredBrowserPage(bridge, worktreeId, browserPageId)) {
@@ -22,7 +22,7 @@ export class RuntimeBrowserTabClose {
| 'retireRuntimeOwnedBrowserSessionTab'
| 'notifyHeadlessBrowserSessionTabsChanged'
| 'getAgentBrowserBridge'
| 'resolveWorktreeSelector'
| 'resolveWorkspaceSelector'
| 'getAvailableAuthoritativeWindow'
| 'getOffscreenBrowserBackend'
| 'getAuthoritativeWindow'
@@ -52,7 +52,7 @@ export class RuntimeBrowserTabClose {
namedPageId &&
params.worktree &&
this.targets.retireGhostBrowserSessionRow(
(await this.host.resolveWorktreeSelector(params.worktree)).id,
(await this.host.resolveWorkspaceSelector(params.worktree)).id,
namedPageId
)
) {
@@ -62,7 +62,7 @@ export class RuntimeBrowserTabClose {
}
const worktreeId = explicitPage
? params.worktree
? (await this.host.resolveWorktreeSelector(params.worktree)).id
? (await this.host.resolveWorkspaceSelector(params.worktree)).id
: undefined
: await this.targets.resolveBrowserWorktreeId(params.worktree)
@@ -267,7 +267,7 @@ export abstract class RuntimeConstructionServices {
protected abstract getTrackedDisplayTitleForPty(ptyId: string): string | null
protected abstract getLeavesForPty(ptyId: string): RuntimeLeafRecord[]
abstract notifyMessageArrived(handle: string, messageType?: string): void
protected abstract resolveWorktreeSelector(selector: string): Promise<ResolvedWorktree>
protected abstract resolveGitWorktreeSelector(selector: string): Promise<ResolvedWorktree>
protected abstract resolveWorkspaceSelector(
selector: string,
options?: { requireUsableFolderPath?: boolean }
@@ -85,14 +85,11 @@ export abstract class OrcaRuntimeWithFileCommands extends OrcaRuntimeWithPreserv
abstract requireStore(): RuntimeStore
protected abstract resolveWorktreeSelector(selector: string): Promise<ResolvedWorktree>
protected abstract resolveRepoSelector(selector: RuntimeRepoSelector): Promise<Repo>
protected readonly fileCommands = new RuntimeFileCommands({
getRuntimeId: () => this.runtimeId,
requireStore: () => this.requireStore(),
resolveWorktreeSelector: (selector) => this.resolveWorktreeSelector(selector),
resolveRuntimeFileTarget: async (selector) => {
// Scratch folders are file-only; the general resolver also places chats.
const scratchFolder = await resolveChatScratchSelectorFolder(selector)
@@ -222,7 +222,7 @@ export abstract class OrcaRuntimeWithPreservedBranchCleanup extends OrcaRuntimeW
const scope = await this.resolveTerminalWorkspaceLaunchScope(`id:${candidate.worktreeId}`)
const resolved = scope.folderWorkspace
? this.folderWorkspaceToResolvedWorktree(scope.folderWorkspace)
: await this.resolveWorktreeSelector(`id:${scope.id}`)
: await this.resolveGitWorktreeSelector(`id:${scope.id}`)
return { scope, resolved }
},
refreshInventory: (worktrees, connectionId) =>
@@ -107,7 +107,7 @@ export abstract class OrcaRuntimeWithStopRequestedPtyIds extends OrcaRuntimeWith
protected readonly worktreeLineage = new RuntimeWorktreeLineageController({
getStore: () => this.store,
getDb: () => this.getOrchestrationDbIfAvailable(),
resolveWorktree: (selector) => this.resolveWorktreeSelector(selector),
resolveWorktree: (selector) => this.resolveGitWorktreeSelector(selector),
listResolvedWorktrees: () => this.listResolvedWorktrees(),
showTerminal: (handle) => this.showTerminal(handle)
})
@@ -133,7 +133,7 @@ export abstract class OrcaRuntimeWithStopRequestedPtyIds extends OrcaRuntimeWith
getExplicitWorktreeId: (selector) => this.getExplicitSessionWorktreeIdSelector(selector),
getResolvedCache: () => this.resolvedWorktrees.peek(),
buildWorktreeFromId: (worktreeId) => this.buildResolvedWorktreeFromId(worktreeId),
resolveWorktree: (selector) => this.resolveWorktreeSelector(selector),
resolveWorktree: (selector) => this.resolveWorkspaceSelector(selector),
listKnownWorktrees: (worktreeId, target) =>
this.listKnownResolvedWorktreesForExplicitTarget(worktreeId, target),
getWorktreeMap: () => this.getResolvedWorktreeMap(),
@@ -7,7 +7,6 @@ import type { RuntimeFileOpenPosition } from '../../../shared/runtime-file-contr
export type RuntimeFileCommandHost = {
getRuntimeId(): string
requireStore(): RuntimeStore
resolveWorktreeSelector(selector: string): Promise<ResolvedRuntimeFileWorktree>
resolveRuntimeFileTarget(selector: string): Promise<ResolvedRuntimeFileTarget>
resolveKnownWorkspaceFileTarget?(
absolutePath: string,
@@ -12,7 +12,7 @@ export abstract class OrcaRuntimeWithLinearCommands extends OrcaRuntimeWithFileC
readonly linearCommands = new RuntimeLinearCommands({
runtimeAvailable: () => this.store !== null,
showTerminal: (handle) => this.showTerminal(handle),
resolveWorktreeSelector: (selector) => this.resolveWorktreeSelector(selector),
resolveWorkspaceSelector: (selector) => this.resolveWorkspaceSelector(selector),
listResolvedWorktrees: () => this.listResolvedWorktrees(),
setWorktreeMeta: (worktree, meta) => {
const target = worktreeRowTarget(this.store!, worktree)
@@ -16,7 +16,7 @@ function makeRuntime() {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the launch reads only these protected members; each is assigned or stubbed before launchAgentTerminal runs.
const internals = runtime as unknown as {
store: Record<string, unknown>
resolveWorktreeSelector: (selector: string) => Promise<unknown>
resolveGitWorktreeSelector: (selector: string) => Promise<unknown>
createTerminal: (selector: string, opts: unknown) => Promise<unknown>
sendStartupFollowupWhenReady: (handle: string, followup: unknown) => void
}
@@ -26,7 +26,7 @@ function makeRuntime() {
getRepos: () => [repo],
getRepo: (id: string) => (id === repo.id ? repo : undefined)
}
vi.spyOn(internals, 'resolveWorktreeSelector').mockResolvedValue({
vi.spyOn(internals, 'resolveGitWorktreeSelector').mockResolvedValue({
id: 'repo-1::/home/me/app-feature',
repoId: 'repo-1',
path: '/home/me/app-feature'
@@ -108,7 +108,7 @@ function commands() {
showTerminal: async () => {
throw new Error('No terminal in HTTP proof')
},
resolveWorktreeSelector: async () => {
resolveWorkspaceSelector: async () => {
throw new Error('No workspace resolution in HTTP proof')
},
listResolvedWorktrees: async () => [],
@@ -72,8 +72,9 @@ function createHost(overrides: Partial<RuntimeBrowserCommandHost> = {}): Runtime
]
}))
} as unknown as AgentBrowserBridge)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial host fake; these tests reach only the methods it defines.
return {
resolveWorktreeSelector: async (selector) => ({ id: selector.replace(/^id:/, '') }),
resolveWorkspaceSelector: async (selector) => ({ id: selector.replace(/^id:/, '') }),
resolveBrowserWorkspace: async (selector) => ({ id: selector.replace(/^id:/, '') }),
getRuntimeBrowserPageRegistry: () => runtimeBrowserPages,
getAuthoritativeWindow: vi.fn(),
@@ -131,7 +131,7 @@ describe('browserTabClose on a host with no browser engine', () => {
createHost({
retireRuntimeOwnedBrowserSessionTab,
// Folder workspaces bypass git selectors; resolve them through browser workspace routing.
resolveWorktreeSelector: async () => {
resolveWorkspaceSelector: async () => {
throw new Error('selector_not_found')
}
})
@@ -192,8 +192,9 @@ function createHost(overrides: Partial<RuntimeBrowserCommandHost> = {}): Runtime
getActiveWebContentsId: vi.fn(() => 100),
tabList: vi.fn(() => ({ tabs: [] }))
} as unknown as AgentBrowserBridge)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial host fake; these tests reach only the methods it defines.
return {
resolveWorktreeSelector: async (selector: string) => ({ id: selector.replace(/^id:/, '') }),
resolveWorkspaceSelector: async (selector: string) => ({ id: selector.replace(/^id:/, '') }),
resolveBrowserWorkspace: async (selector: string) => ({ id: selector.replace(/^id:/, '') }),
getRuntimeBrowserPageRegistry: () => runtimeBrowserPages,
getBrowserHostLeaseRegistry: () => ({ getPlacement: () => undefined }),
@@ -67,8 +67,9 @@ function createHost(overrides: Partial<RuntimeBrowserCommandHost> = {}): Runtime
getActivePageId: vi.fn(() => 'page-1'),
tabList: vi.fn(() => ({ tabs: [] }))
} as unknown as AgentBrowserBridge)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial host fake; these tests reach only the methods it defines.
return {
resolveWorktreeSelector: async (selector) => ({ id: selector.replace(/^id:/, '') }),
resolveWorkspaceSelector: async (selector) => ({ id: selector.replace(/^id:/, '') }),
resolveBrowserWorkspace: async (selector) => ({ id: selector.replace(/^id:/, '') }),
getRuntimeBrowserPageRegistry: () => runtimeBrowserPages,
getAuthoritativeWindow: vi.fn(),
@@ -96,8 +96,9 @@ function createHost(overrides: Partial<RuntimeBrowserCommandHost> = {}): Runtime
]
}))
} as unknown as AgentBrowserBridge)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial host fake; these tests reach only the methods it defines.
return {
resolveWorktreeSelector: async (selector) => ({ id: selector.replace(/^id:/, '') }),
resolveWorkspaceSelector: async (selector) => ({ id: selector.replace(/^id:/, '') }),
resolveBrowserWorkspace: async (selector) => ({ id: selector.replace(/^id:/, '') }),
getRuntimeBrowserPageRegistry: () => runtimeBrowserPages,
getAuthoritativeWindow: vi.fn(),
@@ -15,7 +15,6 @@ const unexpectedHostRead = (): never => {
const host = {
getRuntimeId: () => 'artifact-facade',
requireStore: unexpectedHostRead,
resolveWorktreeSelector: async () => unexpectedHostRead(),
resolveRuntimeFileTarget: async () => unexpectedHostRead(),
resolveRuntimeGitTarget: async () => unexpectedHostRead(),
openFile: unexpectedHostRead,
@@ -62,7 +62,6 @@ export function createRuntimeFileCommands(options?: {
const commands = new RuntimeFileCommands({
getRuntimeId: () => 'runtime-1',
requireStore: () => store,
resolveWorktreeSelector: vi.fn(async () => worktree),
resolveRuntimeFileTarget:
options?.resolveRuntimeFileTarget ??
vi.fn(async () => ({
@@ -73,11 +73,6 @@ function createRuntimeFileCommands(rootPath: string) {
const commands = new RuntimeFileCommands({
getRuntimeId: () => 'runtime-1',
requireStore: () => store,
resolveWorktreeSelector: vi.fn(async () => ({
id: 'wt-1',
repoId: 'repo-1',
path: rootPath
})),
resolveRuntimeFileTarget: vi.fn(async () => ({
worktree: {
id: 'wt-1',
@@ -187,7 +187,7 @@ export abstract class OrcaRuntimeWithMigrationCatalog extends OrcaRuntimeWithRes
}
return readHostTerminalScrollback(
{
resolveWorktreeId: async (selector) => (await this.resolveWorktreeSelector(selector)).id,
resolveWorktreeId: async (selector) => (await this.resolveWorkspaceSelector(selector)).id,
getWorkspaceSession: (worktreeId) => this.workspaceSessions.get(worktreeId),
statSnapshotSize: (ref) => statSnapshotSize.call(store, ref),
readSnapshot: (ref) => readSnapshot.call(store, ref)
@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants'
import { FLOATING_TERMINAL_WORKTREE_ID, getDefaultWorkspaceSession } from '../../shared/constants'
import { OrcaRuntimeService } from './orca-runtime'
import { withDurableRuntimeStore } from './runtime-durable-store-fixture'
const { readFile } = vi.hoisted(() => ({ readFile: vi.fn() }))
vi.mock('./terminal-kitty-file-read', () => ({ readTerminalKittyFile: readFile }))
@@ -9,8 +10,38 @@ const PTY = 'kitty-file-pty'
const LEAF = '11111111-1111-4111-8111-111111111111'
const WORKSPACE = 'folder:kitty-files'
// A project-group folder workspace: no repo row backs it, so a Git-only selector never finds it.
function folderWorkspaceStore() {
const folder = {
id: 'kitty-files',
projectGroupId: 'group-1',
name: 'kitty',
folderPath: '/workspace',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 1,
createdAt: 1,
updatedAt: 1
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: fixture supplies only the store reads workspace resolution makes.
return withDurableRuntimeStore({
getRepos: () => [],
getRepo: () => undefined,
getFolderWorkspaces: () => [folder],
getAllWorktreeMeta: () => ({}),
getWorktreeMeta: () => undefined,
getWorkspaceSession: () => getDefaultWorkspaceSession(),
setWorkspaceSession: () => {},
flushOrThrow: () => {}
}) as never
}
function fixture(ptyId = PTY, connectionId: string | null = null, worktreeId = WORKSPACE) {
const runtime = new OrcaRuntimeService(null)
const runtime = new OrcaRuntimeService(folderWorkspaceStore())
runtime.attachWindow(1)
const handle = runtime.preAllocateHandleForPty(ptyId)
runtime.registerPty(ptyId, worktreeId, connectionId, {
@@ -22,7 +53,6 @@ function fixture(ptyId = PTY, connectionId: string | null = null, worktreeId = W
tabs: [{ tabId: 'file-tab', worktreeId, title: '', activeLeafId: LEAF, layout: null }],
leaves: [{ tabId: 'file-tab', worktreeId, leafId: LEAF, paneRuntimeId: 1, ptyId }]
})
runtime['resolveWorktreeSelector'] = vi.fn().mockResolvedValue({ path: '/workspace' })
return { runtime, handle }
}
@@ -39,7 +69,6 @@ describe('Kitty file execution-host ownership', () => {
expect(await runtime.readKittyFile(handle, request, controller.signal)).toEqual({
base64: 'aW1hZ2U='
})
expect(runtime['resolveWorktreeSelector']).toHaveBeenCalledWith(`id:${WORKSPACE}`)
expect(readFile).toHaveBeenCalledWith(
request,
{ workspacePath: '/workspace', wslDistro: 'Ubuntu' },
@@ -51,7 +80,7 @@ describe('Kitty file execution-host ownership', () => {
'reads floating-terminal temporary files in namespace %s',
async (distro) => {
const { runtime, handle } = fixture(PTY, null, FLOATING_TERMINAL_WORKTREE_ID)
runtime['resolveWorktreeSelector'] = vi
runtime['resolveGitWorktreeSelector'] = vi
.fn()
.mockRejectedValue(new Error('selector_not_found'))
if (distro) {
@@ -60,7 +89,7 @@ describe('Kitty file execution-host ownership', () => {
expect(await runtime.readKittyFile(handle, { path: '/tmp/image' })).toEqual({
base64: 'aW1hZ2U='
})
expect(runtime['resolveWorktreeSelector']).not.toHaveBeenCalled()
expect(runtime['resolveGitWorktreeSelector']).not.toHaveBeenCalled()
expect(readFile).toHaveBeenCalledWith(
{ path: '/tmp/image' },
{ workspacePath: null, wslDistro: distro },
@@ -70,8 +99,7 @@ describe('Kitty file execution-host ownership', () => {
)
it('does not treat an unresolved project as a floating terminal', async () => {
const { runtime, handle } = fixture()
runtime['resolveWorktreeSelector'] = vi.fn().mockRejectedValue(new Error('selector_not_found'))
const { runtime, handle } = fixture(PTY, null, 'folder:deleted-folder')
await expect(runtime.readKittyFile(handle, { path: '/tmp/image' })).rejects.toThrow(
'selector_not_found'
)
@@ -29,7 +29,7 @@ export abstract class OrcaRuntimeWithReadKittyFile extends RuntimeWorkspaceSelec
}
const workspacePath = isFloatingWorkspaceId(pty.worktreeId)
? null
: (await this.resolveWorktreeSelector(`id:${pty.worktreeId}`)).path
: (await this.resolveWorkspaceSelector(`id:${pty.worktreeId}`)).path
const result = await readTerminalKittyFile(
request,
{
@@ -590,7 +590,7 @@ describe('launch runtime dispatch and allocation phases', () => {
Object.defineProperty(runtime, 'store', {
value: { getRepos: () => [repo], getRepo: () => repo }
})
Object.defineProperty(runtime, 'resolveWorktreeSelector', {
Object.defineProperty(runtime, 'resolveGitWorktreeSelector', {
value: async () => ({ id: 'repo::workspace', repoId: 'repo', path: '/workspace' })
})
const events: string[] = []
@@ -0,0 +1,194 @@
import { tmpdir } from 'node:os'
import { beforeAll, describe, expect, it, vi } from 'vitest'
import { withDurableRuntimeStore } from './runtime-durable-store-fixture'
import { getDefaultWorkspaceSession } from '../../shared/constants'
import { folderWorkspaceKey } from '../../shared/workspace-scope'
import { OrcaRuntimeService } from './orca-runtime'
import { setRuntimeBrowserCommandsFactory } from './runtime-browser-commands-factory'
// STA-12562: project-group folder workspaces have no repo row, so a Git-only selector never finds them.
const FOLDER_ID = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb'
const WORKSPACE_ID = folderWorkspaceKey(FOLDER_ID)
// Browser workspaces require the folder on disk.
const FOLDER_PATH = tmpdir()
const connectedLinearWorkspaces = vi.hoisted(() => {
const current: { id: string; organizationUrlKey: string }[] = []
return { current }
})
vi.mock('../linear/issue-context/client', async (importOriginal) => ({
...(await importOriginal<object>()),
getConnectedWorkspaces: () => connectedLinearWorkspaces.current
}))
function createRuntime(
activeWorktreeId: string | null = null,
linkedTask: Record<string, unknown> | null = null,
setWorktreeMeta: (...args: unknown[]) => unknown = () => ({})
) {
const folder = {
id: FOLDER_ID,
projectGroupId: 'group-1',
name: 'home',
folderPath: FOLDER_PATH,
linkedTask,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 1,
createdAt: 1,
updatedAt: 1
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: fixture supplies only the store reads these commands make.
const store = withDurableRuntimeStore({
getRepos: () => [],
getRepo: () => undefined,
getProjectGroups: () => [
{ id: 'group-1', name: 'Home', parentPath: FOLDER_PATH, connectionId: null }
],
getFolderWorkspaces: () => [folder],
getAllWorktreeMeta: () => ({}),
getWorktreeMeta: () => undefined,
setWorktreeMeta,
getWorkspaceSession: () => ({ ...getDefaultWorkspaceSession(), activeWorktreeId }),
setWorkspaceSession: () => {},
readTerminalScrollbackSnapshotAsync: async () => null,
statTerminalScrollbackSnapshotSize: async () => null,
flushOrThrow: () => {}
}) as never
const runtime = new OrcaRuntimeService(store)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fake implements only the controller calls these commands make.
runtime.setPtyController({
write: () => true,
kill: () => true,
stopAndWait: async () => true,
getForegroundProcess: async () => null,
listProcesses: async () => []
} as never)
return runtime
}
describe('workspace commands on a project-group folder workspace', () => {
it('shows it (orca worktree show)', async () => {
const shown = await createRuntime().showManagedWorktree(`id:${WORKSPACE_ID}`)
expect(shown.id).toBe(WORKSPACE_ID)
expect(shown.path).toBe(FOLDER_PATH)
})
it('sleeps it (worktree.sleep)', async () => {
const result = await createRuntime().sleepManagedWorktree(`id:${WORKSPACE_ID}`)
expect(result.worktreeId).toBe(WORKSPACE_ID)
})
it('lists its terminals by its bare workspace key', async () => {
const listed = await createRuntime().listTerminals(WORKSPACE_ID)
expect(listed.terminals).toEqual([])
})
it('resolves its active terminal instead of rejecting the workspace', async () => {
await expect(createRuntime().resolveActiveTerminal(`id:${WORKSPACE_ID}`)).rejects.toThrow(
'no_active_terminal'
)
})
it('reads its stored scrollback instead of rejecting the workspace', async () => {
await expect(
createRuntime().readTerminalScrollbackForClient({
worktree: `id:${WORKSPACE_ID}`,
ref: `v1-${'0'.repeat(32)}`
})
).rejects.toThrow('terminal_scrollback_not_found')
})
it('gives plugins its context while it is the active workspace', async () => {
const context = await createRuntime(WORKSPACE_ID).resolveActiveWorktreeContext()
expect(context).toMatchObject({ worktreeId: WORKSPACE_ID, path: FOLDER_PATH })
})
})
function runtimeWithFolderTerminal(
linkedTask: Record<string, unknown>,
setWorktreeMeta?: (...args: unknown[]) => unknown
) {
const runtime = createRuntime(null, linkedTask, setWorktreeMeta)
const leafId = '22222222-2222-4222-8222-222222222222'
runtime.attachWindow(1)
const handle = runtime.preAllocateHandleForPty('folder-pty')
runtime.registerPty('folder-pty', WORKSPACE_ID, null, { tabId: 'tab-1', leafId })
runtime.syncWindowGraph(1, {
tabs: [
{ tabId: 'tab-1', worktreeId: WORKSPACE_ID, title: '', activeLeafId: leafId, layout: null }
],
leaves: [
{ tabId: 'tab-1', worktreeId: WORKSPACE_ID, leafId, paneRuntimeId: 1, ptyId: 'folder-pty' }
]
})
return { runtime, handle }
}
const LINKED_LINEAR_TASK = {
provider: 'linear',
type: 'issue',
number: 1,
title: 'Fix it',
url: 'https://linear.app/acme/issue/ENG-1',
linearIdentifier: 'ENG-1'
}
describe('Linear --current from a project-group folder workspace terminal', () => {
it('reads the folder workspace’s linked Linear issue', async () => {
const { runtime, handle } = runtimeWithFolderTerminal({
...LINKED_LINEAR_TASK,
linearWorkspaceId: 'linear-workspace-1'
})
const link = await runtime.linearCommands.linearResolveCurrentIssue({ terminalHandle: handle })
expect(link.identifier).toBe('ENG-1')
})
it('answers a legacy link without writing worktree metadata for the folder', async () => {
connectedLinearWorkspaces.current = [{ id: 'linear-workspace-1', organizationUrlKey: 'acme' }]
const setWorktreeMeta = vi.fn()
const { runtime, handle } = runtimeWithFolderTerminal(
{ ...LINKED_LINEAR_TASK, linearOrganizationUrlKey: 'acme' },
setWorktreeMeta
)
const link = await runtime.linearCommands.linearResolveCurrentIssue({ terminalHandle: handle })
expect(link.workspaceId).toBe('linear-workspace-1')
expect(setWorktreeMeta).not.toHaveBeenCalled()
connectedLinearWorkspaces.current = []
})
})
describe('browser commands on a project-group folder workspace', () => {
beforeAll(async () => {
// Install browser RPCs as Electron does; plain Node hosts leave them unavailable.
const { RuntimeBrowserCommands } = await import('./browser/browser-commands')
setRuntimeBrowserCommandsFactory((host) => new RuntimeBrowserCommands(host))
return () => setRuntimeBrowserCommandsFactory(null)
})
it('lists its browser tabs through the agent browser bridge', async () => {
const runtime = createRuntime()
const tabList = vi.fn(() => ({ tabs: [] }))
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: listing reads only these bridge methods.
runtime.setAgentBrowserBridge({
tabList,
getRegisteredTabs: () => new Map(),
getActivePageId: () => null
} as never)
await expect(runtime.browserTabList({ worktree: `id:${WORKSPACE_ID}` })).resolves.toEqual({
tabs: []
})
})
})
@@ -9,8 +9,8 @@ const FIRST = '10000000-0000-4000-8000-000000000003'
const NEXT = '10000000-0000-4000-8000-000000000004'
class ExitAuthorityRuntime extends OrcaRuntimeService {
override resolveWorktreeSelector(selector: string) {
return super.resolveWorktreeSelector(selector)
override resolveGitWorktreeSelector(selector: string) {
return super.resolveGitWorktreeSelector(selector)
}
override getResolvedWorktreeMap() {
@@ -113,7 +113,7 @@ describe('host exit authority over queued renderer graphs', () => {
childWorktreeIds: [],
lineage: null
}
const resolve = vi.spyOn(runtime, 'resolveWorktreeSelector').mockResolvedValue(worktree)
const resolve = vi.spyOn(runtime, 'resolveGitWorktreeSelector').mockResolvedValue(worktree)
const map = vi
.spyOn(runtime, 'getResolvedWorktreeMap')
.mockResolvedValue(new Map([[WORKTREE, worktree]]))
@@ -39,7 +39,7 @@ class StartupRuntime extends OrcaRuntimeService {
return this.repoLookup(selector)
}
protected override resolveWorktreeSelector(selector: string): Promise<ResolvedWorktree> {
protected override resolveGitWorktreeSelector(selector: string): Promise<ResolvedWorktree> {
return this.lookup(selector)
}
}
@@ -285,7 +285,7 @@ describe('workspace activation and metadata reentry', () => {
super(store)
}
override async resolveWorktreeSelector(): Promise<ResolvedWorktree> {
override async resolveGitWorktreeSelector(): Promise<ResolvedWorktree> {
return this.selectedWorktree
}
@@ -25,10 +25,6 @@ import { OrcaRuntimeService } from './orca-runtime'
const REMOTE_PATH = '/srv/app-feature'
const WORKTREE_ID = 'repo-shared::/srv/app-feature'
type RuntimeInternals = {
resolveWorktreeSelector: (selector: string) => Promise<unknown>
}
function makeRuntime(repos: readonly Record<string, unknown>[], hostId?: string) {
const store = {
getSettings: () => ({
@@ -42,7 +38,7 @@ function makeRuntime(repos: readonly Record<string, unknown>[], hostId?: string)
getRepo: (id: string) => repos.find((repo) => repo.id === id)
}
const runtime = new OrcaRuntimeService(store as never)
vi.spyOn(runtime as unknown as RuntimeInternals, 'resolveWorktreeSelector').mockResolvedValue({
runtime['resolveGitWorktreeSelector'] = vi.fn().mockResolvedValue({
id: WORKTREE_ID,
repoId: 'repo-shared',
path: REMOTE_PATH,
@@ -24,10 +24,6 @@ import { OrcaRuntimeService } from './orca-runtime'
const REMOTE_PATH = '/srv/app-feature'
const WORKTREE_ID = 'repo-shared::/srv/app-feature'
type RuntimeInternals = {
resolveWorktreeSelector: (selector: string) => Promise<unknown>
}
function makeRuntime(repos: readonly Record<string, unknown>[], hostId?: string) {
const store = {
getSettings: () => ({ disabledTuiAgents: [], workspaceDir: '/tmp/workspaces' }),
@@ -37,7 +33,7 @@ function makeRuntime(repos: readonly Record<string, unknown>[], hostId?: string)
getRepo: (id: string) => repos.find((repo) => repo.id === id)
}
const runtime = new OrcaRuntimeService(store as never)
vi.spyOn(runtime as unknown as RuntimeInternals, 'resolveWorktreeSelector').mockResolvedValue({
runtime['resolveGitWorktreeSelector'] = vi.fn().mockResolvedValue({
id: WORKTREE_ID,
repoId: 'repo-shared',
path: REMOTE_PATH,
@@ -6,7 +6,7 @@ import { RuntimeLinearBrowseCommands } from './runtime-linear-browse-commands'
export type RuntimeLinearCommandPorts = {
runtimeAvailable: () => boolean
showTerminal: (handle: string) => Promise<RuntimeTerminalShow>
resolveWorktreeSelector: (selector: string) => Promise<ResolvedWorktree>
resolveWorkspaceSelector: (selector: string) => Promise<ResolvedWorktree>
listResolvedWorktrees: () => Promise<ResolvedWorktree[]>
setWorktreeMeta: (
worktree: Pick<ResolvedWorktree, 'id' | 'hostId'>,
@@ -32,8 +32,8 @@ export class RuntimeLinearCommandBase extends RuntimeLinearBrowseCommands {
return this.ports.showTerminal(handle)
}
protected resolveWorktreeSelector(selector: string): Promise<ResolvedWorktree> {
return this.ports.resolveWorktreeSelector(selector)
protected resolveWorkspaceSelector(selector: string): Promise<ResolvedWorktree> {
return this.ports.resolveWorkspaceSelector(selector)
}
protected listResolvedWorktrees(): Promise<ResolvedWorktree[]> {
@@ -89,7 +89,7 @@ function createCommands(emitClientEvent = vi.fn()): RuntimeLinearReadCommands {
showTerminal: async () => {
throw new Error('unused')
},
resolveWorktreeSelector: async () => {
resolveWorkspaceSelector: async () => {
throw new Error('unused')
},
listResolvedWorktrees: async () => [worktree],
@@ -16,7 +16,7 @@ function createCommands(): RuntimeLinearReadCommands {
showTerminal: async () => {
throw new Error('unused')
},
resolveWorktreeSelector: async () => {
resolveWorkspaceSelector: async () => {
throw new Error('unused')
},
listResolvedWorktrees: async () => [],
@@ -17,6 +17,7 @@ import type {
import type { ResolvedWorktree } from './runtime-worktree-path-identity'
import { RuntimeLinearDedupeCommands } from './runtime-linear-dedupe-commands'
import { isWslMoveWorktreeBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff'
import { parseWorkspaceKey } from '../../shared/workspace-scope'
export class RuntimeLinearTeamWriteCommands extends RuntimeLinearDedupeCommands {
public parseLinearAttachmentUrl(value: string): URL {
@@ -232,7 +233,7 @@ export class RuntimeLinearTeamWriteCommands extends RuntimeLinearDedupeCommands
'The provided Linear worktree context does not match the caller terminal.'
)
}
worktree = await this.resolveWorktreeSelector(`id:${terminal.worktreeId}`)
worktree = await this.resolveWorkspaceSelector(`id:${terminal.worktreeId}`)
} catch (error) {
if (error instanceof LinearAgentAccessError) {
throw error
@@ -270,8 +271,11 @@ export class RuntimeLinearTeamWriteCommands extends RuntimeLinearDedupeCommands
worktree.linkedLinearIssueOrganizationUrlKey
)
if (backfill?.workspaceId) {
// The answer still serves this lookup; persisting it waits out a WSL move.
if (!isWslMoveWorktreeBlocked(worktree.id, worktree.hostId, worktree.path)) {
// The answer still serves this lookup; persisting it waits out a WSL move. Folder workspaces keep links on their own row, not in worktree metadata.
if (
parseWorkspaceKey(worktree.id)?.type !== 'folder' &&
!isWslMoveWorktreeBlocked(worktree.id, worktree.hostId, worktree.path)
) {
this.setWorktreeMeta(worktree, {
linkedLinearIssueWorkspaceId: backfill.workspaceId,
linkedLinearIssueOrganizationUrlKey: backfill.organizationUrlKey ?? null
@@ -40,4 +40,14 @@ describe('createSkillInstallAuthority', () => {
await expect(authority.resolveWorktree(WORKTREE_ID)).resolves.toBeNull()
})
it('never resolves a folder workspace as a worktree, where only repo rows fence the host', async () => {
const authority = createSkillInstallAuthority(
createHost({
showManagedWorktree: async () => ({ id: 'folder:remote-folder', path: '/srv/remote' })
})
)
await expect(authority.resolveWorktree('folder:remote-folder')).resolves.toBeNull()
})
})
@@ -10,6 +10,7 @@ import {
type ExecutionHostId
} from '../../shared/execution-host'
import { getRepoIdFromWorktreeId } from '../../shared/worktree/id'
import { parseWorkspaceKey } from '../../shared/workspace-scope'
import type { SkillInstallDestinationAuthority } from '../skills/skill-install-destinations'
import {
resolveEnvironmentSkillProviderRoots,
@@ -110,6 +111,10 @@ export function createSkillInstallAuthority(
environmentId: host.getRuntimeId(),
homeDirectory: homedir(),
resolveWorktree: async (id) => {
// Folder workspaces arrive as folderWorkspaceId, whose own host fence applies; repo rows cannot fence them.
if (parseWorkspaceKey(id)?.type === 'folder') {
return null
}
const repos = host
.listRepos()
.filter((candidate) => candidate.id === getRepoIdFromWorktreeId(id))
@@ -166,7 +166,7 @@ export abstract class OrcaRuntimeWithTransitionGraphReloadToTerminalState extend
: this.resolveFolderWorkspaceSelector(selector)
return folderWorkspace
? this.folderWorkspaceToResolvedWorktree(folderWorkspace)
: this.resolveWorktreeSelector(selector)
: this.resolveGitWorktreeSelector(selector)
}
protected async resolveEmulatorWorkspaceId(selector: string): Promise<string> {
@@ -57,7 +57,7 @@ export abstract class OrcaRuntimeWithHasTerminalsForWorktree extends OrcaRuntime
async hasTerminalsForWorktree(worktreeSelector: string): Promise<boolean> {
const graphEpoch = this.captureReadyGraphEpoch()
const worktree = await this.resolveWorktreeSelector(worktreeSelector)
const worktree = await this.resolveWorkspaceSelector(worktreeSelector)
this.assertStableReadyGraph(graphEpoch)
for (const leaf of this.leaves.values()) {
if (leaf.worktreeId === worktree.id && leaf.ptyId) {
@@ -42,7 +42,7 @@ export abstract class OrcaRuntimeWithCloseMobileSessionTab extends OrcaRuntimeWi
const graphEpoch = options.clientNavigationId ? this.captureReadyGraphEpoch() : null
const explicitWorktreeId = this.getExplicitSessionWorktreeIdSelector(worktreeSelector)
const worktreeId =
explicitWorktreeId ?? (await this.resolveWorktreeSelector(worktreeSelector)).id
explicitWorktreeId ?? (await this.resolveWorkspaceSelector(worktreeSelector)).id
this.hydrateHeadlessMobileSessionTabsFromWorkspaceSession(worktreeId)
const observedPtyIds = await this.refreshMobileSessionPtyRecords()
if (graphEpoch !== null) {
@@ -130,7 +130,8 @@ export abstract class OrcaRuntimeWithPerformMobileSessionPtyRecordsRefresh exten
const navigation = opts.navigation ?? (opts.notifyClients === false ? 'caller' : 'all')
const targetsHost = navigationTargetsHost(navigation)
const explicitWorktreeId = this.getExplicitSessionWorktreeIdSelector(worktreeSelector)
let worktreeId = explicitWorktreeId ?? (await this.resolveWorktreeSelector(worktreeSelector)).id
let worktreeId =
explicitWorktreeId ?? (await this.resolveWorkspaceSelector(worktreeSelector)).id
this.hydrateHeadlessMobileSessionTabsFromWorkspaceSession(worktreeId)
await this.refreshMobileSessionPtyRecords(worktreeId)
// Why: the renderer can publish the host's spelling while inventory refresh is pending.
@@ -29,7 +29,7 @@ export abstract class OrcaRuntimeWithHeadlessPaneMoveToNewTab extends OrcaRuntim
): Promise<TerminalLeafMoveResult> {
const explicitWorktreeId = this.getExplicitSessionWorktreeIdSelector(worktreeSelector)
const worktreeId =
explicitWorktreeId ?? (await this.resolveWorktreeSelector(worktreeSelector)).id
explicitWorktreeId ?? (await this.resolveWorkspaceSelector(worktreeSelector)).id
if (this.getAvailableAuthoritativeWindow()) {
return { status: 'not_held' }
}
@@ -40,7 +40,7 @@ export abstract class RuntimeHeadlessTabProperties extends OrcaRuntimeWithHeadle
): Promise<{ updated: true }> {
const explicitWorktreeId = this.getExplicitSessionWorktreeIdSelector(worktreeSelector)
const worktreeId =
explicitWorktreeId ?? (await this.resolveWorktreeSelector(worktreeSelector)).id
explicitWorktreeId ?? (await this.resolveWorkspaceSelector(worktreeSelector)).id
this.hydrateHeadlessMobileSessionTabsFromWorkspaceSession(worktreeId)
// Attached renderers own and republish props; headless writes would be overwritten.
const snapshot = this.mobileSessionTabsByWorktree.get(worktreeId)
@@ -152,7 +152,7 @@ export abstract class OrcaRuntimeWithCloseHeadlessMobileTerminalTab extends Orca
): Promise<RuntimeMobileSessionTabMoveResult> {
const explicitWorktreeId = this.getExplicitSessionWorktreeIdSelector(worktreeSelector)
const worktreeId =
explicitWorktreeId ?? (await this.resolveWorktreeSelector(worktreeSelector)).id
explicitWorktreeId ?? (await this.resolveWorkspaceSelector(worktreeSelector)).id
this.hydrateHeadlessMobileSessionTabsFromWorkspaceSession(worktreeId)
const snapshot = this.mobileSessionTabsByWorktree.get(worktreeId)
if (!snapshot) {
@@ -209,7 +209,7 @@ export abstract class OrcaRuntimeWithCloseHeadlessMobileTerminalTab extends Orca
): Promise<{ updated: true }> {
const explicitWorktreeId = this.getExplicitSessionWorktreeIdSelector(worktreeSelector)
const worktreeId =
explicitWorktreeId ?? (await this.resolveWorktreeSelector(worktreeSelector)).id
explicitWorktreeId ?? (await this.resolveWorkspaceSelector(worktreeSelector)).id
// Resolve legacy raw ids to the host tab id, matching tab-prop persistence.
const snapshot = this.mobileSessionTabsByWorktree.get(worktreeId)
const hostTabId = snapshot
@@ -249,7 +249,7 @@ class SessionRuntime extends OrcaRuntimeService {
restoreRequests = 0
restoreResult = Promise.resolve()
override async resolveWorktreeSelector(): Promise<ResolvedWorktree> {
override async resolveGitWorktreeSelector(): Promise<ResolvedWorktree> {
const git = {
path: 'workspace',
head: '',
@@ -168,7 +168,7 @@ export abstract class OrcaRuntimeWithScheduleMobileSessionTabsChanged extends Or
): Promise<string> {
const worktreeId =
this.getExplicitSessionWorktreeIdSelector(worktreeSelector) ??
(await this.resolveWorktreeSelector(worktreeSelector)).id
(await this.resolveWorkspaceSelector(worktreeSelector)).id
const snapshot = this.mobileSessionTabsByWorktree.get(worktreeId)
const tab = snapshot?.tabs.find(
(candidate): candidate is RuntimeMobileSessionMarkdownTab =>
@@ -195,7 +195,7 @@ export abstract class OrcaRuntimeWithCollectMobileVisibleGraphChangedWorktrees e
this.restoreLivePairedRendererSessionOwnedMobileTerminals(resolvedId)
return this.getMobileSessionTabsForWorktree(resolvedId, clientNavigationId, options)
}
const worktree = await this.resolveWorktreeSelector(worktreeSelector)
const worktree = await this.resolveWorkspaceSelector(worktreeSelector)
this.hydrateHeadlessMobileSessionTabsFromWorkspaceSession(worktree.id, {
allowAttachedWindow: true,
onlyRuntimeOwnedTerminals: true
@@ -267,7 +267,7 @@ export abstract class OrcaRuntimeWithRestoreStructuredAgentSessionTabsOnce exten
return { repo, repoPath: repo.path }
}
const worktree = await this.resolveWorktreeSelector(args.worktreeSelector)
const worktree = await this.resolveGitWorktreeSelector(args.worktreeSelector)
if (worktree.repoId !== repo.id) {
throw new Error('Access denied: worktree does not belong to repository')
}
@@ -35,7 +35,7 @@ export function createSinglePageBrowserCommandsHost(
} as unknown as AgentBrowserBridge
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: page commands read only these host members.
return {
resolveWorktreeSelector: async () => ({ id: 'wt-1' }),
resolveWorkspaceSelector: async () => ({ id: 'wt-1' }),
getAgentBrowserBridge: () => bridge,
getRuntimeBrowserPageRegistry: () => runtimeBrowserPages,
getAvailableAuthoritativeWindow: vi.fn(() => window),
@@ -120,7 +120,7 @@ describe('implicit sender resolution refuses to guess', () => {
return {
graphStatus: 'ready',
assertGraphReady: () => {},
resolveWorktreeSelector: async () => ({ id: 'wt_1' }),
resolveWorkspaceSelector: async () => ({ id: 'wt_1' }),
tabs: new Map(),
leaves: new Map(
leafIds.map((leafId) => [leafId, { tabId: 'tab_1', leafId, worktreeId: 'wt_1' }])
@@ -148,7 +148,7 @@ describe('implicit sender resolution refuses to guess', () => {
// Keep a focused terminal in the starting graph so only the pre-ready identity guard can refuse this request.
const preReady = {
graphStatus: 'starting',
resolveWorktreeSelector: async () => ({ id: 'wt_1' }),
resolveWorkspaceSelector: async () => ({ id: 'wt_1' }),
getMobileSessionTabsForWorktree: () => ({
tabs: [{ type: 'terminal', isActive: true, status: 'ready', terminal: 'term_focused' }]
}),
@@ -116,7 +116,7 @@ export abstract class OrcaRuntimeWithAdoptTerminalOrphansFromInventory extends O
): Promise<string> {
if (this.graphStatus !== 'ready') {
const targetWorktreeId = worktreeSelector
? (await this.resolveWorktreeSelector(worktreeSelector)).id
? (await this.resolveWorkspaceSelector(worktreeSelector)).id
: null
const snapshots = targetWorktreeId
? [this.getMobileSessionTabsForWorktree(targetWorktreeId)]
@@ -150,7 +150,7 @@ export abstract class OrcaRuntimeWithAdoptTerminalOrphansFromInventory extends O
this.assertGraphReady()
const targetWorktreeId = worktreeSelector
? (await this.resolveWorktreeSelector(worktreeSelector)).id
? (await this.resolveWorkspaceSelector(worktreeSelector)).id
: null
// Prefer last-focused panes only outside identity claims.
@@ -163,8 +163,8 @@ export abstract class OrcaRuntimeWithTerminalDrivers extends OrcaRuntimeWithFitO
},
browserHost: {
getAgentBrowserBridge: () => this.agentBrowserBridge,
resolveWorktreeSelector: (selector) =>
resolveRemoteCliBrowserScopeSelector(selector) ?? this.resolveWorktreeSelector(selector),
resolveWorkspaceSelector: (selector) =>
resolveRemoteCliBrowserScopeSelector(selector) ?? this.resolveWorkspaceSelector(selector),
resolveBrowserWorkspace: (selector) =>
resolveRemoteCliBrowserScopeSelector(selector) ?? this.resolveBrowserWorkspace(selector),
getBrowserHostLeaseRegistry: () => getBrowserHostLeaseRegistry(this),
@@ -132,7 +132,7 @@ export abstract class OrcaRuntimeWithCreateMobileSessionTerminal extends OrcaRun
if (select) {
const worktreeId =
this.getExplicitSessionWorktreeIdSelector(worktreeSelector) ??
(await this.resolveWorktreeSelector(worktreeSelector)).id
(await this.resolveWorkspaceSelector(worktreeSelector)).id
this.applyMobileSessionTabNavigation(
this.getMobileSessionTabsForWorktree(worktreeId),
result.tab.id,
@@ -158,7 +158,7 @@ export abstract class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRun
suppressStartupUpdatePrompt?: boolean
}
): Promise<RuntimeTerminalCreate> {
const worktree = await this.resolveWorktreeSelector(worktreeSelector)
const worktree = await this.resolveGitWorktreeSelector(worktreeSelector)
// Repo ids are host-blind; resolve against the worktree owner before quoting its launch (#11163).
const resolution = resolveWorktreeLaunchHost(this.store?.getRepos() ?? [], worktree)
if (resolution.kind === 'ambiguous') {
@@ -156,7 +156,7 @@ export abstract class OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktr
const worktreeSelector = parsed?.type === 'worktree' ? `id:${parsed.worktreeId}` : selector
const worktree =
resolveCreatedWorktreeTerminalTarget(this.store, selector, createdWorktree) ??
(await this.resolveWorktreeSelector(worktreeSelector))
(await this.resolveGitWorktreeSelector(worktreeSelector))
// Repo ids are host-blind; resolve the worktree owner or refuse as unresolved rather than spawn locally with a remote cwd (#11163).
const resolution = resolveWorktreeLaunchHost(this.store?.getRepos() ?? [], worktree)
if (resolution.kind === 'ambiguous') {
@@ -224,7 +224,7 @@ export abstract class OrcaRuntimeWithSubscribeToTerminalResize extends OrcaRunti
return this.runWorktreeTerminalMutation(workspace.id, async () => {
const resolvedWorkspace = workspace.folderWorkspace
? this.folderWorkspaceToResolvedWorktree(workspace.folderWorkspace)
: await this.resolveWorktreeSelector(`id:${workspace.id}`)
: await this.resolveGitWorktreeSelector(`id:${workspace.id}`)
const inventory = await this.refreshPtyWorktreeRecordsWithControllerInventory(
[resolvedWorkspace],
workspace.id,
@@ -204,7 +204,7 @@ export abstract class OrcaRuntimeWithGetOrchestrationDispatchAuthority extends O
{
store: this.store,
resolveRepo: (selector) => this.resolveRepoSelector(selector),
resolveWorktree: (selector) => this.resolveWorktreeSelector(selector)
resolveWorktree: (selector) => this.resolveGitWorktreeSelector(selector)
},
worktreeId,
repoId
@@ -8,8 +8,8 @@ const LEAF = '10000000-0000-4000-8000-000000000002'
const INCARNATION = '10000000-0000-4000-8000-000000000003'
class ExactStopRuntime extends OrcaRuntimeService {
override resolveWorktreeSelector(selector: string) {
return super.resolveWorktreeSelector(selector)
override resolveGitWorktreeSelector(selector: string) {
return super.resolveGitWorktreeSelector(selector)
}
override getResolvedWorktreeMap() {
@@ -59,7 +59,7 @@ describe('terminal.stopExact post-stop verification', () => {
it('verifies a confirmed stop even when an overlapping inventory voids its first check', async () => {
const runtime = new ExactStopRuntime()
const worktree = folderWorktree()
vi.spyOn(runtime, 'resolveWorktreeSelector').mockResolvedValue(worktree)
vi.spyOn(runtime, 'resolveGitWorktreeSelector').mockResolvedValue(worktree)
vi.spyOn(runtime, 'getResolvedWorktreeMap').mockResolvedValue(new Map([[WORKTREE, worktree]]))
let stopped = false
let overlapped: Promise<unknown> | null = null
@@ -157,7 +157,7 @@ export abstract class OrcaRuntimeWithGetTerminalInteractiveWait extends OrcaRunt
return null
}
try {
const resolved = await this.resolveWorktreeSelector(`id:${worktreeId}`)
const resolved = await this.resolveWorkspaceSelector(`id:${worktreeId}`)
return {
worktreeId: resolved.id,
path: resolved.git.path,
@@ -146,7 +146,7 @@ export abstract class OrcaRuntimeWithListManagedWorktrees extends OrcaRuntimeWit
}
async showManagedWorktree(worktreeSelector: string) {
const worktree = await this.resolveWorktreeSelector(worktreeSelector)
const worktree = await this.resolveWorkspaceSelector(worktreeSelector)
return { ...worktree, linkedItems: getWorkspaceAttachments(worktree) }
}
@@ -250,7 +250,7 @@ export abstract class OrcaRuntimeWithListManagedWorktrees extends OrcaRuntimeWit
worktreeSelector: string,
opts: { replyOnAcceptance?: boolean; markChatsAsleep?: boolean } = {}
): Promise<{ worktreeId: string } & Partial<WorkspaceSleepOutcome>> {
const worktree = await this.resolveWorktreeSelector(worktreeSelector)
const worktree = await this.resolveWorkspaceSelector(worktreeSelector)
return this.workspaceSleep.sleepOwnWorktree(worktree.id, opts)
}
@@ -74,7 +74,7 @@ export abstract class OrcaRuntimeWithActivateManagedWorktree extends OrcaRuntime
sleepingAgentWake: 'requested' | 'unsupported-headless' | 'not-applicable'
}> {
this.assertGraphReady()
const worktree = await this.resolveWorktreeSelector(worktreeSelector)
const worktree = await this.resolveGitWorktreeSelector(worktreeSelector)
const repo = this.store?.getRepo(worktree.repoId)
if (!repo) {
throw new Error('repo_not_found')
@@ -258,7 +258,7 @@ export abstract class OrcaRuntimeWithPersistHeadlessTerminalTitle extends Runtim
localGitOptions?: { wslDistro?: string }
}> {
const store = this.requireStore()
const worktree = await this.resolveWorktreeSelector(worktreeSelector)
const worktree = await this.resolveGitWorktreeSelector(worktreeSelector)
const routing = resolveWorktreeHostRouting(store.getRepos(), worktree)
if (routing.kind === 'ambiguous') {
throw new Error('worktree_execution_host_unresolved')
@@ -298,7 +298,7 @@ export abstract class OrcaRuntimeWithPersistHeadlessTerminalTitle extends Runtim
}
const store = this.requireStore()
const worktree = await this.resolveWorktreeSelector(worktreeSelector)
const worktree = await this.resolveGitWorktreeSelector(worktreeSelector)
const routing = resolveWorktreeHostRouting(store.getRepos(), worktree)
if (routing.kind === 'ambiguous') {
throw new Error('worktree_execution_host_unresolved')
@@ -102,7 +102,7 @@ export abstract class OrcaRuntimeWithCreateManagedRemoteWorktree extends OrcaRun
return probeRuntimeWorktreeDrift({
selector: worktreeSelector,
store: this.store ? this.requireStore() : null,
resolveWorktree: (selector) => this.resolveWorktreeSelector(selector),
resolveWorktree: (selector) => this.resolveGitWorktreeSelector(selector),
resolveRemoteTrackingBase: (repoPath, base, options) =>
this.resolveRemoteTrackingBase(repoPath, base, options),
fetchRemote: (repoPath, remote, options) =>
@@ -128,7 +128,7 @@ export abstract class OrcaRuntimeWithCreateManagedRemoteWorktree extends OrcaRun
updates,
store: this.store,
ports: {
resolveWorktree: (selector) => this.resolveWorktreeSelector(selector),
resolveWorktree: (selector) => this.resolveGitWorktreeSelector(selector),
validateParent: (worktree, parent) => this.worktreeLineage.validateParent(worktree, parent),
invalidateResolved: () => this.invalidateResolvedWorktreeCache(),
invalidateScan: (repoId) => this.invalidateWorktreeScanCacheForRepo(repoId),
@@ -96,7 +96,7 @@ describe('workspace query prototype dispatch', () => {
return resolved
}
run() {
return this.resolveWorktreeSelector(`id:${TEST_WORKTREE_ID}`)
return this.resolveGitWorktreeSelector(`id:${TEST_WORKTREE_ID}`)
}
}
const runtime = new Probe(store)
@@ -64,7 +64,7 @@ export abstract class OrcaRuntimeWithResolveWorktreeRemovalTarget extends OrcaRu
return resolveRuntimeWorktreeRemovalTarget({
selector: worktreeSelector,
store: this.store,
resolveWorktree: (selector) => this.resolveWorktreeSelector(selector),
resolveWorktree: (selector) => this.resolveGitWorktreeSelector(selector),
resolveExplicitWorktreeIdScoped: (worktreeId, hostId) =>
this.resolveExplicitWorktreeIdScoped(worktreeId, hostId),
...(requiredHostId ? { requiredHostId } : {})
@@ -42,7 +42,8 @@ export abstract class RuntimeWorkspaceSelectors extends OrcaRuntimeWithResolveBr
...args: Parameters<RuntimeWorkspaceCatalog['listResolvedWorktrees']>
): ReturnType<RuntimeWorkspaceCatalog['listResolvedWorktrees']>
protected async resolveWorktreeSelector(selector: string): Promise<ResolvedWorktree> {
/** The repo-backed catalog only: project-group folder workspaces never match. Resolve a workspace with resolveWorkspaceSelector. */
protected async resolveGitWorktreeSelector(selector: string): Promise<ResolvedWorktree> {
const explicitWorktreeId = this.getValidatedExplicitWorktreeIdSelector(selector)
// Only id selectors name a repo; scoping other selectors would hide fleet-wide ambiguity.
if (explicitWorktreeId && !this.hasFreshResolvedWorktreeCache()) {
@@ -605,9 +605,7 @@ describe('scoped explicit worktree-id resolution', () => {
it('scans only the owning repo for an id: selector on a cold cache', async () => {
const runtime = new OrcaRuntimeService(makeStore({ repoCount: 10 }) as never)
const resolve = (selector: string): Promise<{ id: string }> =>
(
runtime as unknown as { resolveWorktreeSelector: (s: string) => Promise<{ id: string }> }
).resolveWorktreeSelector(selector)
runtime['resolveGitWorktreeSelector'](selector)
const resolved = await resolve(`id:${MAIN_WORKTREE_ID}`)
@@ -617,9 +615,7 @@ describe('scoped explicit worktree-id resolution', () => {
it('resolves an exact canonical identity without relying on the mutable locator', async () => {
const runtime = new OrcaRuntimeService(makeStore({ repoCount: 10 }) as never)
const resolve = (selector: string): Promise<{ id: string }> =>
(
runtime as unknown as { resolveWorktreeSelector: (s: string) => Promise<{ id: string }> }
).resolveWorktreeSelector(selector)
runtime['resolveGitWorktreeSelector'](selector)
const identityKey = canonicalWorktreeIdentity({
worktreeId: MAIN_WORKTREE_ID,
executionHostId: 'local',
@@ -634,9 +630,7 @@ describe('scoped explicit worktree-id resolution', () => {
it('still finds worktrees in other repos through the fleet path', async () => {
const runtime = new OrcaRuntimeService(makeStore({ repoCount: 10 }) as never)
const resolve = (selector: string): Promise<{ id: string }> =>
(
runtime as unknown as { resolveWorktreeSelector: (s: string) => Promise<{ id: string }> }
).resolveWorktreeSelector(selector)
runtime['resolveGitWorktreeSelector'](selector)
const otherId = `${REPO_ID}-3::${REPO_PATH}-3`
const resolved = await resolve(`id:${otherId}`)
@@ -648,9 +642,7 @@ describe('scoped explicit worktree-id resolution', () => {
it('keeps cross-repo selectors on the fleet path so ambiguity still throws', async () => {
const runtime = new OrcaRuntimeService(makeStore({ repoCount: 10 }) as never)
const resolve = (selector: string): Promise<unknown> =>
(
runtime as unknown as { resolveWorktreeSelector: (s: string) => Promise<unknown> }
).resolveWorktreeSelector(selector)
runtime['resolveGitWorktreeSelector'](selector)
await expect(resolve('branch:main')).rejects.toThrow('selector_ambiguous')
expect(new Set(scannedRepoPaths()).size).toBe(10)
@@ -659,9 +651,7 @@ describe('scoped explicit worktree-id resolution', () => {
it('falls back to the fleet path when the id names no registered repo', async () => {
const runtime = new OrcaRuntimeService(makeStore({ repoCount: 10 }) as never)
const resolve = (selector: string): Promise<unknown> =>
(
runtime as unknown as { resolveWorktreeSelector: (s: string) => Promise<unknown> }
).resolveWorktreeSelector(selector)
runtime['resolveGitWorktreeSelector'](selector)
await expect(resolve('id:missing-repo::/nowhere')).rejects.toThrow('selector_not_found')
expect(new Set(scannedRepoPaths()).size).toBe(10)
+1 -1
View File
@@ -134,7 +134,7 @@ describe('Linear workspace backfill under a WSL move', () => {
showTerminal: async () => {
throw new Error('unused')
},
resolveWorktreeSelector: async () => row,
resolveWorkspaceSelector: async () => row,
listResolvedWorktrees: async () => [row],
setWorktreeMeta,
emitClientEvent: () => {}