mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 00:03:20 +00:00
fix(mobile): harden floating workspace route reuse
This commit is contained in:
@@ -196,4 +196,56 @@ describe('useLiveWorktreeName request volume', () => {
|
||||
expect(subscribe).not.toHaveBeenCalled()
|
||||
expect(name).toBe('Floating Workspace')
|
||||
})
|
||||
|
||||
it('does not render stale names while switching through the floating route', async () => {
|
||||
const firstNameByWorktree = new Map<string, string>()
|
||||
let renderer: ReactTestRenderer | null = null
|
||||
|
||||
function RouteHarness(props: { routeName?: string; worktreeId: string }): null {
|
||||
const name = useLiveWorktreeName({
|
||||
client,
|
||||
connState: 'connected',
|
||||
routeName: props.routeName,
|
||||
worktreeId: props.worktreeId
|
||||
})
|
||||
if (!firstNameByWorktree.has(props.worktreeId)) {
|
||||
firstNameByWorktree.set(props.worktreeId, name)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const restoreConsoleError = suppressReactTestRendererDeprecationWarning()
|
||||
try {
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
createElement(RouteHarness, {
|
||||
routeName: 'Previous workspace',
|
||||
worktreeId: 'repo-1::/worktree'
|
||||
})
|
||||
)
|
||||
await Promise.resolve()
|
||||
})
|
||||
await act(async () => {
|
||||
renderer?.update(
|
||||
createElement(RouteHarness, {
|
||||
worktreeId: 'global-floating-terminal'
|
||||
})
|
||||
)
|
||||
})
|
||||
await act(async () => {
|
||||
renderer?.update(
|
||||
createElement(RouteHarness, {
|
||||
routeName: 'Next workspace',
|
||||
worktreeId: 'repo-2::/worktree'
|
||||
})
|
||||
)
|
||||
})
|
||||
} finally {
|
||||
restoreConsoleError()
|
||||
act(() => renderer?.unmount())
|
||||
}
|
||||
|
||||
expect(firstNameByWorktree.get('global-floating-terminal')).toBe('Floating Workspace')
|
||||
expect(firstNameByWorktree.get('repo-2::/worktree')).toBe('Next workspace')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,11 +20,19 @@ export function useLiveWorktreeName({ client, connState, routeName, worktreeId }
|
||||
// Why: the floating sentinel has no worktree record, so worktree.show would
|
||||
// fail forever and keep the 3s fallback poll alive; its title is fixed.
|
||||
const isFloatingWorkspace = isFloatingWorkspaceWorktreeId(worktreeId)
|
||||
const [worktreeName, setWorktreeName] = useState(() => routeName?.trim() ?? '')
|
||||
const routeNameHint = routeName?.trim() ?? ''
|
||||
const [worktreeName, setWorktreeName] = useState(() => ({
|
||||
worktreeId,
|
||||
name: routeNameHint
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
setWorktreeName(routeName?.trim() ?? '')
|
||||
}, [routeName, worktreeId])
|
||||
setWorktreeName((current) =>
|
||||
current.worktreeId === worktreeId && current.name === routeNameHint
|
||||
? current
|
||||
: { worktreeId, name: routeNameHint }
|
||||
)
|
||||
}, [routeNameHint, worktreeId])
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
@@ -62,7 +70,11 @@ export function useLiveWorktreeName({ client, connState, routeName, worktreeId }
|
||||
? getLiveWorktreeDisplayName([result.worktree], worktreeId)
|
||||
: null
|
||||
if (liveName) {
|
||||
setWorktreeName((current) => (current === liveName ? current : liveName))
|
||||
setWorktreeName((current) =>
|
||||
current.worktreeId === worktreeId && current.name === liveName
|
||||
? current
|
||||
: { worktreeId, name: liveName }
|
||||
)
|
||||
}
|
||||
hasSuccessfulRefresh = true
|
||||
if (eventStreamReady) {
|
||||
@@ -136,7 +148,7 @@ export function useLiveWorktreeName({ client, connState, routeName, worktreeId }
|
||||
)
|
||||
|
||||
if (isFloatingWorkspace) {
|
||||
return worktreeName || FLOATING_WORKSPACE_TITLE
|
||||
return FLOATING_WORKSPACE_TITLE
|
||||
}
|
||||
return worktreeName
|
||||
return worktreeName.worktreeId === worktreeId ? worktreeName.name : routeNameHint
|
||||
}
|
||||
|
||||
@@ -168,7 +168,47 @@ describe('useHostClient', () => {
|
||||
expect(connectMock).toHaveBeenCalledTimes(2)
|
||||
} finally {
|
||||
restore()
|
||||
renderer?.unmount()
|
||||
act(() => renderer?.unmount())
|
||||
}
|
||||
})
|
||||
|
||||
it('stays disconnected while a reused screen resolves an uncached host', async () => {
|
||||
const client = makeFakeClient('connected')
|
||||
connectMock.mockReturnValue(client)
|
||||
loadHostsMock.mockResolvedValueOnce([HOST]).mockReturnValueOnce(new Promise<never>(() => {}))
|
||||
|
||||
let selectedHostId = HOST.id
|
||||
let renderTick = 0
|
||||
const stateByRenderTick = new Map<number, ConnectionState>()
|
||||
let renderer: ReactTestRenderer | null = null
|
||||
function Probe(): null {
|
||||
stateByRenderTick.set(renderTick, useHostClient(selectedHostId).state)
|
||||
return null
|
||||
}
|
||||
|
||||
const restore = suppressReactTestRendererDeprecationWarning()
|
||||
try {
|
||||
await act(async () => {
|
||||
renderer = create(createElement(RpcClientProvider, null, createElement(Probe)))
|
||||
await Promise.resolve()
|
||||
})
|
||||
expect(stateByRenderTick.get(0)).toBe('connected')
|
||||
|
||||
selectedHostId = 'missing-host'
|
||||
renderTick = 1
|
||||
await act(async () => {
|
||||
renderer?.update(createElement(RpcClientProvider, null, createElement(Probe)))
|
||||
})
|
||||
expect(stateByRenderTick.get(1)).toBe('disconnected')
|
||||
|
||||
renderTick = 2
|
||||
await act(async () => {
|
||||
renderer?.update(createElement(RpcClientProvider, null, createElement(Probe)))
|
||||
})
|
||||
expect(stateByRenderTick.get(2)).toBe('disconnected')
|
||||
} finally {
|
||||
restore()
|
||||
act(() => renderer?.unmount())
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -373,9 +373,9 @@ export function useHostClient(hostId: string | undefined): {
|
||||
}
|
||||
})
|
||||
const initial = ctx.acquire(hostId)
|
||||
clientRef.current = initial
|
||||
setState(ctx.getState(hostId))
|
||||
if (initial) {
|
||||
clientRef.current = initial
|
||||
setState(ctx.getState(hostId))
|
||||
// Why: two cached hosts can both be connected, so equal state values cannot reveal the replacement client.
|
||||
force((n) => n + 1)
|
||||
}
|
||||
|
||||
@@ -3623,6 +3623,23 @@ describe('registerPtyHandlers', () => {
|
||||
deletePtyOwnership('remote-pty')
|
||||
})
|
||||
|
||||
it('routes runtime exact liveness without enumerating provider sessions', () => {
|
||||
const provider = getLocalPtyProvider()
|
||||
const hasPty = vi.spyOn(provider, 'hasPty').mockImplementation((id) => id === 'live-pty')
|
||||
const listProcesses = vi.spyOn(provider, 'listProcesses')
|
||||
const runtime = { setPtyController: vi.fn() }
|
||||
handlers.clear()
|
||||
registerPtyHandlers(mainWindow as never, runtime as never)
|
||||
const controller = runtime.setPtyController.mock.calls[0]?.[0] as {
|
||||
hasPty: (ptyId: string) => boolean | null
|
||||
}
|
||||
|
||||
expect(controller.hasPty('live-pty')).toBe(true)
|
||||
expect(controller.hasPty('missing-pty')).toBe(false)
|
||||
expect(hasPty).toHaveBeenCalledTimes(2)
|
||||
expect(listProcesses).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns unavailable runtime confirmation for unsupported or missing providers', async () => {
|
||||
registerSshPtyProvider('ssh-1', {} as never)
|
||||
setPtyOwnership('unsupported-pty', 'ssh-1')
|
||||
|
||||
@@ -1754,14 +1754,18 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
)
|
||||
const runtime = new OrcaRuntimeService({ ...runtimeStore, getRepos } as never)
|
||||
const hasPty = vi.fn(() => true)
|
||||
runtime.setPtyController({
|
||||
const ptyController = {
|
||||
livePtyIds: new Set([floatingPtyId]),
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
hasPty,
|
||||
hasPty(this: { livePtyIds: Set<string> }, ptyId: string) {
|
||||
return this.livePtyIds.has(ptyId)
|
||||
},
|
||||
listProcesses
|
||||
})
|
||||
}
|
||||
const hasPty = vi.spyOn(ptyController, 'hasPty')
|
||||
runtime.setPtyController(ptyController)
|
||||
|
||||
const tabs = await runtime.listMobileSessionTabs(`id:${FLOATING_TERMINAL_WORKTREE_ID}`)
|
||||
const terminals = await runtime.listTerminals(`id:${FLOATING_TERMINAL_WORKTREE_ID}`)
|
||||
|
||||
@@ -22018,8 +22018,8 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
|
||||
private refreshFloatingWorkspacePtyLiveness(): Set<string> | null {
|
||||
const hasPty = this.ptyController?.hasPty
|
||||
if (!hasPty) {
|
||||
const controller = this.ptyController
|
||||
if (!controller?.hasPty) {
|
||||
return null
|
||||
}
|
||||
const knownPtyIds = new Set<string>()
|
||||
@@ -22060,7 +22060,7 @@ export class OrcaRuntimeService {
|
||||
const liveness = new Map<string, boolean>()
|
||||
try {
|
||||
for (const ptyId of knownPtyIds) {
|
||||
const live = hasPty(ptyId)
|
||||
const live = controller.hasPty(ptyId)
|
||||
if (live === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user