mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
fix(mobile): unblock targeted SSH session tab refresh (#17486)
* Fix targeted mobile SSH session tab refresh * Preserve fail-open explicit workspace resolution * Strengthen SSH session refresh oracle
This commit is contained in:
@@ -2220,6 +2220,92 @@ describe('OrcaRuntimeService', () => {
|
||||
expect(getRepos).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not block a targeted mobile session tab list on an unrelated worktree scan', async () => {
|
||||
const remoteWorktreeId = 'repo-ssh::/remote/worktree'
|
||||
const remotePtyId = 'ssh:ssh-target@@remote-pty'
|
||||
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(
|
||||
makeWorkspaceSessionWithHeadlessTerminal({
|
||||
activeRepoId: 'repo-ssh',
|
||||
activeWorktreeId: remoteWorktreeId,
|
||||
activeTabIdByWorktree: { [remoteWorktreeId]: 'remote-tab' },
|
||||
tabsByWorktree: {
|
||||
[remoteWorktreeId]: [
|
||||
{
|
||||
id: 'remote-tab',
|
||||
ptyId: remotePtyId,
|
||||
worktreeId: remoteWorktreeId,
|
||||
title: 'Remote terminal',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
terminalLayoutsByTabId: {
|
||||
'remote-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: remotePtyId })
|
||||
}
|
||||
}),
|
||||
'ssh:ssh-target'
|
||||
)
|
||||
const remoteRepo = {
|
||||
...store.getRepos()[0],
|
||||
id: 'repo-ssh',
|
||||
connectionId: 'ssh-target'
|
||||
}
|
||||
runtimeStore.getRepos = () => [remoteRepo]
|
||||
runtimeStore.getRepo = (id: string) => (id === remoteRepo.id ? remoteRepo : undefined)
|
||||
const runtime = new OrcaRuntimeService(runtimeStore as never)
|
||||
const listProcesses = vi.fn(async () => [
|
||||
{
|
||||
id: remotePtyId,
|
||||
incarnationId: 'remote-incarnation',
|
||||
terminalHandle: 'term_remote',
|
||||
title: 'Remote terminal',
|
||||
cwd: '/remote/worktree',
|
||||
worktreeId: remoteWorktreeId
|
||||
}
|
||||
])
|
||||
runtime.setPtyController({
|
||||
listProcesses,
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null
|
||||
})
|
||||
const listWorktrees = vi.fn(() => new Promise<never>(() => {}))
|
||||
registerSshGitProvider('ssh-target', { listWorktrees } as never)
|
||||
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
||||
const timeout = new Promise<null>((resolve) => {
|
||||
timeoutId = setTimeout(() => resolve(null), 1_000)
|
||||
})
|
||||
const resultPromise = runtime.listMobileSessionTabs(`id:${remoteWorktreeId}`)
|
||||
await Promise.resolve()
|
||||
await vi.advanceTimersByTimeAsync(1_000)
|
||||
const result = await Promise.race([resultPromise, timeout])
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(listWorktrees).not.toHaveBeenCalled()
|
||||
expect(listProcesses).toHaveBeenCalledOnce()
|
||||
expect(listProcesses).toHaveBeenCalledWith(
|
||||
'ssh-target',
|
||||
expect.objectContaining({ deadlineMs: expect.any(Number) })
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
worktree: remoteWorktreeId,
|
||||
tabs: [expect.objectContaining({ type: 'terminal', parentTabId: 'remote-tab' })]
|
||||
})
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
unregisterSshGitProvider('ssh-target')
|
||||
}
|
||||
})
|
||||
|
||||
it('hydrates persisted tabs when the store cannot report repos', async () => {
|
||||
// Why: #9343 read the repo gate as `getRepos?.() ?? []`, so a store that cannot
|
||||
// report its inventory looked like "every repo is gone" and hydrated nothing —
|
||||
|
||||
@@ -9634,10 +9634,13 @@ export class OrcaRuntimeService {
|
||||
private async refreshMobileSessionPtyInventory(
|
||||
targetWorktreeId: string | null = null
|
||||
): Promise<PtyControllerInventory | null> {
|
||||
// Targeted mobile polls must not queue behind an aggregate census that may
|
||||
// be waiting on an unrelated SSH provider.
|
||||
if (targetWorktreeId !== null && targetWorktreeId !== FLOATING_TERMINAL_WORKTREE_ID) {
|
||||
return this.performMobileSessionPtyRecordsRefresh(targetWorktreeId)
|
||||
}
|
||||
if (targetWorktreeId !== FLOATING_TERMINAL_WORKTREE_ID) {
|
||||
// Non-floating refreshes all query the aggregate controller inventory;
|
||||
// coalesce targeted and all-worktree callers so they cannot invalidate
|
||||
// one another through the shared aggregate generation fence.
|
||||
// Fleet-wide refreshes share one aggregate controller inventory.
|
||||
const pending = this.pendingMobileSessionPtyAggregateInventoryRefresh
|
||||
if (pending) {
|
||||
return pending
|
||||
@@ -9663,13 +9666,63 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
// Why: floating PTY identity is explicit, so polling must not resolve every Git/SSH worktree.
|
||||
const isFloatingWorkspace = targetWorktreeId === FLOATING_TERMINAL_WORKTREE_ID
|
||||
const resolvedWorktrees = isFloatingWorkspace ? [] : await this.listResolvedWorktrees()
|
||||
const resolvedWorktrees = isFloatingWorkspace
|
||||
? []
|
||||
: targetWorktreeId
|
||||
? this.listResolvedWorktreesForExplicitTarget(targetWorktreeId)
|
||||
: await this.listResolvedWorktrees()
|
||||
// An explicit mobile worktree belongs to one execution host. Query only
|
||||
// that provider; aggregate inventory would wait on unrelated SSH hosts.
|
||||
const targetExecutionHost = targetWorktreeId
|
||||
? (resolvedWorktrees.find((worktree) => worktree.id === targetWorktreeId)?.hostId ??
|
||||
this.tryGetWorkspaceSessionHostIdForWorktree(targetWorktreeId))
|
||||
: null
|
||||
const parsedTargetHost = targetExecutionHost ? parseExecutionHostId(targetExecutionHost) : null
|
||||
// Paired/runtime-owned workspaces have a separate controller; this runtime
|
||||
// cannot inspect them and must not silently query its local PTY provider.
|
||||
if (parsedTargetHost?.kind === 'runtime') {
|
||||
return null
|
||||
}
|
||||
const targetConnectionId =
|
||||
parsedTargetHost?.kind === 'ssh'
|
||||
? parsedTargetHost.targetId
|
||||
: targetWorktreeId
|
||||
? null
|
||||
: undefined
|
||||
return await this.refreshPtyWorktreeRecordsWithControllerInventory(
|
||||
resolvedWorktrees,
|
||||
isFloatingWorkspace ? targetWorktreeId : null
|
||||
targetWorktreeId,
|
||||
undefined,
|
||||
targetConnectionId
|
||||
)
|
||||
}
|
||||
|
||||
/** Targeted mobile opens must not wait for an unrelated SSH/Git worktree scan. */
|
||||
private listResolvedWorktreesForExplicitTarget(targetWorktreeId: string): ResolvedWorktree[] {
|
||||
const cached =
|
||||
this.resolvedWorktreeCache && this.resolvedWorktreeCache.expiresAt > Date.now()
|
||||
? this.resolvedWorktreeCache.worktrees
|
||||
: null
|
||||
const targetWorktree =
|
||||
cached?.find((worktree) => worktree.id === targetWorktreeId) ??
|
||||
(() => {
|
||||
const scope = parseWorkspaceKey(targetWorktreeId)
|
||||
if (scope?.type === 'folder') {
|
||||
const folder = this.store
|
||||
?.getFolderWorkspaces?.()
|
||||
.find((workspace) => workspace.id === scope.folderWorkspaceId)
|
||||
return folder ? this.folderWorkspaceToResolvedWorktree(folder) : null
|
||||
}
|
||||
return this.buildResolvedWorktreeFromId(targetWorktreeId)
|
||||
})()
|
||||
if (!targetWorktree) {
|
||||
return []
|
||||
}
|
||||
return cached
|
||||
? includeTargetResolvedWorktree(cached, targetWorktree)
|
||||
: this.listKnownResolvedWorktreesForExplicitTarget(targetWorktreeId, targetWorktree)
|
||||
}
|
||||
|
||||
async activateMobileSessionTab(
|
||||
worktreeSelector: string,
|
||||
tabId: string,
|
||||
@@ -34893,7 +34946,7 @@ export class OrcaRuntimeService {
|
||||
if (!parsed?.repoId || !parsed.worktreePath) {
|
||||
return null
|
||||
}
|
||||
const repo = this.store?.getRepos().find((entry) => entry.id === parsed.repoId)
|
||||
const repo = this.store?.getRepos?.()?.find((entry) => entry.id === parsed.repoId)
|
||||
const git = {
|
||||
path: parsed.worktreePath,
|
||||
head: '',
|
||||
@@ -34927,7 +34980,9 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
const target = splitWorktreeIdForFilesystem(targetWorktreeId)
|
||||
if (!target?.repoId || !target.worktreePath) {
|
||||
return []
|
||||
// Folder workspace keys have no repo/path tuple, but the converted row
|
||||
// is already authoritative for this explicit target.
|
||||
return [targetWorktree]
|
||||
}
|
||||
const worktreeIds = new Set(
|
||||
Object.keys(this.store.getAllWorktreeMeta()).filter((worktreeId) => {
|
||||
@@ -35496,7 +35551,8 @@ export class OrcaRuntimeService {
|
||||
resolvedWorktrees: ResolvedWorktree[],
|
||||
targetWorktreeId: string | null = null,
|
||||
deadline?: number,
|
||||
connectionId?: string | null
|
||||
connectionId?: string | null,
|
||||
retryStale = false
|
||||
): Promise<PtyControllerInventory | null> {
|
||||
if (targetWorktreeId === FLOATING_TERMINAL_WORKTREE_ID) {
|
||||
const targetedLiveness = this.refreshFloatingWorkspacePtyLiveness()
|
||||
@@ -35570,6 +35626,18 @@ export class OrcaRuntimeService {
|
||||
inventoryGeneration &&
|
||||
this.ptyControllerAggregateInventoryGeneration <= inventoryGeneration
|
||||
if (!isCurrentInventory) {
|
||||
// A fleet census that began after this targeted poll must not turn a
|
||||
// user-driven open into an empty result. Re-query the owning provider;
|
||||
// the second generation is then fenced against both operations.
|
||||
if (targetWorktreeId !== null && !retryStale) {
|
||||
return this.refreshPtyWorktreeRecordsWithControllerInventory(
|
||||
resolvedWorktrees,
|
||||
targetWorktreeId,
|
||||
deadline,
|
||||
connectionId,
|
||||
true
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
const sessions = sessionsResult.value.processes
|
||||
|
||||
@@ -133,7 +133,7 @@ describe('authoritative session tab inventory publication', () => {
|
||||
expect(collections).toBe(4)
|
||||
})
|
||||
|
||||
it('coalesces targeted and all-host PTY refreshes behind one aggregate census', async () => {
|
||||
it('keeps targeted PTY refreshes independent from an aggregate census', async () => {
|
||||
const runtime = createInventoryRuntime()
|
||||
runtime.attachWindow(1)
|
||||
runtime.syncWindowGraph(1, { tabs: [], leaves: [], mobileSessionTabs: [] })
|
||||
@@ -143,7 +143,7 @@ describe('authoritative session tab inventory publication', () => {
|
||||
terminalIdentityByPtyId: new Map(),
|
||||
queriedHostIds: new Set(['local'])
|
||||
}
|
||||
let resolveRefresh: ((inventory: typeof emptyInventory) => void) | undefined
|
||||
const pendingResolves: ((inventory: typeof emptyInventory) => void)[] = []
|
||||
const internals = runtime as unknown as {
|
||||
refreshMobileSessionPtyInventory: (targetWorktreeId?: string | null) => Promise<unknown>
|
||||
performMobileSessionPtyRecordsRefresh: (targetWorktreeId: string | null) => Promise<unknown>
|
||||
@@ -151,7 +151,7 @@ describe('authoritative session tab inventory publication', () => {
|
||||
const perform = vi.spyOn(internals, 'performMobileSessionPtyRecordsRefresh').mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveRefresh = resolve
|
||||
pendingResolves.push(resolve)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -160,10 +160,13 @@ describe('authoritative session tab inventory publication', () => {
|
||||
const targeted = internals.refreshMobileSessionPtyInventory('repo::/target')
|
||||
await Promise.resolve()
|
||||
|
||||
expect(perform).toHaveBeenCalledOnce()
|
||||
resolveRefresh?.(emptyInventory)
|
||||
expect(perform).toHaveBeenCalledTimes(2)
|
||||
expect(perform).toHaveBeenNthCalledWith(1, null)
|
||||
expect(perform).toHaveBeenNthCalledWith(2, 'repo::/target')
|
||||
pendingResolves[1]?.(emptyInventory)
|
||||
|
||||
await targeted
|
||||
pendingResolves[0]?.(emptyInventory)
|
||||
await expect(allHosts).resolves.toEqual({ snapshots: [], authoritative: true })
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user