mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 08:03:33 +00:00
Improve mobile connection recovery guidance
This commit is contained in:
@@ -116,7 +116,7 @@ export default function ConnectionLogScreen() {
|
||||
<View style={styles.statusRow}>
|
||||
<Text style={styles.statusText}>
|
||||
{state === 'connected' && rpcUnresponsiveSince != null
|
||||
? 'Connected, not responding'
|
||||
? 'Desktop not responding'
|
||||
: state}
|
||||
{reconnectAttempts > 0 ? ` · attempt ${reconnectAttempts}` : ''}
|
||||
</Text>
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
useCloseHost,
|
||||
useForceReconnect
|
||||
} from '../../../src/transport/client-context'
|
||||
import { showForceReconnectError } from '../../../src/transport/force-reconnect-feedback'
|
||||
import { startForceReconnectWithFeedback } from '../../../src/transport/force-reconnect-feedback'
|
||||
import { useWorktreeResync } from '../../../src/transport/use-worktree-resync'
|
||||
import { startHostWorktreeRefresh } from '../../../src/worktree/host-worktree-refresh'
|
||||
import {
|
||||
@@ -147,6 +147,12 @@ export function HostScreen({
|
||||
const newWorktreeModalVisibleRef = useRef(false)
|
||||
const closeHostClient = useCloseHost()
|
||||
const forceReconnectHost = useForceReconnect()
|
||||
const reconnectHostWithFeedback = useCallback(() => {
|
||||
if (!hostId) {
|
||||
return
|
||||
}
|
||||
startForceReconnectWithFeedback(() => forceReconnectHost(hostId))
|
||||
}, [forceReconnectHost, hostId])
|
||||
const [worktrees, setWorktrees] = useState<Worktree[]>(initialCache ?? [])
|
||||
const [worktreesLoaded, setWorktreesLoaded] = useState(initialCache != null)
|
||||
// Why (STA-3123): error code of the last failed worktree.ps, so a broken catalog
|
||||
@@ -836,12 +842,10 @@ export function HostScreen({
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.reconnectButton}
|
||||
onPress={() =>
|
||||
void forceReconnectHost(hostId!).catch(showForceReconnectError)
|
||||
}
|
||||
onPress={reconnectHostWithFeedback}
|
||||
hitSlop={8}
|
||||
>
|
||||
<Text style={styles.reconnectButtonText}>Reconnect</Text>
|
||||
<Text style={styles.reconnectButtonText}>Reconnect now</Text>
|
||||
</Pressable>
|
||||
)
|
||||
})()}
|
||||
@@ -1099,7 +1103,7 @@ export function HostScreen({
|
||||
{connState === 'auth-failed' && (
|
||||
<AuthFailedBanner
|
||||
canRetry={!!hostId}
|
||||
onRetry={() => hostId && void forceReconnectHost(hostId).catch(showForceReconnectError)}
|
||||
onRetry={reconnectHostWithFeedback}
|
||||
onRepair={() => router.push('/pair-scan')}
|
||||
onRemove={() => setConfirmRemoveHost(true)}
|
||||
/>
|
||||
|
||||
@@ -59,7 +59,7 @@ import {
|
||||
type MobileTerminalLinkOpenMode
|
||||
} from '../../../../src/storage/preferences'
|
||||
import { useHostClient, useForceReconnect } from '../../../../src/transport/client-context'
|
||||
import { showForceReconnectError } from '../../../../src/transport/force-reconnect-feedback'
|
||||
import { startForceReconnectWithFeedback } from '../../../../src/transport/force-reconnect-feedback'
|
||||
import {
|
||||
useLastConnectedAt,
|
||||
useReconnectAttempt,
|
||||
@@ -4428,7 +4428,7 @@ export default function SessionScreen() {
|
||||
disabled={!showConnectionRetry}
|
||||
onPress={() => {
|
||||
if (hostId) {
|
||||
void forceReconnectHost(hostId).catch(showForceReconnectError)
|
||||
startForceReconnectWithFeedback(() => forceReconnectHost(hostId))
|
||||
}
|
||||
}}
|
||||
accessibilityRole={showConnectionRetry ? 'button' : undefined}
|
||||
|
||||
+31
-2
@@ -31,7 +31,7 @@ import {
|
||||
useForceReconnect,
|
||||
usePrimeHosts
|
||||
} from '../src/transport/client-context'
|
||||
import { showForceReconnectError } from '../src/transport/force-reconnect-feedback'
|
||||
import { startForceReconnectWithFeedback } from '../src/transport/force-reconnect-feedback'
|
||||
import { useRpcUnresponsiveByHost } from '../src/transport/client-context-connection-metrics'
|
||||
import { classifyConnection } from '../src/transport/connection-health'
|
||||
import { subscribeToDesktopNotifications } from '../src/notifications/mobile-notifications'
|
||||
@@ -244,6 +244,12 @@ export default function HomeScreen() {
|
||||
)
|
||||
const closeHostClient = useCloseHost()
|
||||
const forceReconnectHost = useForceReconnect()
|
||||
const reconnectHostWithFeedback = useCallback(
|
||||
(hostId: string) => {
|
||||
startForceReconnectWithFeedback(() => forceReconnectHost(hostId))
|
||||
},
|
||||
[forceReconnectHost]
|
||||
)
|
||||
const primeHosts = usePrimeHosts()
|
||||
const allClientsRef = useRef<Array<{ hostId: string; client: RpcClient }>>([])
|
||||
// Why: keep the focus callback stable (no refetch per render) while still exposing the latest host clients.
|
||||
@@ -718,6 +724,28 @@ export default function HomeScreen() {
|
||||
rpcUnresponsiveSince: hostRpcUnresponsive[item.id] ?? null,
|
||||
endpoint: item.endpoint
|
||||
})
|
||||
const reconnectAction = {
|
||||
label: state === 'disconnected' ? 'Connect' : 'Reconnect now',
|
||||
onPress: () => reconnectHostWithFeedback(item.id)
|
||||
}
|
||||
let statusActions: { label: string; onPress: () => void }[] | undefined
|
||||
if (verdict.kind === 'auth-failed') {
|
||||
statusActions = [
|
||||
{ label: 'Retry', onPress: reconnectAction.onPress },
|
||||
{ label: 'Re-pair', onPress: () => router.push('/pair-scan') }
|
||||
]
|
||||
} else if (verdict.kind === 'unreachable') {
|
||||
statusActions = [
|
||||
reconnectAction,
|
||||
{ label: 'Re-pair', onPress: () => router.push('/pair-scan') }
|
||||
]
|
||||
} else if (
|
||||
verdict.kind === 'warning' ||
|
||||
state === 'reconnecting' ||
|
||||
state === 'disconnected'
|
||||
) {
|
||||
statusActions = [reconnectAction]
|
||||
}
|
||||
return (
|
||||
<MobileHostCard
|
||||
host={item}
|
||||
@@ -725,6 +753,7 @@ export default function HomeScreen() {
|
||||
verdict={verdict}
|
||||
path={hostPaths[item.id] ?? 'lan'}
|
||||
worktreeInfo={worktreeInfo[item.id]}
|
||||
statusActions={statusActions}
|
||||
onPress={() => router.push(`/h/${item.id}`)}
|
||||
onLongPress={() => {
|
||||
triggerMediumImpact()
|
||||
@@ -908,7 +937,7 @@ export default function HomeScreen() {
|
||||
? (hostLastConnected[actionTarget.id] ?? null) != null
|
||||
: false,
|
||||
onDismiss: () => setActionTarget(null),
|
||||
onReconnect: (hostId) => void forceReconnectHost(hostId).catch(showForceReconnectError),
|
||||
onReconnect: reconnectHostWithFeedback,
|
||||
onDisconnect: closeHostClient,
|
||||
onEdit: openMobileHostEdit,
|
||||
onRemove: setConfirmRemove
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router'
|
||||
import { ChevronLeft } from 'lucide-react-native'
|
||||
import { resolvePairConfirmRouteState } from '../src/transport/pair-confirm-state'
|
||||
import { pairingErrorMessage } from '../src/transport/pairing-error-presentation'
|
||||
import {
|
||||
startPreProfilePairing,
|
||||
type PreProfilePairingAttempt
|
||||
@@ -133,11 +134,7 @@ export default function PairConfirmScreen() {
|
||||
}
|
||||
console.warn('[pair-confirm] connect failed', err)
|
||||
setStatus('error')
|
||||
setErrorMessage(
|
||||
timedOut
|
||||
? `Couldn't connect within ${PAIRING_OVERALL_TIMEOUT_MS / 1000}s — see log below for where it stalled`
|
||||
: `Pairing failed: ${err instanceof Error ? err.message : String(err)}`
|
||||
)
|
||||
setErrorMessage(pairingErrorMessage(err, timedOut, PAIRING_OVERALL_TIMEOUT_MS / 1000))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,8 +183,11 @@ export default function PairConfirmScreen() {
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.actionStack}>
|
||||
<Pressable style={styles.primaryButton} onPress={cancel}>
|
||||
<Text style={styles.primaryButtonText}>Back to home</Text>
|
||||
<Pressable style={styles.primaryButton} onPress={() => router.replace('/pair-scan')}>
|
||||
<Text style={styles.primaryButtonText}>Scan latest QR code</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.secondaryButton} onPress={cancel}>
|
||||
<Text style={styles.secondaryButtonText}>Back to home</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { CameraView, useCameraPermissions } from 'expo-camera'
|
||||
import { useRouter } from 'expo-router'
|
||||
import { ChevronLeft, Clipboard as ClipboardIcon, QrCode } from 'lucide-react-native'
|
||||
import { decodePairingUrl, parsePairingCode } from '../src/transport/pairing'
|
||||
import { pairingErrorMessage } from '../src/transport/pairing-error-presentation'
|
||||
import {
|
||||
startPreProfilePairing,
|
||||
type PreProfilePairingAttempt
|
||||
@@ -177,11 +178,7 @@ export default function PairScanScreen() {
|
||||
}
|
||||
console.warn('[pair] connect failed', err)
|
||||
setStatus('error')
|
||||
setErrorMessage(
|
||||
timedOut
|
||||
? `Couldn't connect within ${PAIRING_OVERALL_TIMEOUT_MS / 1000}s — see log below for where it stalled`
|
||||
: `Pairing failed: ${err instanceof Error ? err.message : String(err)}`
|
||||
)
|
||||
setErrorMessage(pairingErrorMessage(err, timedOut, PAIRING_OVERALL_TIMEOUT_MS / 1000))
|
||||
processingRef.current = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useHostClient, useForceReconnect } from '../transport/client-context'
|
||||
import { showForceReconnectError } from '../transport/force-reconnect-feedback'
|
||||
import { startForceReconnectWithFeedback } from '../transport/force-reconnect-feedback'
|
||||
import type { RpcSuccess } from '../transport/types'
|
||||
import type {
|
||||
AiVaultListResult,
|
||||
@@ -170,7 +170,7 @@ export function useMobileAgentHistoryState(params: MobileAgentHistoryStateParams
|
||||
|
||||
const retry = useCallback(() => {
|
||||
if (connState !== 'connected' && hostId) {
|
||||
void forceReconnect(hostId).catch(showForceReconnectError)
|
||||
startForceReconnectWithFeedback(() => forceReconnect(hostId))
|
||||
return
|
||||
}
|
||||
void loadSessions({ scope, force: false })
|
||||
|
||||
@@ -19,7 +19,8 @@ export function AuthFailedBanner({
|
||||
return (
|
||||
<View style={styles.banner}>
|
||||
<Text style={styles.text}>
|
||||
Authentication failed — try reconnecting first; if it keeps failing, re-pair from desktop.
|
||||
This desktop no longer accepts your phone's pairing. Try reconnecting once; if it still
|
||||
fails, re-pair from the desktop.
|
||||
</Text>
|
||||
<View style={styles.actions}>
|
||||
{canRetry && (
|
||||
|
||||
@@ -90,4 +90,37 @@ describe('MobileHostCard', () => {
|
||||
expect(lines).not.toContain('0 worktrees')
|
||||
expect(lines).not.toContain('Worktree list unavailable')
|
||||
})
|
||||
|
||||
it('explains automatic recovery and exposes Reconnect now', async () => {
|
||||
const reconnect = vi.fn()
|
||||
const stopPropagation = vi.fn()
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
createElement(MobileHostCard, {
|
||||
host,
|
||||
state: 'connected',
|
||||
verdict: {
|
||||
kind: 'warning',
|
||||
label: 'Desktop not responding',
|
||||
reason: 'unresponsive'
|
||||
},
|
||||
path: 'lan',
|
||||
statusActions: [{ label: 'Reconnect now', onPress: reconnect }],
|
||||
onPress: () => {},
|
||||
onLongPress: () => {}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const text = renderer!.root
|
||||
.findAllByType('Text')
|
||||
.flatMap((node) => node.children.filter((child) => typeof child === 'string'))
|
||||
expect(text).toContain(
|
||||
"The connection is open, but desktop Orca isn't answering. Orca is checking the connection and will retry automatically."
|
||||
)
|
||||
const action = renderer!.root.findByProps({ accessibilityLabel: 'Reconnect now' })
|
||||
act(() => action.props.onPress({ stopPropagation }))
|
||||
expect(stopPropagation).toHaveBeenCalledOnce()
|
||||
expect(reconnect).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ChevronRight, Monitor } from 'lucide-react-native'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import type { ConnectionVerdict } from '../transport/connection-health'
|
||||
import { verdictDisplayLabel } from '../transport/connection-health'
|
||||
import { verdictDisplayLabel, verdictSupportingMessage } from '../transport/connection-health'
|
||||
import { mobileConnectionPathLabel } from '../transport/mobile-connection-path-label'
|
||||
import type { MobileConnectionPath } from '../transport/stable-logical-rpc-client'
|
||||
import type { ConnectionState, HostProfile } from '../transport/types'
|
||||
import { colors, radii, spacing } from '../theme/mobile-theme'
|
||||
import { colors, radii, spacing, typography } from '../theme/mobile-theme'
|
||||
import { homeHostWorktreeSummary, type HostWorktreeInfo } from '../worktree/home-worktree-info'
|
||||
import { StatusDot } from './StatusDot'
|
||||
|
||||
@@ -17,12 +17,14 @@ export function MobileHostCard(props: {
|
||||
// Why: the card owns the fresh/stale/unavailable wording so no caller can re-gate the counts
|
||||
// away (STA-3123 shipped that bug once already).
|
||||
worktreeInfo?: HostWorktreeInfo
|
||||
statusActions?: { label: string; onPress: () => void }[]
|
||||
onPress: () => void
|
||||
onLongPress: () => void
|
||||
}) {
|
||||
const connected = props.state === 'connected'
|
||||
const isError = ['warning', 'unreachable', 'auth-failed'].includes(props.verdict.kind)
|
||||
const worktreeSummary = homeHostWorktreeSummary(props.worktreeInfo)
|
||||
const supportingMessage = verdictSupportingMessage(props.verdict)
|
||||
return (
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.card, pressed && styles.cardPressed]}
|
||||
@@ -52,11 +54,35 @@ export function MobileHostCard(props: {
|
||||
{worktreeSummary}
|
||||
</Text>
|
||||
) : null}
|
||||
{supportingMessage ? (
|
||||
<Text style={styles.supportingMessage} numberOfLines={3}>
|
||||
{supportingMessage}
|
||||
</Text>
|
||||
) : null}
|
||||
{props.verdict.kind === 'unreachable' && !props.host.relay ? (
|
||||
<Text style={styles.discoveryHint} numberOfLines={2}>
|
||||
Update desktop Orca and sign in to connect from anywhere
|
||||
</Text>
|
||||
) : null}
|
||||
{props.statusActions?.length ? (
|
||||
<View style={styles.statusActions}>
|
||||
{props.statusActions.map((action) => (
|
||||
<Pressable
|
||||
key={action.label}
|
||||
style={styles.statusAction}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={action.label}
|
||||
onPress={(event) => {
|
||||
event.stopPropagation()
|
||||
action.onPress()
|
||||
}}
|
||||
hitSlop={8}
|
||||
>
|
||||
<Text style={styles.statusActionText}>{action.label}</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
<ChevronRight size={16} color={colors.textMuted} />
|
||||
</Pressable>
|
||||
@@ -94,10 +120,35 @@ const styles = StyleSheet.create({
|
||||
fontSize: 12,
|
||||
color: colors.textMuted
|
||||
},
|
||||
supportingMessage: {
|
||||
marginTop: spacing.xs,
|
||||
fontSize: typography.metaSize,
|
||||
color: colors.textSecondary
|
||||
},
|
||||
discoveryHint: {
|
||||
marginTop: spacing.xs,
|
||||
fontSize: 11,
|
||||
lineHeight: 15,
|
||||
color: colors.textMuted
|
||||
},
|
||||
statusActions: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
marginTop: spacing.sm,
|
||||
flexWrap: 'wrap'
|
||||
},
|
||||
statusAction: {
|
||||
paddingVertical: spacing.xs,
|
||||
paddingHorizontal: spacing.sm,
|
||||
borderRadius: radii.button,
|
||||
backgroundColor: colors.bgRaised,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.borderSubtle
|
||||
},
|
||||
statusActionText: {
|
||||
color: colors.textPrimary,
|
||||
fontSize: typography.metaSize,
|
||||
fontWeight: '600'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -11,7 +11,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { useRouter } from 'expo-router'
|
||||
import { ChevronLeft, X } from 'lucide-react-native'
|
||||
import { useHostClient, useForceReconnect } from '../transport/client-context'
|
||||
import { showForceReconnectError } from '../transport/force-reconnect-feedback'
|
||||
import { startForceReconnectWithFeedback } from '../transport/force-reconnect-feedback'
|
||||
import { getWorktreeLabel } from '../session/worktree-label'
|
||||
import {
|
||||
flattenDirectoryCache,
|
||||
@@ -250,7 +250,7 @@ export function MobileFileExplorerPanel(props: {
|
||||
(relativePath: string) => {
|
||||
if (connState !== 'connected' && hostId) {
|
||||
pendingDirectoryRetriesRef.current.add(relativePath)
|
||||
void forceReconnect(hostId).catch(showForceReconnectError)
|
||||
startForceReconnectWithFeedback(() => forceReconnect(hostId))
|
||||
return
|
||||
}
|
||||
void loadDirectory(relativePath)
|
||||
@@ -334,7 +334,7 @@ export function MobileFileExplorerPanel(props: {
|
||||
style={styles.retryButton}
|
||||
onPress={() =>
|
||||
connState !== 'connected' && hostId
|
||||
? void forceReconnect(hostId).catch(showForceReconnectError)
|
||||
? startForceReconnectWithFeedback(() => forceReconnect(hostId))
|
||||
: void loadDirectory('')
|
||||
}
|
||||
>
|
||||
|
||||
@@ -305,7 +305,11 @@ describe('edit host handleSave', () => {
|
||||
|
||||
expect(dependencies.forceReconnectHost).toHaveBeenCalledWith('host-1', savedHost)
|
||||
expect(dependencies.back).toHaveBeenCalledTimes(1)
|
||||
expect(dependencies.alert).toHaveBeenCalledWith('Unable to reconnect', 'connect failed')
|
||||
expect(dependencies.alert).toHaveBeenCalledWith(
|
||||
"Couldn't reconnect to desktop",
|
||||
'Make sure desktop Orca is open and both devices are online, then try again.',
|
||||
[{ text: 'Dismiss', style: 'cancel' }]
|
||||
)
|
||||
|
||||
act(() => renderer.unmount())
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Dispatch, RefObject, SetStateAction } from 'react'
|
||||
import type { FlatList } from 'react-native'
|
||||
import type { ConnectionState } from '../transport/types'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { forceReconnectErrorPresentation } from '../transport/force-reconnect-feedback'
|
||||
import { triggerSelection } from '../platform/haptics'
|
||||
import { findNextMobileDiffHunkIndex, findPreviousMobileDiffHunkIndex } from './mobile-diff-hunks'
|
||||
import type {
|
||||
@@ -197,7 +198,8 @@ export function useMobileDiffReviewInteractions(input: InteractionInput) {
|
||||
retryAction: () => {
|
||||
if (connState !== 'connected' && hostId) {
|
||||
void Promise.resolve(onReconnect(hostId)).catch((error: unknown) => {
|
||||
setActionError(error instanceof Error ? error.message : 'Unable to reconnect')
|
||||
const presentation = forceReconnectErrorPresentation(error)
|
||||
setActionError(`${presentation.title}. ${presentation.message}`)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { colors, radii, spacing, typography } from '../theme/mobile-theme'
|
||||
import type { ConnectionState, RpcSuccess } from '../transport/types'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { useForceReconnect } from '../transport/client-context'
|
||||
import { showForceReconnectError } from '../transport/force-reconnect-feedback'
|
||||
import { startForceReconnectWithFeedback } from '../transport/force-reconnect-feedback'
|
||||
import {
|
||||
fetchMobileGitHistory,
|
||||
mapMobileCommitRows,
|
||||
@@ -90,7 +90,7 @@ export const MobileGitHistoryList = memo(function MobileGitHistoryList({
|
||||
// MobileSourceControlPanel / issue #5049). The load effect re-runs via
|
||||
// connState once the fresh client connects.
|
||||
if (connState !== 'connected' && hostId) {
|
||||
void forceReconnect(hostId).catch(showForceReconnectError)
|
||||
startForceReconnectWithFeedback(() => forceReconnect(hostId))
|
||||
return
|
||||
}
|
||||
setReloadNonce((n) => n + 1)
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { SourceControlHubTab } from './mobile-source-control-hub-tab'
|
||||
import { buildMobilePrChipSummary, countUnresolvedReviewThreads } from './mobile-pr-chip-summary'
|
||||
import { isMobileConflictAborting } from './mobile-source-control-conflict-abort'
|
||||
import { useMobilePrSidebarController } from '../session/use-mobile-pr-sidebar-controller'
|
||||
import { showForceReconnectError } from '../transport/force-reconnect-feedback'
|
||||
import { startForceReconnectWithFeedback } from '../transport/force-reconnect-feedback'
|
||||
import { prSidebarDetailsNeedFetch } from '../session/mobile-pr-sidebar-state'
|
||||
import { MobilePrViewPanelBody } from '../components/pr-sidebar/MobilePrViewPanel'
|
||||
import { openMobilePrUrl } from '../components/MobilePrComposeSheet'
|
||||
@@ -255,7 +255,7 @@ export function MobileSourceControlPanel({
|
||||
onPress={() => {
|
||||
// Why: a parked reconnect loop makes retry useless — revive the connection instead (issue #5049); loadStatus re-runs on reconnect.
|
||||
if (connState !== 'connected' && hostId) {
|
||||
void forceReconnect(hostId).catch(showForceReconnectError)
|
||||
startForceReconnectWithFeedback(() => forceReconnect(hostId))
|
||||
return
|
||||
}
|
||||
void loadStatus()
|
||||
|
||||
@@ -178,8 +178,8 @@ describe('issue #10119 — what a phone shows while it cannot reach the desktop'
|
||||
// The failure counter survives ws.onopen, so both connection-health gates
|
||||
// (warning >= 3, unreachable >= 12) are reachable.
|
||||
expect(Math.max(...samples.map((s) => s.attempts))).toBeGreaterThanOrEqual(12)
|
||||
expect(samples.some((s) => s.label === "Can't connect")).toBe(true)
|
||||
expect(samples.some((s) => s.label === "Can't reach desktop")).toBe(true)
|
||||
expect(samples.some((s) => s.label === 'Still trying to connect')).toBe(true)
|
||||
expect(samples.some((s) => s.label === 'Desktop unreachable')).toBe(true)
|
||||
|
||||
// Once escalated, no later redial reverts the label to "Connecting…".
|
||||
const after = labelsAfterFirstEscalation(samples)
|
||||
@@ -196,7 +196,7 @@ describe('issue #10119 — what a phone shows while it cannot reach the desktop'
|
||||
const samples = observe('ws://relay.example:443', 900_000)
|
||||
|
||||
expect(Math.max(...samples.map((s) => s.attempts))).toBeGreaterThanOrEqual(12)
|
||||
expect(samples.some((s) => s.label === "Can't reach desktop")).toBe(true)
|
||||
expect(samples.some((s) => s.label === 'Desktop unreachable')).toBe(true)
|
||||
|
||||
const after = labelsAfterFirstEscalation(samples)
|
||||
expect(after.every((s) => s.label !== 'Connecting…')).toBe(true)
|
||||
@@ -219,14 +219,14 @@ describe('issue #10119 — what a phone shows while it cannot reach the desktop'
|
||||
carrier = { kind: 'blackhole' }
|
||||
const samples = observe('ws://192.168.0.56:6769', 900_000)
|
||||
|
||||
const firstUnreachable = samples.findIndex((s) => s.label === "Can't reach desktop")
|
||||
const firstUnreachable = samples.findIndex((s) => s.label === 'Desktop unreachable')
|
||||
expect(firstUnreachable).toBeGreaterThan(0)
|
||||
|
||||
// The loop has given up internally (attempts held at the cap); the label
|
||||
// must say so through the 12s of every 90s trickle dial, not just between them.
|
||||
const after = samples.slice(firstUnreachable)
|
||||
expect(after.every((s) => s.attempts >= 12)).toBe(true)
|
||||
expect(after.every((s) => s.label === "Can't reach desktop")).toBe(true)
|
||||
expect(after.every((s) => s.label === 'Desktop unreachable')).toBe(true)
|
||||
})
|
||||
|
||||
it('resets the failure counter only once a handshake actually completes', () => {
|
||||
@@ -269,7 +269,7 @@ describe('issue #10119 — what a phone shows while it cannot reach the desktop'
|
||||
const samples = observe('ws://192.168.0.56:6769', 900_000)
|
||||
|
||||
expect(Math.max(...samples.map((s) => s.attempts))).toBeGreaterThanOrEqual(12)
|
||||
expect(samples.some((s) => s.label === "Can't reach desktop")).toBe(true)
|
||||
expect(samples.some((s) => s.label === 'Desktop unreachable')).toBe(true)
|
||||
|
||||
const after = labelsAfterFirstEscalation(samples)
|
||||
expect(after.every((s) => s.label !== 'Connecting…')).toBe(true)
|
||||
@@ -279,7 +279,7 @@ describe('issue #10119 — what a phone shows while it cannot reach the desktop'
|
||||
carrier = { kind: 'refused' }
|
||||
const samples = observe('ws://192.168.0.56:6769', 60_000)
|
||||
|
||||
expect(samples.some((s) => s.label === "Can't connect")).toBe(true)
|
||||
expect(samples.some((s) => s.label === 'Still trying to connect')).toBe(true)
|
||||
const after = labelsAfterFirstEscalation(samples)
|
||||
expect(after.every((s) => s.label !== 'Connecting…')).toBe(true)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { classifyConnection, verdictDisplayLabel } from './connection-health'
|
||||
import {
|
||||
classifyConnection,
|
||||
verdictDisplayLabel,
|
||||
verdictSupportingMessage
|
||||
} from './connection-health'
|
||||
|
||||
describe('classifyConnection auth-failed verdict', () => {
|
||||
it('tells the user to re-pair instead of showing a generic auth error', () => {
|
||||
@@ -10,7 +14,8 @@ describe('classifyConnection auth-failed verdict', () => {
|
||||
nowMs: 1_000_000
|
||||
})
|
||||
expect(verdict.kind).toBe('auth-failed')
|
||||
expect(verdictDisplayLabel(verdict)).toBe('Pairing invalid — re-pair with your desktop')
|
||||
expect(verdictDisplayLabel(verdict)).toBe('Pairing no longer works')
|
||||
expect(verdictSupportingMessage(verdict)).toContain('Try reconnecting once')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -80,7 +85,11 @@ describe('classifyConnection while dialing (issue #10119)', () => {
|
||||
it('keeps the warning verdict through a redial instead of reverting to Connecting…', () => {
|
||||
for (const state of ['connecting', 'handshaking'] as const) {
|
||||
const verdict = classifyConnection({ ...base, state, reconnectAttempts: 3 })
|
||||
expect(verdict).toMatchObject({ kind: 'warning', label: "Can't connect" })
|
||||
expect(verdict).toMatchObject({
|
||||
kind: 'warning',
|
||||
label: 'Still trying to connect',
|
||||
reason: 'retrying'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -113,20 +122,71 @@ describe('classifyConnection while dialing (issue #10119)', () => {
|
||||
describe('verdictDisplayLabel', () => {
|
||||
it('appends the hint to warning and unreachable labels', () => {
|
||||
expect(
|
||||
verdictDisplayLabel({ kind: 'warning', label: "Can't connect", hint: 'check Tailscale' })
|
||||
).toBe("Can't connect — check Tailscale")
|
||||
verdictDisplayLabel({
|
||||
kind: 'warning',
|
||||
label: 'Still trying to connect',
|
||||
reason: 'retrying',
|
||||
hint: 'check Tailscale'
|
||||
})
|
||||
).toBe('Still trying to connect — check Tailscale')
|
||||
expect(
|
||||
verdictDisplayLabel({
|
||||
kind: 'unreachable',
|
||||
label: "Can't reach desktop",
|
||||
label: 'Desktop unreachable',
|
||||
reason: 'stale',
|
||||
hint: 'check Tailscale'
|
||||
})
|
||||
).toBe("Can't reach desktop — check Tailscale")
|
||||
).toBe("Can't reach desktop through Tailscale")
|
||||
})
|
||||
|
||||
it('returns the bare label without a hint', () => {
|
||||
expect(verdictDisplayLabel({ kind: 'warning', label: "Can't connect" })).toBe("Can't connect")
|
||||
expect(
|
||||
verdictDisplayLabel({
|
||||
kind: 'warning',
|
||||
label: 'Still trying to connect',
|
||||
reason: 'retrying'
|
||||
})
|
||||
).toBe('Still trying to connect')
|
||||
expect(verdictDisplayLabel({ kind: 'normal', label: 'Connected' })).toBe('Connected')
|
||||
})
|
||||
})
|
||||
|
||||
describe('verdictSupportingMessage', () => {
|
||||
it('explains automatic recovery for an unresponsive desktop', () => {
|
||||
const verdict = classifyConnection({
|
||||
state: 'connected',
|
||||
reconnectAttempts: 4,
|
||||
lastConnectedAt: 900_000,
|
||||
rpcUnresponsiveSince: 999_000,
|
||||
nowMs: 1_000_000
|
||||
})
|
||||
|
||||
expect(verdictSupportingMessage(verdict)).toBe(
|
||||
"The connection is open, but desktop Orca isn't answering. Orca is checking the connection and will retry automatically."
|
||||
)
|
||||
})
|
||||
|
||||
it('explains the slower background retry cadence', () => {
|
||||
const verdict = classifyConnection({
|
||||
state: 'reconnecting',
|
||||
reconnectAttempts: 12,
|
||||
lastConnectedAt: null,
|
||||
nowMs: 1_000_000
|
||||
})
|
||||
|
||||
expect(verdictSupportingMessage(verdict)).toContain('retries have slowed to save battery')
|
||||
})
|
||||
|
||||
it('gives specific Tailscale recovery steps', () => {
|
||||
const verdict = classifyConnection({
|
||||
state: 'reconnecting',
|
||||
reconnectAttempts: 12,
|
||||
lastConnectedAt: null,
|
||||
endpoint: 'ws://100.65.9.106:6768',
|
||||
nowMs: 1_000_000
|
||||
})
|
||||
|
||||
expect(verdictSupportingMessage(verdict)).toContain('Open Tailscale')
|
||||
expect(verdictSupportingMessage(verdict)).toContain('Orca will keep retrying')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,9 +2,9 @@ import { isTailscaleEndpoint } from '../../../src/shared/remote-runtime-tailscal
|
||||
import type { ConnectionState } from './types'
|
||||
|
||||
// Why: thresholds for escalating connection UX from neutral
|
||||
// "Reconnecting…" to alarming "host appears unreachable, re-pair?".
|
||||
// "Reconnecting…" to an actionable unreachable state.
|
||||
//
|
||||
// - WARNING_ATTEMPTS: 3 → label flips to "Can't connect" (existing
|
||||
// - WARNING_ATTEMPTS: 3 → label flips to "Still trying to connect" (existing
|
||||
// behavior). Calibrated to absorb a normal laptop wake / brief
|
||||
// network blip without alarming the user.
|
||||
// - UNREACHABLE_ATTEMPTS: 12 → with the tiered 0.5s→60s backoff this
|
||||
@@ -25,12 +25,17 @@ const STALE_SINCE_LAST_CONNECT_MS = 60_000
|
||||
// Why: a repeatedly-unreachable 100.x/*.ts.net endpoint almost always means
|
||||
// the phone's Tailscale tunnel is down or wedged (a known iOS failure mode
|
||||
// that only a manual toggle fixes) — not that the desktop moved. Say so
|
||||
// instead of leaving the user staring at a generic "Can't connect".
|
||||
// instead of leaving the user staring at a generic unreachable label.
|
||||
const TAILSCALE_HINT = 'check Tailscale'
|
||||
|
||||
export type ConnectionVerdict =
|
||||
| { kind: 'normal'; label: string }
|
||||
| { kind: 'warning'; label: string; hint?: string } // "Can't connect"
|
||||
| {
|
||||
kind: 'warning'
|
||||
label: string
|
||||
reason: 'unresponsive' | 'retrying'
|
||||
hint?: string
|
||||
}
|
||||
| {
|
||||
kind: 'unreachable'
|
||||
label: string
|
||||
@@ -59,12 +64,12 @@ export function classifyConnection(args: {
|
||||
// Why: auth-failed means the desktop no longer recognizes this pairing (e.g. it
|
||||
// lost its device registry) — retrying can't fix it, only re-pairing can, so say so.
|
||||
if (state === 'auth-failed') {
|
||||
return { kind: 'auth-failed', label: 'Pairing invalid — re-pair with your desktop' }
|
||||
return { kind: 'auth-failed', label: 'Pairing no longer works' }
|
||||
}
|
||||
|
||||
if (state === 'connected') {
|
||||
if (args.rpcUnresponsiveSince != null) {
|
||||
return { kind: 'warning', label: 'Connected, not responding', hint }
|
||||
return { kind: 'warning', label: 'Desktop not responding', reason: 'unresponsive', hint }
|
||||
}
|
||||
return { kind: 'normal', label: 'Connected' }
|
||||
}
|
||||
@@ -81,7 +86,7 @@ export function classifyConnection(args: {
|
||||
if (lastConnectedAt == null) {
|
||||
return {
|
||||
kind: 'unreachable',
|
||||
label: "Can't reach desktop",
|
||||
label: 'Desktop unreachable',
|
||||
reason: 'never-connected',
|
||||
hint
|
||||
}
|
||||
@@ -89,7 +94,7 @@ export function classifyConnection(args: {
|
||||
if (now - lastConnectedAt >= STALE_SINCE_LAST_CONNECT_MS) {
|
||||
return {
|
||||
kind: 'unreachable',
|
||||
label: "Can't reach desktop",
|
||||
label: 'Desktop unreachable',
|
||||
reason: 'stale',
|
||||
hint
|
||||
}
|
||||
@@ -97,7 +102,7 @@ export function classifyConnection(args: {
|
||||
}
|
||||
|
||||
if (reconnectAttempts >= WARNING_ATTEMPTS) {
|
||||
return { kind: 'warning', label: "Can't connect", hint }
|
||||
return { kind: 'warning', label: 'Still trying to connect', reason: 'retrying', hint }
|
||||
}
|
||||
|
||||
return { kind: 'normal', label: state === 'reconnecting' ? 'Reconnecting…' : 'Connecting…' }
|
||||
@@ -106,8 +111,39 @@ export function classifyConnection(args: {
|
||||
// Why: single place that turns a verdict into display text so every screen
|
||||
// renders the Tailscale hint the same way.
|
||||
export function verdictDisplayLabel(verdict: ConnectionVerdict): string {
|
||||
if ((verdict.kind === 'warning' || verdict.kind === 'unreachable') && verdict.hint) {
|
||||
if (verdict.kind === 'unreachable' && verdict.hint) {
|
||||
return "Can't reach desktop through Tailscale"
|
||||
}
|
||||
if (verdict.kind === 'warning' && verdict.hint) {
|
||||
return `${verdict.label} — ${verdict.hint}`
|
||||
}
|
||||
return verdict.label
|
||||
}
|
||||
|
||||
export function verdictSupportingMessage(verdict: ConnectionVerdict): string | null {
|
||||
if (verdict.kind === 'auth-failed') {
|
||||
return "This desktop no longer accepts your phone's pairing. Try reconnecting once; if it still fails, re-pair from the desktop."
|
||||
}
|
||||
if (verdict.kind === 'warning') {
|
||||
if (verdict.reason === 'unresponsive') {
|
||||
return "The connection is open, but desktop Orca isn't answering. Orca is checking the connection and will retry automatically."
|
||||
}
|
||||
if (verdict.hint) {
|
||||
return 'Open Tailscale on your phone and desktop and make sure both devices are connected. Orca will keep retrying.'
|
||||
}
|
||||
return "Orca can't reach your desktop yet and will keep retrying. Make sure Orca is open on the desktop and both devices are online."
|
||||
}
|
||||
if (verdict.kind === 'unreachable') {
|
||||
if (verdict.hint) {
|
||||
return 'Open Tailscale on your phone and desktop and make sure both devices are connected. Orca will keep retrying.'
|
||||
}
|
||||
return 'Orca will keep trying in the background, but retries have slowed to save battery. Check your desktop and network, then reconnect.'
|
||||
}
|
||||
if (verdict.label === 'Reconnecting…') {
|
||||
return 'The connection was interrupted. Orca is retrying automatically.'
|
||||
}
|
||||
if (verdict.label === 'Disconnected') {
|
||||
return 'Orca will not reconnect until you choose Connect.'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
forceReconnectErrorPresentation,
|
||||
showForceReconnectError,
|
||||
startForceReconnectWithFeedback
|
||||
} from './force-reconnect-feedback'
|
||||
|
||||
const alert = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('react-native', () => ({ Alert: { alert } }))
|
||||
|
||||
describe('forceReconnectErrorPresentation', () => {
|
||||
it('explains that recovery continues after an application-health timeout', () => {
|
||||
expect(forceReconnectErrorPresentation(new Error('Request timed out: worktree.ps'))).toEqual({
|
||||
title: "Desktop still isn't responding",
|
||||
message:
|
||||
'Orca opened a new connection, but the desktop did not respond within 15 seconds. Orca will keep retrying automatically.'
|
||||
})
|
||||
})
|
||||
|
||||
it('turns a replacement-open failure into a concrete next step', () => {
|
||||
expect(
|
||||
forceReconnectErrorPresentation(new Error('Unable to open a replacement connection'))
|
||||
).toEqual({
|
||||
title: "Couldn't reconnect to desktop",
|
||||
message: 'Make sure desktop Orca is open and both devices are online, then try again.'
|
||||
})
|
||||
})
|
||||
|
||||
it('explains how to recover an invalid pairing', () => {
|
||||
expect(
|
||||
forceReconnectErrorPresentation(new Error('Unauthorized — pairing may be revoked'))
|
||||
).toMatchObject({ title: 'Pairing no longer works' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('showForceReconnectError', () => {
|
||||
beforeEach(() => alert.mockReset())
|
||||
|
||||
it('offers a retry when the caller can repeat the action', () => {
|
||||
const retry = vi.fn()
|
||||
showForceReconnectError(new Error('Request timed out: worktree.ps'), retry)
|
||||
|
||||
expect(alert).toHaveBeenCalledWith(
|
||||
"Desktop still isn't responding",
|
||||
expect.stringContaining('keep retrying automatically'),
|
||||
[
|
||||
{ text: 'Dismiss', style: 'cancel' },
|
||||
{ text: 'Try again', onPress: retry }
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
it('never exposes an unknown internal error', () => {
|
||||
showForceReconnectError(new Error('secure store exploded'))
|
||||
|
||||
expect(alert).toHaveBeenCalledWith(
|
||||
"Couldn't reconnect to desktop",
|
||||
expect.not.stringContaining('secure store'),
|
||||
[{ text: 'Dismiss', style: 'cancel' }]
|
||||
)
|
||||
})
|
||||
|
||||
it('repeats the same reconnect action from Try again', async () => {
|
||||
const reconnect = vi.fn().mockRejectedValue(new Error('connection failed'))
|
||||
startForceReconnectWithFeedback(reconnect)
|
||||
await vi.waitFor(() => expect(alert).toHaveBeenCalledOnce())
|
||||
|
||||
const buttons = alert.mock.calls[0]?.[2] as { text: string; onPress?: () => void }[]
|
||||
buttons.find((button) => button.text === 'Try again')?.onPress?.()
|
||||
await vi.waitFor(() => expect(reconnect).toHaveBeenCalledTimes(2))
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,46 @@
|
||||
import { Alert } from 'react-native'
|
||||
import { Alert, type AlertButton } from 'react-native'
|
||||
|
||||
export function showForceReconnectError(error: unknown): void {
|
||||
Alert.alert('Unable to reconnect', error instanceof Error ? error.message : 'Please try again.')
|
||||
export type ForceReconnectErrorPresentation = {
|
||||
title: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export function forceReconnectErrorPresentation(error: unknown): ForceReconnectErrorPresentation {
|
||||
const detail = error instanceof Error ? error.message : String(error)
|
||||
if (/unauthorized|pairing may be revoked/i.test(detail)) {
|
||||
return {
|
||||
title: 'Pairing no longer works',
|
||||
message:
|
||||
"This desktop no longer accepts your phone's pairing. Try reconnecting once; if it still fails, re-pair from the desktop."
|
||||
}
|
||||
}
|
||||
if (
|
||||
/Request timed out: worktree\.ps|Application RPC channel is still not responding/.test(detail)
|
||||
) {
|
||||
return {
|
||||
title: "Desktop still isn't responding",
|
||||
message:
|
||||
'Orca opened a new connection, but the desktop did not respond within 15 seconds. Orca will keep retrying automatically.'
|
||||
}
|
||||
}
|
||||
return {
|
||||
title: "Couldn't reconnect to desktop",
|
||||
message: 'Make sure desktop Orca is open and both devices are online, then try again.'
|
||||
}
|
||||
}
|
||||
|
||||
export function showForceReconnectError(error: unknown, onRetry?: () => void): void {
|
||||
const presentation = forceReconnectErrorPresentation(error)
|
||||
const buttons: AlertButton[] = [{ text: 'Dismiss', style: 'cancel' }]
|
||||
if (onRetry) {
|
||||
buttons.push({ text: 'Try again', onPress: onRetry })
|
||||
}
|
||||
Alert.alert(presentation.title, presentation.message, buttons)
|
||||
}
|
||||
|
||||
export function startForceReconnectWithFeedback(reconnect: () => Promise<void>): void {
|
||||
const attempt = () => {
|
||||
void reconnect().catch((error: unknown) => showForceReconnectError(error, attempt))
|
||||
}
|
||||
attempt()
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ describe('foregrounding a phone that was suspended mid-dial', () => {
|
||||
}
|
||||
// Four failed dials is past WARNING_ATTEMPTS, so the card reads "Can't
|
||||
// connect — check Tailscale" rather than a neutral "Connecting…".
|
||||
expect(label(client, TAILSCALE_ENDPOINT)).toBe("Can't connect — check Tailscale")
|
||||
expect(label(client, TAILSCALE_ENDPOINT)).toBe('Still trying to connect — check Tailscale')
|
||||
await advanceUntilDialing(client)
|
||||
const doomed = latest()
|
||||
|
||||
@@ -247,7 +247,7 @@ describe('foregrounding a phone that was suspended mid-dial', () => {
|
||||
}
|
||||
|
||||
expect(client.getReconnectAttempt()).toBeGreaterThanOrEqual(12)
|
||||
expect(label(client, TAILSCALE_ENDPOINT)).toBe("Can't reach desktop — check Tailscale")
|
||||
expect(label(client, TAILSCALE_ENDPOINT)).toBe("Can't reach desktop through Tailscale")
|
||||
|
||||
// And the escalation is not sticky: the desktop comes back, the card clears.
|
||||
latest().authenticate()
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { pairingErrorMessage } from './pairing-error-presentation'
|
||||
|
||||
describe('pairingErrorMessage', () => {
|
||||
it('turns a retired publication into a recovery instruction', () => {
|
||||
expect(pairingErrorMessage(new Error('host profile publication was retired'), false, 25)).toBe(
|
||||
"Pairing needs to be restarted. The desktop's pairing information changed before setup finished. Open Mobile settings on the desktop and scan the latest QR code."
|
||||
)
|
||||
})
|
||||
|
||||
it('explains how to retry a pairing timeout', () => {
|
||||
expect(pairingErrorMessage(new Error('timeout'), true, 25)).toBe(
|
||||
"Couldn't reach your desktop within 25 seconds. Make sure Orca is open on the desktop and both devices are online, then try scanning again."
|
||||
)
|
||||
})
|
||||
|
||||
it('does not expose an unknown internal pairing error', () => {
|
||||
expect(pairingErrorMessage(new Error('relay credential write failed'), false, 25)).toBe(
|
||||
'Pairing could not be completed. Make sure Orca is open on the desktop and both devices are online, then try again.'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
const RETIRED_PUBLICATION_ERROR = 'host profile publication was retired'
|
||||
|
||||
export function pairingErrorMessage(
|
||||
error: unknown,
|
||||
timedOut: boolean,
|
||||
timeoutSeconds: number
|
||||
): string {
|
||||
if (timedOut) {
|
||||
return `Couldn't reach your desktop within ${timeoutSeconds} seconds. Make sure Orca is open on the desktop and both devices are online, then try scanning again.`
|
||||
}
|
||||
const detail = error instanceof Error ? error.message : String(error)
|
||||
if (detail === RETIRED_PUBLICATION_ERROR) {
|
||||
return "Pairing needs to be restarted. The desktop's pairing information changed before setup finished. Open Mobile settings on the desktop and scan the latest QR code."
|
||||
}
|
||||
return 'Pairing could not be completed. Make sure Orca is open on the desktop and both devices are online, then try again.'
|
||||
}
|
||||
@@ -109,7 +109,7 @@ describe('direct RPC application responsiveness', () => {
|
||||
lastConnectedAt: client.getLastConnectedAt(),
|
||||
rpcUnresponsiveSince
|
||||
})
|
||||
).toMatchObject({ kind: 'warning', label: 'Connected, not responding' })
|
||||
).toMatchObject({ kind: 'warning', label: 'Desktop not responding' })
|
||||
|
||||
const second = client
|
||||
.sendRequest('browser.screenshot', {}, { timeoutMs: 100, applicationHealthProbe: true })
|
||||
@@ -217,7 +217,7 @@ describe('direct RPC application responsiveness', () => {
|
||||
lastConnectedAt: client.getLastConnectedAt(),
|
||||
rpcUnresponsiveSince
|
||||
})
|
||||
).toMatchObject({ kind: 'warning', label: 'Connected, not responding' })
|
||||
).toMatchObject({ kind: 'warning', label: 'Desktop not responding' })
|
||||
client.close()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user