mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
test(mobile): stabilize hosted Android journey
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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']
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user