mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 16:03:53 +00:00
* fix(mobile): bound pending terminal recovery polls * wip(mobile): partial STA-4407 bound pending-handle poll * fix(mobile): finish bounded pending-handle recovery * fix(mobile): preserve pending-handle recovery attempts on slow links * fix(mobile): retain pending-handle cadence budget semantics * test(mobile): pin pending recovery parked state resets * chore(mobile): drop the STA-4407 worker status logbook * fix(mobile): preserve pending recovery liveness * fix(mobile): coalesce repeated recovery retries * fix(mobile): memoize pending recovery context * fix(mobile): type the pending recovery poll test renderer explicitly * fix(mobile): type the poll test renderer without an any union * fix(mobile): keep recovery context refs current * test(mobile): tighten pending recovery coverage * test(mobile): preserve recovery-source liveness * fix(mobile): keep the poll test renderer union free of any * test(mobile): prove parked recovery isolation * fix(mobile): write the parked-recovery callback ref after commit * test(mobile): isolate recovery identity changes * test(mobile): prove recovery publication boundaries
225 lines
11 KiB
TypeScript
225 lines
11 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
const source = readFileSync(
|
|
new URL('../../app/h/[hostId]/session/[worktreeId].tsx', import.meta.url),
|
|
'utf8'
|
|
)
|
|
const reconciliationHookSource = readFileSync(
|
|
new URL('./use-mobile-session-tabs-reconciliation.ts', import.meta.url),
|
|
'utf8'
|
|
)
|
|
const autoCreateHookSource = readFileSync(
|
|
new URL('./use-initial-session-terminal-autocreate.ts', import.meta.url),
|
|
'utf8'
|
|
)
|
|
|
|
function sliceBetween(startPattern: string, endPattern: string): string {
|
|
const start = source.indexOf(startPattern)
|
|
expect(start).toBeGreaterThanOrEqual(0)
|
|
const end = source.indexOf(endPattern, start)
|
|
expect(end).toBeGreaterThan(start)
|
|
return source.slice(start, end)
|
|
}
|
|
|
|
describe('mobile session startup', () => {
|
|
it('auto-creates one terminal for a newly created empty session', () => {
|
|
expect(source).toContain('useWorktreeSessionTabsLoaded(worktreeId)')
|
|
expect(source).toContain(
|
|
'initialSessionAutoCreateRef.current = createInitialSessionAutoCreateState()'
|
|
)
|
|
|
|
const autoCreateCall = sliceBetween(
|
|
'useInitialSessionTerminalAutoCreate({',
|
|
'const connectionVerdict ='
|
|
)
|
|
expect(autoCreateCall).toContain('stateRef: initialSessionAutoCreateRef')
|
|
expect(autoCreateCall).toContain(
|
|
'consumeCreationRoute: () => router.setParams({ created: undefined })'
|
|
)
|
|
expect(autoCreateCall).toContain("newlyCreatedWorkspace: created === '1'")
|
|
expect(autoCreateCall).toContain('visibleTabCount: visibleTabs.length')
|
|
expect(autoCreateCall).toContain('createTerminal: () => void handleCreateTerminal()')
|
|
|
|
expect(autoCreateHookSource).toContain('shouldAutoCreateInitialSessionTerminal({')
|
|
expect(autoCreateHookSource).toContain('stateRef.current.autoCreatedForWorktree === worktreeId')
|
|
expect(autoCreateHookSource).toContain('stateRef.current.autoCreatedForWorktree = worktreeId')
|
|
expect(autoCreateHookSource).toContain("connState === 'connected'")
|
|
expect(autoCreateHookSource).toContain('(visibleTabCount > 0 || activeHandle !== null)')
|
|
// Why: both callbacks are re-created every render, so the effect must reach them
|
|
// through useEffectEvent rather than deps or a render-time ref write.
|
|
expect(autoCreateHookSource).toContain('useEffectEvent(args.consumeCreationRoute)')
|
|
expect(autoCreateHookSource).toContain('useEffectEvent(args.createTerminal)')
|
|
expect(autoCreateHookSource).toContain('consumeCreationRoute()')
|
|
expect(autoCreateHookSource).toContain('createTerminal()')
|
|
})
|
|
|
|
it('arms the auto-create only until the route has published a tab (#9717)', () => {
|
|
// Emptiness after a populated list is a close, not a cold hydrate.
|
|
expect(source).toContain(
|
|
'initialSessionAutoCreateRef.current.sawSessionTabs ||= nextTabs.length > 0'
|
|
)
|
|
|
|
const autoCreateCall = sliceBetween(
|
|
'useInitialSessionTerminalAutoCreate({',
|
|
'const connectionVerdict ='
|
|
)
|
|
expect(autoCreateCall).toContain('stateRef: initialSessionAutoCreateRef')
|
|
expect(autoCreateHookSource).toContain('sawSessionTabs: stateRef.current.sawSessionTabs')
|
|
})
|
|
|
|
it('delegates stream ownership while retaining the exact terminal polling cadence', () => {
|
|
expect(source).toContain('useMobileSessionTabsReconciliation<')
|
|
expect(source).toContain('const applicationRevision = ++appliedSessionTabsRevisionRef.current')
|
|
expect(source).toContain('getApplicationRevision: getSessionTabsApplicationRevision')
|
|
expect(source).not.toContain("client.subscribe(\n 'session.tabs.subscribe'")
|
|
expect(reconciliationHookSource).toContain("client.subscribe(\n 'session.tabs.subscribe'")
|
|
expect(reconciliationHookSource).toContain(
|
|
"if (AppState.currentState !== 'active') {\n controller.setReconciliationActive(false)"
|
|
)
|
|
expect(reconciliationHookSource).toContain('void controller.poll()')
|
|
expect(reconciliationHookSource).toContain('void fetchTerminals()')
|
|
expect(reconciliationHookSource).toContain("AppState.addEventListener('change'")
|
|
expect(reconciliationHookSource).toContain('const interval = setInterval(')
|
|
expect(reconciliationHookSource).toContain('2000')
|
|
expect(reconciliationHookSource).toContain('controller.setReconciliationActive(false)')
|
|
expect(reconciliationHookSource).toContain('clearInterval(interval)')
|
|
expect(reconciliationHookSource).toContain('appStateSubscription.remove()')
|
|
})
|
|
|
|
it('loads session tabs without waiting for desktop activation', () => {
|
|
const startupEffect = sliceBetween(
|
|
'void (async () => {',
|
|
'return () => {\n disposed = true'
|
|
)
|
|
|
|
expect(startupEffect).toContain("void client\n .sendRequest('worktree.activate'")
|
|
expect(startupEffect).toContain("if (client && created !== '1' && !isFloatingWorkspaceRoute)")
|
|
expect(startupEffect).toContain("if (client && created === '1' && !isFloatingWorkspaceRoute)")
|
|
expect(startupEffect).toContain('notifyClients: false')
|
|
expect(startupEffect).toContain("navigation: 'caller'")
|
|
expect(startupEffect).not.toContain("await client\n .sendRequest('worktree.activate'")
|
|
expect(startupEffect.indexOf("sendRequest('worktree.activate'")).toBeLessThan(
|
|
startupEffect.indexOf('await ensureSessionTabs()')
|
|
)
|
|
expect(startupEffect).toContain('headlessActivationNeedsHostRenderer(response.result)')
|
|
expect(startupEffect).toContain("showToast('Open Orca on the host to wake sleeping agents.'")
|
|
})
|
|
|
|
it('fails runtime capability gates closed before probing a replacement client', () => {
|
|
const capabilityEffect = sliceBetween(
|
|
'const hostQueryReplyInputSupportedRef = useRef(false)',
|
|
'// Why: read deviceToken from host record'
|
|
)
|
|
const probeStart = capabilityEffect.indexOf('startRuntimeCapabilityProbe(client,')
|
|
|
|
expect(probeStart).toBeGreaterThanOrEqual(0)
|
|
for (const reset of [
|
|
'setBrowserScreencastSupported(null)',
|
|
'setAgentSessionHistorySupported(null)',
|
|
'setQuickCommandsSupported(null)',
|
|
'setShowQuickCommands(false)',
|
|
'hostQueryReplyInputSupportedRef.current = false'
|
|
]) {
|
|
const resetIndex = capabilityEffect.lastIndexOf(reset)
|
|
expect(resetIndex).toBeGreaterThanOrEqual(0)
|
|
expect(resetIndex).toBeLessThan(probeStart)
|
|
}
|
|
})
|
|
|
|
it('activates an already-selected pending terminal tab after hydration', () => {
|
|
expect(source).toContain(
|
|
'const pendingTerminalActivationAttemptRef = useRef<string | null>(null)'
|
|
)
|
|
expect(source).toContain('pendingTerminalActivationAttemptRef.current = null')
|
|
|
|
const pendingActivationEffect = sliceBetween(
|
|
"if (!client || connState !== 'connected' || !activePendingTerminalTab) {",
|
|
'const showLoadingState ='
|
|
)
|
|
expect(pendingActivationEffect).toContain(
|
|
'pendingTerminalActivationAttemptRef.current === activationKey'
|
|
)
|
|
expect(pendingActivationEffect).toContain('activateMobileSessionTab(client,')
|
|
expect(pendingActivationEffect).toContain('tabId: activePendingTerminalTab.id')
|
|
expect(pendingActivationEffect).toContain('leafId: activePendingTerminalTab.leafId')
|
|
expect(pendingActivationEffect).toContain('notifyClients: false')
|
|
expect(pendingActivationEffect).toContain("navigation: 'caller'")
|
|
expect(pendingActivationEffect).toContain(
|
|
'applySessionTabs((response as RpcSuccess).result as SessionTabsResult)'
|
|
)
|
|
expect(pendingActivationEffect).toContain('scheduleDelayedAction(() => void fetchSessionTabs()')
|
|
})
|
|
|
|
it('keeps ready terminal taps local while publishing caller selection', () => {
|
|
const readyTerminalSwitch = sliceBetween(
|
|
'const switchTab = useCallback(',
|
|
'const switchSessionTab = useCallback('
|
|
)
|
|
|
|
expect(readyTerminalSwitch).not.toContain('focusMobileTerminal(client, handle)')
|
|
expect(readyTerminalSwitch).toContain('activateMobileSessionTab(client,')
|
|
expect(readyTerminalSwitch).toContain('notifyClients: false')
|
|
expect(readyTerminalSwitch).toContain("navigation: 'caller'")
|
|
})
|
|
|
|
it('keeps background and pending session-tab activation local to the phone', () => {
|
|
const activationRequests = source.split('activateMobileSessionTab(client,').slice(1)
|
|
|
|
expect(activationRequests).toHaveLength(4)
|
|
for (const request of activationRequests) {
|
|
expect(request.slice(0, request.indexOf('})'))).toContain('notifyClients: false')
|
|
expect(request.slice(0, request.indexOf('})'))).toContain("navigation: 'caller'")
|
|
}
|
|
})
|
|
|
|
it('keeps dynamic agent rows above fixed New Tab actions', () => {
|
|
const newTabActions = sliceBetween('title="New Tab"', 'onClose={() => setShowCreateTabDrawer')
|
|
|
|
expect(newTabActions.indexOf('...createTabAgentActions')).toBeLessThan(
|
|
newTabActions.indexOf("label: 'Terminal'")
|
|
)
|
|
expect(newTabActions.indexOf("label: 'Terminal'")).toBeLessThan(
|
|
newTabActions.indexOf("label: 'Browser'")
|
|
)
|
|
expect(newTabActions.indexOf("label: 'Browser'")).toBeLessThan(
|
|
newTabActions.indexOf("label: 'Markdown Note'")
|
|
)
|
|
})
|
|
|
|
it('wires pending-handle recovery through its bounded context (STA-4256)', () => {
|
|
const applySessionTabs = sliceBetween(
|
|
'const applySessionTabs = useCallback(',
|
|
'const consumeAcceptedSessionTabs = useCallback('
|
|
)
|
|
const recoveryContext = sliceBetween(
|
|
'const pendingTerminalRecoveryContextCache = useMemo(',
|
|
'const getSessionTabsApplicationRevision'
|
|
)
|
|
|
|
const tabsRefWrite = 'sessionTabsRef.current = nextTabs'
|
|
const tabsStateWrite = 'setSessionTabs((prev)'
|
|
const activeRefWrite = 'activeSessionTabIdRef.current = active?.id ?? null'
|
|
const activeStateWrite = 'setActiveSessionTabId(active?.id ?? null)'
|
|
for (const write of [tabsRefWrite, tabsStateWrite, activeRefWrite, activeStateWrite]) {
|
|
expect(applySessionTabs).toContain(write)
|
|
}
|
|
expect(applySessionTabs.indexOf(tabsRefWrite)).toBeLessThan(
|
|
applySessionTabs.indexOf(tabsStateWrite)
|
|
)
|
|
expect(applySessionTabs.indexOf(activeRefWrite)).toBeLessThan(
|
|
applySessionTabs.indexOf(activeStateWrite)
|
|
)
|
|
expect(recoveryContext).toContain('() => new PendingTerminalHandleRecoveryContextCache()')
|
|
expect(recoveryContext).toContain('sessionTabsRef.current,')
|
|
expect(recoveryContext).toContain('activeSessionTabIdRef.current')
|
|
expect(recoveryContext).toContain(
|
|
'const pendingTerminalRecoveryContextKey = getPendingTerminalRecoveryContextKey()'
|
|
)
|
|
expect(source).toContain('hasRecoveryNeed: hasSessionTabsRecoveryNeed')
|
|
expect(source).toContain('getPendingTerminalRecoveryContextKey,')
|
|
expect(source).toContain('onPendingTerminalRecoveryParked: setParkedPendingTerminalContext')
|
|
expect(source).toContain('retryPendingTerminalRecovery()')
|
|
})
|
|
})
|