mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
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
226 lines
8.4 KiB
JavaScript
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()
|
|
}
|
|
}
|
|
}
|
|
}
|