mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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:
@@ -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')
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+2
-1
@@ -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)
|
||||
|
||||
@@ -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: () => {}
|
||||
|
||||
Reference in New Issue
Block a user