mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 08:03:12 +00:00
fix(mobile): arm the created-workspace activate recovery after hydration as before
This commit is contained in:
@@ -414,6 +414,52 @@ describe('HostProtocolGate', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('sends one activate when hydration lands after the creation route is consumed', async () => {
|
||||
vi.useFakeTimers()
|
||||
startupScope.created = '1'
|
||||
// Slow hydration is the window the recovery timer must not beat.
|
||||
startupScope.ensureSessionTabs = () => new Promise<void>((resolve) => setTimeout(resolve, 3000))
|
||||
const { activate, client, settle } = clientWithDeferredStatus()
|
||||
hostClient.current = { client: null, state: 'connecting' }
|
||||
renderer = await renderStartupGate()
|
||||
await connectWithPendingStatus(client)
|
||||
await settleVerdict(settle)
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(3000)
|
||||
})
|
||||
// Hydration landed: a tab claims the active handle and autocreate consumes ?created=1.
|
||||
startupScope.activeHandleRef.current = 'term-1'
|
||||
startupScope.created = undefined
|
||||
await act(async () => {
|
||||
renderer?.update(startupGateElement())
|
||||
await Promise.resolve()
|
||||
})
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(5000)
|
||||
})
|
||||
|
||||
expect(activate).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('never activates when the verdict comes back blocked', async () => {
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const { activate, client, settle } = clientWithDeferredStatus()
|
||||
hostClient.current = { client: null, state: 'connecting' }
|
||||
renderer = await renderStartupGate()
|
||||
await connectWithPendingStatus(client)
|
||||
|
||||
await act(async () => {
|
||||
settle({ protocolVersion: 1, minCompatibleMobileVersion: 0 })
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
const output = renderedText(renderer)
|
||||
expect(output).toContain('Update Orca on your computer')
|
||||
expect(output).not.toContain('StartupProbe')
|
||||
expect(activate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('skips the created-workspace activate recovery when a terminal is already active', async () => {
|
||||
vi.useFakeTimers()
|
||||
startupScope.created = '1'
|
||||
|
||||
@@ -62,12 +62,12 @@ const HOST_COMPONENT_NAMES = new Set([
|
||||
'View'
|
||||
])
|
||||
|
||||
const HEAD_MAIN_HOOK_SHA256 = 'c4e5825b6fe1ba9c64b5555fb3b0c6d37e7a72558cb5bd27df306b6649555891'
|
||||
const HEAD_HOOK_BINDING_SHA256 = '969b8bc0193af0e817dcdb682fbed4cfd5a44c228b3e664860c4c94dbd426b4f'
|
||||
const HEAD_MAIN_HOOK_SHA256 = '06878d6a74b932f5279fdf0bbe89f4f6410e2890271da386deb3b6c04ed178bf'
|
||||
const HEAD_HOOK_BINDING_SHA256 = 'c7997ece2e5643276f7569455cc24d88381626b410f590281b1ca5b7539ec739'
|
||||
const HEAD_CALLBACK_IDENTITY_SHA256 =
|
||||
'2a9e4825df007f6ef53b81aa5004991d6318eee7507b44d625c07e630be432eb'
|
||||
const HEAD_CALLBACK_BODY_SHA256 = '22103ba85a86e3a3fcb80a7509c7a455d79863010cde3af02db6565b55e3ebe9'
|
||||
const HEAD_EFFECT_SHA256 = '327c84aa19f531e2303cd8efa6cbe35304965131697c5081f09edba29fa3d645'
|
||||
const HEAD_EFFECT_SHA256 = '258135b4b1809f15033abcbfd12cd6249f4985feb7ac9d320b6ba275a4e090ca'
|
||||
const HEAD_CONTENT_HOOK_SHA256 = '9c3b612fef3f370d66873aefdbe1d701f20cb64ded31fef5cc45fde6f8189581'
|
||||
const HEAD_NESTED_FUNCTION_SHA256 =
|
||||
'0e553eb5ec7aeda8f8336b8da85ff87eb3657a21fa32d3c75c9cc32e36860244'
|
||||
@@ -472,7 +472,7 @@ describe('mobile session route extraction parity', () => {
|
||||
const contentBindings = CONTENT_COMPONENT_NAMES.flatMap(
|
||||
(name) => readHookFacts(name, definitions).bindings
|
||||
)
|
||||
expect(main.hooks).toHaveLength(268)
|
||||
expect(main.hooks).toHaveLength(269)
|
||||
expect(hash(main.hooks)).toBe(HEAD_MAIN_HOOK_SHA256)
|
||||
expect(hash(main.bindings)).toBe(HEAD_HOOK_BINDING_SHA256)
|
||||
expect(main.callbacks).toHaveLength(77)
|
||||
|
||||
@@ -149,7 +149,7 @@ describe('mobile session startup', () => {
|
||||
|
||||
it('loads session tabs without waiting for desktop activation', () => {
|
||||
const readsEffect = sliceBetween(
|
||||
'void (async () => {',
|
||||
'hydrationRef.current = (async () => {',
|
||||
'return () => {\n disposed = true',
|
||||
startupSource
|
||||
)
|
||||
@@ -188,6 +188,11 @@ describe('mobile session startup', () => {
|
||||
expect(activateEffect).toContain('notifyClients: false')
|
||||
expect(activateEffect).toContain("navigation: 'caller'")
|
||||
expect(activateEffect).not.toContain('await ensureSessionTabs()')
|
||||
// The recovery arms only once hydration settled, so a consumed `created` cannot add a second send.
|
||||
expect(activateEffect).toContain('await hydrationRef.current')
|
||||
expect(activateEffect.indexOf('await hydrationRef.current')).toBeLessThan(
|
||||
activateEffect.indexOf('}, 1800)')
|
||||
)
|
||||
// The created-workspace recovery still yields to a terminal that claimed the route first.
|
||||
expect(activateEffect).toContain('if (activeHandleRef.current)')
|
||||
expect(activateEffect).toContain('}, 1800)')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import type { RpcSuccess } from '../transport/types'
|
||||
import { headlessActivationNeedsHostRenderer } from '../worktree/worktree-activation-result'
|
||||
import { createInitialSessionAutoCreateState } from './use-initial-session-terminal-autocreate'
|
||||
@@ -45,6 +45,8 @@ export function useMobileSessionStartup(scope: MobileSessionKeyboardStateModel)
|
||||
fetchTerminals,
|
||||
ensureSessionTabs
|
||||
} = scope
|
||||
// Holds the in-flight tab/terminal hydration so the activation effect can wait on it.
|
||||
const hydrationRef = useRef<Promise<void> | null>(null)
|
||||
useEffect(() => {
|
||||
// Why: Expo reuses this screen across worktrees; reset route state so it can't open stale UI or reject the next snapshot.
|
||||
sessionTabActionSheetRequestSeqRef.current += 1
|
||||
@@ -116,7 +118,7 @@ export function useMobileSessionStartup(scope: MobileSessionKeyboardStateModel)
|
||||
}
|
||||
timers.push(setTimeout(fn, ms))
|
||||
}
|
||||
void (async () => {
|
||||
hydrationRef.current = (async () => {
|
||||
await ensureSessionTabs().catch(() => null)
|
||||
if (disposed) {
|
||||
return
|
||||
@@ -177,26 +179,34 @@ export function useMobileSessionStartup(scope: MobileSessionKeyboardStateModel)
|
||||
.then((response) => reportActivationOutcome(response.ok ? response : null))
|
||||
.catch(() => null)
|
||||
} else {
|
||||
addTimer(() => {
|
||||
if (activeHandleRef.current) {
|
||||
// Why: hydration can claim the active handle and consume `created`; arming the recovery
|
||||
// before it settles would let this route send a second activate once `created` clears.
|
||||
void (async () => {
|
||||
await hydrationRef.current
|
||||
if (disposed) {
|
||||
return
|
||||
}
|
||||
void (async () => {
|
||||
const activationResponse = await client
|
||||
.sendRequest('worktree.activate', {
|
||||
worktree: `id:${worktreeId}`,
|
||||
notifyClients: false,
|
||||
navigation: 'caller'
|
||||
})
|
||||
.catch(() => null)
|
||||
reportActivationOutcome(activationResponse?.ok ? activationResponse : null)
|
||||
if (disposed) {
|
||||
addTimer(() => {
|
||||
if (activeHandleRef.current) {
|
||||
return
|
||||
}
|
||||
await fetchTerminals({ allowEmptyLoaded: true })
|
||||
addTimer(() => void fetchTerminals({ allowEmptyLoaded: true }), 750)
|
||||
})()
|
||||
}, 1800)
|
||||
void (async () => {
|
||||
const activationResponse = await client
|
||||
.sendRequest('worktree.activate', {
|
||||
worktree: `id:${worktreeId}`,
|
||||
notifyClients: false,
|
||||
navigation: 'caller'
|
||||
})
|
||||
.catch(() => null)
|
||||
reportActivationOutcome(activationResponse?.ok ? activationResponse : null)
|
||||
if (disposed) {
|
||||
return
|
||||
}
|
||||
await fetchTerminals({ allowEmptyLoaded: true })
|
||||
addTimer(() => void fetchTerminals({ allowEmptyLoaded: true }), 750)
|
||||
})()
|
||||
}, 1800)
|
||||
})()
|
||||
}
|
||||
return () => {
|
||||
disposed = true
|
||||
|
||||
Reference in New Issue
Block a user