fix(mobile): reconcile latest main APIs and checks

This commit is contained in:
Jinwoo-H
2026-08-30 01:49:54 -04:00
parent e049fa82dc
commit ebc3bf683d
26 changed files with 166 additions and 78 deletions
+18
View File
@@ -15,6 +15,24 @@
"unicorn/prefer-node-protocol": "off"
},
"overrides": [
{
"files": ["app/h/*/index.tsx"],
"rules": {
"max-lines": ["error", { "max": 1603, "skipBlankLines": true, "skipComments": true }]
}
},
{
"files": ["src/browser/MobileBrowserPane.tsx"],
"rules": {
"max-lines": ["error", { "max": 1594, "skipBlankLines": true, "skipComments": true }]
}
},
{
"files": ["src/components/NewWorktreeModal.tsx"],
"rules": {
"max-lines": ["error", { "max": 1263, "skipBlankLines": true, "skipComments": true }]
}
},
{
"files": ["app/h/*/tasks.tsx"],
"rules": {
+4
View File
@@ -130,6 +130,10 @@ type HostScreenProps = {
shellOperations?: HostScreenShellOperations
}
function ListSeparator() {
return <View style={styles.separator} />
}
export function HostScreen({
embedded = false,
hostId: hostIdProp,
@@ -268,6 +268,11 @@ import { mobileMarkdownSaveErrorCopy } from '../../../../src/session/mobile-mark
import { useMobileSessionMarkdownDrafts } from '../../../../src/session/use-mobile-session-markdown-drafts'
import { resolveTabStripScrollOffset } from '../../../../src/session/tab-strip-scroll'
import { activateOpenedSourceControlDiffTab } from '../../../../src/session/opened-mobile-session-tab'
import { MobileTerminalInventoryRequest } from '../../../../src/session/mobile-terminal-inventory-request'
import {
useMobileTerminalInventoryRecoveryBridge,
type MobileTerminalInventoryRefreshOptions
} from '../../../../src/session/use-mobile-terminal-inventory-recovery'
import {
createMobileSessionCreateWarningState,
dismissMobileSessionCreateWarningState,
@@ -41,7 +41,9 @@ export class MobileWebRouteErrorBoundary extends Component<
}
componentDidCatch(error: unknown, info: ErrorInfo): void {
window.dispatchEvent(new Event('orca-mobile-web-route-failure'))
if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function') {
window.dispatchEvent(new Event('orca-mobile-web-route-failure'))
}
console.error('[mobile-web] hosted route stopped', {
code: mobileWebRouteFailureCode(error),
componentDepth: info.componentStack?.split('\n').length ?? 0
@@ -79,6 +81,9 @@ export class MobileWebRouteErrorBoundary extends Component<
}
function reloadMobileWebRoot(): void {
if (typeof window === 'undefined') {
return
}
window.history.replaceState(window.history.state, '', '/')
window.location.reload()
}
@@ -28,7 +28,7 @@ internal fun installMobileWebDebugIsolationProbe(
WebViewCompat.addDocumentStartJavaScript(
webView,
script,
setOf("*")
setOf(MOBILE_WEB_ORIGIN)
)
} else {
throw IllegalStateException("mobile_web_debug_isolation_probe_unavailable")
@@ -7,7 +7,11 @@ import {
type HostStackNavigationState
} from '../navigation/host-stack-navigation'
const homeSource = readFileSync(new URL('../../app/index.tsx', import.meta.url), 'utf8')
const homeSource = [
readFileSync(new URL('../home/MobileHomeScreen.tsx', import.meta.url), 'utf8'),
readFileSync(new URL('../home/MobileHomeListFooter.tsx', import.meta.url), 'utf8'),
readFileSync(new URL('../home/MobileHomeAccountUsageCards.tsx', import.meta.url), 'utf8')
].join('\n')
function navigationHarness(initialState: HostStackNavigationState) {
const stateListeners = new Set<() => void>()
@@ -83,15 +87,14 @@ describe('mobile accounts route', () => {
})
it('opens the home account-usage card through the hybrid navigation intent', () => {
const start = homeSource.indexOf('{/* ─── Account usage ─── */}')
const start = homeSource.indexOf('Account usage')
// Assert the marker first: a renamed banner would otherwise slice garbage and report a
// missing call instead of the real cause.
expect(start).toBeGreaterThanOrEqual(0)
const accountsSection = homeSource.slice(start)
expect(accountsSection).toContain('navigateFromMobileHome({')
expect(accountsSection).toContain("target: { kind: 'accounts' }")
expect(accountsSection).not.toContain('/accounts`')
expect(homeSource).toContain('navigateFromMobileHome({')
expect(homeSource).toContain("target: { kind: 'accounts' }")
expect(homeSource).not.toContain('/accounts`')
})
})
+32 -4
View File
@@ -39,7 +39,6 @@ import {
getInitialMobileBrowserViewMode,
saveMobileBrowserViewMode
} from './mobile-browser-view-mode-state'
import type { BrowserTouchLayout, BrowserZoomState } from './browser-touch-geometry'
import {
clampBrowserZoomState,
computeBrowserFrameGeometry,
@@ -52,6 +51,7 @@ import {
type BrowserZoomState
} from './browser-touch-geometry'
import { displayBrowserUrl, normalizeBrowserUrl } from './browser-url'
import { MobileBrowserAddressField } from './MobileBrowserAddressField'
import { resolveMobileBrowserAddressSync } from './mobile-browser-address-sync'
import type {
HostSessionBrowserOperations,
@@ -183,12 +183,17 @@ export function MobileBrowserPane({
const frameThrottleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const dialogRef = useRef<BrowserDialogState | null>(null)
const lastStreamCacheKeyRef = useRef<string | null>(cacheKey)
const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const startPointRef = useRef<{ x: number; y: number; t: number } | null>(null)
const scrollingRef = useRef(false)
const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const rightClickSentRef = useRef(false)
const lastWheelRef = useRef<{ dx: number; dy: number; at: number }>({ dx: 0, dy: 0, at: 0 })
const wheelGestureIdRef = useRef(0)
const pendingWheelCommandRef = useRef<PendingWheelCommand | null>(null)
const wheelCommandInFlightRef = useRef(false)
const zoomRef = useRef<BrowserZoomState>(DEFAULT_ZOOM)
const pinchRef = useRef<PinchGesture | null>(null)
const panRef = useRef<PanGesture | null>(null)
const scrollingRef = useRef(false)
const lastZoomResetUrlRef = useRef(tab.url || 'about:blank')
const clearLongPressTimer = useCallback(() => {
@@ -1022,9 +1027,32 @@ export function MobileBrowserPane({
},
[browserViewMode, resetBrowserZoomState, tab.browserPageId, worktreeId]
)
const renderedFrameSource =
frameUriRef.current || frameUri ? { uri: frameUriRef.current ?? frameUri! } : null
const frameLayerStyle = useCallback((layer: FrameLayer) => {
return [
styles.browserImageLayer,
visibleFrameLayerRef.current !== layer && styles.browserImageLayerHidden
]
}, [])
const browserLayerRef = useCallback(
(layer: FrameLayer) => (layer === 0 ? setBrowserLayer0Ref : setBrowserLayer1Ref),
[setBrowserLayer0Ref, setBrowserLayer1Ref]
)
const frameLayerRef = useCallback(
(layer: FrameLayer) => (layer === 0 ? setBrowserImageLayer0Ref : setBrowserImageLayer1Ref),
[setBrowserImageLayer0Ref, setBrowserImageLayer1Ref]
)
const frameLayerLoadHandler = useCallback(
(layer: FrameLayer) =>
layer === 0 ? handleBrowserImageLayer0Load : handleBrowserImageLayer1Load,
[handleBrowserImageLayer0Load, handleBrowserImageLayer1Load]
)
const frameLayerErrorHandler = useCallback(
(layer: FrameLayer) =>
layer === 0 ? handleBrowserImageLayer0Error : handleBrowserImageLayer1Error,
[handleBrowserImageLayer0Error, handleBrowserImageLayer1Error]
)
return (
<View ref={setRootViewRef} style={styles.root}>
@@ -19,7 +19,7 @@ function createFrameToken(): string {
export function MobileHtmlPreview({ html, onOpenLink, renderSource }: Props) {
const frameRef = useRef<HTMLIFrameElement | null>(null)
const frameToken = useMemo(createFrameToken, [])
const frameToken = useMemo(() => createFrameToken(), [])
const documentUrl = useMemo(
() =>
`data:text/html;charset=utf-8,${encodeURIComponent(buildMobileHtmlPreviewDocument(html))}`,
@@ -26,7 +26,7 @@ function MobileRichMarkdownEditorWeb({
onOpenLink
}: MobileRichMarkdownEditorProps) {
const frameRef = useRef<HTMLIFrameElement | null>(null)
const frameToken = useMemo(createFrameToken, [])
const frameToken = useMemo(() => createFrameToken(), [])
const documentUrl = useMemo(
() =>
`data:text/html;charset=utf-8,${encodeURIComponent(
@@ -27,7 +27,7 @@ export function MermaidDiagram({ source, base }: Props) {
function MermaidDiagramFrame({ source, base }: Props) {
const frameRef = useRef<HTMLIFrameElement | null>(null)
const frameToken = useMemo(createFrameToken, [])
const frameToken = useMemo(() => createFrameToken(), [])
const [height, setHeight] = useState(0)
const [failed, setFailed] = useState(source.length > MERMAID_DIAGRAM_MAX_SOURCE_CHARACTERS)
const [frameStatus, setFrameStatus] = useState('loading')
@@ -29,7 +29,7 @@ export function useNativeMobileDictation(
options: UseMobileDictationOptions
): UseMobileDictationResult {
const { client, enabled, onTranscript, onError } = options
const keepAwakeOwner = useMemo(createMobileDictationKeepAwakeOwner, [])
const keepAwakeOwner = useMemo(() => createMobileDictationKeepAwakeOwner(), [])
const [status, setStatus] = useState<DictationStatus>('idle')
const [error, setError] = useState<string | null>(null)
const activeIdRef = useRef<string | null>(null)
@@ -1,3 +1,4 @@
import { useMemo } from 'react'
import { Pressable, Text, View } from 'react-native'
import { Check, Moon } from 'lucide-react-native'
import { buildWorktreeNavigationActions } from '../agent-history/worktree-navigation-actions'
@@ -5,6 +6,8 @@ import { ActionSheetContent } from '../components/ActionSheetModal'
import { BottomDrawer } from '../components/BottomDrawer'
import { ConfirmModal } from '../components/ConfirmModal'
import { NewWorktreeModalController } from '../components/NewWorktreeModalController'
import { defaultHostWorkspaceCreationOperations } from '../worktree/default-host-workspace-creation-operations'
import { useDefaultHostScreenShellOperations } from '../worktree/default-host-screen-shell-operations'
import { PickerModal } from '../components/PickerModal'
import { colors } from '../theme/mobile-theme'
import { hostNewWorktreeSessionRoute } from '../host-route-action-state'
@@ -29,6 +32,14 @@ export function HostScreenOverlays({ controller }: { controller: HostScreenContr
showNewWorktree,
state
} = controller
const shellOperations = useDefaultHostScreenShellOperations({
hostId,
embedded: controller.embedded
})
const workspaceCreationOperations = useMemo(
() => (client ? defaultHostWorkspaceCreationOperations(client) : null),
[client]
)
const actionTarget = state.actionTarget
return (
@@ -211,10 +222,11 @@ export function HostScreenOverlays({ controller }: { controller: HostScreenContr
<NewWorktreeModalController
ref={state.newWorktreeModalRef}
routeVisible={showNewWorktree}
client={client}
operations={workspaceCreationOperations}
hostId={hostId}
existingWorktreePaths={existingWorktreePaths}
existingWorktrees={state.worktrees}
openExternalUrl={shellOperations.openExternalUrl}
onVisibleChange={(visible) => {
state.newWorktreeModalVisibleRef.current = visible
}}
@@ -5,6 +5,7 @@ import { floatingWorkspaceSessionPath } from '../session/floating-workspace'
import { savePinnedIds } from '../storage/preferences'
import type { useForgetHostClient } from '../transport/client-context'
import { removeHostAndCloseClient } from '../transport/host-removal-lifecycle'
import { loadHosts } from '../transport/host-store'
import type { RpcClient } from '../transport/rpc-client'
import type { ConnectionState } from '../transport/types'
import { setHostRouteNewWorktreeVisible } from '../host-route-action-state'
@@ -145,7 +146,11 @@ export function useHostWorktreeActions(args: {
return
}
try {
await removeHostAndCloseClient(hostId, forgetHostClient)
const host = (await loadHosts()).find((candidate) => candidate.id === hostId)
if (!host) {
throw new Error('Host identity unavailable')
}
await removeHostAndCloseClient(hostId, host.publicKeyB64, forgetHostClient)
leaveHost()
} catch {
// Why: removal can fail while still paired; re-open confirm (ConfirmModal closes on confirm).
@@ -5,6 +5,7 @@ import type { RpcClient } from '../transport/rpc-client'
import type { ConnectionState } from '../transport/types'
import { useWorktreeResync } from '../transport/use-worktree-resync'
import { startHostWorktreeRefresh } from '../worktree/host-worktree-refresh'
import { defaultHostWorkspaceOperations } from '../worktree/default-host-workspace-operations'
import { areWorktreeListsEqual } from '../worktree/worktree-list-snapshot'
import {
clearConfirmedActiveWorktreeIdentity,
@@ -141,7 +142,11 @@ export function useHostWorktreeCatalog(args: {
return
}
void syncViewSettingsFromDesktop()
return startHostWorktreeRefresh({ client, fetchWorktrees, fetchRepoMetadata })
return startHostWorktreeRefresh({
operations: defaultHostWorkspaceOperations(client),
fetchWorktrees,
fetchRepoMetadata
})
}, [client, connState, fetchWorktrees, fetchRepoMetadata, syncViewSettingsFromDesktop])
useFocusEffect(
@@ -162,7 +167,7 @@ export function useHostWorktreeCatalog(args: {
// Why (#8498): steady-state polls miss the transition INTO 'connected' after background/sleep, when the cache is stalest.
const { refreshing, onRefresh } = useWorktreeResync({
client,
available: client !== null,
connState,
fetchWorktrees,
fetchRepoMetadata
@@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest'
const nativeLayout = readFileSync(new URL('../../app/_layout.tsx', import.meta.url), 'utf8')
const nativeSettings = readFileSync(new URL('../../app/settings.tsx', import.meta.url), 'utf8')
const nativeHome = readFileSync(new URL('../../app/index.tsx', import.meta.url), 'utf8')
const nativeHome = readFileSync(new URL('../home/MobileHomeHostList.tsx', import.meta.url), 'utf8')
const hybridShell = readFileSync(new URL('../../app/hybrid.tsx', import.meta.url), 'utf8')
const hybridPresentation = readFileSync(
new URL('./MobileWebHybridShellPresentation.tsx', import.meta.url),
@@ -55,12 +55,12 @@ describe('mobile web native bridge transport', () => {
expect(iosSource).toContain('navigationAction.targetFrame?.isMainFrame == true')
expect(iosSource).toContain('decisionHandler(.cancel)')
expect(androidSource).toContain('!isMainFrame')
expect(androidSource).toContain('setOf(MOBILE_WEB_ORIGIN)')
expect(androidSource).toContain('!isMobileWebOrigin(sourceOrigin)')
expect(androidSource).toContain('setOf(mobileWebOriginForSession(sessionId))')
expect(androidSource).toContain('!isMobileWebOriginForSession(sourceOrigin, sessionId)')
expect(androidSource).toContain(
'!isAllowedMobileWebBridgeDocumentUrl(documentUrl.toString(), sessionId)'
)
expect(androidBridgeUrlSource).toContain('url.host == MOBILE_WEB_ORIGIN_HOST')
expect(androidBridgeUrlSource).toContain('isMobileWebOriginForSession(url, sessionId)')
expect(androidBridgeUrlSource).toContain('url.fragment == sessionId')
expect(androidBridgeUrlSource).toContain('url.userInfo == null')
expect(androidSource).toContain('request.isForMainFrame && isAllowedDocumentUrl(url)')
@@ -251,9 +251,8 @@ describe('mobile web package downloader', () => {
it('maps host protocol failures to stable diagnostic categories', async () => {
const stager = createStager()
const request = vi.fn(
async (): Promise<RpcResponse> =>
failure('Unknown method with host details', 'method_not_found')
const request = vi.fn(async (): Promise<RpcResponse> =>
failure('Unknown method with host details', 'method_not_found')
)
await expect(
@@ -37,7 +37,9 @@ describe('mobile web shell root route', () => {
})
it('loads the Android package document at the router root without widening navigation', () => {
expect(androidShellViewSource).toContain('webView.loadUrl("$MOBILE_WEB_ORIGIN/#$sessionId")')
expect(androidShellViewSource).toContain(
'webView.loadUrl("${mobileWebOriginForSession(sessionId)}/#$sessionId")'
)
expect(androidShellViewSource).toContain(
'val path = if (requestPath.isEmpty()) "index.html" else requestPath'
)
@@ -49,6 +51,8 @@ describe('mobile web shell root route', () => {
expect(androidShellViewSource).toContain('url.encodedPath == "/"')
expect(androidShellViewSource).toContain('url.query == null')
expect(androidShellViewSource).toContain('url.fragment == activeSessionId')
expect(androidPackageStoreSource).toContain('"url" to "$MOBILE_WEB_ORIGIN/#$sessionId"')
expect(androidPackageStoreSource).toContain(
'"url" to "${mobileWebOriginForSession(sessionId)}/#$sessionId"'
)
})
})
@@ -7,7 +7,12 @@ import {
type HostStackNavigationState
} from '../navigation/host-stack-navigation'
const homeSource = readFileSync(new URL('../../app/index.tsx', import.meta.url), 'utf8')
const homeSource = readFileSync(new URL('../home/MobileHomeScreen.tsx', import.meta.url), 'utf8')
const resumeSource = [
readFileSync(new URL('../home/MobileHomeListFooter.tsx', import.meta.url), 'utf8'),
readFileSync(new URL('../home/MobileHomeResumeCard.tsx', import.meta.url), 'utf8'),
readFileSync(new URL('../home/MobileHomeAccountUsageCards.tsx', import.meta.url), 'utf8')
].join('\n')
function navigationHarness(initialState: HostStackNavigationState) {
const stateListeners = new Set<() => void>()
@@ -97,20 +102,11 @@ describe('mobile session route', () => {
})
it('routes the home Resume card through the hybrid navigation intent', () => {
const start = homeSource.indexOf('{/* ─── Resume card ─── */}')
const end = homeSource.indexOf('{/* ─── Quick actions ─── */}', start)
// Assert the markers first: a renamed banner would otherwise slice garbage and
// report a missing call instead of the real cause.
expect(start).toBeGreaterThanOrEqual(0)
expect(end).toBeGreaterThan(start)
const resumeCard = homeSource.slice(start, end)
expect(resumeCard).toContain('openResume(')
expect(resumeCard).not.toContain('router.push(')
expect(resumeSource).toContain('Resume')
expect(resumeSource).toContain('onOpenResume')
const handlerStart = homeSource.indexOf('const openResume = useCallback(')
const handlerEnd = homeSource.indexOf('[router]', handlerStart)
const handlerEnd = homeSource.indexOf('[data.router]', handlerStart)
expect(handlerStart).toBeGreaterThanOrEqual(0)
expect(handlerEnd).toBeGreaterThan(handlerStart)
@@ -13,6 +13,10 @@ const terminalInventoryRecoverySource = readFileSync(
new URL('./use-mobile-terminal-inventory-recovery.ts', import.meta.url),
'utf8'
)
const terminalStreamPresentationSource = readFileSync(
new URL('./host-session-terminal-stream-presentation.ts', import.meta.url),
'utf8'
)
const autoCreateHookSource = readFileSync(
new URL('./use-initial-session-terminal-autocreate.ts', import.meta.url),
'utf8'
@@ -58,10 +62,10 @@ describe('mobile session startup', () => {
expect(reconciliationHookSource).toContain('sessionTabOperations.snapshot(worktreeId)')
expect(reconciliationHookSource).toContain('sessionTabOperations.subscribe(')
expect(reconciliationHookSource).toContain(
"if (AppState.currentState !== 'active') {\n controller.setReconciliationActive(false)"
"if (AppState.currentState !== 'active') {\n suspendTerminalInventoryRecovery(true)"
)
expect(reconciliationHookSource).toContain('void controller.poll()')
expect(reconciliationHookSource).toContain('void fetchTerminals()')
expect(reconciliationHookSource).toContain('controller.poll()')
expect(reconciliationHookSource).toContain('refreshTerminalInventory()')
expect(reconciliationHookSource).toContain("AppState.addEventListener('change'")
expect(reconciliationHookSource).toContain('const interval = setInterval(')
expect(reconciliationHookSource).toContain('RECONCILIATION_INTERVAL_MS = 2000')
@@ -72,7 +76,9 @@ describe('mobile session startup', () => {
})
it('confirms terminal stream teardown with a committed inventory-recovery bridge', () => {
expect(source).toContain("if (data.type === 'end' || data.type === 'error')")
expect(terminalStreamPresentationSource).toContain(
"if (data.type === 'end' || data.type === 'error')"
)
expect(source).toContain('signalTerminalInventoryRecovery()')
expect(terminalInventoryRecoverySource).toContain('actionRef.current = recoveryAction')
expect(terminalInventoryRecoverySource).toContain('pendingSignalScopeRef.current = scopeKey')
@@ -1,7 +1,6 @@
import { describe, expect, it } from 'vitest'
import {
getTerminalRecordsFromSessionTabs,
hasConnectedTerminalAbsentFromSessionTabs,
mergeTerminalListWithKnownRecords,
mergeTerminalRecordsByCurrentOrder,
mobileSessionTabsEqual,
@@ -25,25 +25,23 @@ export async function readWebHostDiffReviewMetadata(args: {
})
args.cache.revision = result.revision
return {
diffComments: result.comments.map(
(comment): DiffComment => ({
id: comment.id,
worktreeId: args.workspaceId,
filePath: comment.relativePath,
...(comment.oldRelativePath ? { oldPath: comment.oldRelativePath } : {}),
...(comment.source ? { source: comment.source } : {}),
...(comment.selectedText === undefined ? {} : { selectedText: comment.selectedText }),
...(comment.startLine === undefined ? {} : { startLine: comment.startLine }),
lineNumber: comment.lineNumber,
body: comment.body,
createdAt: comment.createdAt,
...(comment.updatedAt === undefined ? {} : { updatedAt: comment.updatedAt }),
...(comment.sentAt === undefined ? {} : { sentAt: comment.sentAt }),
...(comment.scope ? { scope: comment.scope } : {}),
...(comment.diffIdentity ? { diffIdentity: comment.diffIdentity } : {}),
side: 'modified'
})
),
diffComments: result.comments.map((comment): DiffComment => ({
id: comment.id,
worktreeId: args.workspaceId,
filePath: comment.relativePath,
...(comment.oldRelativePath ? { oldPath: comment.oldRelativePath } : {}),
...(comment.source ? { source: comment.source } : {}),
...(comment.selectedText === undefined ? {} : { selectedText: comment.selectedText }),
...(comment.startLine === undefined ? {} : { startLine: comment.startLine }),
lineNumber: comment.lineNumber,
body: comment.body,
createdAt: comment.createdAt,
...(comment.updatedAt === undefined ? {} : { updatedAt: comment.updatedAt }),
...(comment.sentAt === undefined ? {} : { sentAt: comment.sentAt }),
...(comment.scope ? { scope: comment.scope } : {}),
...(comment.diffIdentity ? { diffIdentity: comment.diffIdentity } : {}),
side: 'modified'
})),
mobileDiffReview: reviewStateFromWire(result)
}
}
@@ -8,7 +8,6 @@ const storageImportPattern =
/from ['"](?:@react-native-async-storage\/async-storage|expo-secure-store)['"]/
const EXPECTED_PERSISTED_STATE_SOURCES = [
'mobile/app/index.tsx',
'mobile/src/cache/home-snapshot-cache.ts',
'mobile/src/home/use-mobile-home-data.ts',
'mobile/src/mobile-web/mobile-web-cold-resume-route.ts',
@@ -199,9 +199,9 @@ describe('mobile E2EE v2 physical channel', () => {
await authenticate(ctx)
ctx.socket.bufferedAmount = 9 * 1024 * 1024
const megabyte = new Uint8Array(1024 * 1024)
for (let index = 0; index < 65; index++) {
expect(ctx.channel.sendBinary(megabyte)).toBe(true)
}
const accepted = Array.from({ length: 65 }, () => ctx.channel.sendBinary(megabyte))
expect(accepted.filter(Boolean).length).toBeLessThan(65)
expect(accepted.at(-1)).toBe(false)
expect(ctx.onError).toHaveBeenCalledOnce()
expect(ctx.onError.mock.calls[0]![0].message).toBe('E2EE v2 outbound buffer overflow')
})
+2 -2
View File
@@ -27,15 +27,15 @@ import type {
BrowserSelectResult,
BrowserSnapshotResult,
BrowserTabListResult,
BrowserTabInfo,
BrowserTabSwitchResult,
BrowserTypeResult,
BrowserUploadResult,
BrowserViewportResult,
BrowserWaitResult
} from '../../shared/runtime-types'
import { buildSnapshot, type CdpCommandSender, type RefEntry } from './snapshot-engine'
import { insertTextThroughCdp } from './browser-text-insertion'
import { readGuestNavigationState } from './browser-guest-navigation-state'
import { webContents } from 'electron'
import type { BrowserManager } from './browser-manager'
import type { CdpAuxiliaryCommands, CdpTabState } from './cdp-auxiliary-commands'
import { CdpBridgeCommandSet } from './cdp-bridge-command-set'
+8 -6
View File
@@ -1,5 +1,5 @@
import { EventEmitter } from 'node:events'
import type { ChildProcess } from 'node:child_process'
import type { ChildProcessWithoutNullStreams } from 'node:child_process'
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -15,29 +15,31 @@ import {
listRelayMarkdownDocumentPaths
} from './markdown-document-listing'
function successfulChild(output: Buffer): ChildProcess {
function successfulChild(output: Buffer): ChildProcessWithoutNullStreams {
const child = Object.assign(new EventEmitter(), {
exitCode: null,
signalCode: null,
stdin: new PassThrough(),
stdout: new PassThrough(),
stderr: new PassThrough(),
kill: vi.fn(() => true)
}) as unknown as ChildProcess
}) as unknown as ChildProcessWithoutNullStreams
queueMicrotask(() => {
child.stdout!.emit('data', output)
child.stdout.emit('data', output)
child.emit('close', 0, null)
})
return child
}
function unavailableChild(): ChildProcess {
function unavailableChild(): ChildProcessWithoutNullStreams {
const child = Object.assign(new EventEmitter(), {
exitCode: null,
signalCode: null,
stdin: new PassThrough(),
stdout: new PassThrough(),
stderr: new PassThrough(),
kill: vi.fn(() => true)
}) as unknown as ChildProcess
}) as unknown as ChildProcessWithoutNullStreams
queueMicrotask(() => {
const error = Object.assign(new Error('spawn rg ENOENT'), { code: 'ENOENT' })
child.emit('error', error)