Files
orca/mobile/scripts/hosted-ios-chat-e2e.mjs
T
Jinwoo-H 15d19c308b feat(mobile): move mobile sessions and settings into the hosted page
Session snapshot/feed/activate/close/createBrowser/createTerminal,
quick commands and agent options now run as Desktop mobileWeb.* adapters
behind the generic host-request forwarder instead of shell translators.
Voice, notification, terminal, browser, chat, About and diagnostics
settings render as hosted page routes with native-*/web-* operation
backends. Adds the hosted WebView e2e journeys used to validate them.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-07 04:16:39 -04:00

226 lines
8.4 KiB
JavaScript

import { longPressHostedIosPoint } from './hosted-ios-emulator-long-press.mjs'
import { readHostedWebViewControlPoint } from './hosted-webview-control-point.mjs'
import { captureAgentHistorySimulatorScreenshot as captureScreenshot } from './hosted-ios-agent-history-parity.mjs'
import { verifyHostedSessionActions } from './hosted-webview-session-actions-journey.mjs'
import {
readHostedChatPreference,
waitHostedChatPreference,
waitHostedChatPreferenceReady
} from './hosted-ios-chat-settings-journey.mjs'
import { mkdir } from 'node:fs/promises'
import path from 'node:path'
import { startCdpServer } from 'inspect-webkit'
import { stopHostedChildProcess } from './hosted-child-process-shutdown.mjs'
import { findAvailableHostedLoopbackPort } from './hosted-loopback-port.mjs'
import { startHostedIosEmulatorController } from './hosted-ios-emulator-controller.mjs'
import {
startHostedIosMobileLauncher,
waitForHostedIosMobileLauncher
} from './hosted-ios-mobile-launcher.mjs'
import {
bootHostedIosSimulator,
resolveHostedIosSimulatorUdid
} from './hosted-ios-simulator-device.mjs'
import { hostedIosSimulatorAppPreparation } from './hosted-ios-simulator-app-preparation.mjs'
import { completeHostedIosNativeOnboarding } from './hosted-ios-native-onboarding.mjs'
import { openHostedIosHybridRoute } from './hosted-ios-hybrid-route-handoff.mjs'
import {
createHostedChatRuntimeFixture,
HOSTED_CHAT_TERMINAL_TITLE
} from './hosted-chat-runtime-fixture.mjs'
import { verifyHostedChatJourney } from './hosted-webview-chat-journey.mjs'
import {
activateHostedWebViewControl,
evaluateHostedDocumentWithRetry,
readHostedWebViewState,
waitForVisibleHostedWebView
} from './hosted-webview-cdp-session.mjs'
import { openHostedIosSettings, closeHostedIosSettings } from './hosted-ios-settings-navigation.mjs'
import { activateHostedWorkspaceRow } from './hosted-webview-workspace-activation.mjs'
import { waitForHostedIosBuildActivation } from './hosted-ios-build-activation.mjs'
export async function runHostedIosChatE2e({ options, runtimeDirectory, orcaCli, worktree }) {
if (process.platform !== 'darwin') {
throw new Error('Hosted iOS chat requires macOS')
}
await mkdir(runtimeDirectory, { recursive: true, mode: 0o700 })
const deviceUdid = await resolveHostedIosSimulatorUdid(options.device)
let launcher, inspector, controller, fixture, observedDocument
try {
await bootHostedIosSimulator(deviceUdid)
controller = await startHostedIosEmulatorController({ orcaCli, runtimeDirectory, worktree })
const preparation = hostedIosSimulatorAppPreparation({ deviceUdid, worktree, ...options })
const nativeAppPath = await preparation.run()
launcher = startHostedIosMobileLauncher({
deviceUdid,
emulatorControlUserDataPath: controller.userData,
orcaCli,
runtimeDirectory,
worktree
})
await waitForHostedIosMobileLauncher(launcher, options.timeoutMs)
const emulator = { deviceUdid, orcaCli, userDataDir: controller.userData, worktree }
const expectedWorkspace = path.basename(worktree)
await completeHostedIosNativeOnboarding(emulator, expectedWorkspace, options.timeoutMs)
fixture = await createHostedChatRuntimeFixture({
orcaCli,
runtimeDirectory,
worktree,
timeoutMs: options.timeoutMs
})
const inspectorPort = await findAvailableHostedLoopbackPort()
inspector = await startCdpServer({ port: inspectorPort })
const discoveryUrl = `http://127.0.0.1:${inspectorPort}`
await openHostedIosHybridRoute(emulator, options.timeoutMs)
const args = { discoveryUrl, timeoutMs: options.timeoutMs, expectedWorkspace }
let workspace = await waitForVisibleHostedWebView({ ...args, expectedText: expectedWorkspace })
observedDocument = workspace
await evaluateHostedDocumentWithRetry(
workspace,
`(() => {
const entries = globalThis.__orcaChatBridgeErrors = [];
addEventListener('message', event => {
try {
const frame = JSON.parse(event.data);
if (frame.status === 'error' || frame.type === 'subscriptionClosed') {
entries.push({ type: frame.type, code: frame.error?.code, requestId: frame.requestId });
if (entries.length > 32) entries.shift();
}
} catch {}
});
return 'observing';
})()`
)
const settings = await openHostedIosSettings(
args,
workspace,
'Chat UI',
'/native-chat-settings',
'for this paired host'
)
await waitHostedChatPreferenceReady(settings, options.timeoutMs)
const originalPreference = await readHostedChatPreference(settings)
if (!originalPreference) {
await activateHostedWebViewControl(settings, {
kind: 'label',
value: 'Open sessions in Chat UI'
})
await waitHostedChatPreference(settings, true, options.timeoutMs)
}
workspace = await closeHostedIosSettings(args, settings)
await activateHostedWorkspaceRow(
workspace,
expectedWorkspace,
activateHostedWebViewControl,
options.timeoutMs,
() => waitForVisibleHostedWebView({ ...args, expectedText: expectedWorkspace })
)
const session = await waitForVisibleHostedWebView({
...args,
expectedHrefIncludes: '/session/',
expectedText: HOSTED_CHAT_TERMINAL_TITLE
})
await activateHostedWebViewControl(session, { kind: 'text', value: HOSTED_CHAT_TERMINAL_TITLE })
const chat = await verifyHostedChatJourney({
discoveryUrl,
timeoutMs: options.timeoutMs,
...fixture
}).catch(async (error) => {
console.error(
'Chat bridge errors:',
await evaluateHostedDocumentWithRetry(
session,
'JSON.stringify(globalThis.__orcaChatBridgeErrors ?? [])'
)
)
throw error
})
const screenshot = path.join(runtimeDirectory, 'hosted-chat-transcript.png')
await captureScreenshot(deviceUdid, screenshot)
const sessionActions = await verifyHostedSessionActions({
discoveryUrl,
timeoutMs: options.timeoutMs,
document: chat.document,
originalTitle: HOSTED_CHAT_TERMINAL_TITLE,
readTerminals: fixture.readTerminals,
longPress: async (document, title) =>
longPressHostedIosPoint(
emulator,
await readHostedWebViewControlPoint(document, title, undefined, { matchText: true })
)
})
await activateHostedWebViewControl(sessionActions.document, {
kind: 'label',
value: 'Back to worktrees'
})
workspace = await waitForVisibleHostedWebView({ ...args, expectedText: expectedWorkspace })
const restored = await openHostedIosSettings(
args,
workspace,
'Chat UI',
'/native-chat-settings',
'for this paired host'
)
await waitHostedChatPreferenceReady(restored, options.timeoutMs)
if (!originalPreference) {
await activateHostedWebViewControl(restored, {
kind: 'label',
value: 'Open sessions in Chat UI'
})
}
await waitHostedChatPreference(restored, originalPreference, options.timeoutMs)
await waitForHostedIosBuildActivation(deviceUdid, options, runtimeDirectory)
const bridgeErrors = JSON.parse(
await evaluateHostedDocumentWithRetry(
observedDocument,
'JSON.stringify(globalThis.__orcaChatBridgeErrors ?? [])'
)
)
if (bridgeErrors.some((entry) => entry.code === 'rate_limited')) {
throw new Error('Ordinary chat/session journey exceeded bridge admission')
}
console.log(
JSON.stringify(
{
ok: true,
device: deviceUdid,
nativeAppPath,
chat: { ...chat.evidence, screenshot },
sessionActions: sessionActions.evidence,
fixtureTerminal: fixture.handle,
bridgeErrors
},
null,
2
)
)
} catch (error) {
if (observedDocument) {
console.error(
'Chat failure page:',
JSON.stringify(await readHostedWebViewState(observedDocument))
)
console.error(
'Chat bridge errors:',
await evaluateHostedDocumentWithRetry(
observedDocument,
'JSON.stringify(globalThis.__orcaChatBridgeErrors ?? [])'
)
)
await captureScreenshot(deviceUdid, path.join(runtimeDirectory, 'hosted-chat-failure.png'))
}
throw error
} finally {
try {
await fixture?.cleanup()
} finally {
inspector?.stop()
try {
await stopHostedChildProcess(launcher)
} finally {
await controller?.stop()
}
}
}
}