test(mobile): stabilize hosted Android journey

This commit is contained in:
OrcaWin
2026-08-09 18:35:02 -04:00
committed by Jinwoo-H
parent 279a1ff68e
commit 5112aa837d
8 changed files with 150 additions and 40 deletions
@@ -2924,4 +2924,7 @@ passes 3,854 files / 40,508 tests with 71 expected skips.
| 2026-07-29 | Complete | The exact Pixel 9 Pro API 36 arm64 Debug app passes the complete Android hostile route journey: Tasks provider title/body, a bounded hostile provider error, Session, live OSC-8 terminal links, Source Control/Review, hostile filename/diff/Markdown/HTML/SVG/PNG, and normal/malicious/invalid Mermaid. The private-origin network/navigation/executable, bridge-log, DOM/storage/cookie, and `ApplicationExitInfo` audits record zero executed marker or sentinel observation. |
| 2026-07-29 | Complete | Post-repair validation passes 596 mobile files / 3,552 tests with 2 expected skips, mobile formatting, root typecheck/lint/code-quality/reliability/localization/max-lines gates, and diff hygiene. Android reports 41 JVM tests plus exact Debug packaging. Independently verified RNW build `240b80e7da8601d062daf17e2dc88e0ca32c984f7580d4060bd5b877e0022c3d` contains 51 assets / 9,151,939 raw / 2,649,167 gzip bytes. The root suite passes 40,515 tests but hit one unrelated load-sensitive transcript watcher assertion; that 28-test file passes standalone. |
| 2026-07-29 | Finding | A stability rerun exposed a harness-only missed workspace activation: the automation call returned before proving the `/session/` transition and then consumed the full route timeout on the workspace list. The Android journey now bounds each transition wait, rereads the active document, and retries a missed activation up to three times. |
| 2026-07-29 | Complete | Rebased all 62 migration commits onto `origin/main` at `6c3b2cfb39`; the branch is zero behind. The conflict resolution preserves upstream orchestration and legacy-terminal SSH recovery together with bounded mobile recovery generations. The focused merged path passes 940 tests with 1 expected skip. |
| 2026-07-29 | Complete | The Android exact-app harness waits for the current process to mount React before pairing, selects the existing Tasks control through a nonvisual accessibility label instead of a fixed coordinate, proves each route transition before retrying, and scopes its final bridge audit to the Orca app PID. The final hostile route journey returns `ok: true` with zero executed marker, sentinel observation, bridge-log finding, or new failure exit record. |
| 2026-07-29 | Complete | Final post-rebase validation passes 597 mobile files / 3,562 tests with 2 expected skips, mobile typecheck/lint/format, and diff hygiene. Independently verified RNW build `121fe8682fc221fd7e6f2955fe1f246017d164db3122d71526bc3f66b19578c5` contains 51 assets / 9,151,993 raw / 2,649,166 gzip bytes. Android retains 41 passing JVM tests and successful Debug APK packaging; root typecheck/lint/code-quality, 56 reliability gates, localization, and max-lines passed after the rebase. |
| 2026-07-29 | Next | Continue broader live cross-host/workspace/topology races, sustained allocation and performance testing, physical-device/accessibility validation, independent security review, store-signed release drills, and App Store review. Feature implementation and the Android emulator content corpus are complete. |
@@ -150,7 +150,7 @@ prototype:
inventory entry, and fixtures are removed. The production `/hybrid` route,
production bridge clients, native fallback, and Experimental Settings entry
remain intentionally until the external cutover gates pass.
- The 61-commit branch is rebased onto `origin/main` at `4543bb6826` and remains
- The 62-commit branch is rebased onto `origin/main` at `6c3b2cfb39` and remains
zero behind. Upstream
native-chat launch-draft, transcript identity, loading, reconnect, and
orchestration behavior is retained in both native and hosted adapters.
@@ -1146,15 +1146,21 @@ and nested syntax text keeps the effective native font behavior. On iPhone 17
Pro Simulator, Source Control passes at 0.736% changed pixels / 0.910 mean
channel difference and Review at 2.134% / 1.947, within the 3% / 4 budgets.
Current validation passes 596 mobile files / 3,552 tests with 2 expected skips.
Current validation passes 597 mobile files / 3,562 tests with 2 expected skips.
The root suite passes 40,515 tests but hit one unrelated load-sensitive
transcript watcher assertion; its 28-test file passes standalone. All project
typechecks, root/mobile/mobile-web lint and code-quality audits, formatting,
localization, max-lines and diff hygiene, and the current 56 reliability gates
pass. React Doctor reports zero blocking migration error without suppressions.
The independently verified production package
`240b80e7da8601d062daf17e2dc88e0ca32c984f7580d4060bd5b877e0022c3d`
contains 51 assets and verifies at 9,151,939 raw bytes / 2,649,167 gzip bytes.
`121fe8682fc221fd7e6f2955fe1f246017d164db3122d71526bc3f66b19578c5`
contains 51 assets and verifies at 9,151,993 raw bytes / 2,649,166 gzip bytes.
The post-rebase exact Android journey passes Tasks, Session, terminal links,
Source Control/Review, hostile provider/file/diff/Markdown/HTML/SVG/PNG/Mermaid
content, all isolation probes, the app-scoped bridge audit, and the privacy and
exit-info audit with zero executed marker or sentinel observation. The harness
waits for React before pairing and locates the existing Tasks control through
its nonvisual accessibility label rather than a fixed screen coordinate.
The preceding exact package `3c0f364f…` passes the unpacked macOS arm64 → Docker SSH → actual iOS
WKWebView journey from a clean app reinstall in 1.9 minutes. Authenticated RPC
returned the packaged build with no checkout-output fallback; the unchanged
@@ -102,11 +102,12 @@ longer restart Preview loads, and RNW preserves the native iOS font fallback.
The unchanged Accounts screen now also passes deterministic iOS
native-versus-hosted parity at 0.050% changed pixels, 0.099 mean channel
difference, and 0.000544 vertical-title delta, within the 3% / 4 / 0.005
budgets. Its existing non-embedded toolbar icon has no native accessibility
label, so the fixture uses the unchanged icon position and leaves the semantic
gap for the broader VoiceOver review. The complete cached-app journey passes
with Accounts inserted before Tasks, Session, Files/Preview, Agent History,
Desktop restart/recovery, Source Control, Review, and both isolation probes.
budgets. Its existing non-embedded toolbar icon now exposes a nonvisual `Tasks`
accessibility label without changing layout or styling. The exact Android
fixture locates that control semantically and retries only after proving the
route did not open. The complete cached-app journey passes with Accounts
inserted before Tasks, Session, Files/Preview, Agent History, Desktop
restart/recovery, Source Control, Review, and both isolation probes.
The base workspace screen now has the same deterministic proof. Native and
hosted mount the unchanged `HostScreen` and pass at 0.879% changed pixels,
@@ -124,17 +125,22 @@ pixels and 0.910 mean channel difference; Review passes at 2.134% and 1.947,
within the 3% / 4 budgets. The packaged document opts into native safe-area
insets, and nested syntax text retains the native effective font behavior.
The migration is based on `origin/main` at `4543bb6826`; the final rebase is
complete and the branch is 61 commits ahead and zero behind. Post-rebase
validation plus the hostile-content slice now passes 593 mobile files / 3,530
tests with 2 expected skips. The latest full root run passes 3,854 files /
40,508 tests with 71 expected skips. All project typechecks,
The migration is based on `origin/main` at `6c3b2cfb39`; the final rebase is
complete and the branch is 62 commits ahead and zero behind. Post-rebase
validation plus the hostile-content slice now passes 597 mobile files / 3,562
tests with 2 expected skips. The focused merged SSH recovery path passes 940
tests with 1 expected skip. The latest full root run passes 40,515 tests apart
from one unrelated load-sensitive transcript watcher assertion whose 28-test
file passes standalone. All project typechecks,
root/mobile/mobile-web lint and code-quality audits, 56 reliability gates,
changed-file and full-mobile formatting, localization, the max-lines ratchet,
and diff hygiene pass. React Doctor reports zero new migration findings. The
independently verified React Native Web package is
`3c0f364f9cb6f1785d1d08fdeb81ca5367b091706c24d124374a88578839e745`:
50 assets, 9,135,273 raw bytes, and 2,644,558 gzip bytes.
`121fe8682fc221fd7e6f2955fe1f246017d164db3122d71526bc3f66b19578c5`:
51 assets, 9,151,993 raw bytes, and 2,649,166 gzip bytes. The final exact
Android journey passes the complete hostile route and isolation corpus with
zero executed marker, sentinel observation, bridge-log finding, or new failure
exit record.
The immediately preceding `7c7c673d…` package passed the unpacked macOS arm64 →
Docker SSH → actual iOS WKWebView journey from a clean app reinstall in 1.9
+2
View File
@@ -1056,6 +1056,8 @@ export function HostScreen({
style={styles.searchToggle}
onPress={() => navigateFromHostList(`/h/${hostId}/tasks`)}
disabled={connState !== 'connected'}
accessibilityRole="button"
accessibilityLabel="Tasks"
>
<List
size={16}
@@ -11,6 +11,7 @@ import {
readHostedWebViewTextPoint,
waitForVisibleHostedWebView
} from './hosted-webview-cdp-session.mjs'
import { readHostedWebViewControlPoint } from './hosted-webview-control-point.mjs'
const PROVIDER_DOM_EXPRESSION = `JSON.stringify((() => {
const mermaidFrames = Array.from(
@@ -48,33 +49,70 @@ const PROVIDER_DOM_EXPRESSION = `JSON.stringify((() => {
mermaidFrames
};
})())`
const TASKS_TOOLBAR_X = 0.87
export async function verifyHostedAdversarialTasks({
activatePoint,
discoveryUrl,
document,
timeoutMs
}) {
const filterPoint = await readHostedWebViewTextPoint(document, 'Filter')
await activatePoint({ x: TASKS_TOOLBAR_X, y: filterPoint.y })
const tasks = await waitForVisibleHostedWebView({
discoveryUrl,
expectedText: HOSTED_ADVERSARIAL_PROVIDER_TITLE_MARKER,
expectedHrefIncludes: '/tasks',
timeoutMs
})
let activeDocument = document
let tasks
let lastError
for (let attempt = 0; attempt < 3 && !tasks; attempt += 1) {
if (!activeDocument.href.includes('/tasks')) {
await activatePoint(await readHostedWebViewControlPoint(activeDocument, 'Tasks'))
}
try {
tasks = await waitForVisibleHostedWebView({
discoveryUrl,
expectedText: HOSTED_ADVERSARIAL_PROVIDER_TITLE_MARKER,
expectedHrefIncludes: '/tasks',
timeoutMs: Math.min(timeoutMs, 15_000)
})
} catch (error) {
lastError = error
activeDocument = await waitForVisibleHostedWebView({
discoveryUrl,
expectedText: '',
requireInteractiveControls: false,
timeoutMs
})
}
}
if (!tasks) {
throw lastError ?? new Error('Hosted adversarial Tasks route did not open')
}
const evidence = await waitForProviderEvidence(tasks, timeoutMs, {
errorMarker: true,
titleMarker: true
})
const titlePoint = await readHostedWebViewTextPoint(tasks, 'Tasks')
await activatePoint({ x: Math.max(0.04, titlePoint.x - 0.12), y: titlePoint.y })
const workspaceDocument = await waitForVisibleHostedWebView({
discoveryUrl,
expectedText: 'Orca Desktop',
timeoutMs
})
activeDocument = tasks
let workspaceDocument
lastError = undefined
for (let attempt = 0; attempt < 3 && !workspaceDocument; attempt += 1) {
if (activeDocument.href.includes('/tasks')) {
const titlePoint = await readHostedWebViewTextPoint(activeDocument, 'Tasks')
await activatePoint({ x: Math.max(0.04, titlePoint.x - 0.12), y: titlePoint.y })
}
try {
workspaceDocument = await waitForVisibleHostedWebView({
discoveryUrl,
expectedText: 'Orca Desktop',
timeoutMs: Math.min(timeoutMs, 15_000)
})
} catch (error) {
lastError = error
activeDocument = await waitForVisibleHostedWebView({
discoveryUrl,
expectedText: '',
requireInteractiveControls: false,
timeoutMs
})
}
}
if (!workspaceDocument) {
throw lastError ?? new Error('Hosted adversarial Tasks route did not close')
}
return { evidence, workspaceDocument }
}
@@ -123,6 +123,28 @@ export async function launchHostedAndroidDevClient(adb, metroPort, probe) {
)
}
export async function waitForHostedAndroidReactReady(adb, timeoutMs, runAdb = runAndroidAdb) {
const deadline = Date.now() + timeoutMs
let lastError = 'Android app process is unavailable'
while (Date.now() < deadline) {
try {
const pid = await runAdb(adb, ['shell', 'pidof', packageName])
if (!/^\d+$/u.test(pid)) {
throw new Error('Android app process is unavailable')
}
const logcat = await runAdb(adb, ['logcat', '--pid', pid, '-d', '-v', 'brief'])
if (/Running "main"/u.test(logcat)) {
return pid
}
lastError = 'React main has not mounted'
} catch (error) {
lastError = error instanceof Error ? error.message : String(error)
}
await delay(250)
}
throw new Error(`Android React runtime was unavailable: ${lastError}`)
}
export function openHostedAndroidUrl(adb, url) {
return runAndroidAdb(
adb,
@@ -188,11 +210,12 @@ export function findHostedAndroidBridgeLogFailures(logcat, appPid) {
})
}
export async function assertHostedAndroidBridgeLogClean(adb) {
const [logcat, appPid] = await Promise.all([
runAndroidAdb(adb, ['logcat', '-d', '-v', 'brief']),
runAndroidAdb(adb, ['shell', 'pidof', packageName])
])
export async function assertHostedAndroidBridgeLogClean(adb, runAdb = runAndroidAdb) {
const appPid = await runAdb(adb, ['shell', 'pidof', packageName])
if (!/^\d+$/u.test(appPid)) {
throw new Error('Android app process is unavailable')
}
const logcat = await runAdb(adb, ['logcat', '--pid', appPid, '-d', '-v', 'brief'])
const failures = findHostedAndroidBridgeLogFailures(logcat, appPid)
if (failures.length > 0) {
throw new Error(`Android bridge emitted errors:\n${failures.slice(0, 16).join('\n')}`)
@@ -35,7 +35,8 @@ import {
openHostedAndroidUrl,
resolveHostedAndroidAdb,
startHostedAndroidMetro,
stopHostedAndroidApp
stopHostedAndroidApp,
waitForHostedAndroidReactReady
} from './hosted-android-emulator-session.mjs'
import { runAndroidAdb } from './hosted-android-mobile-web-cache.mjs'
import { HOSTED_MOBILE_APP_ROUTE_URL } from './hosted-mobile-e2e-launch.mjs'
@@ -164,6 +165,7 @@ async function main() {
await stage('development client launch', () =>
launchHostedAndroidDevClient(adb, metro.port, probe)
)
await stage('React runtime', () => waitForHostedAndroidReactReady(adb, options.timeoutMs))
const emulator = { adb }
await stage('native pairing', () =>
pairAndroidApp(emulator, runtime.pairingUrl, options.timeoutMs)
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest'
import { findHostedAndroidBridgeLogFailures } from '../../scripts/hosted-android-emulator-session.mjs'
import {
assertHostedAndroidBridgeLogClean,
findHostedAndroidBridgeLogFailures,
waitForHostedAndroidReactReady
} from '../../scripts/hosted-android-emulator-session.mjs'
describe('hosted Android emulator session', () => {
it('finds native bridge rejection, conversion, cast, and process failures', () => {
@@ -33,4 +37,30 @@ describe('hosted Android emulator session', () => {
' E/AndroidRuntime(20200): FATAL EXCEPTION: main'
])
})
it('waits for the current Android app process to mount React main', async () => {
const responses = [
'7301',
'I/ReactNativeJS: Loading bundle',
'7301',
'I/ReactNativeJS: Running "main"'
]
const runAdb = async () => responses.shift() ?? ''
await expect(waitForHostedAndroidReactReady('adb', 2_000, runAdb)).resolves.toBe('7301')
})
it('scopes the bridge audit to the current Android app process', async () => {
const calls: string[][] = []
const runAdb = async (_adb: string, args: string[]) => {
calls.push(args)
return calls.length === 1 ? '7301' : 'I/chromium: hosted route'
}
await expect(assertHostedAndroidBridgeLogClean('adb', runAdb)).resolves.toBeUndefined()
expect(calls).toEqual([
['shell', 'pidof', 'com.stably.orca.mobile'],
['logcat', '--pid', '7301', '-d', '-v', 'brief']
])
})
})