diff --git a/docs/reference/mobile-hybrid-webview-devtools.md b/docs/reference/mobile-hybrid-webview-devtools.md
index 33911f52806..543df655e49 100644
--- a/docs/reference/mobile-hybrid-webview-devtools.md
+++ b/docs/reference/mobile-hybrid-webview-devtools.md
@@ -141,7 +141,6 @@ inspectable APK again.
## Gates
```bash
-pnpm test mobile/src/mobile-web/hosted-webview-cdp-session.test.ts
pnpm test mobile/src/mobile-web/hosted-android-inspectable-release.test.ts
cd mobile && npx tsc --noEmit -p .
pnpm run check:code-quality:changed
diff --git a/mobile/src/mobile-web/hosted-adversarial-provider-fixture.test.ts b/mobile/src/mobile-web/hosted-adversarial-provider-fixture.test.ts
deleted file mode 100644
index 5e2523b05ae..00000000000
--- a/mobile/src/mobile-web/hosted-adversarial-provider-fixture.test.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import { readFileSync } from 'node:fs'
-import { readFile } from 'node:fs/promises'
-import path from 'node:path'
-import { describe, expect, it } from 'vitest'
-import { hostedAdversarialGitHubResponse } from '../../scripts/hosted-adversarial-github-cli.mjs'
-import {
- createHostedAdversarialProviderFixture,
- HOSTED_ADVERSARIAL_MERMAID_MARKER,
- HOSTED_ADVERSARIAL_PROVIDER_ERROR_MARKER,
- HOSTED_ADVERSARIAL_PROVIDER_MARKER,
- HOSTED_ADVERSARIAL_PROVIDER_TITLE_MARKER,
- removeHostedAdversarialProviderFixture
-} from '../../scripts/hosted-adversarial-provider-fixture.mjs'
-import {
- createHostedAdversarialRepositoryFixture,
- removeHostedAdversarialRepositoryFixture
-} from '../../scripts/hosted-adversarial-repository-fixture.mjs'
-
-const config = {
- baseOid: 'a'.repeat(40),
- branch: 'orca-adversarial-row',
- body: 'provider body',
- comment: `${HOSTED_ADVERSARIAL_MERMAID_MARKER}\n\n\`\`\`mermaid\ngraph TD\nStart --> Done\n\`\`\``,
- error: `${HOSTED_ADVERSARIAL_PROVIDER_ERROR_MARKER}: hostile`,
- headOid: 'b'.repeat(40),
- title: `${HOSTED_ADVERSARIAL_PROVIDER_TITLE_MARKER}: hostile`,
- updatedAt: '2026-07-29T00:00:00Z'
-}
-const androidHarnessSource = readFileSync(
- new URL('../../scripts/run-hosted-android-source-control-review-e2e.mjs', import.meta.url),
- 'utf8'
-)
-
-describe('hosted adversarial provider fixture', () => {
- it('returns one hostile task while another repository error stays bounded', async () => {
- const tasks = await hostedAdversarialGitHubResponse(
- ['api', 'search/issues?q=repo%3Aorca-e2e%2Fadversarial'],
- config
- )
- const issues = await hostedAdversarialGitHubResponse(
- ['api', 'search/issues?q=repo%3Astablyai%2Forca'],
- config
- )
-
- expect(tasks.code).toBe(0)
- expect(JSON.parse(tasks.stdout)).toMatchObject([
- {
- number: 17,
- title: expect.stringContaining(HOSTED_ADVERSARIAL_PROVIDER_TITLE_MARKER)
- }
- ])
- expect(issues).toMatchObject({
- code: 1,
- stderr: expect.stringContaining(HOSTED_ADVERSARIAL_PROVIDER_ERROR_MARKER)
- })
- expect(issues.stderr.length).toBeLessThanOrEqual(1_000)
- })
-
- it('registers success and error repositories in the Android runtime', () => {
- expect(androidHarnessSource).toContain("stage('provider error workspace registration'")
- expect(androidHarnessSource).toContain('registerWorktreeForPairingRuntime(runtime, worktree, {')
- expect(androidHarnessSource).toContain(
- 'registerWorktreeForPairingRuntime(runtime, testWorkspace, {'
- )
- })
-
- it('returns a provider comment with the Mermaid corpus', async () => {
- const response = await hostedAdversarialGitHubResponse(
- [
- 'api',
- 'graphql',
- '-f',
- 'query=query { repository { pullRequest { reviewThreads(first: 100) { nodes { id } } } } }'
- ],
- config
- )
- const parsed = JSON.parse(response.stdout)
-
- expect(parsed.data.repository.pullRequest.comments.nodes[0].body).toContain(
- HOSTED_ADVERSARIAL_MERMAID_MARKER
- )
- expect(parsed.data.repository.pullRequest.comments.nodes[0].body).toContain('```mermaid')
- })
-
- it('installs an isolated cross-platform gh shim and repository remote', async () => {
- const repository = await createHostedAdversarialRepositoryFixture()
- let fixture
- try {
- fixture = await createHostedAdversarialProviderFixture({
- probePort: 43210,
- repositoryRoot: repository.root
- })
- const fixtureConfig = JSON.parse(await readFile(fixture.configPath, 'utf8'))
-
- expect(fixture.environment.PATH.split(path.delimiter)[0]).toContain(fixture.root)
- expect(fixtureConfig.body).toContain(`\`${HOSTED_ADVERSARIAL_PROVIDER_MARKER}\``)
- expect(fixtureConfig.comment).toContain(`\`${HOSTED_ADVERSARIAL_MERMAID_MARKER}\``)
- expect(fixtureConfig.comment).toContain('http://127.0.0.1:43210/')
- expect(fixtureConfig.comment).toContain(
- 'Start --> Parse --> Sanitize --> Render --> Verify --> Done'
- )
- expect(await readFile(path.join(fixture.root, 'bin', 'gh.cmd'), 'utf8')).toContain(
- 'hosted-adversarial-github-cli.mjs'
- )
- } finally {
- if (fixture) {
- await removeHostedAdversarialProviderFixture(fixture, repository.root)
- }
- await removeHostedAdversarialRepositoryFixture(repository)
- }
- })
-})
diff --git a/mobile/src/mobile-web/hosted-adversarial-terminal-links.test.ts b/mobile/src/mobile-web/hosted-adversarial-terminal-links.test.ts
deleted file mode 100644
index f9be97adf39..00000000000
--- a/mobile/src/mobile-web/hosted-adversarial-terminal-links.test.ts
+++ /dev/null
@@ -1,299 +0,0 @@
-import { readFileSync } from 'node:fs'
-import { describe, expect, it, vi } from 'vitest'
-import {
- HOSTED_TERMINAL_HTTP_LINK_LABEL,
- HOSTED_TERMINAL_JAVASCRIPT_LINK_LABEL,
- HOSTED_TERMINAL_FILE_LINK_LABEL,
- hostedAdversarialTerminalLinkCommand,
- hostedAdversarialTerminalLinkSafetyEvidence,
- stageHostedAdversarialTerminalLinksWithInput,
- verifyHostedAdversarialTerminalLinks
-} from '../../scripts/hosted-adversarial-terminal-links.mjs'
-
-const androidHarnessSource = readFileSync(
- new URL('../../scripts/run-hosted-android-source-control-review-e2e.mjs', import.meta.url),
- 'utf8'
-)
-const androidTerminalHarnessSource = readFileSync(
- new URL('../../scripts/hosted-android-adversarial-terminal-links.mjs', import.meta.url),
- 'utf8'
-)
-const iosHarnessSource = readFileSync(
- new URL('../../scripts/run-hosted-webview-simulator-e2e.mjs', import.meta.url),
- 'utf8'
-)
-
-describe('hosted adversarial terminal links', () => {
- it('emits bounded OSC-8 HTTP and javascript rows through a portable Node command', () => {
- const command = hostedAdversarialTerminalLinkCommand(43210, 'PROBE-TOKEN', '001-adversarial.md')
- const encoded = command.match(/Buffer\.from\('([^']+)'/)?.[1]
-
- expect(command).not.toContain('javascript:')
- expect(encoded).toBeTruthy()
- const output = Buffer.from(encoded ?? '', 'base64').toString()
- expect(output).toContain('\u001B]8;;http://127.0.0.1:43210/terminal-link/PROBE-TOKEN\u001B\\')
- expect(output).toContain(
- "\u001B]8;;javascript:globalThis.__ORCA_HOSTED_TERMINAL_LINK_EXECUTED__='executed'\u001B\\"
- )
- expect(output).toContain('\u001B]8;;001-adversarial.md:1\u001B\\')
- expect(output.match(new RegExp(HOSTED_TERMINAL_HTTP_LINK_LABEL, 'gu'))).toHaveLength(2)
- expect(output.match(new RegExp(HOSTED_TERMINAL_JAVASCRIPT_LINK_LABEL, 'gu'))).toHaveLength(2)
- expect(output.match(new RegExp(HOSTED_TERMINAL_FILE_LINK_LABEL, 'gu'))).toHaveLength(2)
- })
-
- it('proves the allowed file tap before rejecting a native javascript tap', async () => {
- const initialDocument = {
- href: 'orca-mobile-web://build/h/host/session/workspace'
- }
- const resumedDocument = {
- href: 'orca-mobile-web://build/h/host/session/workspace',
- targetId: 'resumed'
- }
- const filePoint = { x: 0.2, y: 0.7 }
- const httpPoint = { x: 0.2, y: 0.6 }
- const javascriptPoint = { x: 0.2, y: 0.8 }
- const probe = {
- observations: [],
- port: 43210,
- reset() {
- this.observations.splice(0)
- },
- token: 'PROBE-TOKEN'
- }
- const tapPoint = vi.fn().mockResolvedValue(undefined)
- const waitForDocument = vi.fn().mockResolvedValue(resumedDocument)
- const writeLinks = vi.fn().mockResolvedValue('terminal-handle')
-
- await expect(
- verifyHostedAdversarialTerminalLinks(
- {
- discoveryUrl: 'http://127.0.0.1:9222',
- document: initialDocument,
- emulator: { deviceUdid: 'simulator' },
- orcaCli: './config/scripts/orca-dev.mjs',
- pairingRuntimeUserDataPath: '/tmp/pairing/userData',
- positiveFilePath: '001-adversarial.md',
- probe,
- tapPoint,
- terminalHandle: 'terminal-handle',
- timeoutMs: 30_000,
- worktree: '/repo/mobile-rearch'
- },
- {
- activateTerminal: vi.fn().mockResolvedValue(undefined),
- enableDiagnostics: vi.fn().mockResolvedValue(undefined),
- readPoints: vi
- .fn()
- .mockResolvedValueOnce({
- file: filePoint,
- http: httpPoint,
- javascript: javascriptPoint
- })
- .mockResolvedValueOnce({
- file: filePoint,
- http: httpPoint,
- javascript: javascriptPoint
- }),
- readState: vi.fn().mockResolvedValue({
- href: resumedDocument.href,
- javascriptMarkerPresent: false,
- javascriptMarkerValue: null
- }),
- settle: vi.fn().mockResolvedValue(undefined),
- waitForDocument,
- writeLinks
- }
- )
- ).resolves.toEqual({
- evidence: {
- fileLinkOpenedAfterNativeTap: true,
- httpLinkRequestedWithoutTap: false,
- javascriptLinkEscaped: false,
- javascriptLinkExecuted: false,
- javascriptTapPoint: javascriptPoint,
- routeRetained: true,
- terminalHandle: 'terminal-handle'
- },
- sessionDocument: resumedDocument
- })
-
- expect(tapPoint).toHaveBeenNthCalledWith(1, { deviceUdid: 'simulator' }, filePoint)
- expect(tapPoint).toHaveBeenNthCalledWith(2, { deviceUdid: 'simulator' }, javascriptPoint)
- expect(writeLinks).toHaveBeenCalledWith({
- orcaCli: './config/scripts/orca-dev.mjs',
- pairingRuntimeUserDataPath: '/tmp/pairing/userData',
- positiveFilePath: '001-adversarial.md',
- probePort: 43210,
- probeToken: 'PROBE-TOKEN',
- terminalHandle: 'terminal-handle',
- timeoutMs: 30_000,
- worktree: '/repo/mobile-rearch'
- })
- expect(waitForDocument).toHaveBeenCalledWith({
- discoveryUrl: 'http://127.0.0.1:9222',
- expectedHrefIncludes: '/files/preview/',
- expectedText: '001-adversarial.md',
- requireInteractiveControls: false,
- timeoutMs: 5_000
- })
- expect(probe.observations).toEqual([])
- })
-
- it('fails closed on execution, navigation, or escaped network traffic', () => {
- const safeState = {
- href: 'orca-mobile-web://build/h/host/session/workspace',
- javascriptMarkerPresent: false,
- javascriptMarkerValue: null
- }
- expect(
- hostedAdversarialTerminalLinkSafetyEvidence({
- observations: [],
- state: safeState
- })
- ).toEqual({
- javascriptLinkEscaped: false,
- javascriptLinkExecuted: false,
- routeRetained: true
- })
- expect(() =>
- hostedAdversarialTerminalLinkSafetyEvidence({
- observations: [],
- state: {
- ...safeState,
- javascriptMarkerPresent: true,
- javascriptMarkerValue: 'executed'
- }
- })
- ).toThrow('javascript link executed')
- expect(() =>
- hostedAdversarialTerminalLinkSafetyEvidence({
- observations: ['http:/terminal-link/PROBE-TOKEN'],
- state: safeState
- })
- ).toThrow('javascript link escaped')
- })
-
- it('retries a missed native file-link activation before testing javascript', async () => {
- const document = { href: 'orca-mobile-web://build/h/host/session/workspace' }
- const tapPoint = vi.fn().mockResolvedValue(undefined)
- const waitForDocument = vi
- .fn()
- .mockRejectedValueOnce(new Error('missed native touch'))
- .mockResolvedValue(document)
- const readPoints = vi.fn().mockResolvedValue({
- file: { x: 0.2, y: 0.7 },
- http: { x: 0.2, y: 0.6 },
- javascript: { x: 0.2, y: 0.8 }
- })
-
- await verifyHostedAdversarialTerminalLinks(
- {
- discoveryUrl: 'http://127.0.0.1:9222',
- document,
- emulator: {},
- positiveFilePath: '001-adversarial.md',
- probe: { observations: [], port: 43210, token: 'PROBE-TOKEN' },
- tapPoint,
- timeoutMs: 30_000
- },
- {
- activateTerminal: vi.fn().mockResolvedValue(undefined),
- enableDiagnostics: vi.fn().mockResolvedValue(undefined),
- readPoints,
- readState: vi.fn().mockResolvedValue({
- href: document.href,
- javascriptMarkerPresent: false,
- javascriptMarkerValue: null
- }),
- settle: vi.fn().mockResolvedValue(undefined),
- waitForDocument,
- writeLinks: vi.fn().mockResolvedValue('terminal-handle')
- }
- )
-
- expect(waitForDocument).toHaveBeenCalledTimes(3)
- expect(readPoints).toHaveBeenCalledTimes(3)
- expect(tapPoint).toHaveBeenCalledTimes(3)
- })
-
- it('taps the duplicate file-link row after a missed native activation', async () => {
- const document = { href: 'orca-mobile-web://build/h/host/session/workspace' }
- const file = { x: 0.2, y: 0.7 }
- const fileAlternate = { x: 0.2, y: 0.72 }
- const tapPoint = vi.fn().mockResolvedValue(undefined)
- const prepareFileTap = vi.fn().mockResolvedValue(undefined)
-
- await verifyHostedAdversarialTerminalLinks(
- {
- discoveryUrl: 'http://127.0.0.1:9222',
- document,
- emulator: {},
- positiveFilePath: '001-adversarial.md',
- probe: { observations: [], port: 43210, token: 'PROBE-TOKEN' },
- tapPoint,
- timeoutMs: 30_000
- },
- {
- activateTerminal: vi.fn().mockResolvedValue(undefined),
- enableDiagnostics: vi.fn().mockResolvedValue(undefined),
- prepareFileTap,
- readPoints: vi.fn().mockResolvedValue({
- file,
- fileAlternate,
- http: { x: 0.2, y: 0.6 },
- javascript: { x: 0.2, y: 0.8 }
- }),
- readState: vi.fn().mockResolvedValue({
- href: document.href,
- javascriptMarkerPresent: false,
- javascriptMarkerValue: null
- }),
- settle: vi.fn().mockResolvedValue(undefined),
- waitForDocument: vi
- .fn()
- .mockRejectedValueOnce(new Error('missed'))
- .mockResolvedValue(document),
- writeLinks: vi.fn().mockResolvedValue('terminal-handle')
- }
- )
-
- expect(tapPoint).toHaveBeenNthCalledWith(1, {}, file)
- expect(tapPoint).toHaveBeenNthCalledWith(2, {}, fileAlternate)
- expect(prepareFileTap).toHaveBeenCalledTimes(2)
- })
-
- it('retries missed live terminal activations without changing the staged command', async () => {
- const inputCommand = vi.fn().mockResolvedValue(undefined)
- const waitForStage = vi
- .fn()
- .mockRejectedValueOnce(new Error('missed'))
- .mockRejectedValueOnce(new Error('missed'))
- .mockResolvedValue(undefined)
- const prepared = {
- command: 'node .git/stage.cjs',
- terminalHandle: 'terminal-handle'
- }
- const prepare = vi.fn().mockResolvedValue(prepared)
-
- await expect(
- stageHostedAdversarialTerminalLinksWithInput({ timeoutMs: 30_000 }, inputCommand, {
- prepare,
- waitForStage
- })
- ).resolves.toBe('terminal-handle')
-
- expect(inputCommand).toHaveBeenCalledTimes(3)
- expect(inputCommand).toHaveBeenCalledWith(prepared.command)
- expect(prepare).toHaveBeenCalledWith({ timeoutMs: 30_000 })
- expect(waitForStage).toHaveBeenCalledTimes(3)
- expect(waitForStage).toHaveBeenCalledWith(prepared, 5_000)
- })
-
- it('keeps the exact iOS and Android adversarial gates wired to the same corpus', () => {
- expect(iosHarnessSource).toContain('verifyHostedIosAdversarialTerminalLinks')
- expect(androidHarnessSource).toContain('verifyHostedAndroidAdversarialTerminalLinks')
- expect(androidTerminalHarnessSource).toContain('verifyHostedAdversarialTerminalLinks')
- expect(iosHarnessSource).toContain('adversarial terminal links')
- expect(androidHarnessSource).toContain('adversarial terminal links')
- })
-})
diff --git a/mobile/src/mobile-web/hosted-android-adversarial-terminal-links.test.ts b/mobile/src/mobile-web/hosted-android-adversarial-terminal-links.test.ts
deleted file mode 100644
index b65a44a322c..00000000000
--- a/mobile/src/mobile-web/hosted-android-adversarial-terminal-links.test.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import {
- dismissHostedAndroidKeyboardIfShown,
- hostedAndroidAdversarialTerminalInputText,
- verifyHostedAndroidAdversarialTerminalLinks
-} from '../../scripts/hosted-android-adversarial-terminal-links.mjs'
-
-describe('hosted Android adversarial terminal links', () => {
- it('stages links through the focused live terminal input', async () => {
- const activateTerminal = vi.fn().mockResolvedValue(undefined)
- const dismissKeyboard = vi.fn().mockResolvedValue(false)
- const runAdb = vi.fn().mockResolvedValue('')
- const stageWithInput = vi.fn(async (_args, inputCommand) => {
- await inputCommand('node ".git/orca-mobile-terminal-links.cjs"')
- return 'terminal-handle'
- })
- const tapControl = vi.fn().mockResolvedValue(undefined)
- const verifyLinks = vi.fn(async (_args, operations) => {
- return operations.writeLinks({ timeoutMs: 30_000 })
- })
- const args = {
- document: { targetId: 'android-webview' },
- emulator: { adb: '/sdk/platform-tools/adb' },
- timeoutMs: 30_000
- }
-
- await expect(
- verifyHostedAndroidAdversarialTerminalLinks(args, {
- activateTerminal,
- dismissKeyboard,
- runAdb,
- stageWithInput,
- tapControl,
- verifyLinks
- })
- ).resolves.toBe('terminal-handle')
-
- expect(activateTerminal).toHaveBeenCalledWith(args.document, {
- kind: 'text',
- value: 'Mobile Emulator'
- })
- expect(tapControl).toHaveBeenCalledWith(
- args.emulator,
- 'Show keyboard for live terminal input',
- 30_000
- )
- expect(runAdb).toHaveBeenNthCalledWith(1, args.emulator.adb, [
- 'shell',
- 'input',
- 'text',
- 'node%s.git/orca-mobile-terminal-links.cjs'
- ])
- expect(runAdb).toHaveBeenNthCalledWith(2, args.emulator.adb, [
- 'shell',
- 'input',
- 'keyevent',
- 'KEYCODE_ENTER'
- ])
- expect(runAdb).toHaveBeenCalledTimes(2)
- expect(dismissKeyboard).toHaveBeenCalledWith(args.emulator.adb, runAdb)
- })
-
- it('dismisses only an observed Android input method', async () => {
- const visibleRunAdb = vi
- .fn()
- .mockResolvedValueOnce(
- 'InsetsSource id=20b40001 type=navigationBars frame=[0,2712][1280,2856] visible=true\nmImeShowing=true'
- )
- .mockResolvedValueOnce('')
- .mockResolvedValueOnce('mImeShowing=false')
- await expect(
- dismissHostedAndroidKeyboardIfShown('/sdk/platform-tools/adb', visibleRunAdb)
- ).resolves.toBe(true)
- expect(visibleRunAdb).toHaveBeenNthCalledWith(2, '/sdk/platform-tools/adb', [
- 'shell',
- 'input',
- 'tap',
- '128',
- '2784'
- ])
-
- const hiddenRunAdb = vi.fn().mockResolvedValue('mImeShowing=false')
- await expect(
- dismissHostedAndroidKeyboardIfShown('/sdk/platform-tools/adb', hiddenRunAdb)
- ).resolves.toBe(false)
- expect(hiddenRunAdb).toHaveBeenCalledOnce()
- })
-
- it('accepts only the generated disposable script command', () => {
- expect(
- hostedAndroidAdversarialTerminalInputText('node ".git/orca-mobile-terminal-links.cjs"')
- ).toBe('node%s.git/orca-mobile-terminal-links.cjs')
- expect(() => hostedAndroidAdversarialTerminalInputText('node other.cjs')).toThrow(
- 'staging command is invalid'
- )
- })
-})
diff --git a/mobile/src/mobile-web/hosted-android-agent-history-journey.test.ts b/mobile/src/mobile-web/hosted-android-agent-history-journey.test.ts
deleted file mode 100644
index 275604c6a92..00000000000
--- a/mobile/src/mobile-web/hosted-android-agent-history-journey.test.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-const mocks = vi.hoisted(() => ({
- activateControl: vi.fn(),
- readControlPoint: vi.fn(),
- readState: vi.fn(),
- readStablePoint: vi.fn(),
- readTextPoint: vi.fn(),
- setInput: vi.fn(),
- tapPoint: vi.fn(),
- waitForDocument: vi.fn()
-}))
-
-vi.mock('../../scripts/hosted-webview-cdp-session.mjs', () => ({
- activateHostedWebViewControl: mocks.activateControl,
- readHostedWebViewState: mocks.readState,
- readHostedWebViewTextPoint: mocks.readTextPoint,
- setHostedWebViewInput: mocks.setInput,
- waitForVisibleHostedWebView: mocks.waitForDocument
-}))
-
-vi.mock('../../scripts/hosted-android-emulator-accessibility.mjs', () => ({
- tapHostedAndroidPoint: mocks.tapPoint
-}))
-
-vi.mock('../../scripts/hosted-android-webview-touch-point.mjs', () => ({
- readStableHostedAndroidWebViewPoint: mocks.readStablePoint
-}))
-
-vi.mock('../../scripts/hosted-webview-control-point.mjs', () => ({
- readHostedWebViewControlPoint: mocks.readControlPoint
-}))
-
-import { verifyHostedAndroidAgentHistoryJourney } from '../../scripts/hosted-android-agent-history-journey.mjs'
-
-describe('hosted Android Agent History journey', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- mocks.activateControl.mockResolvedValue(undefined)
- mocks.readControlPoint.mockResolvedValue({ x: 0.4, y: 0.3 })
- mocks.readStablePoint.mockImplementation((readPoint) => readPoint())
- mocks.readTextPoint.mockResolvedValue({ x: 0.5, y: 0.4 })
- mocks.setInput.mockResolvedValue(undefined)
- mocks.tapPoint.mockImplementation((_emulator, point) => point)
- mocks.readState.mockResolvedValue({
- href: 'orca-mobile-web://build/h/host/agent-history/workspace',
- bodyText: 'Workspace Project All',
- labels: ['Back', 'Refresh agent sessions', 'Resume agent session'],
- placeholders: ['Search sessions, repo:, path:']
- })
- mocks.waitForDocument.mockImplementation(({ expectedHrefIncludes, expectedText }) => ({
- href: `orca-mobile-web://build/h/host${
- expectedHrefIncludes ??
- (expectedText === '2 tabs' || expectedText === 'Agent History'
- ? '/session/'
- : '/agent-history/')
- }workspace`
- }))
- })
-
- it('covers the existing scopes, preview, search, native resume, and Back flow', async () => {
- const sessionDocument = {
- href: 'orca-mobile-web://build/h/host/session/workspace'
- }
-
- const result = await verifyHostedAndroidAgentHistoryJourney({
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { adb: '/sdk/adb' },
- sessionDocument,
- timeoutMs: 30_000
- })
-
- const { returnedSessionDocument, ...evidence } = result
- expect(evidence).toEqual({
- route: 'orca-mobile-web://build/h/host/agent-history/workspace',
- scopes: ['Workspace', 'Project', 'All'],
- search: 'present',
- row: 'Hybrid Agent History Fixture',
- preview: 'E2E_AGENT_HISTORY_PREVIEW_MARKER',
- resume: {
- native: 'queued',
- nativeTouchPoint: { x: 0.4, y: 0.3 },
- resumedRoute: 'orca-mobile-web://build/h/host/session/workspace'
- },
- headerControls: ['Back', 'Refresh agent sessions']
- })
- expect(returnedSessionDocument.href).toContain('/session/')
- expect(mocks.setInput).toHaveBeenNthCalledWith(1, expect.anything(), {
- placeholder: 'Search sessions, repo:, path:',
- value: 'NO_MATCHING_AGENT_HISTORY_SESSION'
- })
- expect(mocks.setInput).toHaveBeenNthCalledWith(2, expect.anything(), {
- placeholder: 'Search sessions, repo:, path:',
- value: ''
- })
- expect(mocks.readControlPoint.mock.calls.map((call) => call[1])).toEqual([
- 'More session actions',
- 'Resume agent session'
- ])
- expect(mocks.readTextPoint.mock.calls.map((call) => call[1])).toEqual(['Agent History'])
- expect(mocks.tapPoint).toHaveBeenCalledTimes(3)
- })
-})
diff --git a/mobile/src/mobile-web/hosted-android-emulator-accessibility.test.ts b/mobile/src/mobile-web/hosted-android-emulator-accessibility.test.ts
deleted file mode 100644
index bee57a9806d..00000000000
--- a/mobile/src/mobile-web/hosted-android-emulator-accessibility.test.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import {
- parseHostedAndroidAccessibilityControls,
- parseHostedAndroidDisplaySize,
- tapHostedAndroidAccessibilityControl,
- tapHostedAndroidPoint
-} from '../../scripts/hosted-android-emulator-accessibility.mjs'
-
-const emulator = { adb: '/sdk/platform-tools/adb' }
-
-describe('hosted Android emulator accessibility controls', () => {
- it('parses enabled labels, descriptions, entities, and bounds', () => {
- const controls = parseHostedAndroidAccessibilityControls(`
-
-
-
-
-
- `)
-
- expect(controls).toEqual([
- {
- label: 'Pair & continue',
- enabled: true,
- bounds: { left: 10, top: 20, right: 110, bottom: 220 }
- },
- {
- label: 'Open source control',
- enabled: true,
- bounds: { left: 20, top: 30, right: 220, bottom: 330 }
- },
- {
- label: 'Disabled',
- enabled: false,
- bounds: { left: 0, top: 0, right: 10, bottom: 10 }
- }
- ])
- })
-
- it('prefers the override display size', () => {
- expect(
- parseHostedAndroidDisplaySize('Physical size: 1280x2856\nOverride size: 1000x2200')
- ).toEqual({ width: 1000, height: 2200 })
- })
-
- it('taps the center of an exact accessible label', async () => {
- const runAdb = vi
- .fn()
- .mockResolvedValueOnce(
- ''
- )
- .mockResolvedValueOnce('')
-
- await expect(
- tapHostedAndroidAccessibilityControl(emulator, 'Pair', 1_000, runAdb)
- ).resolves.toEqual({ x: 200, y: 300 })
- expect(runAdb).toHaveBeenLastCalledWith(emulator.adb, ['shell', 'input', 'tap', '200', '300'])
- })
-
- it('maps a normalized WebView point to physical display pixels', async () => {
- const runAdb = vi
- .fn()
- .mockResolvedValueOnce('Physical size: 1280x2856')
- .mockResolvedValueOnce('')
-
- await expect(tapHostedAndroidPoint(emulator, { x: 0.25, y: 0.5 }, runAdb)).resolves.toEqual({
- x: 320,
- y: 1428
- })
- expect(runAdb).toHaveBeenLastCalledWith(emulator.adb, ['shell', 'input', 'tap', '320', '1428'])
- })
-})
diff --git a/mobile/src/mobile-web/hosted-android-emulator-session.test.ts b/mobile/src/mobile-web/hosted-android-emulator-session.test.ts
deleted file mode 100644
index deabeb0a86c..00000000000
--- a/mobile/src/mobile-web/hosted-android-emulator-session.test.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import {
- assertHostedAndroidBridgeLogClean,
- findHostedAndroidBridgeLogFailures,
- installAndResetHostedAndroidApp,
- waitForHostedAndroidReactReady
-} from '../../scripts/hosted-android-emulator-session.mjs'
-
-describe('hosted Android emulator session', () => {
- it('finds native bridge rejection, conversion, cast, and process failures', () => {
- const failures = findHostedAndroidBridgeLogFailures(`
- E/ReactNativeJS: Call to function 'ExpoMobileWebShell.postViewMessage' has been rejected.
- E/ReactNativeJS: java.lang.IllegalArgumentException: mobile_web_shell_view_unavailable
- E/ExpoModules: Cannot convert '{}' to a Kotlin type
- E/AndroidRuntime: java.lang.ClassCastException: View cannot be cast to MobileWebShellView
- E/AndroidRuntime: FATAL EXCEPTION: main
- `)
-
- expect(failures).toHaveLength(5)
- })
-
- it('ignores normal Android runtime and WebView output', () => {
- expect(
- findHostedAndroidBridgeLogFailures(`
- D/AndroidRuntime: Calling main entry com.android.commands.uiautomator.Launcher
- I/chromium: source: https://orca-mobile-web.invalid/
- `)
- ).toEqual([])
- })
-
- it('ignores automation-process fatals while retaining app-process fatals', () => {
- const logcat = `
- E/AndroidRuntime(20196): FATAL EXCEPTION: UiAutomation
- E/AndroidRuntime(20200): FATAL EXCEPTION: main
- `
-
- expect(findHostedAndroidBridgeLogFailures(logcat, '20200')).toEqual([
- ' 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('allows an exact debug APK to replace a higher-version beta install', async () => {
- const calls: { args: string[]; timeout?: number }[] = []
- const runAdb = async (_adb: string, args: string[], timeout?: number) => {
- calls.push({ args, timeout })
- return ''
- }
-
- await installAndResetHostedAndroidApp('adb', '/tmp/app-debug.apk', runAdb)
-
- expect(calls[0]).toEqual({
- args: ['install', '-r', '-d', '-t', '/tmp/app-debug.apk'],
- timeout: 120_000
- })
- expect(calls[1]?.args).toEqual(['shell', 'pm', 'clear', 'com.stably.orca.mobile'])
- })
-
- 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']
- ])
- })
-})
diff --git a/mobile/src/mobile-web/hosted-android-hardware-back-journey.test.ts b/mobile/src/mobile-web/hosted-android-hardware-back-journey.test.ts
deleted file mode 100644
index b30acc39deb..00000000000
--- a/mobile/src/mobile-web/hosted-android-hardware-back-journey.test.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import {
- openHostedAndroidHardwareBackNestedRoute,
- sendHostedAndroidHardwareBack,
- waitForHostedAndroidWorkspaceRoot,
- verifyHostedAndroidHardwareBackJourney
-} from '../../scripts/hosted-android-hardware-back-journey.mjs'
-
-describe('hosted Android hardware Back journey', () => {
- it('uses the real Android Back key event', async () => {
- const runAdb = vi.fn(async () => '')
-
- await sendHostedAndroidHardwareBack('/sdk/adb', runAdb)
-
- expect(runAdb).toHaveBeenCalledWith('/sdk/adb', ['shell', 'input', 'keyevent', 'KEYCODE_BACK'])
- })
-
- it('opens the nested route through the hosted UI navigation stack', async () => {
- const sessionDocument = { href: 'orca-mobile-web://build/h/host/session/workspace' }
- const actionsDocument = { href: sessionDocument.href }
- const historyDocument = {
- href: 'orca-mobile-web://build/h/host/agent-history/workspace'
- }
- const activate = vi.fn().mockResolvedValue(undefined)
- const settle = vi.fn().mockResolvedValue(undefined)
- const waitForDocument = vi
- .fn()
- .mockResolvedValueOnce(actionsDocument)
- .mockResolvedValueOnce(historyDocument)
-
- await expect(
- openHostedAndroidHardwareBackNestedRoute(
- {
- discoveryUrl: 'http://127.0.0.1:9222',
- sessionDocument,
- timeoutMs: 30_000
- },
- { activate, settle, waitForDocument }
- )
- ).resolves.toBe(historyDocument)
-
- expect(activate.mock.calls.map((call) => call[1])).toEqual([
- { kind: 'label', value: 'More session actions' },
- { kind: 'text', value: 'Agent History' }
- ])
- expect(waitForDocument).toHaveBeenLastCalledWith(
- expect.objectContaining({ expectedHrefIncludes: '/agent-history/' })
- )
- expect(settle).toHaveBeenCalledWith(500)
- })
-
- it('waits for the Session route to leave before accepting the workspace root', async () => {
- const waitForDocument = vi
- .fn()
- .mockResolvedValueOnce({ href: '/session/workspace' })
- .mockResolvedValueOnce({ href: '/session/workspace' })
- .mockResolvedValueOnce({ href: '/' })
- const settle = vi.fn().mockResolvedValue(undefined)
-
- await expect(
- waitForHostedAndroidWorkspaceRoot({
- discoveryUrl: 'http://127.0.0.1:9222',
- settle,
- timeoutMs: 30_000,
- waitForDocument,
- workspaceMarker: 'MOBILE-REARCH'
- })
- ).resolves.toEqual({ href: '/' })
-
- expect(settle).toHaveBeenCalledTimes(2)
- })
-
- it('returns through Session and workspace root before dismissing to native home', async () => {
- const events: string[] = []
- const pressBack = vi.fn(async () => events.push('back'))
- const openAgentHistory = vi.fn(async () => {
- events.push('agent-history')
- return { href: 'orca-mobile-web://build/h/host/agent-history/workspace' }
- })
- const waitForDocument = vi.fn(async ({ expectedHrefIncludes }) => {
- const route = expectedHrefIncludes ? 'session' : 'workspace'
- events.push(route)
- return { href: `orca-mobile-web://build/h/host/${route}` }
- })
- const waitForNativeControl = vi.fn(async () => {
- events.push('native-shell')
- return { label: 'Open settings' }
- })
-
- const evidence = await verifyHostedAndroidHardwareBackJourney(
- {
- adb: '/sdk/adb',
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { adb: '/sdk/adb' },
- sessionDocument: { href: 'orca-mobile-web://build/h/host/session/workspace' },
- timeoutMs: 30_000,
- workspaceMarker: 'MOBILE-REARCH'
- },
- { openAgentHistory, pressBack, waitForDocument, waitForNativeControl }
- )
-
- expect(events).toEqual([
- 'agent-history',
- 'back',
- 'session',
- 'back',
- 'workspace',
- 'back',
- 'native-shell'
- ])
- expect(evidence).toEqual({
- nestedRoute: 'orca-mobile-web://build/h/host/agent-history/workspace',
- returnedSessionRoute: 'orca-mobile-web://build/h/host/session',
- workspaceRootRoute: 'orca-mobile-web://build/h/host/workspace',
- nativeShellControl: 'Open settings',
- hardwareBackPresses: 3
- })
- expect(waitForDocument).toHaveBeenNthCalledWith(
- 1,
- expect.objectContaining({ expectedHrefIncludes: '/session/', expectedText: '1 tab' })
- )
- expect(waitForNativeControl).toHaveBeenCalledWith(
- { adb: '/sdk/adb' },
- [
- 'Open settings',
- 'Open sessions in Chat UI',
- 'Open sessions in the terminal',
- 'Show paired hosts'
- ],
- 30_000
- )
- })
-
- it('rejects a nested route that remains visible at the workspace step', async () => {
- await expect(
- verifyHostedAndroidHardwareBackJourney(
- {
- adb: '/sdk/adb',
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { adb: '/sdk/adb' },
- sessionDocument: { href: 'orca-mobile-web://build/h/host/session/workspace' },
- timeoutMs: 30_000,
- workspaceMarker: 'MOBILE-REARCH'
- },
- {
- openAgentHistory: async () => ({ href: '/agent-history/' }),
- pressBack: async () => {},
- waitForWorkspaceRoot: async () => ({ href: '/agent-history/' }),
- waitForDocument: async ({ expectedHrefIncludes }) => ({
- href: expectedHrefIncludes ? '/session/' : '/agent-history/'
- }),
- waitForNativeControl: vi.fn()
- }
- )
- ).rejects.toThrow('did not reach the workspace root')
- })
-})
diff --git a/mobile/src/mobile-web/hosted-android-mobile-web-cache.test.ts b/mobile/src/mobile-web/hosted-android-mobile-web-cache.test.ts
deleted file mode 100644
index bf3322f880b..00000000000
--- a/mobile/src/mobile-web/hosted-android-mobile-web-cache.test.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { readFileSync } from 'node:fs'
-import { describe, expect, it, vi } from 'vitest'
-import {
- parseAndroidMobileWebActivation,
- readAndroidRollbackActivation,
- readSingleAndroidActivation
-} from '../../scripts/hosted-android-mobile-web-cache.mjs'
-
-const active = 'a'.repeat(64)
-const previous = 'b'.repeat(64)
-const harnessSource = readFileSync(
- new URL('../../scripts/run-hosted-android-corrupt-cache.mjs', import.meta.url),
- 'utf8'
-)
-
-describe('hosted Android mobile web cache evidence', () => {
- it('normalizes an omitted previous generation and rejects invalid identities', () => {
- expect(parseAndroidMobileWebActivation(JSON.stringify({ active }))).toEqual({
- active,
- previous: null
- })
- expect(() => parseAndroidMobileWebActivation(JSON.stringify({ active: '../active' }))).toThrow(
- 'Android cache returned an invalid activation record'
- )
- })
-
- it('selects the only rollback candidate', async () => {
- const runAdb = activationExecutor([
- ['cache/one/activation.json', { active }],
- ['cache/two/activation.json', { active, previous }]
- ])
-
- await expect(readAndroidRollbackActivation('adb', runAdb)).resolves.toEqual({
- path: 'cache/two/activation.json',
- active,
- previous
- })
- })
-
- it('requires exactly one activation record for corruption drills', async () => {
- const runAdb = activationExecutor([['cache/one/activation.json', { active }]])
-
- await expect(readSingleAndroidActivation('adb', runAdb)).resolves.toEqual({
- path: 'cache/one/activation.json',
- active,
- previous: null
- })
- })
-
- it('keeps the offline corruption drill bounded and reversible', () => {
- expect(harnessSource).toContain("process.kill(options.desktopPid, 'SIGSTOP')")
- expect(harnessSource).toContain("process.kill(options.desktopPid, 'SIGCONT')")
- expect(harnessSource).toContain('repairFixture(')
- expect(harnessSource).toContain('requireGenerationRemoved(')
- expect(harnessSource).toContain('expectedText: options.expectedText')
- })
-})
-
-function activationExecutor(records: [string, { active: string; previous?: string }][]) {
- return vi.fn(async (_command: string, args: string[]) => {
- if (args.includes('find')) {
- return records.map(([path]) => path).join('\n')
- }
- const path = args.at(-1)
- const record = records.find(([candidate]) => candidate === path)?.[1]
- if (!record) {
- throw new Error('unexpected Android activation path')
- }
- return JSON.stringify(record)
- })
-}
diff --git a/mobile/src/mobile-web/hosted-android-privacy-audit.test.ts b/mobile/src/mobile-web/hosted-android-privacy-audit.test.ts
deleted file mode 100644
index d2df19ad2db..00000000000
--- a/mobile/src/mobile-web/hosted-android-privacy-audit.test.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import { readFileSync } from 'node:fs'
-import { describe, expect, it } from 'vitest'
-import {
- hostedAndroidExitInfoEvidence,
- hostedAndroidPrivacyLogEvidence
-} from '../../scripts/hosted-android-privacy-audit.mjs'
-
-const priorExit = exitInfo(0, 100, 10, 'USER REQUESTED')
-const routeHarnessSource = readFileSync(
- new URL('../../scripts/run-hosted-android-source-control-review-e2e.mjs', import.meta.url),
- 'utf8'
-)
-
-describe('hosted Android privacy audit', () => {
- it('keeps DOM, log, and process-exit audits in the live route gate', () => {
- expect(routeHarnessSource).toContain('verifyHostedWebViewPrivacyIsolation')
- expect(routeHarnessSource).toContain('readHostedAndroidExitInfo')
- expect(routeHarnessSource).toContain('verifyHostedAndroidPrivacyAudit')
- })
-
- it('accepts fixed-category logs', () => {
- expect(hostedAndroidPrivacyLogEvidence('I/Orca: state=connected retries=1')).toMatchObject({
- logBytes: 33,
- counts: {
- privilegedField: 0,
- tokenStorage: 0,
- nativeAuthority: 0,
- privateOriginUrl: 0,
- webSocketUrl: 0,
- fixtureMarker: 0
- }
- })
- })
-
- it.each([
- 'deviceToken',
- 'orca.host-token.host-a',
- 'openHostLogicalClient',
- 'orca-mobile-web://abcdefghijklmnopqrstuvwxyz/',
- 'wss://paired-host.invalid/socket',
- 'ORCA_E2E_MOBILE_WEB_HOST_PUBLIC_KEY'
- ])('rejects privileged log marker %s', (marker) => {
- expect(() => hostedAndroidPrivacyLogEvidence(marker)).toThrow(
- 'Hosted Android privacy log audit failed'
- )
- })
-
- it('allows only exact debug-server and deliberate probe WebSockets', () => {
- const evidence = hostedAndroidPrivacyLogEvidence(
- [
- 'ws://10.0.2.2:8081/message?device=Pixel%209%20-%2016%20-%20API%2036&app=com.stably.orca.mobile&clientid=BridgelessDevSupportManager',
- 'ws://10.0.2.2:8081/message?device=Pixel%209%20-%2016%20-%20API%2036&app=com.stably.orca.mobile&clientid=DevLauncherBridgelessDevSupportManager',
- 'ws://10.0.2.2:57999/message?device=Pixel%209&app=com.stably.orca.mobile&clientid=BridgelessDevSupportManager',
- 'ws://127.0.0.1:53250/socket-probe'
- ].join('\n'),
- { devServerPort: 57999, probePort: 53250 }
- )
-
- expect(evidence.expectedDebugWebSocketUrls).toBe(4)
- expect(evidence.counts.webSocketUrl).toBe(0)
- })
-
- it.each([
- 'ws://10.0.2.2:8081/message?device=Pixel&app=com.stably.orca.mobile&clientid=BridgelessDevSupportManager&token=secret',
- 'ws://10.0.2.2:8081/message?device=Pixel&app=other&clientid=BridgelessDevSupportManager',
- 'ws://10.0.2.2:8082/message?device=Pixel&app=com.stably.orca.mobile&clientid=BridgelessDevSupportManager',
- 'wss://127.0.0.1:53250/socket-probe',
- 'ws://127.0.0.1:53250/socket-probe?token=secret'
- ])('rejects mutated debug WebSocket %s', (url) => {
- expect(() =>
- hostedAndroidPrivacyLogEvidence(url, { devServerPort: 8081, probePort: 53250 })
- ).toThrow('Hosted Android privacy log audit failed')
- })
-
- it('ignores renumbered baseline exits and accepts a new force-stop', () => {
- const evidence = hostedAndroidExitInfoEvidence(
- priorExit,
- `${exitInfo(0, 200, 10, 'USER REQUESTED')}\n${exitInfo(1, 100, 10, 'USER REQUESTED')}`
- )
-
- expect(evidence).toMatchObject({
- currentRecords: 2,
- newRecords: 1,
- failureRecords: 0
- })
- })
-
- it.each([
- [4, 'CRASH'],
- [5, 'NATIVE CRASH'],
- [6, 'ANR'],
- [7, 'INITIALIZATION FAILURE'],
- [9, 'EXCESSIVE RESOURCE USAGE']
- ])('rejects new process failure reason %s', (reason, label) => {
- expect(() =>
- hostedAndroidExitInfoEvidence(priorExit, `${exitInfo(0, 200, reason, label)}\n${priorExit}`)
- ).toThrow('recorded a process failure')
- })
-
- it('rejects privileged markers in new exit records', () => {
- expect(() =>
- hostedAndroidExitInfoEvidence(
- priorExit,
- `${exitInfo(0, 200, 10, 'USER REQUESTED', 'publicKeyB64')}\n${priorExit}`
- )
- ).toThrow('Hosted Android exit-info privacy log audit failed')
- })
-})
-
-function exitInfo(index, timestamp, reason, label, description = 'test') {
- return `ApplicationExitInfo #${index}:
- timestamp=${timestamp} pid=${timestamp}
- process=com.stably.orca.mobile reason=${reason} (${label}) status=0
- importance=100 description=${description} trace=null`
-}
diff --git a/mobile/src/mobile-web/hosted-android-workspace-activation.test.ts b/mobile/src/mobile-web/hosted-android-workspace-activation.test.ts
deleted file mode 100644
index 8b487eef633..00000000000
--- a/mobile/src/mobile-web/hosted-android-workspace-activation.test.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-
-const mocks = vi.hoisted(() => ({
- activateControl: vi.fn(),
- readTextPoint: vi.fn(),
- tapPoint: vi.fn()
-}))
-
-vi.mock('../../scripts/hosted-webview-cdp-session.mjs', () => ({
- activateHostedWebViewControl: mocks.activateControl,
- readHostedWebViewTextPoint: mocks.readTextPoint
-}))
-
-vi.mock('../../scripts/hosted-android-emulator-accessibility.mjs', () => ({
- tapHostedAndroidPoint: mocks.tapPoint
-}))
-
-import {
- activateHostedAndroidWorkspaceControl,
- prepareHostedAndroidWorkspaceInput
-} from '../../scripts/hosted-android-workspace-activation.mjs'
-
-describe('hosted Android workspace activation', () => {
- beforeEach(() => {
- vi.useFakeTimers()
- vi.clearAllMocks()
- mocks.activateControl.mockResolvedValue(undefined)
- mocks.readTextPoint.mockResolvedValue({ x: 0.2, y: 0.3 })
- mocks.tapPoint.mockResolvedValue(undefined)
- })
-
- afterEach(() => {
- vi.useRealTimers()
- })
-
- it('primes WebView focus before workspace input', async () => {
- const emulator = { adb: '/sdk/adb' }
- const result = prepareHostedAndroidWorkspaceInput(emulator)
-
- await vi.advanceTimersByTimeAsync(10_000)
- await result
-
- expect(mocks.tapPoint).toHaveBeenCalledWith(emulator, { x: 0.5, y: 0.75 })
- })
-
- it('uses the grouped text control instead of a non-hittable ARIA node', async () => {
- const document = { href: 'https://orca-mobile-web.invalid/' }
-
- await expect(
- activateHostedAndroidWorkspaceControl({}, document, {
- kind: 'label',
- value: 'Open mobile-rearch',
- reveal: true
- })
- ).rejects.toThrow('Hosted WebView control was not found')
-
- await activateHostedAndroidWorkspaceControl({}, document, {
- kind: 'text',
- value: 'mobile-rearch',
- reveal: true
- })
-
- expect(mocks.activateControl).toHaveBeenCalledOnce()
- })
-
- it('waits for visible row coordinates to stabilize before tapping', async () => {
- const emulator = { adb: '/sdk/adb' }
- const result = activateHostedAndroidWorkspaceControl(
- emulator,
- {},
- {
- kind: 'text',
- value: 'mobile-rearch',
- occurrence: 1,
- reveal: true
- }
- )
-
- await vi.advanceTimersByTimeAsync(3_000)
- await result
-
- expect(mocks.readTextPoint).toHaveBeenCalledWith(
- {},
- 'mobile-rearch',
- undefined,
- expect.objectContaining({ occurrence: 1 })
- )
- expect(mocks.tapPoint).toHaveBeenCalledWith(emulator, { x: 0.2, y: 0.3 })
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-accounts-parity.test.ts b/mobile/src/mobile-web/hosted-ios-accounts-parity.test.ts
deleted file mode 100644
index d21319794ff..00000000000
--- a/mobile/src/mobile-web/hosted-ios-accounts-parity.test.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-const mocks = vi.hoisted(() => ({
- captureScreenshot: vi.fn(),
- compareScreenshots: vi.fn(),
- dismissDeveloperMenu: vi.fn(),
- readTextPoint: vi.fn(),
- tapPoint: vi.fn(),
- waitForControl: vi.fn(),
- waitForControlByPrefix: vi.fn(),
- waitForDocument: vi.fn()
-}))
-
-vi.mock('node:child_process', () => ({
- execFile: mocks.captureScreenshot
-}))
-vi.mock('../../scripts/emulator-developer-menu-dismissal.mjs', () => ({
- dismissEmulatorDeveloperMenuIfPresent: mocks.dismissDeveloperMenu
-}))
-vi.mock('../../scripts/hosted-ios-emulator-accessibility.mjs', () => ({
- tapHostedIosPoint: mocks.tapPoint,
- waitForHostedIosAccessibilityControl: mocks.waitForControl,
- waitForHostedIosAccessibilityControlByLabelPrefix: mocks.waitForControlByPrefix
-}))
-vi.mock('../../scripts/hosted-ios-screenshot-parity.mjs', () => ({
- assertHostedIosScreenshotParity: mocks.compareScreenshots
-}))
-vi.mock('../../scripts/hosted-webview-cdp-session.mjs', () => ({
- readHostedWebViewTextPoint: mocks.readTextPoint,
- waitForVisibleHostedWebView: mocks.waitForDocument
-}))
-
-import {
- captureHostedAccountsParity,
- captureNativeAccountsBaseline
-} from '../../scripts/hosted-ios-accounts-parity.mjs'
-
-describe('hosted iOS Accounts parity', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- mocks.captureScreenshot.mockImplementation((_command, _args, callback) =>
- callback(null, '', '')
- )
- mocks.compareScreenshots.mockResolvedValue({ changedPixelRatio: 0.01 })
- mocks.readTextPoint.mockResolvedValue({ x: 0.2, y: 0.1 })
- mocks.tapPoint.mockResolvedValue(undefined)
- mocks.waitForControl.mockResolvedValue({ x: 0.2, y: 0.1 })
- mocks.waitForControlByPrefix.mockResolvedValue({ x: 0.2, y: 0.1 })
- mocks.waitForDocument.mockImplementation(({ expectedHrefIncludes }) => ({
- href: `orca-mobile-web://build${expectedHrefIncludes ?? '/h/host'}`,
- targetId: expectedHrefIncludes ?? 'workspace'
- }))
- })
-
- it('captures native Accounts and returns to worktrees', async () => {
- const baseline = await captureNativeAccountsBaseline({
- deviceUdid: 'simulator',
- emulator: { deviceUdid: 'simulator' },
- expectedWorkspace: 'mobile-rearch',
- runtimeDirectory: '/tmp/parity',
- timeoutMs: 30_000
- })
-
- expect(mocks.tapPoint).toHaveBeenNthCalledWith(
- 1,
- { deviceUdid: 'simulator' },
- { x: 0.8, y: 0.1 }
- )
- expect(mocks.waitForControl).toHaveBeenCalledWith(
- { deviceUdid: 'simulator' },
- 'Add or re-authenticate accounts from desktop Settings → Accounts.',
- 30_000
- )
- expect(baseline).toEqual({
- screenTitlePoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-accounts-portrait.png'
- })
- })
-
- it('compares hosted Accounts and returns to the workspace route', async () => {
- const result = await captureHostedAccountsParity({
- deviceUdid: 'simulator',
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- expectedWorkspace: 'mobile-rearch',
- nativeBaseline: {
- screenTitlePoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-accounts-portrait.png'
- },
- runtimeDirectory: '/tmp/parity',
- timeoutMs: 30_000,
- workspaceDocument: { href: 'orca-mobile-web://build/h/host' }
- })
-
- expect(mocks.readTextPoint).toHaveBeenCalledWith(expect.anything(), 'Filter')
- expect(mocks.tapPoint).toHaveBeenCalledWith({ deviceUdid: 'simulator' }, { x: 0.8, y: 0.1 })
- expect(mocks.compareScreenshots).toHaveBeenCalledOnce()
- expect(result.evidence.screenshotParity).toEqual({ changedPixelRatio: 0.01 })
- expect(result.workspaceDocument.href).toContain('/h/host')
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-adversarial-terminal-staging.test.ts b/mobile/src/mobile-web/hosted-ios-adversarial-terminal-staging.test.ts
deleted file mode 100644
index 1a846be421b..00000000000
--- a/mobile/src/mobile-web/hosted-ios-adversarial-terminal-staging.test.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import { verifyHostedIosAdversarialTerminalLinks } from '../../scripts/hosted-ios-adversarial-terminal-links.mjs'
-
-describe('hosted iOS adversarial terminal staging', () => {
- it('reuses the registration-stage corpus without a second terminal send', async () => {
- const stageLinks = vi.fn()
- const verifyLinks = vi.fn(async (args, operations) => ({
- evidence: {
- terminalHandle: await operations.writeLinks({
- terminalHandle: args.terminalHandle
- })
- },
- sessionDocument: { href: 'orca-mobile-web://build/session' }
- }))
-
- await expect(
- verifyHostedIosAdversarialTerminalLinks(
- {
- stagedTerminalHandle: 'pre-staged-terminal'
- },
- { stageLinks, verifyLinks }
- )
- ).resolves.toMatchObject({
- evidence: { terminalHandle: 'pre-staged-terminal' },
- yOffset: 0
- })
-
- expect(verifyLinks).toHaveBeenCalledWith(
- expect.objectContaining({ terminalHandle: 'pre-staged-terminal' }),
- expect.any(Object)
- )
- expect(stageLinks).not.toHaveBeenCalled()
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-agent-history-parity.test.ts b/mobile/src/mobile-web/hosted-ios-agent-history-parity.test.ts
deleted file mode 100644
index 31fe69319d4..00000000000
--- a/mobile/src/mobile-web/hosted-ios-agent-history-parity.test.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-const {
- dismissDeveloperMenu,
- rotateEmulator,
- tapControl,
- tapControlByPrefix,
- typeText,
- waitByPrefix
-} = vi.hoisted(() => ({
- dismissDeveloperMenu: vi.fn(),
- rotateEmulator: vi.fn(),
- tapControl: vi.fn(),
- tapControlByPrefix: vi.fn(),
- typeText: vi.fn(),
- waitByPrefix: vi.fn()
-}))
-
-vi.mock('../../scripts/emulator-developer-menu-dismissal.mjs', () => ({
- dismissEmulatorDeveloperMenuIfPresent: dismissDeveloperMenu
-}))
-vi.mock('../../scripts/hosted-ios-emulator-accessibility.mjs', () => ({
- rotateHostedIosEmulator: rotateEmulator,
- tapHostedIosAccessibilityControl: tapControl,
- tapHostedIosAccessibilityControlByLabelPrefix: tapControlByPrefix,
- typeHostedIosText: typeText,
- waitForHostedIosAccessibilityControl: vi.fn(),
- waitForHostedIosAccessibilityControlByLabelPrefix: waitByPrefix
-}))
-
-import { openNativeAgentHistoryBaseline } from '../../scripts/hosted-ios-agent-history-parity.mjs'
-
-describe('native Agent History baseline', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- vi.useFakeTimers()
- })
-
- it('dismisses a reopened Expo developer overlay before navigating', async () => {
- const emulator = { deviceUdid: 'simulator' }
- const opened = openNativeAgentHistoryBaseline({
- emulator,
- expectedWorkspace: 'mobile-rearch',
- timeoutMs: 30_000
- })
-
- await vi.runAllTimersAsync()
- await opened
-
- expect(dismissDeveloperMenu).toHaveBeenCalledWith(emulator)
- expect(rotateEmulator).toHaveBeenCalledWith(emulator, 'portrait')
- expect(tapControlByPrefix).toHaveBeenNthCalledWith(1, emulator, 'mobile-rearch', 30_000)
- expect(tapControl).toHaveBeenCalledWith(emulator, 'More session actions', 30_000)
- expect(tapControlByPrefix).toHaveBeenNthCalledWith(2, emulator, 'Agent History', 30_000)
- expect(tapControl).toHaveBeenCalledWith(emulator, 'Search sessions, repo:, path:', 30_000)
- expect(typeText).toHaveBeenCalledWith(emulator, 'hybrid agent history fixture')
- expect(waitByPrefix).toHaveBeenCalledWith(emulator, 'Hybrid Agent History Fixture', 30_000)
- expect(tapControl).toHaveBeenCalledWith(emulator, 'Agent Session History', 30_000)
- expect(rotateEmulator.mock.invocationCallOrder[0]).toBeLessThan(
- dismissDeveloperMenu.mock.invocationCallOrder[0]
- )
- expect(dismissDeveloperMenu.mock.invocationCallOrder[0]).toBeLessThan(
- tapControlByPrefix.mock.invocationCallOrder[0]
- )
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-build-activation.test.ts b/mobile/src/mobile-web/hosted-ios-build-activation.test.ts
deleted file mode 100644
index dafd797444d..00000000000
--- a/mobile/src/mobile-web/hosted-ios-build-activation.test.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import { createHash } from 'node:crypto'
-import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
-import os from 'node:os'
-import path from 'node:path'
-import { afterEach, describe, expect, it, vi } from 'vitest'
-import { waitForHostedIosBuildActivation } from '../../scripts/hosted-ios-build-activation.mjs'
-
-const temporaryDirectories: string[] = []
-
-afterEach(async () => {
- await Promise.all(
- temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true }))
- )
-})
-
-describe('hosted iOS build activation wait', () => {
- it('waits against the installed app data container', async () => {
- const appDataPath = await mkdtemp(path.join(os.tmpdir(), 'orca-ios-activation-'))
- const runtimeDirectory = await mkdtemp(path.join(os.tmpdir(), 'orca-ios-runtime-'))
- temporaryDirectories.push(appDataPath)
- temporaryDirectories.push(runtimeDirectory)
- const publicKeyB64 = 'paired-public-key'
- const hostIdentity = createHash('sha256').update(publicKeyB64).digest('hex')
- const buildId = 'a'.repeat(64)
- const hostRoot = path.join(
- appDataPath,
- 'Library',
- 'Application Support',
- 'OrcaMobileWeb',
- hostIdentity
- )
- await mkdir(hostRoot, { recursive: true })
- await writeFile(
- path.join(hostRoot, 'activation.json'),
- JSON.stringify({ active: buildId, previous: null })
- )
- const keypairRoot = path.join(runtimeDirectory, 'paired-host', 'userData')
- await mkdir(keypairRoot, { recursive: true })
- await writeFile(
- path.join(keypairRoot, 'orca-e2ee-keypair.json'),
- JSON.stringify({ publicKeyB64 })
- )
- const runCommand = vi.fn().mockResolvedValue({ stdout: `${appDataPath}\n` })
-
- await expect(
- waitForHostedIosBuildActivation(
- 'simulator',
- { expectedBuild: buildId, timeoutMs: 1_000 },
- runtimeDirectory,
- runCommand
- )
- ).resolves.toBeUndefined()
- expect(runCommand).toHaveBeenCalledWith('xcrun', [
- 'simctl',
- 'get_app_container',
- 'simulator',
- 'com.stably.orca.mobile',
- 'data'
- ])
- })
-
- it('does not inspect the simulator without an expected build', async () => {
- const runCommand = vi.fn()
-
- await waitForHostedIosBuildActivation(
- 'simulator',
- { expectedBuild: undefined, timeoutMs: 1_000 },
- '/runtime',
- runCommand
- )
- expect(runCommand).not.toHaveBeenCalled()
- })
-
- it('ignores the same active build under an unrelated host identity', async () => {
- const appDataPath = await mkdtemp(path.join(os.tmpdir(), 'orca-ios-activation-'))
- const runtimeDirectory = await mkdtemp(path.join(os.tmpdir(), 'orca-ios-runtime-'))
- temporaryDirectories.push(appDataPath)
- temporaryDirectories.push(runtimeDirectory)
- const publicKeyB64 = 'expected-paired-public-key'
- const expectedIdentity = createHash('sha256').update(publicKeyB64).digest('hex')
- const unrelatedIdentity = 'f'.repeat(64)
- const buildId = 'b'.repeat(64)
- const cacheRoot = path.join(appDataPath, 'Library', 'Application Support', 'OrcaMobileWeb')
- const keypairRoot = path.join(runtimeDirectory, 'paired-host', 'userData')
- await mkdir(path.join(cacheRoot, unrelatedIdentity), { recursive: true })
- await mkdir(keypairRoot, { recursive: true })
- await writeFile(
- path.join(cacheRoot, unrelatedIdentity, 'activation.json'),
- JSON.stringify({ active: buildId })
- )
- await writeFile(
- path.join(keypairRoot, 'orca-e2ee-keypair.json'),
- JSON.stringify({ publicKeyB64 })
- )
- const expectedHostRoot = path.join(cacheRoot, expectedIdentity)
- const activateExpectedHost = setTimeout(async () => {
- await mkdir(expectedHostRoot, { recursive: true })
- await writeFile(
- path.join(expectedHostRoot, 'activation.json'),
- JSON.stringify({ active: buildId })
- )
- }, 20)
- const runCommand = vi.fn().mockResolvedValue({ stdout: appDataPath })
-
- await expect(
- waitForHostedIosBuildActivation(
- 'simulator',
- { expectedBuild: buildId, timeoutMs: 1_000 },
- runtimeDirectory,
- runCommand
- )
- ).resolves.toBeUndefined()
- clearTimeout(activateExpectedHost)
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-core-route-parity.test.ts b/mobile/src/mobile-web/hosted-ios-core-route-parity.test.ts
deleted file mode 100644
index 1cb880e23da..00000000000
--- a/mobile/src/mobile-web/hosted-ios-core-route-parity.test.ts
+++ /dev/null
@@ -1,213 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-const mocks = vi.hoisted(() => ({
- activateControl: vi.fn(),
- activateWorkspace: vi.fn(),
- captureScreenshot: vi.fn(),
- compareScreenshots: vi.fn(),
- dismissDeveloperMenu: vi.fn(),
- readControlPoint: vi.fn(),
- readTextPoint: vi.fn(),
- tapControl: vi.fn(),
- tapControlByPrefix: vi.fn(),
- tapPoint: vi.fn(),
- waitForControl: vi.fn(),
- waitForControlByPrefix: vi.fn(),
- waitForControlMatch: vi.fn(),
- waitForControlMatching: vi.fn(),
- waitForDocument: vi.fn()
-}))
-
-vi.mock('node:child_process', () => ({
- execFile: mocks.captureScreenshot
-}))
-vi.mock('../../scripts/emulator-developer-menu-dismissal.mjs', () => ({
- dismissEmulatorDeveloperMenuIfPresent: mocks.dismissDeveloperMenu
-}))
-vi.mock('../../scripts/hosted-ios-emulator-accessibility.mjs', () => ({
- tapHostedIosAccessibilityControl: mocks.tapControl,
- tapHostedIosAccessibilityControlByLabelPrefix: mocks.tapControlByPrefix,
- tapHostedIosPoint: mocks.tapPoint,
- waitForHostedIosAccessibilityControl: mocks.waitForControl,
- waitForHostedIosAccessibilityControlByLabelPrefix: mocks.waitForControlByPrefix,
- waitForHostedIosAccessibilityControlMatch: mocks.waitForControlMatch,
- waitForHostedIosAccessibilityControlMatching: mocks.waitForControlMatching
-}))
-vi.mock('../../scripts/hosted-ios-screenshot-parity.mjs', () => ({
- assertHostedIosScreenshotParity: mocks.compareScreenshots
-}))
-vi.mock('../../scripts/hosted-webview-cdp-session.mjs', () => ({
- activateHostedWebViewControl: mocks.activateControl,
- readHostedWebViewTextPoint: mocks.readTextPoint,
- waitForVisibleHostedWebView: mocks.waitForDocument
-}))
-vi.mock('../../scripts/hosted-webview-control-point.mjs', () => ({
- readHostedWebViewControlPoint: mocks.readControlPoint
-}))
-vi.mock('../../scripts/hosted-webview-workspace-activation.mjs', () => ({
- activateHostedWorkspaceRow: mocks.activateWorkspace
-}))
-
-import {
- captureHostedCoreRouteParity,
- captureNativeCoreRouteBaselines
-} from '../../scripts/hosted-ios-core-route-parity.mjs'
-
-describe('hosted iOS core-route parity', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- mocks.captureScreenshot.mockImplementation((_command, _args, callback) =>
- callback(null, '', '')
- )
- mocks.compareScreenshots.mockResolvedValue({ changedPixelRatio: 0.01 })
- mocks.readControlPoint.mockResolvedValue({ x: 0.8, y: 0.1 })
- mocks.readTextPoint.mockResolvedValue({ x: 0.2, y: 0.1 })
- mocks.tapControl.mockResolvedValue(undefined)
- mocks.tapControlByPrefix.mockResolvedValue(undefined)
- mocks.tapPoint.mockResolvedValue(undefined)
- mocks.waitForControl.mockResolvedValue({ x: 0.2, y: 0.1 })
- mocks.waitForControlByPrefix.mockResolvedValue({ x: 0.2, y: 0.1 })
- mocks.waitForControlMatch.mockResolvedValue({
- label: 'Connect your Linear account',
- x: 0.5,
- y: 0.5
- })
- mocks.waitForControlMatching.mockResolvedValue({
- label: 'Stable task title, 3m, Issue · orca #1, Open',
- value: '',
- x: 0.5,
- y: 0.5
- })
- mocks.waitForDocument.mockImplementation(({ expectedHrefIncludes }) => ({
- href: `orca-mobile-web://build${expectedHrefIncludes ?? '/h/host'}`
- }))
- })
-
- it('captures native Tasks and Session from the same presentation source', async () => {
- const baselines = await captureNativeCoreRouteBaselines({
- deviceUdid: 'simulator',
- emulator: { deviceUdid: 'simulator' },
- expectedWorkspace: 'mobile-rearch',
- runtimeDirectory: '/tmp/parity',
- timeoutMs: 30_000
- })
-
- expect(baselines.tasks.stableText).toBe('Connect your Linear account')
- expect(mocks.tapPoint).toHaveBeenNthCalledWith(
- 1,
- { deviceUdid: 'simulator' },
- { x: 0.87, y: 0.1 }
- )
- expect(mocks.tapPoint).toHaveBeenNthCalledWith(
- 2,
- { deviceUdid: 'simulator' },
- expect.objectContaining({ y: 0.1 })
- )
- expect(mocks.tapPoint.mock.calls[1][1].x).toBeCloseTo(0.08)
- expect(mocks.tapControl).toHaveBeenCalledWith(
- { deviceUdid: 'simulator' },
- 'Back to worktrees',
- 30_000
- )
- expect(baselines.session.screenshot).toBe('/tmp/parity/native-session-portrait.png')
- })
-
- it('captures a populated GitHub Tasks list after pagination appears', async () => {
- mocks.waitForControlMatch.mockRejectedValueOnce(new Error('no empty state'))
-
- const baselines = await captureNativeCoreRouteBaselines({
- deviceUdid: 'simulator',
- emulator: { deviceUdid: 'simulator' },
- expectedWorkspace: 'mobile-rearch',
- runtimeDirectory: '/tmp/parity',
- timeoutMs: 30_000
- })
-
- expect(mocks.waitForControlMatch).toHaveBeenCalledWith(
- { deviceUdid: 'simulator' },
- expect.any(Array),
- 5_000
- )
- expect(mocks.waitForControlMatching).toHaveBeenCalledWith(
- { deviceUdid: 'simulator' },
- expect.any(Function),
- 25_000
- )
- expect(baselines.tasks.stableText).toBe('Stable task title')
- })
-
- it('compares hosted Tasks and Session and returns to the workspace route', async () => {
- const nativeBaselines = {
- tasks: {
- screenTitlePoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-tasks-portrait.png',
- stableText: 'Connect your Linear account'
- },
- session: {
- screenTitlePoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-session-portrait.png'
- }
- }
-
- const result = await captureHostedCoreRouteParity({
- deviceUdid: 'simulator',
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- expectedWorkspace: 'mobile-rearch',
- nativeBaselines,
- runtimeDirectory: '/tmp/parity',
- timeoutMs: 30_000,
- workspaceDocument: { href: 'orca-mobile-web://build/h/host' }
- })
-
- expect(result.evidence.tasks.screenshotParity).toEqual({
- changedPixelRatio: 0.01
- })
- expect(result.evidence.session.screenshotParity).toEqual({
- changedPixelRatio: 0.01
- })
- expect(result.workspaceDocument.href).toContain('/h/host')
- expect(mocks.activateWorkspace).toHaveBeenCalledWith(
- expect.anything(),
- 'mobile-rearch',
- mocks.activateControl,
- 30_000,
- expect.any(Function)
- )
- expect(mocks.readTextPoint).toHaveBeenCalledWith(expect.anything(), 'Filter')
- expect(mocks.tapControl).toHaveBeenCalledWith(
- { deviceUdid: 'simulator' },
- 'Back to worktrees',
- 5_000
- )
- expect(mocks.compareScreenshots).toHaveBeenCalledTimes(2)
- })
-
- it('uses a measured point when the hosted session back control is missing from AX', async () => {
- mocks.tapControl.mockRejectedValueOnce(new Error('missing accessibility descendant'))
-
- await captureHostedCoreRouteParity({
- deviceUdid: 'simulator',
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- expectedWorkspace: 'mobile-rearch',
- nativeBaselines: {
- tasks: {
- screenTitlePoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-tasks-portrait.png',
- stableText: 'Connect your Linear account'
- },
- session: {
- screenTitlePoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-session-portrait.png'
- }
- },
- runtimeDirectory: '/tmp/parity',
- timeoutMs: 30_000,
- workspaceDocument: { href: 'orca-mobile-web://build/h/host' }
- })
-
- expect(mocks.readControlPoint).toHaveBeenCalledWith(expect.anything(), 'Back to worktrees')
- expect(mocks.tapPoint).toHaveBeenCalledWith({ deviceUdid: 'simulator' }, { x: 0.8, y: 0.1 })
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-document-upload.test.ts b/mobile/src/mobile-web/hosted-ios-document-upload.test.ts
deleted file mode 100644
index 2198dc7a316..00000000000
--- a/mobile/src/mobile-web/hosted-ios-document-upload.test.ts
+++ /dev/null
@@ -1,277 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import {
- HOSTED_IOS_DOCUMENT_FIXTURE_NAME,
- openHostedIosDocumentPicker,
- parseLocalFileProviderGroupPath,
- seedHostedIosDocumentFixture,
- selectHostedIosDocumentFixture,
- uploadedPathsFromTerminalSnapshot,
- verifyHostedIosDocumentUpload
-} from '../../scripts/hosted-ios-document-upload.mjs'
-
-const fixture = {
- destinationPath: '/simulator/File Provider Storage/orca-document-upload-fixture.png',
- fixtureName: HOSTED_IOS_DOCUMENT_FIXTURE_NAME,
- sha256: 'dec4a91731905b9e8ed450a6c46931258528fc034fcfc64d95b0b23264f8e9d4',
- size: 123
-}
-const uploadedPath =
- '/var/folders/x/y/T/orca-paste-1780000000000-00000000-0000-4000-8000-000000000000.png'
-const args = {
- discoveryUrl: 'http://127.0.0.1:9222',
- documentFixture: fixture,
- emulator: { deviceUdid: 'simulator' },
- orcaCli: './config/scripts/orca-dev.mjs',
- pairingRuntimeUserDataPath: '/tmp/pairing/userData',
- sessionDocument: { href: 'orca-mobile-web://build/h/host/session/workspace' },
- terminalHandle: 'terminal-handle',
- timeoutMs: 30_000,
- worktree: '/repo/mobile-rearch'
-}
-const fixtureBytes = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7Q', 'base64')
-
-describe('hosted iOS document upload', () => {
- it('resolves and seeds the simulator Local File Provider container', async () => {
- const runCommand = vi.fn().mockResolvedValue({
- stdout:
- 'group.com.apple.DocumentManager\t/simulator/document-manager\n' +
- 'group.com.apple.FileProvider.LocalStorage\t/simulator/local-files\n'
- })
- const copy = vi.fn().mockResolvedValue(undefined)
- const createDirectory = vi.fn().mockResolvedValue(undefined)
- const readFixture = vi.fn().mockResolvedValue(Buffer.alloc(123))
- const touch = vi.fn().mockResolvedValue(undefined)
-
- await seedHostedIosDocumentFixture(
- {
- deviceUdid: 'simulator',
- fixturePath: '/repo/mobile/assets/favicon.png'
- },
- { copy, createDirectory, readFixture, runCommand, touch }
- )
-
- expect(runCommand).toHaveBeenCalledWith(
- 'xcrun',
- ['simctl', 'get_app_container', 'simulator', 'com.apple.DocumentsApp', 'groups'],
- { encoding: 'utf8' }
- )
- expect(createDirectory).toHaveBeenCalledWith('/simulator/local-files/File Provider Storage', {
- recursive: true
- })
- expect(copy).toHaveBeenCalledWith(
- '/repo/mobile/assets/favicon.png',
- '/simulator/local-files/File Provider Storage/orca-document-upload-fixture.png'
- )
- expect(touch).toHaveBeenCalled()
- })
-
- it('rejects a missing Local File Provider group', () => {
- expect(() =>
- parseLocalFileProviderGroupPath(
- 'group.com.apple.DocumentManager\t/simulator/document-manager'
- )
- ).toThrow('Local File Provider container was not found')
- })
-
- it('falls back through Browse and On My iPhone to select the seeded file', async () => {
- const tapByPrefix = vi.fn().mockResolvedValueOnce({ x: 0.5, y: 0.4 })
- const tapFixture = vi
- .fn()
- .mockRejectedValueOnce(new Error('not in Recents'))
- .mockRejectedValueOnce(new Error('not in Browse root'))
- .mockResolvedValueOnce({ x: 0.5, y: 0.3 })
- const tapControl = vi.fn().mockResolvedValue({ x: 0.2, y: 0.9 })
- const waitForPickerDismissal = vi.fn().mockResolvedValue(undefined)
-
- await expect(
- selectHostedIosDocumentFixture(
- args.emulator,
- HOSTED_IOS_DOCUMENT_FIXTURE_NAME,
- args.timeoutMs,
- { tapByPrefix, tapControl, tapFixture, waitForPickerDismissal }
- )
- ).resolves.toEqual({ x: 0.5, y: 0.3 })
-
- expect(tapControl).toHaveBeenCalledWith(args.emulator, 'Browse', 5_000)
- expect(tapByPrefix).toHaveBeenCalledWith(args.emulator, 'On My iPhone', 5_000)
- expect(tapFixture).toHaveBeenLastCalledWith(
- args.emulator,
- 'orca-document-upload-fixture',
- { x: 0.5, y: 0.25 },
- args.timeoutMs
- )
- expect(waitForPickerDismissal).toHaveBeenCalledWith(args.emulator, 'Browse', 3_000)
- })
-
- it('splits abutting uploaded paths instead of fusing them', () => {
- const first =
- '/var/folders/x/y/T/orca-paste-1780000000000-00000000-0000-4000-8000-000000000000.png'
- const second =
- '/var/folders/x/y/T/orca-paste-1780000000001-00000000-0000-4000-8000-000000000001.png'
- expect(uploadedPathsFromTerminalSnapshot([first + second])).toEqual([first, second])
- })
-
- it('reconstructs wrapped uploaded paths from terminal rows', () => {
- expect(
- uploadedPathsFromTerminalSnapshot([
- '/orca/ORCA_HOSTED_CLIPBOARD_TEXT_PASTE/var/folders/x/y/T/orca-paste-1780000',
- '000000-00000000-0000-4000-8000-000000000000.png'
- ])
- ).toEqual([
- '/var/folders/x/y/T/orca-paste-1780000000000-00000000-0000-4000-8000-000000000000.png'
- ])
- })
-
- it('uses the physical long press when it opens the native picker', async () => {
- const longPressPoint = vi.fn().mockResolvedValue(undefined)
- const waitForPicker = vi.fn().mockResolvedValue({ x: 0.2, y: 0.9 })
- const dispatchWebLongPress = vi.fn()
-
- await expect(
- openHostedIosDocumentPicker(
- {
- document: args.sessionDocument,
- emulator: args.emulator,
- point: { x: 0.75, y: 0.9 }
- },
- { dispatchWebLongPress, longPressPoint, waitForPicker }
- )
- ).resolves.toBe('native-long-press')
-
- expect(dispatchWebLongPress).not.toHaveBeenCalled()
- })
-
- it('retains native gesture authority while completing the RNW long press', async () => {
- const longPressPoint = vi.fn().mockResolvedValue(undefined)
- const waitForPicker = vi
- .fn()
- .mockRejectedValueOnce(new Error('picker not visible'))
- .mockResolvedValueOnce({ x: 0.2, y: 0.9 })
- const dispatchWebLongPress = vi.fn().mockResolvedValue(undefined)
-
- await expect(
- openHostedIosDocumentPicker(
- {
- document: args.sessionDocument,
- emulator: args.emulator,
- point: { x: 0.75, y: 0.9 }
- },
- { dispatchWebLongPress, longPressPoint, waitForPicker }
- )
- ).resolves.toBe('native-touch-plus-web-responder')
-
- expect(dispatchWebLongPress).toHaveBeenCalledWith(args.sessionDocument, 'Attach a photo')
- expect(waitForPicker).toHaveBeenLastCalledWith(args.emulator, 'Browse', 5_000)
- })
-
- it('repeats the combined gesture when WebKit drops the first responder hold', async () => {
- const longPressPoint = vi.fn().mockResolvedValue(undefined)
- const waitForPicker = vi
- .fn()
- .mockRejectedValueOnce(new Error('physical hold did not open picker'))
- .mockRejectedValueOnce(new Error('first responder hold did not open picker'))
- .mockRejectedValueOnce(new Error('second physical hold did not open picker'))
- .mockResolvedValueOnce({ x: 0.2, y: 0.9 })
- const dispatchWebLongPress = vi.fn().mockResolvedValue(undefined)
-
- await expect(
- openHostedIosDocumentPicker(
- {
- document: args.sessionDocument,
- emulator: args.emulator,
- point: { x: 0.75, y: 0.9 }
- },
- { dispatchWebLongPress, longPressPoint, waitForPicker }
- )
- ).resolves.toBe('native-touch-plus-web-responder')
-
- expect(longPressPoint).toHaveBeenCalledTimes(2)
- expect(dispatchWebLongPress).toHaveBeenCalledTimes(2)
- })
-
- it('selects through unchanged Attach and verifies terminal-only byte identity', async () => {
- const before = ['ORCA_HOSTED_CLIPBOARD_TEXT_PASTE']
- const after = [...before, uploadedPath]
- const operations = {
- fileStat: vi.fn().mockResolvedValue({ size: fixtureBytes.byteLength }),
- openPicker: vi.fn().mockResolvedValue('native-touch-plus-web-responder'),
- readAccessibilityPoint: vi.fn().mockResolvedValue({ x: 0.74, y: 0.89 }),
- readControlPoint: vi.fn().mockResolvedValue({ x: 0.75, y: 0.9 }),
- readState: vi.fn().mockResolvedValue({ bodyText: 'Mobile Emulator' }),
- readTerminal: vi.fn().mockResolvedValueOnce(before).mockResolvedValueOnce(after),
- readUploadedFile: vi.fn().mockResolvedValue(fixtureBytes),
- selectFixture: vi.fn().mockResolvedValue({ x: 0.5, y: 0.5 }),
- waitForDocument: vi.fn().mockResolvedValue(args.sessionDocument)
- }
- const documentFixture = {
- ...fixture,
- sha256: '1b0a38dc09fb7fe56599abeab066965c1b7791815e7dd08483938630b0d15226',
- size: fixtureBytes.byteLength
- }
-
- await expect(
- verifyHostedIosDocumentUpload({ ...args, documentFixture }, operations)
- ).resolves.toEqual({
- evidence: {
- attachAccessibilityPoint: { x: 0.74, y: 0.89 },
- attachControlPoint: { x: 0.75, y: 0.9 },
- fixtureName: HOSTED_IOS_DOCUMENT_FIXTURE_NAME,
- privilegedPageMarkers: 'absent',
- pickerActivation: 'native-touch-plus-web-responder',
- route: args.sessionDocument.href,
- selectedFilePoint: { x: 0.5, y: 0.5 },
- sha256: documentFixture.sha256,
- size: fixtureBytes.byteLength,
- terminalPathInjected: true
- },
- sessionDocument: args.sessionDocument
- })
- expect(operations.readControlPoint).toHaveBeenCalledWith(args.sessionDocument, 'Attach a photo')
- expect(operations.openPicker).toHaveBeenCalledWith({
- document: args.sessionDocument,
- emulator: args.emulator,
- point: { x: 0.75, y: 0.9 }
- })
- expect(operations.readUploadedFile).toHaveBeenCalledWith(uploadedPath)
- })
-
- it('rejects selected bytes exposed in hosted page state', async () => {
- const documentFixture = {
- ...fixture,
- sha256: '1b0a38dc09fb7fe56599abeab066965c1b7791815e7dd08483938630b0d15226',
- size: fixtureBytes.byteLength
- }
- const operations = {
- fileStat: vi.fn().mockResolvedValue({ size: fixtureBytes.byteLength }),
- openPicker: vi.fn().mockResolvedValue('native-touch-plus-web-responder'),
- readAccessibilityPoint: vi.fn().mockResolvedValue({ x: 0.74, y: 0.89 }),
- readControlPoint: vi.fn().mockResolvedValue({ x: 0.75, y: 0.9 }),
- readState: vi.fn().mockResolvedValue({
- bodyText: `Mobile Emulator ${fixtureBytes.toString('base64').slice(0, 16)}`
- }),
- readTerminal: vi.fn().mockResolvedValueOnce([]).mockResolvedValueOnce([uploadedPath]),
- readUploadedFile: vi.fn().mockResolvedValue(fixtureBytes),
- selectFixture: vi.fn().mockResolvedValue({ x: 0.5, y: 0.5 }),
- waitForDocument: vi.fn().mockResolvedValue(args.sessionDocument)
- }
-
- await expect(
- verifyHostedIosDocumentUpload({ ...args, documentFixture }, operations)
- ).rejects.toThrow('exposed privileged page marker')
- })
-
- it('reports the native shell attachment error before the terminal timeout', async () => {
- const operations = {
- openPicker: vi.fn().mockResolvedValue('native-touch-plus-web-responder'),
- readAccessibilityPoint: vi.fn().mockResolvedValue({ x: 0.74, y: 0.89 }),
- readControlPoint: vi.fn().mockResolvedValue({ x: 0.75, y: 0.9 }),
- readState: vi.fn().mockResolvedValue({ bodyText: 'Mobile Emulator Attach failed' }),
- readTerminal: vi.fn().mockResolvedValue([]),
- selectFixture: vi.fn().mockResolvedValue({ x: 0.5, y: 0.3 })
- }
-
- await expect(
- verifyHostedIosDocumentUpload({ ...args, timeoutMs: 100 }, operations)
- ).rejects.toThrow('Selected document upload failed in the native shell: Attach failed')
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-emulator-accessibility.test.ts b/mobile/src/mobile-web/hosted-ios-emulator-accessibility.test.ts
deleted file mode 100644
index cda7ed23314..00000000000
--- a/mobile/src/mobile-web/hosted-ios-emulator-accessibility.test.ts
+++ /dev/null
@@ -1,426 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import {
- rotateHostedIosEmulator,
- tapHostedIosAccessibilityControl,
- tapHostedIosAccessibilityControlAtOccurrence,
- tapHostedIosAccessibilityControlAtLastOccurrence,
- tapHostedIosAccessibilityControlByLabelPrefix,
- tapHostedIosAccessibilityControlByLabelPrefixAtPosition,
- tapHostedIosAccessibilityControlStartingWith,
- tapHostedIosPoint,
- typeHostedIosText,
- waitForHostedIosAccessibilityControl,
- waitForHostedIosAccessibilityControlByLabelPrefix,
- waitForHostedIosAccessibilityControlEndingWith,
- waitForHostedIosAccessibilityControlMatching,
- waitForHostedIosAccessibilityLabel,
- waitForHostedIosAccessibilityLabelToDisappear
-} from '../../scripts/hosted-ios-emulator-accessibility.mjs'
-
-const emulator = {
- deviceUdid: 'simulator-a',
- orcaCli: '/repo/config/scripts/orca-dev.mjs',
- userDataDir: '/tmp/orca-mobile/userData',
- worktree: '/repo'
-}
-
-describe('hosted iOS emulator accessibility controls', () => {
- it('rotates through the isolated Orca emulator controller', async () => {
- const runCommand = vi.fn().mockResolvedValue({ stderr: '', stdout: '{}' })
-
- await rotateHostedIosEmulator(emulator, 'landscape_left', runCommand)
-
- expect(runCommand).toHaveBeenCalledWith(emulator, ['rotate', 'landscape_left'])
- })
-
- it('finds a nested enabled control and taps its normalized center', async () => {
- const runCommand = vi
- .fn()
- .mockResolvedValueOnce({
- stderr: '',
- stdout: JSON.stringify({
- ok: true,
- result: [
- {
- label: 'Orca',
- children: [
- {
- label: 'Resume agent session',
- enabled: true,
- frame: { x: 0.6, y: 0.7, width: 0.2, height: 0.1 }
- }
- ]
- }
- ]
- })
- })
- .mockResolvedValueOnce({ stderr: '', stdout: JSON.stringify({ ok: true }) })
-
- await expect(
- tapHostedIosAccessibilityControl(emulator, 'Resume agent session', 1_000, runCommand)
- ).resolves.toEqual({ x: 0.7, y: 0.75 })
- expect(runCommand).toHaveBeenLastCalledWith(emulator, ['tap', '0.7', '0.75'])
- })
-
- it('selects a specific visible occurrence without changing the label contract', async () => {
- const runCommand = vi
- .fn()
- .mockResolvedValueOnce({
- stderr: '',
- stdout: JSON.stringify({
- ok: true,
- result: [
- {
- label: 'mobile-rearch',
- enabled: true,
- frame: { x: 0.1, y: 0.1, width: 0.3, height: 0.1 }
- },
- {
- label: 'mobile-rearch',
- enabled: true,
- frame: { x: 0.1, y: 0.3, width: 0.3, height: 0.1 }
- }
- ]
- })
- })
- .mockResolvedValueOnce({ stderr: '', stdout: JSON.stringify({ ok: true }) })
-
- await expect(
- tapHostedIosAccessibilityControlAtOccurrence(emulator, 'mobile-rearch', 1, 1_000, runCommand)
- ).resolves.toEqual({ x: 0.25, y: 0.35 })
- expect(runCommand).toHaveBeenLastCalledWith(emulator, ['tap', '0.25', '0.35'])
- })
-
- it('targets the last visible occurrence for native photo grids', async () => {
- const runCommand = vi
- .fn()
- .mockResolvedValueOnce({
- stderr: '',
- stdout: JSON.stringify({
- ok: true,
- result: [
- {
- label: 'Photo',
- enabled: true,
- frame: { x: 0, y: 0.2, width: 0.3, height: 0.1 }
- },
- {
- label: 'Photo',
- enabled: true,
- frame: { x: 0.3, y: 0.4, width: 0.3, height: 0.1 }
- }
- ]
- })
- })
- .mockResolvedValueOnce({ stderr: '', stdout: JSON.stringify({ ok: true }) })
-
- await expect(
- tapHostedIosAccessibilityControlAtLastOccurrence(emulator, 'Photo', 1_000, runCommand)
- ).resolves.toEqual({ x: 0.44999999999999996, y: 0.45 })
- expect(runCommand).toHaveBeenLastCalledWith(emulator, ['tap', '0.44999999999999996', '0.45'])
- })
-
- it('targets a composite native row by its leading visible label', async () => {
- const runCommand = vi
- .fn()
- .mockResolvedValueOnce({
- stderr: '',
- stdout: JSON.stringify({
- ok: true,
- result: [
- {
- label: 'mobile-rearch, mobile-rearch, 1',
- enabled: true,
- frame: { x: 0, y: 0.2, width: 1, height: 0.1 }
- }
- ]
- })
- })
- .mockResolvedValueOnce({ stderr: '', stdout: JSON.stringify({ ok: true }) })
-
- await expect(
- tapHostedIosAccessibilityControlByLabelPrefix(emulator, 'mobile-rearch', 1_000, runCommand)
- ).resolves.toEqual({ x: 0.5, y: 0.25 })
- expect(runCommand).toHaveBeenLastCalledWith(emulator, ['tap', '0.5', '0.25'])
- })
-
- it('taps a relative position within a composite native control', async () => {
- const runCommand = vi
- .fn()
- .mockResolvedValueOnce({
- stderr: '',
- stdout: JSON.stringify({
- ok: true,
- result: [
- {
- label: 'orca-document-upload-fixture, Image',
- enabled: true,
- frame: { x: 0.1, y: 0.2, width: 0.2, height: 0.4 }
- }
- ]
- })
- })
- .mockResolvedValueOnce({ stderr: '', stdout: JSON.stringify({ ok: true }) })
-
- await expect(
- tapHostedIosAccessibilityControlByLabelPrefixAtPosition(
- emulator,
- 'orca-document-upload-fixture',
- { x: 0.5, y: 0.25 },
- 1_000,
- runCommand
- )
- ).resolves.toEqual({ x: 0.2, y: 0.30000000000000004 })
- expect(runCommand).toHaveBeenLastCalledWith(emulator, ['tap', '0.2', '0.30000000000000004'])
- })
-
- it('targets a dynamic control whose label starts with a stable action prefix', async () => {
- const runCommand = vi
- .fn()
- .mockResolvedValueOnce({
- stderr: '',
- stdout: JSON.stringify({
- ok: true,
- result: [
- {
- label: 'Open changed file mobile/app/index.tsx',
- enabled: true,
- frame: { x: 0.1, y: 0.3, width: 0.8, height: 0.1 }
- }
- ]
- })
- })
- .mockResolvedValueOnce({ stderr: '', stdout: JSON.stringify({ ok: true }) })
-
- await expect(
- tapHostedIosAccessibilityControlStartingWith(
- emulator,
- 'Open changed file ',
- 1_000,
- runCommand
- )
- ).resolves.toEqual({ x: 0.5, y: 0.35 })
- expect(runCommand).toHaveBeenLastCalledWith(emulator, ['tap', '0.5', '0.35'])
- })
-
- it.each(['label', 'value'])(
- 'reads a composite row from its %s without tapping it',
- async (field) => {
- const runCommand = vi.fn().mockResolvedValueOnce({
- stderr: '',
- stdout: JSON.stringify({
- ok: true,
- result: [
- {
- [field]: 'Hybrid Agent History Fixture, 2h, Preview',
- enabled: true,
- frame: { x: 0.1, y: 0.2, width: 0.8, height: 0.1 }
- }
- ]
- })
- })
-
- await expect(
- waitForHostedIosAccessibilityControlByLabelPrefix(
- emulator,
- 'Hybrid Agent History Fixture',
- 1_000,
- runCommand
- )
- ).resolves.toEqual({ x: 0.5, y: 0.25 })
- expect(runCommand).toHaveBeenCalledTimes(1)
- }
- )
-
- it('waits for a dynamic count by its stable suffix', async () => {
- const runCommand = vi.fn().mockResolvedValueOnce({
- stderr: '',
- stdout: JSON.stringify({
- ok: true,
- result: [
- {
- label: '128 on branch',
- enabled: true,
- frame: { x: 0.6, y: 0.3, width: 0.3, height: 0.1 }
- }
- ]
- })
- })
-
- await expect(
- waitForHostedIosAccessibilityControlEndingWith(emulator, ' on branch', 1_000, runCommand)
- ).resolves.toEqual({ x: 0.75, y: 0.35 })
- })
-
- it('returns metadata for a visible control matched by shape', async () => {
- const runCommand = vi.fn().mockResolvedValueOnce({
- stderr: '',
- stdout: JSON.stringify({
- ok: true,
- result: [
- {
- label: 'Stable task title, 3m, Issue · orca #1, Open',
- value: '',
- enabled: true,
- frame: { x: 0.1, y: 0.3, width: 0.8, height: 0.1 }
- }
- ]
- })
- })
-
- await expect(
- waitForHostedIosAccessibilityControlMatching(
- emulator,
- (node) => node.label?.endsWith(', Open'),
- 1_000,
- runCommand
- )
- ).resolves.toEqual({
- label: 'Stable task title, 3m, Issue · orca #1, Open',
- value: '',
- x: 0.5,
- y: 0.35
- })
- })
-
- it('accepts a tall visible label whose center is below the viewport', async () => {
- const runCommand = vi.fn().mockResolvedValueOnce({
- stderr: '',
- stdout: JSON.stringify({
- ok: true,
- result: [
- {
- label: 'File preview',
- value: '',
- enabled: true,
- frame: { x: 0.03, y: 0.28, width: 0.94, height: 1.52 }
- }
- ]
- })
- })
-
- await expect(
- waitForHostedIosAccessibilityLabel(emulator, 'File preview', 1_000, runCommand)
- ).resolves.toEqual({
- frame: { x: 0.03, y: 0.28, width: 0.94, height: 1.52 },
- label: 'File preview',
- value: ''
- })
- })
-
- it('rejects an invalid accessibility response', async () => {
- await expect(
- waitForHostedIosAccessibilityControl(emulator, 'Resume agent session', 1_000, async () => ({
- stderr: '',
- stdout: JSON.stringify({ ok: true, result: null })
- }))
- ).rejects.toThrow('invalid accessibility response')
- })
-
- it('restarts a wedged emulator controller before retrying accessibility', async () => {
- const runCommand = vi.fn(async (_args, command) => {
- if (command[0] === 'ax' && runCommand.mock.calls.length === 1) {
- throw new Error('emulator_helper_failed: request timed out')
- }
- if (command[0] === 'ax') {
- return {
- stderr: '',
- stdout: JSON.stringify({
- ok: true,
- result: [
- {
- label: 'Resume agent session',
- enabled: true,
- frame: { x: 0.6, y: 0.7, width: 0.2, height: 0.1 }
- }
- ]
- })
- }
- }
- return { stderr: '', stdout: JSON.stringify({ ok: true }) }
- })
-
- await expect(
- waitForHostedIosAccessibilityControl(emulator, 'Resume agent session', 1_000, runCommand)
- ).resolves.toEqual({ x: 0.7, y: 0.75 })
- expect(runCommand.mock.calls.map(([, command]) => command[0])).toEqual([
- 'ax',
- 'kill',
- 'attach',
- 'ax'
- ])
- expect(runCommand).toHaveBeenNthCalledWith(3, emulator, ['attach', emulator.deviceUdid])
- })
-
- it('reattaches an explicit device before retrying a point tap', async () => {
- const runCommand = vi
- .fn()
- .mockRejectedValueOnce(new Error('emulator_no_active'))
- .mockResolvedValue({ stderr: '', stdout: JSON.stringify({ ok: true }) })
- const point = { x: 0.25, y: 0.75 }
-
- await expect(tapHostedIosPoint(emulator, point, runCommand)).resolves.toEqual(point)
- expect(runCommand.mock.calls.map(([, command]) => command[0])).toEqual([
- 'tap',
- 'kill',
- 'attach',
- 'tap'
- ])
- expect(runCommand).toHaveBeenNthCalledWith(3, emulator, ['attach', emulator.deviceUdid])
- })
-
- it('types through the isolated emulator controller', async () => {
- const runCommand = vi.fn().mockResolvedValue({
- stderr: '',
- stdout: JSON.stringify({ ok: true })
- })
-
- await typeHostedIosText(emulator, 'Hybrid Agent History Fixture', runCommand)
-
- expect(runCommand.mock.calls.map(([, command]) => command)).toEqual(
- Array.from('Hybrid Agent History Fixture', (character) => ['type', character])
- )
- })
-
- it('retries transient helper failures while reattaching a point tap', async () => {
- let attachAttempts = 0
- const runCommand = vi.fn(async (_args, command) => {
- if (command[0] === 'tap' && runCommand.mock.calls.length === 1) {
- throw new Error('emulator_no_active')
- }
- if (command[0] === 'attach' && attachAttempts++ === 0) {
- throw new Error('emulator_helper_failed: stream endpoints unavailable')
- }
- return { stderr: '', stdout: JSON.stringify({ ok: true }) }
- })
-
- await expect(tapHostedIosPoint(emulator, { x: 0.25, y: 0.75 }, runCommand)).resolves.toEqual({
- x: 0.25,
- y: 0.75
- })
- expect(runCommand.mock.calls.map(([, command]) => command[0])).toEqual([
- 'tap',
- 'kill',
- 'attach',
- 'attach',
- 'tap'
- ])
- })
-
- it('waits for a reconnect label to leave the native accessibility tree', async () => {
- const runCommand = vi
- .fn()
- .mockResolvedValueOnce({
- stderr: '',
- stdout: JSON.stringify({ ok: true, result: [{ label: 'Reconnecting' }] })
- })
- .mockResolvedValueOnce({
- stderr: '',
- stdout: JSON.stringify({ ok: true, result: [{ label: '1 tab' }] })
- })
-
- await expect(
- waitForHostedIosAccessibilityLabelToDisappear(emulator, 'Reconnecting', 1_000, runCommand)
- ).resolves.toBeUndefined()
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-emulator-controller.test.ts b/mobile/src/mobile-web/hosted-ios-emulator-controller.test.ts
deleted file mode 100644
index 7634adbdb53..00000000000
--- a/mobile/src/mobile-web/hosted-ios-emulator-controller.test.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import path from 'node:path'
-import { describe, expect, it, vi } from 'vitest'
-import { startHostedIosEmulatorController } from '../../scripts/hosted-ios-emulator-controller.mjs'
-
-const args = {
- orcaCli: '/repo/config/scripts/orca-dev.mjs',
- runtimeDirectory: '/tmp/mobile-e2e',
- worktree: '/repo'
-}
-
-describe('hosted iOS emulator controller', () => {
- it('registers the worktree in an isolated stable runtime', async () => {
- const runtime = { env: { ORCA_USER_DATA_PATH: '/controller' }, stop: vi.fn() }
- const startRuntime = vi.fn(async () => runtime)
- const runCli = vi.fn(async () => ({ stdout: '{}', stderr: '' }))
-
- await expect(startHostedIosEmulatorController(args, startRuntime, runCli)).resolves.toBe(
- runtime
- )
- expect(startRuntime).toHaveBeenCalledWith(
- expect.objectContaining({
- enabled: true,
- orcaCli: args.orcaCli,
- cwd: args.worktree,
- runDirectory: path.join(args.runtimeDirectory, 'emulator-control')
- })
- )
- expect(runCli).toHaveBeenCalledWith(
- args.orcaCli,
- ['repo', 'add', '--path', args.worktree, '--json'],
- expect.objectContaining({ cwd: args.worktree, env: runtime.env })
- )
- })
-
- it('stops the controller when worktree registration fails', async () => {
- const runtime = { env: {}, stop: vi.fn(async () => {}) }
- const error = new Error('registration failed')
-
- await expect(
- startHostedIosEmulatorController(
- args,
- async () => runtime,
- async () => {
- throw error
- }
- )
- ).rejects.toBe(error)
- expect(runtime.stop).toHaveBeenCalledOnce()
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-emulator-long-press.test.ts b/mobile/src/mobile-web/hosted-ios-emulator-long-press.test.ts
deleted file mode 100644
index 1f4c6905610..00000000000
--- a/mobile/src/mobile-web/hosted-ios-emulator-long-press.test.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import { longPressHostedIosAccessibilityControlByLabelPrefix } from '../../scripts/hosted-ios-emulator-long-press.mjs'
-
-const emulator = {
- deviceUdid: 'simulator-a',
- orcaCli: '/repo/config/scripts/orca-dev.mjs',
- userDataDir: '/tmp/orca-mobile/userData',
- worktree: '/repo'
-}
-
-const rowTree = {
- ok: true,
- result: [
- {
- label: 'mobile-rearch, mobile-rearch, 1',
- enabled: true,
- frame: { x: 0, y: 0.2, width: 1, height: 0.1 }
- }
- ]
-}
-
-const sheetTree = {
- ok: true,
- result: [
- { label: 'Source Control', enabled: true, frame: { x: 0, y: 0.7, width: 1, height: 0.05 } }
- ]
-}
-
-function axResponse(tree: unknown) {
- return { stderr: '', stdout: JSON.stringify(tree) }
-}
-
-function gesturePoints(call: [unknown, string[]] | undefined) {
- return JSON.parse(call?.[1][1] as string)
-}
-
-describe('hosted iOS emulator long press', () => {
- it('holds the touch down across two gesture commands', async () => {
- const runCommand = vi
- .fn()
- .mockResolvedValueOnce(axResponse(rowTree))
- .mockResolvedValueOnce({ stderr: '', stdout: JSON.stringify({ ok: true }) })
- .mockResolvedValueOnce(axResponse(sheetTree))
- .mockResolvedValueOnce({ stderr: '', stdout: JSON.stringify({ ok: true }) })
-
- await expect(
- longPressHostedIosAccessibilityControlByLabelPrefix(
- emulator,
- 'mobile-rearch',
- 20_000,
- runCommand,
- 'Source Control'
- )
- ).resolves.toEqual({ x: 0.5, y: 0.25 })
-
- // Why: a single command batches its points, so the press must span two commands.
- expect(runCommand.mock.calls[1]?.[1][0]).toBe('gesture')
- expect(gesturePoints(runCommand.mock.calls[1])).toEqual([
- { type: 'begin', x: 0.5, y: 0.25 },
- { type: 'move', x: 0.5, y: 0.25 }
- ])
- expect(runCommand.mock.calls[3]?.[1][0]).toBe('gesture')
- expect(gesturePoints(runCommand.mock.calls[3])).toEqual([
- { type: 'move', x: 0.5, y: 0.25 },
- { type: 'end', x: 0.5, y: 0.25 }
- ])
- })
-
- it('releases the touch when the press never settles', async () => {
- const runCommand = vi.fn(async (_args: unknown, command: string[]) => {
- if (command[0] === 'ax') {
- return axResponse(rowTree)
- }
- return { stderr: '', stdout: JSON.stringify({ ok: true }) }
- })
-
- await expect(
- longPressHostedIosAccessibilityControlByLabelPrefix(
- emulator,
- 'mobile-rearch',
- 2_000,
- runCommand,
- 'Source Control'
- )
- ).rejects.toThrow('Source Control was not accessible')
-
- const endGestures = runCommand.mock.calls.filter(
- (call) => call[1][0] === 'gesture' && gesturePoints(call).at(-1).type === 'end'
- )
- expect(endGestures).toHaveLength(1)
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-files-preview-parity.test.ts b/mobile/src/mobile-web/hosted-ios-files-preview-parity.test.ts
deleted file mode 100644
index 7a79d837757..00000000000
--- a/mobile/src/mobile-web/hosted-ios-files-preview-parity.test.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-const mocks = vi.hoisted(() => ({
- activateControl: vi.fn(),
- activateWorkspace: vi.fn(),
- captureScreenshot: vi.fn(),
- compareScreenshots: vi.fn(),
- dismissDeveloperMenu: vi.fn(),
- readControlPoint: vi.fn(),
- readState: vi.fn(),
- readTextPoint: vi.fn(),
- tapControl: vi.fn(),
- tapControlByPrefix: vi.fn(),
- tapPoint: vi.fn(),
- waitForControl: vi.fn(),
- waitForControlByPrefix: vi.fn(),
- waitForLabel: vi.fn(),
- waitForDocument: vi.fn()
-}))
-
-vi.mock('node:child_process', () => ({
- execFile: mocks.captureScreenshot
-}))
-vi.mock('../../scripts/emulator-developer-menu-dismissal.mjs', () => ({
- dismissEmulatorDeveloperMenuIfPresent: mocks.dismissDeveloperMenu
-}))
-vi.mock('../../scripts/hosted-ios-emulator-accessibility.mjs', () => ({
- tapHostedIosAccessibilityControl: mocks.tapControl,
- tapHostedIosAccessibilityControlByLabelPrefix: mocks.tapControlByPrefix,
- tapHostedIosPoint: mocks.tapPoint,
- waitForHostedIosAccessibilityControl: mocks.waitForControl,
- waitForHostedIosAccessibilityControlByLabelPrefix: mocks.waitForControlByPrefix,
- waitForHostedIosAccessibilityLabel: mocks.waitForLabel
-}))
-vi.mock('../../scripts/hosted-ios-screenshot-parity.mjs', () => ({
- assertHostedIosScreenshotParity: mocks.compareScreenshots
-}))
-vi.mock('../../scripts/hosted-webview-cdp-session.mjs', () => ({
- activateHostedWebViewControl: mocks.activateControl,
- readHostedWebViewState: mocks.readState,
- readHostedWebViewTextPoint: mocks.readTextPoint,
- waitForVisibleHostedWebView: mocks.waitForDocument
-}))
-vi.mock('../../scripts/hosted-webview-control-point.mjs', () => ({
- readHostedWebViewControlPoint: mocks.readControlPoint
-}))
-vi.mock('../../scripts/hosted-webview-workspace-activation.mjs', () => ({
- activateHostedWorkspaceRow: mocks.activateWorkspace
-}))
-
-import {
- captureHostedFilesPreviewParity,
- captureNativeFilesPreviewBaselines
-} from '../../scripts/hosted-ios-files-preview-parity.mjs'
-
-describe('hosted iOS Files and Preview parity', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- mocks.captureScreenshot.mockImplementation((_command, _args, callback) =>
- callback(null, '', '')
- )
- mocks.compareScreenshots.mockResolvedValue({ changedPixelRatio: 0.01 })
- mocks.readControlPoint.mockResolvedValue({ x: 0.8, y: 0.1 })
- mocks.readState.mockResolvedValue({
- href: 'orca-mobile-web://build/h/host/files/worktree',
- bodyText: 'Files Casks orca.rb',
- labels: ['Open folder Casks', 'Preview file orca.rb', 'File preview'],
- placeholders: []
- })
- mocks.readTextPoint.mockResolvedValue({ x: 0.2, y: 0.1 })
- mocks.tapControl.mockResolvedValue(undefined)
- mocks.tapControlByPrefix.mockResolvedValue(undefined)
- mocks.tapPoint.mockResolvedValue(undefined)
- mocks.waitForControl.mockResolvedValue({ x: 0.2, y: 0.1 })
- mocks.waitForControlByPrefix.mockResolvedValue({ x: 0.2, y: 0.1 })
- mocks.waitForLabel.mockResolvedValue({
- frame: { x: 0, y: 0.3, width: 1, height: 1.5 },
- label: 'File preview',
- value: ''
- })
- mocks.waitForDocument.mockImplementation(({ expectedHrefIncludes }) => ({
- href: `orca-mobile-web://build${expectedHrefIncludes ?? '/h/host'}`,
- targetId: expectedHrefIncludes ?? 'workspace'
- }))
- })
-
- it('captures native Files and Preview and returns to worktrees', async () => {
- const baselines = await captureNativeFilesPreviewBaselines({
- deviceUdid: 'simulator',
- emulator: { deviceUdid: 'simulator' },
- expectedWorkspace: 'mobile-rearch',
- runtimeDirectory: '/tmp/parity',
- timeoutMs: 30_000
- })
-
- expect(mocks.tapControl).toHaveBeenCalledWith(
- { deviceUdid: 'simulator' },
- 'Open file explorer',
- 30_000
- )
- expect(mocks.tapControl).toHaveBeenCalledWith(
- { deviceUdid: 'simulator' },
- 'Preview file orca.rb',
- 30_000
- )
- expect(mocks.tapControl).toHaveBeenCalledWith(
- { deviceUdid: 'simulator' },
- 'Back to worktrees',
- 30_000
- )
- expect(baselines).toEqual({
- files: {
- screenTitlePoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-files-portrait.png'
- },
- preview: {
- screenTitlePoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-file-preview-portrait.png'
- }
- })
- })
-
- it('compares hosted Files and Preview and returns to the workspace route', async () => {
- const result = await captureHostedFilesPreviewParity({
- deviceUdid: 'simulator',
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- expectedWorkspace: 'mobile-rearch',
- nativeBaselines: {
- files: {
- screenTitlePoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-files-portrait.png'
- },
- preview: {
- screenTitlePoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-file-preview-portrait.png'
- }
- },
- runtimeDirectory: '/tmp/parity',
- timeoutMs: 30_000,
- workspaceDocument: { href: 'orca-mobile-web://build/h/host' }
- })
-
- expect(result.evidence.files.screenshotParity).toEqual({ changedPixelRatio: 0.01 })
- expect(result.evidence.preview.screenshotParity).toEqual({ changedPixelRatio: 0.01 })
- expect(result.workspaceDocument.href).toContain('/h/host')
- expect(mocks.activateWorkspace).toHaveBeenCalledOnce()
- expect(mocks.tapControl).toHaveBeenCalledWith(
- { deviceUdid: 'simulator' },
- 'Open file explorer',
- 5_000
- )
- expect(mocks.tapControl).toHaveBeenCalledWith(
- { deviceUdid: 'simulator' },
- 'Preview file orca.rb',
- 5_000
- )
- expect(mocks.compareScreenshots).toHaveBeenCalledTimes(2)
- })
-
- it('uses a measured WebView point when a control is missing from native accessibility', async () => {
- mocks.tapControl.mockRejectedValueOnce(new Error('missing accessibility descendant'))
-
- await captureHostedFilesPreviewParity({
- deviceUdid: 'simulator',
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- expectedWorkspace: 'mobile-rearch',
- nativeBaselines: {
- files: {
- screenTitlePoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-files-portrait.png'
- },
- preview: {
- screenTitlePoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-file-preview-portrait.png'
- }
- },
- runtimeDirectory: '/tmp/parity',
- timeoutMs: 30_000,
- workspaceDocument: { href: 'orca-mobile-web://build/h/host' }
- })
-
- expect(mocks.readControlPoint).toHaveBeenCalledWith(expect.anything(), 'Open file explorer')
- expect(mocks.tapPoint).toHaveBeenCalledWith({ deviceUdid: 'simulator' }, { x: 0.8, y: 0.1 })
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-host-origin-source-control-journey.test.ts b/mobile/src/mobile-web/hosted-ios-host-origin-source-control-journey.test.ts
deleted file mode 100644
index 8a744cde5e0..00000000000
--- a/mobile/src/mobile-web/hosted-ios-host-origin-source-control-journey.test.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import {
- runHostedHostOriginSourceControlStep,
- verifyHostedHostOriginSourceControlJourney
-} from '../../scripts/hosted-ios-host-origin-source-control-journey.mjs'
-
-const tapByLabelPrefix = vi.hoisted(() => vi.fn())
-vi.mock('../../scripts/hosted-ios-emulator-accessibility.mjs', async (importOriginal) => ({
- ...(await importOriginal>()),
- tapHostedIosAccessibilityControlByLabelPrefix: tapByLabelPrefix
-}))
-
-const CHANGED_FILE = 'mobile/src/mobile-web/bridge.ts'
-const CHANGED_FILE_LABEL = `Open changed file ${CHANGED_FILE}`
-
-function journeyOperations(labels: string[]) {
- const sourceControl = {
- href: 'orca-mobile-web://build/h/host/source-control/workspace'
- }
- const review = { href: 'orca-mobile-web://build/h/host/review/workspace' }
- const returnedWorkspace = { href: 'orca-mobile-web://build/' }
- const activate = vi.fn().mockResolvedValue(undefined)
- const longPress = vi.fn().mockResolvedValue(undefined)
- const tapNative = vi.fn().mockResolvedValue(undefined)
- const waitForDocument = vi
- .fn()
- .mockResolvedValueOnce(sourceControl)
- .mockResolvedValueOnce(review)
- .mockResolvedValueOnce(sourceControl)
- .mockResolvedValueOnce(sourceControl)
- .mockResolvedValueOnce(returnedWorkspace)
- const readState = vi
- .fn()
- .mockResolvedValueOnce({ href: sourceControl.href, labels })
- .mockResolvedValueOnce({ href: review.href, labels: ['Show all review files'] })
- .mockResolvedValueOnce({ href: review.href, labels: ['Back', 'Open review actions'] })
- return {
- documents: { returnedWorkspace, review, sourceControl },
- operations: { activate, longPress, readState, tapNative, waitForDocument }
- }
-}
-
-describe('hosted iOS host-origin Source Control journey', () => {
- it.each([
- ['native baseline', { changedFileLabel: CHANGED_FILE_LABEL }, undefined],
- ['fixture changed file', null, CHANGED_FILE]
- ])('opens mobile Review from the %s oracle', async (_name, nativeBaseline, changedFilePath) => {
- const { documents, operations } = journeyOperations([
- 'Refresh source control',
- CHANGED_FILE_LABEL
- ])
-
- const result = await verifyHostedHostOriginSourceControlJourney({
- changedFilePath,
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { udid: 'SIMULATOR-1' },
- nativeBaseline,
- timeoutMs: 30_000,
- workspaceName: 'mobile-rearch',
- operations
- })
-
- expect(operations.longPress).toHaveBeenCalledWith(
- { udid: 'SIMULATOR-1' },
- 'mobile-rearch',
- 30_000,
- undefined,
- 'Source Control'
- )
- expect(operations.tapNative).toHaveBeenCalledWith(
- { udid: 'SIMULATOR-1' },
- 'Source Control',
- 30_000
- )
- expect(operations.activate.mock.calls.map((call) => call[1])).toEqual([
- { kind: 'label', value: CHANGED_FILE_LABEL },
- { kind: 'label', value: 'Back' },
- { kind: 'label', value: 'Back to session' }
- ])
- expect(operations.waitForDocument).toHaveBeenNthCalledWith(
- 2,
- expect.objectContaining({ expectedHrefIncludes: '/review/' })
- )
- expect(operations.waitForDocument).toHaveBeenCalledTimes(5)
- expect(result).toMatchObject({
- changedFileLabel: CHANGED_FILE_LABEL,
- reviewRoute: documents.review.href,
- sourceControlRoute: documents.sourceControl.href,
- workspaceDocument: documents.returnedWorkspace
- })
- })
-
- it('fails when the fixture changed file is absent from Source Control', async () => {
- const { operations } = journeyOperations([
- 'Refresh source control',
- 'Open changed file mobile/app/index.tsx'
- ])
-
- await expect(
- verifyHostedHostOriginSourceControlJourney({
- changedFilePath: CHANGED_FILE,
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { udid: 'SIMULATOR-1' },
- nativeBaseline: null,
- timeoutMs: 30_000,
- workspaceName: 'mobile-rearch',
- operations
- })
- ).rejects.toThrow(`Host-origin Source Control is missing ${CHANGED_FILE_LABEL}`)
- expect(operations.activate).not.toHaveBeenCalled()
- })
-
- it('refuses to run without an independent expected changed file', async () => {
- const { operations } = journeyOperations(['Refresh source control', CHANGED_FILE_LABEL])
-
- await expect(
- verifyHostedHostOriginSourceControlJourney({
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { udid: 'SIMULATOR-1' },
- nativeBaseline: null,
- timeoutMs: 30_000,
- workspaceName: 'mobile-rearch',
- operations
- })
- ).rejects.toThrow('needs a native baseline or a fixture changed file path')
- expect(operations.activate).not.toHaveBeenCalled()
- })
-
- it.each([
- [
- 'the adversarial fixture row',
- { workspaceRowName: 'orca-adversarial-row' },
- 'orca-adversarial-row'
- ],
- ['the worktree basename', undefined, 'mobile-rearch']
- ])('lands back on %s after the host-origin step', async (_name, adversarialFixture, expected) => {
- const hostOrigin = { workspaceDocument: { href: 'orca-mobile-web://build/' } }
- // The journey itself is covered above; the step only owns the workspace row it returns to.
- const evidenceStep = vi.fn().mockResolvedValue(hostOrigin)
-
- await expect(
- runHostedHostOriginSourceControlStep({
- adversarialFixture,
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { udid: 'SIMULATOR-1' },
- evidenceStep,
- expectedWorkspace: 'mobile-rearch',
- nativeBaseline: null,
- timeoutMs: 30_000
- })
- ).resolves.toBe(hostOrigin)
-
- expect(evidenceStep).toHaveBeenCalledWith(
- 'host-origin Source Control journey',
- expect.any(Function)
- )
- expect(tapByLabelPrefix).toHaveBeenLastCalledWith({ udid: 'SIMULATOR-1' }, expected, 30_000)
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-hybrid-route-handoff.test.ts b/mobile/src/mobile-web/hosted-ios-hybrid-route-handoff.test.ts
deleted file mode 100644
index 952cb5c8515..00000000000
--- a/mobile/src/mobile-web/hosted-ios-hybrid-route-handoff.test.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import { openHostedIosHybridRoute } from '../../scripts/hosted-ios-hybrid-route-handoff.mjs'
-
-describe('hosted iOS hybrid route handoff', () => {
- it('opens the production route without an Experimental Settings entry', async () => {
- const openUrl = vi.fn().mockResolvedValue(undefined)
-
- await openHostedIosHybridRoute({ deviceUdid: 'simulator' }, 10_000, openUrl)
-
- expect(openUrl).toHaveBeenCalledWith('simulator', 'orca:///hybrid')
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-mobile-web-cache.test.ts b/mobile/src/mobile-web/hosted-ios-mobile-web-cache.test.ts
deleted file mode 100644
index beb07f7341a..00000000000
--- a/mobile/src/mobile-web/hosted-ios-mobile-web-cache.test.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
-import { tmpdir } from 'node:os'
-import path from 'node:path'
-import { readFileSync } from 'node:fs'
-import { afterEach, describe, expect, it } from 'vitest'
-import {
- readIosActivation,
- readIosRollbackActivation
-} from '../../scripts/hosted-ios-mobile-web-cache.mjs'
-import { selectHostedIosWebContentPid } from '../../scripts/hosted-ios-webcontent-process.mjs'
-
-const active = 'a'.repeat(64)
-const previous = 'b'.repeat(64)
-const temporaryRoots: string[] = []
-const harnessSource = readFileSync(
- new URL('../../scripts/run-hosted-ios-webview-crash-loop.mjs', import.meta.url),
- 'utf8'
-)
-
-afterEach(async () => {
- await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true })))
-})
-
-describe('hosted iOS mobile web cache evidence', () => {
- it('selects the only host with an active and previous generation', async () => {
- const root = await createAppData()
- await writeActivation(root, '1'.repeat(64), { active })
- const candidate = await writeActivation(root, '2'.repeat(64), { active, previous })
-
- await expect(readIosRollbackActivation(root)).resolves.toEqual({
- path: candidate,
- active,
- previous
- })
- })
-
- it('normalizes an omitted previous generation and rejects invalid identities', async () => {
- const root = await createAppData()
- const valid = await writeActivation(root, '1'.repeat(64), { active })
- const invalid = await writeActivation(root, '2'.repeat(64), { active: '../active' })
-
- await expect(readIosActivation(valid)).resolves.toEqual({ active, previous: null })
- await expect(readIosActivation(invalid)).rejects.toThrow(
- 'iOS cache returned an invalid activation record'
- )
- })
-
- it('selects only the simulator WebContent child', () => {
- const processes = [
- ' 100 1 /System/Library/com.apple.WebKit.WebContent',
- ' 200 67323 /Runtime/WebContentExtension.appex/com.apple.WebKit.WebContent -LaunchArguments',
- ' 300 90000 /Runtime/WebContentExtension.appex/com.apple.WebKit.WebContent -LaunchArguments'
- ].join('\n')
-
- expect(selectHostedIosWebContentPid(processes, 67323)).toBe(200)
- expect(() => selectHostedIosWebContentPid(`${processes}\n${processes}`, 67323)).toThrow(
- 'Expected one iOS WebContent process, found 2'
- )
- })
-
- it('kills three WebContent processes and requires native activation rollback', () => {
- expect(harnessSource).toContain('const failureCount = 3')
- expect(harnessSource).toContain('terminateHostedIosWebContent(deviceUdid)')
- expect(harnessSource).toContain('initial.previous')
- expect(harnessSource).toContain('waitForIosActivation(')
- expect(harnessSource).toContain('>= 60_000')
- expect(harnessSource).toContain('documents.at(-1)?.href === documents[0]?.href')
- })
-})
-
-async function createAppData() {
- const root = await mkdtemp(path.join(tmpdir(), 'orca-ios-cache-evidence-'))
- temporaryRoots.push(root)
- return root
-}
-
-async function writeActivation(
- root: string,
- hostIdentity: string,
- value: { active: string; previous?: string }
-) {
- const hostRoot = path.join(root, 'Library', 'Application Support', 'OrcaMobileWeb', hostIdentity)
- await mkdir(hostRoot, { recursive: true })
- const activationPath = path.join(hostRoot, 'activation.json')
- await writeFile(activationPath, JSON.stringify(value))
- return activationPath
-}
diff --git a/mobile/src/mobile-web/hosted-ios-native-alert-journey.test.ts b/mobile/src/mobile-web/hosted-ios-native-alert-journey.test.ts
deleted file mode 100644
index e370b954314..00000000000
--- a/mobile/src/mobile-web/hosted-ios-native-alert-journey.test.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import { verifyHostedIosNativeAlertJourney } from '../../scripts/hosted-ios-native-alert-journey.mjs'
-
-describe('hosted iOS native Alert journey', () => {
- it('presents an OS alert, correlates its action, and returns to workspaces', async () => {
- const workspaceDocument = { href: 'orca-mobile-web://build/h/host' }
- const sessionDocument = { href: 'orca-mobile-web://build/h/host/session/workspace' }
- const returnedDocument = { href: 'orca-mobile-web://build/h/host' }
- const evaluate = vi
- .fn()
- .mockResolvedValueOnce(JSON.stringify({ started: true }))
- .mockResolvedValueOnce(JSON.stringify({ posted: true }))
- .mockResolvedValueOnce(
- JSON.stringify({ status: 'success', payload: { kind: 'button', buttonIndex: 0 } })
- )
- const waitForDocument = vi
- .fn()
- .mockResolvedValueOnce(sessionDocument)
- .mockResolvedValueOnce(returnedDocument)
- const activateWorkspace = vi.fn().mockResolvedValue(undefined)
- const activateControl = vi.fn().mockResolvedValue(undefined)
- const waitForLabel = vi.fn().mockResolvedValue({ frame: { x: 0, y: 0, width: 1, height: 1 } })
- const tapControl = vi.fn().mockResolvedValue({ x: 0.5, y: 0.5 })
- const waitForLabelToDisappear = vi.fn().mockResolvedValue(undefined)
-
- const result = await verifyHostedIosNativeAlertJourney(
- {
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- expectedWorkspace: 'mobile-rearch',
- timeoutMs: 30_000,
- workspaceDocument
- },
- {
- activateControl,
- activateWorkspace,
- evaluate,
- tapControl,
- waitForDocument,
- waitForLabel,
- waitForLabelToDisappear
- }
- )
-
- expect(tapControl).toHaveBeenCalledWith({ deviceUdid: 'simulator' }, 'Keep editing', 30_000)
- expect(activateControl).toHaveBeenCalledWith(sessionDocument, {
- kind: 'label',
- value: 'Back to worktrees'
- })
- expect(waitForDocument).toHaveBeenLastCalledWith({
- discoveryUrl: 'http://127.0.0.1:9222',
- expectedText: 'mobile-rearch',
- expectedPathname: '/h/host',
- timeoutMs: 30_000
- })
- expect(result).toEqual({
- evidence: {
- button: { x: 0.5, y: 0.5 },
- buttonIndex: 0,
- presentation: 'native',
- title: { x: 0, y: 0, width: 1, height: 1 }
- },
- workspaceDocument: returnedDocument
- })
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-native-onboarding.test.ts b/mobile/src/mobile-web/hosted-ios-native-onboarding.test.ts
deleted file mode 100644
index b2710a1bc67..00000000000
--- a/mobile/src/mobile-web/hosted-ios-native-onboarding.test.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-const mocks = vi.hoisted(() => ({
- tap: vi.fn(),
- wait: vi.fn()
-}))
-
-vi.mock('../../scripts/hosted-ios-emulator-accessibility.mjs', () => ({
- tapHostedIosAccessibilityControl: mocks.tap,
- waitForHostedIosAccessibilityControlByLabelPrefix: mocks.wait
-}))
-
-import { completeHostedIosNativeOnboarding } from '../../scripts/hosted-ios-native-onboarding.mjs'
-
-describe('hosted iOS native onboarding', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- mocks.tap.mockResolvedValue({ x: 0.5, y: 0.5 })
- mocks.wait.mockResolvedValue({ x: 0.5, y: 0.5 })
- })
-
- it('completes the existing native decisions before entering the hosted workspace', async () => {
- mocks.wait.mockRejectedValueOnce(new Error('workspace is not visible'))
- const emulator = { deviceUdid: 'simulator' }
-
- await expect(
- completeHostedIosNativeOnboarding(emulator, 'mobile-rearch', 30_000)
- ).resolves.toEqual({
- sessionView: 'terminal',
- notifications: 'skipped'
- })
- expect(mocks.tap).toHaveBeenNthCalledWith(1, emulator, 'Open sessions in the terminal', 30_000)
- expect(mocks.tap).toHaveBeenNthCalledWith(2, emulator, 'Skip notifications for now', 30_000)
- expect(mocks.wait).toHaveBeenNthCalledWith(1, emulator, 'mobile-rearch', 1_000)
- expect(mocks.wait).toHaveBeenNthCalledWith(2, emulator, 'mobile-rearch', 5_000)
- })
-
- it('returns from an automatically opened session before selecting the workspace', async () => {
- mocks.wait
- .mockRejectedValueOnce(new Error('workspace is not visible'))
- .mockRejectedValueOnce(new Error('workspace is not visible'))
- .mockResolvedValueOnce({ x: 0.5, y: 0.5 })
- const emulator = { deviceUdid: 'simulator' }
-
- await completeHostedIosNativeOnboarding(emulator, 'mobile-rearch', 30_000)
-
- expect(mocks.tap).toHaveBeenNthCalledWith(3, emulator, 'Back to worktrees', 5_000)
- expect(mocks.wait).toHaveBeenNthCalledWith(3, emulator, 'mobile-rearch', 30_000)
- })
-
- it('preserves an already completed native route', async () => {
- const emulator = { deviceUdid: 'simulator' }
-
- await expect(
- completeHostedIosNativeOnboarding(emulator, 'mobile-rearch', 30_000)
- ).resolves.toEqual({
- sessionView: 'retained',
- notifications: 'retained'
- })
- expect(mocks.wait).toHaveBeenCalledWith(emulator, 'mobile-rearch', 1_000)
- expect(mocks.tap).not.toHaveBeenCalled()
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-native-settings-handoff.test.ts b/mobile/src/mobile-web/hosted-ios-native-settings-handoff.test.ts
deleted file mode 100644
index 2c8d920daa2..00000000000
--- a/mobile/src/mobile-web/hosted-ios-native-settings-handoff.test.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-const mocks = vi.hoisted(() => ({
- activate: vi.fn(),
- restartController: vi.fn(),
- waitForDocument: vi.fn(),
- tapAccessibility: vi.fn(),
- waitForAccessibility: vi.fn()
-}))
-
-vi.mock('../../scripts/hosted-webview-cdp-session.mjs', () => ({
- activateHostedWebViewControl: mocks.activate,
- waitForVisibleHostedWebView: mocks.waitForDocument
-}))
-
-vi.mock('../../scripts/hosted-ios-emulator-accessibility.mjs', () => ({
- restartHostedIosEmulatorController: mocks.restartController,
- tapHostedIosAccessibilityControl: mocks.tapAccessibility,
- waitForHostedIosAccessibilityControl: mocks.waitForAccessibility
-}))
-
-import { verifyHostedNativeTerminalSettingsHandoff } from '../../scripts/hosted-ios-native-settings-handoff.mjs'
-
-describe('hosted iOS native settings handoff', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- mocks.activate.mockResolvedValue(undefined)
- mocks.restartController.mockResolvedValue(undefined)
- mocks.waitForDocument
- .mockResolvedValueOnce({ href: 'orca-mobile-web://build/session/workspace' })
- .mockResolvedValueOnce({ href: 'orca-mobile-web://build/session/workspace' })
- mocks.tapAccessibility.mockResolvedValue({ x: 0.5, y: 0.7 })
- mocks.waitForAccessibility.mockResolvedValue({ x: 0.5, y: 0.2 })
- })
-
- it('uses a native touch and returns to the same hosted session', async () => {
- const emulator = { deviceUdid: 'simulator' }
-
- await expect(
- verifyHostedNativeTerminalSettingsHandoff({
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator,
- sessionDocument: { href: 'orca-mobile-web://build/session/workspace' },
- timeoutMs: 30_000
- })
- ).resolves.toEqual({
- route: 'orca-mobile-web://build/session/workspace',
- nativeTapPoint: { x: 0.5, y: 0.7 }
- })
-
- expect(mocks.activate).toHaveBeenCalledWith(expect.anything(), {
- kind: 'label',
- value: 'Add custom shortcut',
- reveal: true
- })
- expect(mocks.restartController).toHaveBeenCalledWith(emulator)
- expect(mocks.tapAccessibility).toHaveBeenCalledWith(emulator, 'Manage Shortcuts', 30_000)
- expect(mocks.waitForAccessibility).toHaveBeenCalledWith(
- emulator,
- 'WHEN YOU LEAVE THE APP',
- 30_000
- )
- expect(mocks.tapAccessibility).toHaveBeenNthCalledWith(
- 2,
- emulator,
- 'Back to hosted session',
- 30_000
- )
- })
-
- it('rejects a native return that changes the hosted session route', async () => {
- mocks.waitForDocument
- .mockReset()
- .mockResolvedValueOnce({ href: 'orca-mobile-web://build/session/one' })
- .mockResolvedValueOnce({ href: 'orca-mobile-web://build/session/two' })
-
- await expect(
- verifyHostedNativeTerminalSettingsHandoff({
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: {},
- sessionDocument: {},
- timeoutMs: 30_000
- })
- ).rejects.toThrow('same hosted session route')
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-photo-permission-denial.test.ts b/mobile/src/mobile-web/hosted-ios-photo-permission-denial.test.ts
deleted file mode 100644
index 2946db3ba65..00000000000
--- a/mobile/src/mobile-web/hosted-ios-photo-permission-denial.test.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import {
- resetHostedIosPhotosPermission,
- verifyHostedIosPhotoPermissionDenial
-} from '../../scripts/hosted-ios-photo-permission-denial.mjs'
-
-const args = {
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- orcaCli: './config/scripts/orca-dev.mjs',
- pairingRuntimeUserDataPath: '/tmp/pairing/userData',
- sessionDocument: {
- href: 'orca-mobile-web://build/h/host/session/workspace'
- },
- terminalHandle: 'terminal-handle',
- timeoutMs: 30_000,
- worktree: '/repo/mobile-rearch'
-}
-
-function createOperations() {
- return {
- readState: vi.fn().mockResolvedValue({
- bodyText: 'Mobile Emulator\nPhoto permission denied'
- }),
- readTerminal: vi.fn().mockResolvedValue(['ORCA_HOSTED_CLIPBOARD_TEXT_PASTE']),
- tapControl: vi.fn().mockResolvedValue({ x: 0.75, y: 0.9 }),
- tapPoint: vi.fn().mockResolvedValue(undefined),
- wait: vi.fn().mockResolvedValue(undefined),
- waitForDocument: vi.fn().mockResolvedValue(args.sessionDocument),
- waitForPrompt: vi.fn().mockResolvedValue({
- label: 'Don’t Allow',
- x: 0.5,
- y: 0.6
- })
- }
-}
-
-describe('hosted iOS Photos permission denial', () => {
- it('resets only the app Photos permission before launch', async () => {
- const runCommand = vi.fn().mockResolvedValue({ stdout: '', stderr: '' })
-
- await resetHostedIosPhotosPermission('simulator', runCommand)
-
- expect(runCommand).toHaveBeenCalledWith('xcrun', [
- 'simctl',
- 'privacy',
- 'simulator',
- 'reset',
- 'photos',
- 'com.stably.orca.mobile'
- ])
- })
-
- it('denies through the unchanged Attach flow without changing terminal output', async () => {
- const operations = createOperations()
-
- await expect(verifyHostedIosPhotoPermissionDenial(args, operations)).resolves.toEqual({
- evidence: {
- activationAttempts: 1,
- attachControlPoint: { x: 0.75, y: 0.9 },
- denialControl: { label: 'Don’t Allow', x: 0.5, y: 0.6 },
- privilegedPageMarkers: 'absent',
- route: args.sessionDocument.href,
- terminalOutput: 'unchanged',
- toast: 'Photo permission denied'
- },
- sessionDocument: args.sessionDocument
- })
-
- expect(operations.tapControl).toHaveBeenCalledWith(
- args.emulator,
- 'Attach a photo',
- args.timeoutMs
- )
- expect(operations.waitForPrompt).toHaveBeenCalledWith(
- args.emulator,
- ['Don’t Allow', "Don't Allow"],
- 5_000
- )
- expect(operations.tapPoint).toHaveBeenCalledWith(args.emulator, {
- label: 'Don’t Allow',
- x: 0.5,
- y: 0.6
- })
- expect(operations.readTerminal).toHaveBeenCalledTimes(2)
- })
-
- it('retries a silent Attach activation before denying the prompt', async () => {
- const operations = createOperations()
- operations.waitForPrompt
- .mockRejectedValueOnce(new Error('prompt not shown'))
- .mockResolvedValueOnce({ label: "Don't Allow", x: 0.5, y: 0.6 })
-
- const result = await verifyHostedIosPhotoPermissionDenial(args, operations)
-
- expect(result.evidence).toMatchObject({
- activationAttempts: 2,
- denialControl: { label: "Don't Allow" }
- })
- expect(operations.tapControl).toHaveBeenCalledTimes(2)
- })
-
- it('rejects a privileged image marker in hosted page state', async () => {
- const operations = createOperations()
- operations.readState.mockResolvedValue({
- bodyText: 'Photo permission denied\ndata:image/png;base64,AAAA'
- })
-
- await expect(verifyHostedIosPhotoPermissionDenial(args, operations)).rejects.toThrow(
- 'exposed privileged page marker: data:image/'
- )
- })
-
- it('rejects any terminal output change after denial', async () => {
- const operations = createOperations()
- operations.readTerminal
- .mockResolvedValueOnce(['before'])
- .mockResolvedValueOnce(['before', '/tmp/orca-paste-private.png'])
-
- await expect(verifyHostedIosPhotoPermissionDenial(args, operations)).rejects.toThrow(
- 'changed the Desktop terminal'
- )
- })
-
- it('accepts terminal row reflow when the character stream is unchanged', async () => {
- const operations = createOperations()
- operations.readTerminal
- .mockResolvedValueOnce([
- 'jinwoo ~/orca/',
- 'ORCA_HOSTED_CLIPBOARD_TEXT_PASTE',
- '/tmp/orca-paste.png'
- ])
- .mockResolvedValueOnce([
- 'jinwoo ~/orca/workspaces/orca/mobile-rearch [mobile-rearch] $ ',
- 'ORCA_HOSTED_CLIPBOARD_',
- 'TEXT_PASTE/tmp/orca-paste.png'
- ])
-
- await expect(verifyHostedIosPhotoPermissionDenial(args, operations)).resolves.toBeDefined()
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-photo-permission-revocation.test.ts b/mobile/src/mobile-web/hosted-ios-photo-permission-revocation.test.ts
deleted file mode 100644
index 25141fde5db..00000000000
--- a/mobile/src/mobile-web/hosted-ios-photo-permission-revocation.test.ts
+++ /dev/null
@@ -1,331 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import {
- backgroundHostedIosMobileApp,
- grantHostedIosPhotosPermission,
- launchHostedIosMobileApp,
- revokeHostedIosPhotosPermission,
- verifyHostedIosPhotoPermissionRevocation
-} from '../../scripts/hosted-ios-photo-permission-revocation.mjs'
-
-const args = {
- deviceUdid: 'simulator',
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- expectedWorkspace: 'mobile-rearch',
- orcaCli: './config/scripts/orca-dev.mjs',
- pairingRuntimeUserDataPath: '/tmp/pairing/userData',
- sessionDocument: {
- href: 'orca-mobile-web://build/h/host/session/workspace'
- },
- terminalHandle: 'terminal-handle',
- timeoutMs: 30_000,
- worktree: '/repo/mobile-rearch'
-}
-const grantedDocument = {
- href: args.sessionDocument.href
- .replace('://build/', '://grant-restart/')
- .replace('/workspace', '/grant-workspace')
-}
-const revokedDocument = {
- href: args.sessionDocument.href
- .replace('://build/', '://revocation-restart/')
- .replace('/workspace', '/revoked-workspace')
-}
-
-function createOperations() {
- return {
- activateWorkspace: vi.fn().mockResolvedValue(undefined),
- backgroundApp: vi.fn().mockResolvedValue(undefined),
- dismissDeveloperMenu: vi.fn().mockResolvedValue(false),
- grantPermission: vi.fn().mockResolvedValue(undefined),
- launchApp: vi.fn().mockResolvedValue(undefined),
- openHybridRoute: vi.fn().mockResolvedValue(undefined),
- readState: vi.fn().mockResolvedValue({
- bodyText: 'Mobile Emulator\nPhoto permission denied'
- }),
- readTerminal: vi.fn().mockResolvedValue(['ORCA_HOSTED_CLIPBOARD_TEXT_PASTE']),
- revokePermission: vi.fn().mockResolvedValue(undefined),
- tapControl: vi
- .fn()
- .mockResolvedValueOnce({ x: 0.75, y: 0.9 })
- .mockResolvedValueOnce({ x: 0.75, y: 0.9 }),
- tapPoint: vi.fn().mockResolvedValue(undefined),
- wait: vi.fn().mockResolvedValue(undefined),
- waitForControl: vi.fn().mockResolvedValue({ label: 'Cancel', x: 0.9, y: 0.1 }),
- waitForPickerDismissal: vi.fn().mockResolvedValue(undefined),
- waitForDocument: vi
- .fn()
- .mockResolvedValueOnce(grantedDocument)
- .mockResolvedValueOnce(grantedDocument)
- .mockResolvedValueOnce(revokedDocument)
- }
-}
-
-describe('hosted iOS Photos permission revocation', () => {
- it.each([
- ['grant', grantHostedIosPhotosPermission],
- ['revoke', revokeHostedIosPhotosPermission]
- ])('%ss only the app Photos permission', async (action, changePermission) => {
- const runCommand = vi.fn().mockResolvedValue({ stdout: '', stderr: '' })
-
- await changePermission('simulator', runCommand)
-
- expect(runCommand).toHaveBeenCalledWith('xcrun', [
- 'simctl',
- 'privacy',
- 'simulator',
- action,
- 'photos',
- 'com.stably.orca.mobile'
- ])
- })
-
- it('launches only the exact mobile app', async () => {
- const runCommand = vi.fn().mockResolvedValue({ stdout: '', stderr: '' })
-
- await launchHostedIosMobileApp('simulator', runCommand)
-
- expect(runCommand).toHaveBeenCalledWith('xcrun', [
- 'simctl',
- 'launch',
- 'simulator',
- 'com.stably.orca.mobile'
- ])
- })
-
- it('backgrounds through the emulator Home button', async () => {
- const runCommand = vi.fn().mockResolvedValue(undefined)
-
- await backgroundHostedIosMobileApp(args.emulator, runCommand)
-
- expect(runCommand).toHaveBeenCalledWith(args.emulator, ['button', 'home'])
- })
-
- it('opens under a grant, revokes, and denies without changing terminal output', async () => {
- const operations = createOperations()
-
- await expect(verifyHostedIosPhotoPermissionRevocation(args, operations)).resolves.toEqual({
- evidence: {
- grantedAttachControlPoint: { x: 0.75, y: 0.9 },
- grantPrivateOriginRotated: true,
- grantPermissionPrompt: 'not-shown',
- grantSessionRecovery: 'session-retained',
- grantWorkspaceAuthorityRotated: true,
- interruptionPrivateOrigin: 'retained',
- interruptionSessionRecovery: 'session-retained',
- interruptionWorkspaceAuthority: 'retained',
- permissionState: 'revoked-after-grant',
- pickerInterruption: 'resumed-then-cancelled',
- pickerCancelControl: { label: 'Cancel', x: 0.9, y: 0.1 },
- privilegedPageMarkers: 'absent',
- revokedAttachControlPoint: { x: 0.75, y: 0.9 },
- revocationPrivateOriginRotated: true,
- revocationSessionRecovery: 'session-retained',
- revocationWorkspaceAuthorityRotated: true,
- route: revokedDocument.href,
- routeRestored: true,
- resumedPickerControl: { label: 'Cancel', x: 0.9, y: 0.1 },
- terminalOutput: 'unchanged',
- toast: 'Photo permission denied'
- },
- sessionDocument: revokedDocument
- })
- expect(operations.grantPermission).toHaveBeenCalledWith('simulator')
- expect(operations.revokePermission).toHaveBeenCalledWith('simulator')
- expect(operations.backgroundApp).toHaveBeenCalledOnce()
- expect(operations.backgroundApp).toHaveBeenCalledWith(args.emulator)
- expect(operations.waitForPickerDismissal).toHaveBeenCalledWith(args.emulator, 'Cancel', 5_000)
- expect(operations.launchApp).toHaveBeenCalledTimes(3)
- expect(operations.dismissDeveloperMenu).toHaveBeenCalledTimes(3)
- expect(operations.tapControl).toHaveBeenNthCalledWith(
- 1,
- args.emulator,
- 'Attach a photo',
- args.timeoutMs
- )
- expect(operations.tapControl).toHaveBeenNthCalledWith(
- 2,
- args.emulator,
- 'Attach a photo',
- args.timeoutMs
- )
- expect(operations.waitForControl).toHaveBeenCalledWith(
- args.emulator,
- ['Cancel', 'Allow Full Access'],
- 10_000
- )
- expect(operations.waitForControl).toHaveBeenCalledWith(args.emulator, ['Cancel'], 5_000)
- expect(operations.readTerminal).toHaveBeenCalledTimes(2)
- })
-
- it('rejects privileged page markers after revocation', async () => {
- const operations = createOperations()
- operations.readState.mockResolvedValue({
- bodyText: 'Photo permission denied\norca-paste-secret.png'
- })
-
- await expect(verifyHostedIosPhotoPermissionRevocation(args, operations)).rejects.toThrow(
- 'exposed privileged page marker: orca-paste-'
- )
- })
-
- it('accepts the real full-access prompt before cancelling the granted picker', async () => {
- const operations = createOperations()
- operations.waitForControl
- .mockResolvedValueOnce({ label: 'Allow Full Access', x: 0.5, y: 0.7 })
- .mockResolvedValueOnce({ label: 'Cancel', x: 0.9, y: 0.1 })
-
- const result = await verifyHostedIosPhotoPermissionRevocation(args, operations)
-
- expect(result.evidence.grantPermissionPrompt).toBe('allowed-full-access')
- expect(operations.tapPoint).toHaveBeenNthCalledWith(1, args.emulator, {
- label: 'Allow Full Access',
- x: 0.5,
- y: 0.7
- })
- expect(operations.tapPoint).toHaveBeenNthCalledWith(2, args.emulator, {
- label: 'Cancel',
- x: 0.9,
- y: 0.1
- })
- expect(operations.backgroundApp).toHaveBeenCalledWith(args.emulator)
- })
-
- it('rejects a private origin reused across the grant restart', async () => {
- const operations = createOperations()
- operations.waitForDocument.mockReset().mockResolvedValue(args.sessionDocument)
-
- await expect(verifyHostedIosPhotoPermissionRevocation(args, operations)).rejects.toThrow(
- 'grant reused the private WebView origin'
- )
- })
-
- it('rejects a private origin reused across the revocation restart', async () => {
- const operations = createOperations()
- operations.waitForDocument
- .mockReset()
- .mockResolvedValueOnce(grantedDocument)
- .mockResolvedValueOnce(grantedDocument)
- .mockResolvedValueOnce(grantedDocument)
-
- await expect(verifyHostedIosPhotoPermissionRevocation(args, operations)).rejects.toThrow(
- 'revocation reused the private WebView origin'
- )
- })
-
- it('rejects private origin replacement across picker interruption', async () => {
- const operations = createOperations()
- const replacedOrigin = {
- href: grantedDocument.href.replace('://grant-restart/', '://interruption-restart/')
- }
- operations.waitForDocument
- .mockReset()
- .mockResolvedValueOnce(grantedDocument)
- .mockResolvedValueOnce(replacedOrigin)
-
- await expect(verifyHostedIosPhotoPermissionRevocation(args, operations)).rejects.toThrow(
- 'picker interruption authority did not match session-retained recovery'
- )
- })
-
- it('rejects opaque authority replacement across picker interruption', async () => {
- const operations = createOperations()
- const replacedAuthority = {
- href: grantedDocument.href.replace('/grant-workspace', '/interrupted-workspace')
- }
- operations.waitForDocument
- .mockReset()
- .mockResolvedValueOnce(grantedDocument)
- .mockResolvedValueOnce(replacedAuthority)
-
- await expect(verifyHostedIosPhotoPermissionRevocation(args, operations)).rejects.toThrow(
- 'picker interruption authority did not match session-retained recovery'
- )
- })
-
- it('requires rotated authority when picker interruption needs route handoff', async () => {
- const operations = createOperations()
- const workspaceDocument = { href: 'orca-mobile-web://workspace/h/host' }
- const interruptedDocument = {
- href: grantedDocument.href
- .replace('://grant-restart/', '://interruption-restart/')
- .replace('/grant-workspace', '/interrupted-workspace')
- }
- operations.waitForDocument
- .mockReset()
- .mockResolvedValueOnce(grantedDocument)
- .mockRejectedValueOnce(new Error('session not retained'))
- .mockResolvedValueOnce(workspaceDocument)
- .mockResolvedValueOnce(interruptedDocument)
- .mockResolvedValueOnce(revokedDocument)
-
- const result = await verifyHostedIosPhotoPermissionRevocation(args, operations)
-
- expect(result.evidence).toMatchObject({
- interruptionPrivateOrigin: 'rotated',
- interruptionSessionRecovery: 'hybrid-route-handoff',
- interruptionWorkspaceAuthority: 'rotated'
- })
- })
-
- it('skips row activation when the hybrid route restores the session itself', async () => {
- const operations = createOperations()
- operations.waitForDocument
- .mockReset()
- .mockRejectedValueOnce(new Error('session not retained'))
- .mockResolvedValueOnce(grantedDocument)
- .mockResolvedValueOnce(grantedDocument)
- .mockResolvedValueOnce(grantedDocument)
- .mockResolvedValueOnce(revokedDocument)
-
- const result = await verifyHostedIosPhotoPermissionRevocation(args, operations)
-
- expect(result.evidence).toMatchObject({ grantSessionRecovery: 'hybrid-route-handoff' })
- expect(operations.openHybridRoute).toHaveBeenCalledOnce()
- expect(operations.activateWorkspace).not.toHaveBeenCalled()
- })
-
- it('reopens the production hybrid route after permission-triggered termination', async () => {
- const operations = createOperations()
- const workspaceDocument = { href: 'orca-mobile-web://workspace/h/host' }
- operations.waitForDocument
- .mockReset()
- .mockRejectedValueOnce(new Error('session not retained'))
- .mockResolvedValueOnce(workspaceDocument)
- .mockResolvedValueOnce(grantedDocument)
- .mockResolvedValueOnce(grantedDocument)
- .mockRejectedValueOnce(new Error('session not retained'))
- .mockResolvedValueOnce(workspaceDocument)
- .mockResolvedValueOnce(revokedDocument)
-
- const result = await verifyHostedIosPhotoPermissionRevocation(args, operations)
-
- expect(result.evidence).toMatchObject({
- grantSessionRecovery: 'hybrid-route-handoff',
- interruptionSessionRecovery: 'session-retained',
- revocationSessionRecovery: 'hybrid-route-handoff',
- routeRestored: true
- })
- expect(operations.openHybridRoute).toHaveBeenCalledTimes(2)
- expect(operations.openHybridRoute).toHaveBeenCalledWith(args.emulator, args.timeoutMs)
- expect(operations.activateWorkspace).toHaveBeenCalledTimes(2)
- expect(operations.activateWorkspace).toHaveBeenCalledWith(
- workspaceDocument,
- args.expectedWorkspace,
- expect.any(Function),
- args.timeoutMs,
- expect.any(Function)
- )
- })
-
- it('rejects terminal mutations after revocation', async () => {
- const operations = createOperations()
- operations.readTerminal
- .mockResolvedValueOnce(['ORCA_HOSTED_CLIPBOARD_TEXT_PASTE'])
- .mockResolvedValueOnce(['ORCA_HOSTED_CLIPBOARD_TEXT_PASTE', '/tmp/orca-paste-private.png'])
-
- await expect(verifyHostedIosPhotoPermissionRevocation(args, operations)).rejects.toThrow(
- 'changed the Desktop terminal'
- )
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-privacy-log-audit.test.ts b/mobile/src/mobile-web/hosted-ios-privacy-log-audit.test.ts
deleted file mode 100644
index d83438a63ba..00000000000
--- a/mobile/src/mobile-web/hosted-ios-privacy-log-audit.test.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import {
- hostedIosLogStartTime,
- hostedIosPrivacyLogEvidence
-} from '../../scripts/hosted-ios-privacy-log-audit.mjs'
-
-describe('hosted iOS privacy log audit', () => {
- it('formats simulator log start times without ISO separators', () => {
- const value = hostedIosLogStartTime(new Date(2026, 6, 29, 12, 34, 56).getTime())
-
- expect(value).toBe('2026-07-29 12:34:56')
- })
-
- it('accepts fixed-category connection and recovery logs', () => {
- expect(
- hostedIosPrivacyLogEvidence(
- '[net] state {"endpoint":"websocket","from":"connecting","to":"connected"}'
- )
- ).toEqual({
- logBytes: 73,
- counts: {
- privilegedField: 0,
- tokenStorage: 0,
- nativeAuthority: 0,
- privateOriginUrl: 0,
- webSocketUrl: 0,
- fixtureMarker: 0
- }
- })
- })
-
- it.each([
- 'deviceToken',
- 'orca.host-token.host-a',
- 'openHostLogicalClient',
- 'orca-mobile-web://abcdefghijklmnopqrstuvwxyz/',
- 'wss://paired-host.invalid/socket',
- 'ORCA_E2E_MOBILE_WEB_HOST_PUBLIC_KEY'
- ])('rejects privileged log marker %s', (marker) => {
- expect(() => hostedIosPrivacyLogEvidence(marker)).toThrow('privacy log audit failed')
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-screenshot-parity.test.ts b/mobile/src/mobile-web/hosted-ios-screenshot-parity.test.ts
deleted file mode 100644
index 726b8ebd575..00000000000
--- a/mobile/src/mobile-web/hosted-ios-screenshot-parity.test.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-import { mkdtemp, writeFile } from 'node:fs/promises'
-import os from 'node:os'
-import path from 'node:path'
-import { PNG } from 'pngjs'
-import { describe, expect, it } from 'vitest'
-import { assertHostedIosScreenshotParity } from '../../scripts/hosted-ios-screenshot-parity.mjs'
-
-describe('hosted iOS screenshot parity', () => {
- it('masks status-bar changes while enforcing the shared content surface', async () => {
- const native = createScreenshot(100, 100)
- const hosted = createScreenshot(100, 100)
- paintRows(hosted, 0, 6, 255)
- const paths = await writeScreenshots(native, hosted)
-
- const result = await assertHostedIosScreenshotParity({
- ...paths,
- nativeLandmark: { x: 0.5, y: 0.1 },
- hostedLandmark: { x: 0.7, y: 0.104 }
- })
- expect(result.changedPixelRatio).toBe(0)
- expect(result.landmarkDelta.x).toBeCloseTo(0.2)
- expect(result.landmarkDelta.y).toBeCloseTo(0.004)
- })
-
- it('rejects a material change below the status bar', async () => {
- const native = createScreenshot(100, 100)
- const hosted = createScreenshot(100, 100)
- paintRows(hosted, 20, 40, 255)
- const paths = await writeScreenshots(native, hosted)
-
- await expect(
- assertHostedIosScreenshotParity({
- ...paths,
- nativeLandmark: { x: 0.5, y: 0.1 },
- hostedLandmark: { x: 0.5, y: 0.1 }
- })
- ).rejects.toThrow('exceeded parity budgets')
- })
-
- it('tolerates local text-rasterization shifts without hiding the raw difference', async () => {
- const native = createScreenshot(200, 200)
- const hosted = createScreenshot(200, 200)
- paintVerticalStripes(native, 20, 0)
- paintVerticalStripes(hosted, 20, 4)
- const paths = await writeScreenshots(native, hosted)
-
- const result = await assertHostedIosScreenshotParity({
- ...paths,
- nativeLandmark: { x: 0.5, y: 0.1 },
- hostedLandmark: { x: 0.5, y: 0.1 }
- })
- expect(result.rawChangedPixelRatio).toBeGreaterThan(0.1)
- expect(result.changedPixelRatio).toBe(0)
- expect(result.neighborhoodRadiusPixels).toBe(16)
- })
-
- it('rejects a safe-area landmark shift even when the pixels match', async () => {
- const screenshot = createScreenshot(100, 100)
- const paths = await writeScreenshots(screenshot, screenshot)
-
- await expect(
- assertHostedIosScreenshotParity({
- ...paths,
- nativeLandmark: { x: 0.5, y: 0.1 },
- hostedLandmark: { x: 0.5, y: 0.11 }
- })
- ).rejects.toThrow('exceeded parity budgets')
- })
-
- it('supports pixel-only evidence when rotated WebView accessibility is unavailable', async () => {
- const screenshot = createScreenshot(100, 100)
- const paths = await writeScreenshots(screenshot, screenshot)
-
- await expect(
- assertHostedIosScreenshotParity({
- ...paths,
- nativeLandmark: null,
- hostedLandmark: null
- })
- ).resolves.toMatchObject({ changedPixelRatio: 0, landmarkDelta: null })
- })
-})
-
-function createScreenshot(width: number, height: number) {
- const png = new PNG({ width, height })
- png.data.fill(0)
- return png
-}
-
-function paintRows(png: PNG, firstRow: number, lastRow: number, value: number) {
- for (let y = firstRow; y <= lastRow; y++) {
- for (let x = 0; x < png.width; x++) {
- const offset = (y * png.width + x) * 4
- png.data[offset] = value
- png.data[offset + 1] = value
- png.data[offset + 2] = value
- png.data[offset + 3] = 255
- }
- }
-}
-
-function paintVerticalStripes(png: PNG, firstRow: number, xOffset: number) {
- for (let x = xOffset; x < png.width; x += 12) {
- for (let y = firstRow; y < png.height; y++) {
- paintPixel(png, x, y, 255)
- paintPixel(png, x + 1, y, 128)
- }
- }
-}
-
-function paintPixel(png: PNG, x: number, y: number, value: number) {
- if (x >= png.width) {
- return
- }
- const offset = (y * png.width + x) * 4
- png.data[offset] = value
- png.data[offset + 1] = value
- png.data[offset + 2] = value
- png.data[offset + 3] = 255
-}
-
-async function writeScreenshots(native: PNG, hosted: PNG) {
- const directory = await mkdtemp(path.join(os.tmpdir(), 'orca-hosted-ios-parity-'))
- const nativeScreenshot = path.join(directory, 'native.png')
- const hostedScreenshot = path.join(directory, 'hosted.png')
- await Promise.all([
- writeFile(nativeScreenshot, PNG.sync.write(native)),
- writeFile(hostedScreenshot, PNG.sync.write(hosted))
- ])
- return { nativeScreenshot, hostedScreenshot }
-}
diff --git a/mobile/src/mobile-web/hosted-ios-simulator-app-build.test.ts b/mobile/src/mobile-web/hosted-ios-simulator-app-build.test.ts
deleted file mode 100644
index 3eaf4618417..00000000000
--- a/mobile/src/mobile-web/hosted-ios-simulator-app-build.test.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import { buildAndInstallHostedIosSimulatorApp } from '../../scripts/hosted-ios-simulator-app-build.mjs'
-
-describe('hosted iOS simulator app build', () => {
- it('removes persisted app state before installing the exact build', async () => {
- const runCommand = vi.fn().mockResolvedValue({ stdout: '', stderr: '' })
-
- const appPath = await buildAndInstallHostedIosSimulatorApp({
- deviceUdid: 'simulator-1',
- worktree: '/worktree',
- runCommand
- })
-
- expect(runCommand).toHaveBeenNthCalledWith(2, 'xcrun', [
- 'simctl',
- 'uninstall',
- 'simulator-1',
- 'com.stably.orca.mobile'
- ])
- expect(runCommand).toHaveBeenNthCalledWith(
- 3,
- 'xcrun',
- ['simctl', 'install', 'simulator-1', appPath],
- { maxBuffer: 4 * 1024 * 1024 }
- )
- expect(runCommand).toHaveBeenNthCalledWith(4, 'xcrun', [
- 'simctl',
- 'spawn',
- 'simulator-1',
- 'defaults',
- 'write',
- 'com.stably.orca.mobile',
- 'EXDevMenuShowFloatingActionButton',
- '-bool',
- 'false'
- ])
- })
-
- it('continues when no prior app is installed', async () => {
- const runCommand = vi
- .fn()
- .mockResolvedValueOnce({ stdout: '', stderr: '' })
- .mockRejectedValueOnce(new Error('not installed'))
- .mockResolvedValueOnce({ stdout: '', stderr: '' })
-
- await expect(
- buildAndInstallHostedIosSimulatorApp({
- deviceUdid: 'simulator-1',
- worktree: '/worktree',
- runCommand
- })
- ).resolves.toContain('Orca.app')
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-simulator-app-preparation.test.ts b/mobile/src/mobile-web/hosted-ios-simulator-app-preparation.test.ts
deleted file mode 100644
index 2f2838f2aab..00000000000
--- a/mobile/src/mobile-web/hosted-ios-simulator-app-preparation.test.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-const mocks = vi.hoisted(() => ({
- buildAndInstall: vi.fn(),
- install: vi.fn()
-}))
-
-vi.mock('../../scripts/hosted-ios-simulator-app-build.mjs', () => ({
- buildAndInstallHostedIosSimulatorApp: mocks.buildAndInstall,
- hostedIosSimulatorAppPath: (worktree: string) => `${worktree}/Orca.app`,
- installHostedIosSimulatorApp: mocks.install
-}))
-
-import { hostedIosSimulatorAppPreparation } from '../../scripts/hosted-ios-simulator-app-preparation.mjs'
-
-describe('hosted iOS simulator app preparation', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- mocks.buildAndInstall.mockResolvedValue('/worktree/Orca.app')
- mocks.install.mockResolvedValue(undefined)
- })
-
- it('reuses the installed app without reinstalling it', async () => {
- const runCommand = vi.fn().mockResolvedValue({ stdout: '/installed/Orca.app' })
- const preparation = hostedIosSimulatorAppPreparation({
- deviceUdid: 'simulator',
- reuseNativeInstall: true,
- skipNativeBuild: false,
- worktree: '/worktree',
- runCommand
- })
-
- await expect(preparation.run()).resolves.toBe('/installed/Orca.app')
- expect(preparation.label).toBe('existing native simulator app')
- expect(runCommand).toHaveBeenCalledWith('xcrun', [
- 'simctl',
- 'get_app_container',
- 'simulator',
- 'com.stably.orca.mobile',
- 'app'
- ])
- expect(mocks.install).not.toHaveBeenCalled()
- })
-
- it('retains the cached reinstall and build paths', async () => {
- const cached = hostedIosSimulatorAppPreparation({
- deviceUdid: 'simulator',
- reuseNativeInstall: false,
- skipNativeBuild: true,
- worktree: '/worktree'
- })
- const build = hostedIosSimulatorAppPreparation({
- deviceUdid: 'simulator',
- reuseNativeInstall: false,
- skipNativeBuild: false,
- worktree: '/worktree'
- })
-
- await expect(cached.run()).resolves.toBe('/worktree/Orca.app')
- await expect(build.run()).resolves.toBe('/worktree/Orca.app')
- expect(mocks.install).toHaveBeenCalledWith({
- deviceUdid: 'simulator',
- appPath: '/worktree/Orca.app',
- runCommand: expect.any(Function)
- })
- expect(mocks.buildAndInstall).toHaveBeenCalledWith({
- deviceUdid: 'simulator',
- worktree: '/worktree',
- runCommand: expect.any(Function)
- })
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-source-control-review-journey.test.ts b/mobile/src/mobile-web/hosted-ios-source-control-review-journey.test.ts
deleted file mode 100644
index a4c4af6d551..00000000000
--- a/mobile/src/mobile-web/hosted-ios-source-control-review-journey.test.ts
+++ /dev/null
@@ -1,531 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-const mocks = vi.hoisted(() => ({
- captureParityScreen: vi.fn(),
- readBridgeErrors: vi.fn(),
- navigateRoute: vi.fn(),
- parityEvidence: vi.fn(),
- readControlPoint: vi.fn(),
- readState: vi.fn(),
- tapAccessibilityControl: vi.fn(),
- tapPoint: vi.fn(),
- startBridgeErrorObservation: vi.fn(),
- waitForDocument: vi.fn()
-}))
-
-vi.mock('../../scripts/hosted-webview-cdp-session.mjs', () => ({
- readHostedWebViewState: mocks.readState,
- waitForVisibleHostedWebView: mocks.waitForDocument
-}))
-
-vi.mock('../../scripts/hosted-webview-control-point.mjs', () => ({
- readHostedWebViewControlPoint: mocks.readControlPoint
-}))
-
-vi.mock('../../scripts/hosted-ios-emulator-accessibility.mjs', () => ({
- tapHostedIosAccessibilityControl: mocks.tapAccessibilityControl,
- tapHostedIosPoint: mocks.tapPoint
-}))
-
-vi.mock('../../scripts/hosted-webview-route-navigation.mjs', () => ({
- navigateHostedWebViewRoute: mocks.navigateRoute
-}))
-
-vi.mock('../../scripts/hosted-webview-bridge-error-observation.mjs', () => ({
- readHostedWebViewBridgeErrors: mocks.readBridgeErrors,
- startHostedWebViewBridgeErrorObservation: mocks.startBridgeErrorObservation
-}))
-
-vi.mock('../../scripts/hosted-ios-source-control-review-parity.mjs', () => ({
- captureHostedSourceControlReviewScreen: mocks.captureParityScreen,
- HEADLESS_REVIEW_OPEN_ERROR: 'renderer_unavailable',
- sourceControlReviewParityEvidence: mocks.parityEvidence
-}))
-
-import { verifyHostedSourceControlReviewJourney } from '../../scripts/hosted-ios-source-control-review-journey.mjs'
-
-describe('hosted iOS Source Control and Review journey', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- mocks.readControlPoint
- .mockResolvedValueOnce({ x: 0.4, y: 0.2 })
- .mockResolvedValueOnce({ x: 0.5, y: 0.4 })
- mocks.navigateRoute.mockResolvedValue(undefined)
- mocks.captureParityScreen
- .mockResolvedValueOnce({ screenshot: '/tmp/hosted-source-control.png' })
- .mockResolvedValueOnce({ screenshot: '/tmp/hosted-review.png' })
- mocks.parityEvidence
- .mockReturnValueOnce({ screen: 'source-control' })
- .mockReturnValueOnce({ screen: 'review' })
- mocks.tapAccessibilityControl.mockResolvedValue(undefined)
- mocks.tapPoint.mockResolvedValue(undefined)
- mocks.startBridgeErrorObservation.mockResolvedValue(undefined)
- mocks.readBridgeErrors.mockResolvedValue([])
- mocks.waitForDocument
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/source-control/workspace'
- })
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/session/workspace'
- })
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/review/workspace'
- })
- mocks.readState
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/source-control/workspace',
- bodyText: 'Source Control Changes Pull Request Commits Create pull request 128 on branch',
- labels: ['Refresh source control', 'Open changed file mobile/app/index.tsx']
- })
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/review/workspace',
- bodyText: 'reviewed',
- labels: ['Back', 'Open review actions']
- })
- })
-
- it('opens the unchanged Session diff flow and standalone Review route', async () => {
- const emulator = { deviceUdid: 'simulator' }
- const sessionDocument = {
- href: 'orca-mobile-web://build/h/host/session/workspace'
- }
-
- await expect(
- verifyHostedSourceControlReviewJourney({
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator,
- sessionDocument,
- timeoutMs: 30_000
- })
- ).resolves.toEqual({
- sourceControlRoute: 'orca-mobile-web://build/h/host/source-control/workspace',
- sourceControlSegments: ['Changes', 'Pull Request', 'Commits'],
- sessionDiffRoute: 'orca-mobile-web://build/h/host/session/workspace',
- reviewOpen: { headless: false, native: null },
- reviewRoute: 'orca-mobile-web://build/h/host/review/workspace',
- reviewControls: ['Back', 'Open review actions']
- })
-
- expect(mocks.readControlPoint).toHaveBeenNthCalledWith(
- 1,
- sessionDocument,
- 'Open source control'
- )
- expect(mocks.tapAccessibilityControl).toHaveBeenNthCalledWith(
- 1,
- emulator,
- 'Open source control',
- 5_000
- )
- expect(mocks.tapAccessibilityControl).toHaveBeenNthCalledWith(
- 2,
- emulator,
- 'Open changed file mobile/app/index.tsx',
- 5_000
- )
- expect(mocks.tapPoint).not.toHaveBeenCalled()
- expect(mocks.navigateRoute).toHaveBeenCalledWith(
- { href: 'orca-mobile-web://build/h/host/session/workspace' },
- '/h/host/review/workspace?scope=all'
- )
- })
-
- it('accepts a platform-specific native tap implementation', async () => {
- const tapPoint = vi.fn().mockResolvedValue(undefined)
- const inspectChangedContent = vi.fn().mockResolvedValue(undefined)
-
- await verifyHostedSourceControlReviewJourney({
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { adb: '/sdk/adb' },
- sessionDocument: {
- href: 'orca-mobile-web://build/h/host/session/workspace'
- },
- expectedSessionDiffText: '3 tabs',
- inspectChangedContent,
- timeoutMs: 30_000,
- tapPoint
- })
-
- expect(tapPoint).toHaveBeenCalledTimes(2)
- expect(tapPoint.mock.calls.map((call) => call[2])).toEqual([
- 'Open source control',
- 'Open changed file mobile/app/index.tsx'
- ])
- expect(inspectChangedContent.mock.calls.map((call) => call[0].phase)).toEqual([
- 'sourceControl',
- 'sessionDiff',
- 'review'
- ])
- expect(mocks.waitForDocument).toHaveBeenNthCalledWith(
- 2,
- expect.objectContaining({ expectedText: '3 tabs' })
- )
- })
-
- it('captures strict Source Control and Review parity when native baselines are provided', async () => {
- const result = await verifyHostedSourceControlReviewJourney({
- deviceUdid: 'simulator',
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- nativeBaselines: {
- sourceControl: {
- changedFileLabel: 'Open changed file mobile/app/index.tsx',
- pullRequestState: {
- kind: 'create',
- label: 'Create pull request'
- },
- screenshot: '/tmp/native-source-control.png'
- },
- review: { screenshot: '/tmp/native-review.png' }
- },
- runtimeDirectory: '/tmp/parity',
- sessionDocument: {
- href: 'orca-mobile-web://build/h/host/session/workspace'
- },
- timeoutMs: 30_000
- })
-
- expect(mocks.captureParityScreen).toHaveBeenNthCalledWith(
- 1,
- expect.objectContaining({
- deviceUdid: 'simulator',
- screenshotName: 'hosted-source-control-portrait.png',
- title: 'Source Control'
- })
- )
- expect(mocks.captureParityScreen).toHaveBeenNthCalledWith(
- 2,
- expect.objectContaining({
- deviceUdid: 'simulator',
- screenshotName: 'hosted-review-portrait.png',
- title: 'Changes'
- })
- )
- expect(result.parityFixture).toEqual({
- sourceControl: { screen: 'source-control' },
- review: { screen: 'review' }
- })
- })
-
- it('settles parity against the existing pull request captured by native', async () => {
- const pullRequestLabel = 'Pull request #13386, Draft, No checks. Open pull request.'
- mocks.readState
- .mockReset()
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/source-control/workspace',
- bodyText: 'Source Control Changes Pull Request Commits #13386 Draft 128 on branch',
- labels: [
- 'Refresh source control',
- pullRequestLabel,
- 'Open changed file mobile/app/index.tsx'
- ]
- })
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/review/workspace',
- bodyText: 'reviewed',
- labels: ['Back', 'Open review actions']
- })
-
- await expect(
- verifyHostedSourceControlReviewJourney({
- deviceUdid: 'simulator',
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- nativeBaselines: {
- sourceControl: {
- changedFileLabel: 'Open changed file mobile/app/index.tsx',
- pullRequestState: { kind: 'ready', label: pullRequestLabel, number: '13386' },
- screenshot: '/tmp/native-source-control.png'
- },
- review: { screenshot: '/tmp/native-review.png' }
- },
- runtimeDirectory: '/tmp/parity',
- sessionDocument: {
- href: 'orca-mobile-web://build/h/host/session/workspace'
- },
- timeoutMs: 30_000
- })
- ).resolves.toMatchObject({ sourceControlSegments: ['Changes', 'Pull Request', 'Commits'] })
- })
-
- it('opens the exact changed file captured by the native baseline', async () => {
- const nativeChangedFileLabel = 'Open changed file mobile/src/mobile-web/bridge.ts'
- mocks.readState
- .mockReset()
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/source-control/workspace',
- bodyText: 'Source Control Changes Pull Request Commits Create pull request 128 on branch',
- labels: [
- 'Refresh source control',
- 'Open changed file mobile/app/index.tsx',
- nativeChangedFileLabel
- ]
- })
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/review/workspace',
- bodyText: 'reviewed',
- labels: ['Back', 'Open review actions']
- })
-
- await verifyHostedSourceControlReviewJourney({
- deviceUdid: 'simulator',
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- nativeBaselines: {
- sourceControl: {
- changedFileLabel: nativeChangedFileLabel,
- pullRequestState: { kind: 'create', label: 'Create pull request' },
- screenshot: '/tmp/native-source-control.png'
- },
- review: { screenshot: '/tmp/native-review.png' }
- },
- runtimeDirectory: '/tmp/parity',
- sessionDocument: { href: 'orca-mobile-web://build/h/host/session/workspace' },
- timeoutMs: 30_000
- })
-
- expect(mocks.tapAccessibilityControl).toHaveBeenNthCalledWith(
- 2,
- { deviceUdid: 'simulator' },
- nativeChangedFileLabel,
- 5_000
- )
- })
-
- it('falls back to measured points when WebKit omits accessibility descendants', async () => {
- mocks.tapAccessibilityControl.mockRejectedValue(new Error('missing descendant'))
-
- await verifyHostedSourceControlReviewJourney({
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- sessionDocument: {
- href: 'orca-mobile-web://build/h/host/session/workspace'
- },
- timeoutMs: 30_000
- })
-
- expect(mocks.tapPoint).toHaveBeenNthCalledWith(1, expect.anything(), {
- x: 0.4,
- y: 0.2
- })
- expect(mocks.tapPoint).toHaveBeenNthCalledWith(2, expect.anything(), {
- x: 0.5,
- y: 0.4
- })
- })
-
- it('uses measured native taps for duplicated Source Control segment labels', async () => {
- const sourceState = {
- href: 'orca-mobile-web://build/h/host/source-control/workspace',
- bodyText: 'Source Control Changes Pull Request Commits Create pull request 128 on branch',
- labels: ['Refresh source control', 'Open changed file mobile/app/index.tsx']
- }
- mocks.readControlPoint
- .mockReset()
- .mockResolvedValueOnce({ x: 0.4, y: 0.2 })
- .mockResolvedValueOnce({ x: 0.6, y: 0.3 })
- .mockResolvedValueOnce({ x: 0.3, y: 0.3 })
- .mockResolvedValueOnce({ x: 0.5, y: 0.4 })
- mocks.readState
- .mockReset()
- .mockResolvedValueOnce(sourceState)
- .mockResolvedValueOnce(sourceState)
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/review/workspace',
- bodyText: 'reviewed',
- labels: ['Back', 'Open review actions']
- })
-
- await verifyHostedSourceControlReviewJourney({
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- inspectProviderContent: vi.fn().mockResolvedValue(undefined),
- sessionDocument: {
- href: 'orca-mobile-web://build/h/host/session/workspace'
- },
- timeoutMs: 30_000
- })
-
- expect(mocks.tapPoint).toHaveBeenNthCalledWith(1, expect.anything(), {
- x: 0.6,
- y: 0.3
- })
- expect(mocks.tapPoint).toHaveBeenNthCalledWith(2, expect.anything(), {
- x: 0.3,
- y: 0.3
- })
- })
-
- it('transforms only the measured fallback while retaining semantic activation', async () => {
- const transformPoint = vi.fn((point) => ({ ...point, y: point.y + 0.1 }))
- mocks.tapAccessibilityControl.mockRejectedValue(new Error('missing descendant'))
-
- await verifyHostedSourceControlReviewJourney({
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- sessionDocument: {
- href: 'orca-mobile-web://build/h/host/session/workspace'
- },
- timeoutMs: 30_000,
- transformPoint
- })
-
- expect(mocks.tapAccessibilityControl).toHaveBeenCalledTimes(2)
- expect(mocks.tapPoint.mock.calls[0][1]).toMatchObject({ x: 0.4 })
- expect(mocks.tapPoint.mock.calls[0][1].y).toBeCloseTo(0.3)
- expect(mocks.tapPoint.mock.calls[1][1]).toEqual({ x: 0.5, y: 0.5 })
- })
-
- it('falls back to a measured point when an accessibility tap silently misses', async () => {
- mocks.readControlPoint
- .mockReset()
- .mockResolvedValueOnce({ x: 0.4, y: 0.2 })
- .mockResolvedValueOnce({ x: 0.5, y: 0.4 })
- .mockResolvedValueOnce({ x: 0.5, y: 0.4 })
- mocks.waitForDocument
- .mockReset()
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/source-control/workspace'
- })
- .mockRejectedValueOnce(new Error('route unchanged'))
- .mockRejectedValueOnce(new Error('route unchanged'))
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/session/workspace'
- })
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/review/workspace'
- })
-
- await verifyHostedSourceControlReviewJourney({
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- sessionDocument: {
- href: 'orca-mobile-web://build/h/host/session/workspace'
- },
- timeoutMs: 30_000
- })
-
- expect(mocks.tapAccessibilityControl).toHaveBeenCalledTimes(2)
- expect(mocks.tapPoint).toHaveBeenCalledWith(expect.anything(), {
- x: 0.5,
- y: 0.4
- })
- })
-
- it('waits for tab state after the route changes', async () => {
- const sessionDocument = {
- href: 'orca-mobile-web://build/h/host/session/workspace'
- }
- mocks.waitForDocument
- .mockReset()
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/source-control/workspace'
- })
- .mockRejectedValueOnce(new Error('tab count pending'))
- .mockResolvedValueOnce(sessionDocument)
- .mockResolvedValueOnce(sessionDocument)
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/review/workspace'
- })
-
- await verifyHostedSourceControlReviewJourney({
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- sessionDocument: {
- href: 'orca-mobile-web://build/h/host/session/workspace'
- },
- timeoutMs: 30_000
- })
-
- expect(mocks.tapAccessibilityControl).toHaveBeenCalledTimes(2)
- expect(mocks.tapPoint).not.toHaveBeenCalled()
- expect(mocks.waitForDocument).toHaveBeenNthCalledWith(
- 3,
- expect.objectContaining({ expectedText: '' })
- )
- expect(mocks.waitForDocument).toHaveBeenNthCalledWith(
- 4,
- expect.objectContaining({ expectedText: '2 tabs' })
- )
- })
- it('records the headless host outcome when the diff route never opens', async () => {
- // Why: `orca serve --mobile-pairing` registers no renderer, so files.openDiff
- // answers renderer_unavailable. The claim is parity with native on this host.
- mocks.waitForDocument.mockReset()
- mocks.waitForDocument.mockImplementation(({ expectedHrefIncludes }) => {
- if (expectedHrefIncludes === '/session/') {
- return Promise.reject(new Error('Session diff route did not open'))
- }
- return Promise.resolve({
- href: `orca-mobile-web://build/h/host${expectedHrefIncludes ?? '/review/'}workspace`
- })
- })
- mocks.readBridgeErrors.mockResolvedValue([
- { capability: 'sourceControl', operation: 'reviewOpen', code: 'host_error', retryable: true }
- ])
- mocks.readState.mockReset()
- mocks.readState
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/source-control/workspace',
- bodyText: 'Source Control Changes Pull Request Commits Create pull request 128 on branch',
- labels: ['Refresh source control', 'Open changed file mobile/app/index.tsx']
- })
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/source-control/workspace',
- bodyText: 'Source Control\nSource control action failed\nStage All'
- })
- .mockResolvedValueOnce({
- href: 'orca-mobile-web://build/h/host/review/workspace',
- bodyText: 'reviewed',
- labels: ['Back', 'Open review actions']
- })
-
- const result = await verifyHostedSourceControlReviewJourney({
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- nativeReviewOpen: { headless: 'renderer_unavailable' },
- sessionDocument: { href: 'orca-mobile-web://build/h/host/session/workspace' },
- timeoutMs: 30_000
- })
-
- expect(result.sessionDiffRoute).toBeNull()
- expect(result.reviewOpen).toEqual({
- headless: 'renderer_unavailable',
- native: 'renderer_unavailable',
- bridgeError: {
- capability: 'sourceControl',
- operation: 'reviewOpen',
- code: 'host_error',
- retryable: true
- },
- visibleMessage: 'Source control action failed'
- })
- // The standalone Review route still opens, from the Source Control document.
- expect(result.reviewRoute).toBe('orca-mobile-web://build/h/host/review/workspace')
- })
-
- it('fails when the hybrid outcome diverges from what native did on the same host', async () => {
- mocks.waitForDocument.mockReset()
- mocks.waitForDocument.mockImplementation(({ expectedHrefIncludes }) => {
- if (expectedHrefIncludes === '/session/') {
- return Promise.reject(new Error('Session diff route did not open'))
- }
- return Promise.resolve({
- href: `orca-mobile-web://build/h/host${expectedHrefIncludes ?? '/review/'}workspace`
- })
- })
- mocks.readBridgeErrors.mockResolvedValue([
- { capability: 'sourceControl', operation: 'reviewOpen', code: 'host_error', retryable: true }
- ])
-
- await expect(
- verifyHostedSourceControlReviewJourney({
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- nativeReviewOpen: { headless: false },
- sessionDocument: { href: 'orca-mobile-web://build/h/host/session/workspace' },
- timeoutMs: 30_000
- })
- ).rejects.toThrow('Session-origin reviewOpen diverged')
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-source-control-review-parity.test.ts b/mobile/src/mobile-web/hosted-ios-source-control-review-parity.test.ts
deleted file mode 100644
index 0a223574ba9..00000000000
--- a/mobile/src/mobile-web/hosted-ios-source-control-review-parity.test.ts
+++ /dev/null
@@ -1,203 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-const mocks = vi.hoisted(() => ({
- captureScreenshot: vi.fn(),
- compareScreenshots: vi.fn(),
- dismissDeveloperMenu: vi.fn(),
- longPressControlByPrefix: vi.fn(),
- readLabels: vi.fn(),
- readTextPoint: vi.fn(),
- tapControl: vi.fn(),
- tapControlByPrefix: vi.fn(),
- waitForControl: vi.fn(),
- waitForControlByPrefix: vi.fn(),
- waitForControlEndingWith: vi.fn(),
- waitForControlMatching: vi.fn()
-}))
-
-vi.mock('node:child_process', () => ({
- execFile: mocks.captureScreenshot
-}))
-vi.mock('../../scripts/emulator-developer-menu-dismissal.mjs', () => ({
- dismissEmulatorDeveloperMenuIfPresent: mocks.dismissDeveloperMenu
-}))
-vi.mock('../../scripts/hosted-ios-emulator-accessibility.mjs', () => ({
- readHostedIosAccessibilityLabels: mocks.readLabels,
- tapHostedIosAccessibilityControl: mocks.tapControl,
- tapHostedIosAccessibilityControlByLabelPrefix: mocks.tapControlByPrefix,
- waitForHostedIosAccessibilityControl: mocks.waitForControl,
- waitForHostedIosAccessibilityControlByLabelPrefix: mocks.waitForControlByPrefix,
- waitForHostedIosAccessibilityControlEndingWith: mocks.waitForControlEndingWith,
- waitForHostedIosAccessibilityControlMatching: mocks.waitForControlMatching
-}))
-vi.mock('../../scripts/hosted-ios-emulator-long-press.mjs', () => ({
- longPressHostedIosAccessibilityControlByLabelPrefix: mocks.longPressControlByPrefix
-}))
-vi.mock('../../scripts/hosted-ios-screenshot-parity.mjs', () => ({
- assertHostedIosScreenshotParity: mocks.compareScreenshots
-}))
-vi.mock('../../scripts/hosted-webview-cdp-session.mjs', () => ({
- readHostedWebViewTextPoint: mocks.readTextPoint
-}))
-
-import {
- captureHostedSourceControlReviewScreen,
- captureNativeSourceControlReviewBaselines,
- HEADLESS_REVIEW_OPEN_ERROR
-} from '../../scripts/hosted-ios-source-control-review-parity.mjs'
-
-describe('hosted iOS Source Control and Review parity', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- mocks.captureScreenshot.mockImplementation((_command, _args, callback) =>
- callback(null, '', '')
- )
- mocks.compareScreenshots.mockResolvedValue({ changedPixelRatio: 0.01 })
- mocks.longPressControlByPrefix.mockResolvedValue({ x: 0.5, y: 0.4 })
- mocks.readLabels.mockResolvedValue([
- 'Open source control',
- 'Source Control',
- 'Back to session',
- HEADLESS_REVIEW_OPEN_ERROR
- ])
- mocks.readTextPoint.mockResolvedValue({ x: 0.2, y: 0.1 })
- mocks.tapControl.mockResolvedValue({ x: 0.2, y: 0.1 })
- mocks.tapControlByPrefix.mockResolvedValue({ x: 0.5, y: 0.4 })
- mocks.waitForControl.mockResolvedValue({ x: 0.2, y: 0.1 })
- mocks.waitForControlByPrefix.mockResolvedValue({ x: 0.5, y: 0.4 })
- mocks.waitForControlEndingWith.mockResolvedValue({ x: 0.5, y: 0.4 })
- mocks.waitForControlMatching
- .mockResolvedValueOnce({
- label: 'Open changed file mobile/app/index.tsx',
- value: '',
- x: 0.5,
- y: 0.5
- })
- .mockResolvedValueOnce({
- label: 'Pull request #13386, Draft, No checks. Open pull request.',
- value: '',
- x: 0.5,
- y: 0.3
- })
- .mockResolvedValue({
- label: 'Open changed file mobile/app/index.tsx',
- value: '',
- x: 0.5,
- y: 0.5
- })
- })
-
- it('captures the real host-origin Source Control and standalone Review path', async () => {
- const baselines = await captureNativeSourceControlReviewBaselines({
- deviceUdid: 'simulator',
- emulator: { deviceUdid: 'simulator' },
- expectedWorkspace: 'mobile-rearch',
- runtimeDirectory: '/tmp/parity',
- timeoutMs: 30_000
- })
-
- expect(mocks.longPressControlByPrefix).toHaveBeenCalledWith(
- { deviceUdid: 'simulator' },
- 'mobile-rearch',
- 30_000,
- undefined,
- 'Source Control'
- )
- expect(mocks.tapControl).toHaveBeenCalledWith(
- { deviceUdid: 'simulator' },
- 'Source Control',
- 30_000
- )
- expect(mocks.tapControl).toHaveBeenCalledWith(
- { deviceUdid: 'simulator' },
- 'Open changed file mobile/app/index.tsx',
- 30_000
- )
- expect(baselines).toEqual({
- review: {
- screenTitlePoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-review-portrait.png'
- },
- // Why: the session-origin probe records what the paired host answers, so the
- // hosted journey can assert parity instead of asserting a failure.
- sessionOriginReviewOpen: {
- changedFileLabel: 'Open changed file mobile/app/index.tsx',
- headless: HEADLESS_REVIEW_OPEN_ERROR
- },
- sourceControl: {
- changedFileLabel: 'Open changed file mobile/app/index.tsx',
- pullRequestState: {
- kind: 'ready',
- label: 'Pull request #13386, Draft, No checks. Open pull request.',
- number: '13386'
- },
- screenTitlePoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-source-control-portrait.png'
- }
- })
- })
-
- it('uses value-backed accessibility text for the changed file and pull request', async () => {
- mocks.waitForControlMatching
- .mockReset()
- .mockResolvedValue({
- label: 'Changed file',
- value: 'Open changed file mobile/src/mobile-web/bridge.ts',
- x: 0.5,
- y: 0.5
- })
- .mockResolvedValueOnce({
- label: 'Changed file',
- value: 'Open changed file mobile/src/mobile-web/bridge.ts',
- x: 0.5,
- y: 0.5
- })
- .mockResolvedValueOnce({
- label: '',
- value: 'Pull request #13386, Draft, No checks. Open pull request.',
- x: 0.5,
- y: 0.3
- })
-
- const baselines = await captureNativeSourceControlReviewBaselines({
- deviceUdid: 'simulator',
- emulator: { deviceUdid: 'simulator' },
- expectedWorkspace: 'mobile-rearch',
- runtimeDirectory: '/tmp/parity',
- timeoutMs: 30_000
- })
-
- expect(baselines.sourceControl).toMatchObject({
- changedFileLabel: 'Open changed file mobile/src/mobile-web/bridge.ts',
- pullRequestState: { kind: 'ready', number: '13386' }
- })
- expect(mocks.tapControl).toHaveBeenCalledWith(
- { deviceUdid: 'simulator' },
- 'Open changed file mobile/src/mobile-web/bridge.ts',
- 30_000
- )
- })
-
- it('compares a hosted route against its native screenshot and title landmark', async () => {
- const capture = await captureHostedSourceControlReviewScreen({
- deviceUdid: 'simulator',
- document: { href: 'orca-mobile-web://build/h/host/review/worktree' },
- nativeBaseline: {
- screenTitlePoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-review-portrait.png'
- },
- runtimeDirectory: '/tmp/parity',
- screenshotName: 'hosted-review-portrait.png',
- title: 'Changes',
- timeoutMs: 30_000
- })
-
- expect(capture.screenshotParity).toEqual({ changedPixelRatio: 0.01 })
- expect(mocks.compareScreenshots).toHaveBeenCalledWith({
- hostedLandmark: { x: 0.2, y: 0.1 },
- hostedScreenshot: '/tmp/parity/hosted-review-portrait.png',
- nativeLandmark: { x: 0.2, y: 0.1 },
- nativeScreenshot: '/tmp/parity/native-review-portrait.png'
- })
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-terminal-clipboard-image-paste.test.ts b/mobile/src/mobile-web/hosted-ios-terminal-clipboard-image-paste.test.ts
deleted file mode 100644
index ef37a3584a1..00000000000
--- a/mobile/src/mobile-web/hosted-ios-terminal-clipboard-image-paste.test.ts
+++ /dev/null
@@ -1,247 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import { PNG } from 'pngjs'
-import {
- copyHostedIosPhotoFixtureToClipboard,
- stageFreshHostedIosPhotoFixture,
- pngPixelIdentity,
- verifyHostedIosTerminalClipboardImagePaste
-} from '../../scripts/hosted-ios-terminal-clipboard-image-paste.mjs'
-
-const uploadedPath =
- '/var/folders/x/y/T/orca-paste-1780000000000-00000000-0000-4000-8000-000000000000.png'
-const sessionDocument = {
- href: 'orca-mobile-web://build/h/host/session/workspace'
-}
-const args = {
- deviceUdid: 'simulator',
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- orcaCli: './config/scripts/orca-dev.mjs',
- pairingRuntimeUserDataPath: '/tmp/pairing/userData',
- sessionDocument,
- terminalHandle: 'terminal-handle',
- timeoutMs: 30_000,
- worktree: '/repo/mobile-rearch'
-}
-const fixtureBytes = createPng([18, 22, 41, 255])
-const fixturePixelIdentity = pngPixelIdentity(fixtureBytes)
-
-describe('hosted iOS terminal clipboard image paste', () => {
- it('normalizes Photos to its library and copies the latest visible photo', async () => {
- const runCommand = vi.fn().mockResolvedValue({ stdout: '', stderr: '' })
- const tapControl = vi.fn().mockResolvedValue({ x: 0.5, y: 0.5 })
- const tapLastControl = vi.fn().mockResolvedValue({ x: 0.16, y: 0.52 })
- const tapPoint = vi.fn()
- const waitForControl = vi.fn().mockResolvedValue({ x: 0.5, y: 0.5 })
- const waitForMatch = vi.fn().mockResolvedValue({ label: 'Share', x: 0.1, y: 0.9 })
-
- await expect(
- copyHostedIosPhotoFixtureToClipboard(
- {
- deviceUdid: 'simulator',
- emulator: args.emulator,
- fixturePath: '/repo/mobile/assets/favicon.png',
- timeoutMs: args.timeoutMs
- },
- {
- readFixture: vi.fn().mockResolvedValue(fixtureBytes),
- runCommand,
- stageFixture: vi.fn().mockResolvedValue('/repo/mobile/assets/favicon.png'),
- tapControl,
- tapLastControl,
- tapPoint,
- waitForControl,
- waitForMatch
- }
- )
- ).resolves.toMatchObject({
- fixtureName: 'favicon.png',
- fixturePixelIdentity,
- photoPoint: { x: 0.16, y: 0.52 }
- })
-
- expect(runCommand).toHaveBeenNthCalledWith(1, 'xcrun', [
- 'simctl',
- 'addmedia',
- 'simulator',
- '/repo/mobile/assets/favicon.png'
- ])
- expect(runCommand).toHaveBeenNthCalledWith(2, 'xcrun', [
- 'simctl',
- 'launch',
- 'simulator',
- 'com.apple.mobileslideshow'
- ])
- expect(tapControl.mock.calls.map(([, label]) => label)).toEqual([
- 'Back',
- 'Share',
- 'Copy Photo',
- 'Return to Orca'
- ])
- expect(tapLastControl).toHaveBeenCalledWith(args.emulator, 'Photo', args.timeoutMs)
- expect(tapPoint).not.toHaveBeenCalled()
- })
-
- it('stages a freshly written copy so the fixture is the newest library photo', async () => {
- const writeFixture = vi.fn().mockResolvedValue(undefined)
-
- await expect(
- stageFreshHostedIosPhotoFixture('/repo/mobile/assets/favicon.png', {
- createDirectory: vi.fn().mockResolvedValue('/tmp/staged'),
- readFixture: vi.fn().mockResolvedValue(fixtureBytes),
- writeFixture
- })
- ).resolves.toBe('/tmp/staged/favicon.png')
- expect(writeFixture).toHaveBeenCalledWith('/tmp/staged/favicon.png', fixtureBytes)
- })
-
- it('dismisses first-launch Photos surfaces before copying', async () => {
- const waitForMatch = vi
- .fn()
- .mockResolvedValueOnce({ label: 'Continue', x: 0.5, y: 0.9 })
- .mockResolvedValueOnce({ label: 'Don’t Allow', x: 0.3, y: 0.6 })
- .mockResolvedValueOnce({ label: 'Continue', x: 0.5, y: 0.9 })
- .mockResolvedValueOnce({ label: 'Select', x: 0.8, y: 0.1 })
- const tapPoint = vi.fn().mockResolvedValue(undefined)
-
- await copyHostedIosPhotoFixtureToClipboard(
- {
- deviceUdid: 'simulator',
- emulator: args.emulator,
- fixturePath: '/repo/mobile/assets/favicon.png',
- timeoutMs: args.timeoutMs
- },
- {
- readFixture: vi.fn().mockResolvedValue(fixtureBytes),
- runCommand: vi.fn().mockResolvedValue({ stdout: '', stderr: '' }),
- stageFixture: vi.fn().mockResolvedValue('/repo/mobile/assets/favicon.png'),
- tapControl: vi.fn().mockResolvedValue({ x: 0.5, y: 0.5 }),
- tapLastControl: vi.fn().mockResolvedValue({ x: 0.16, y: 0.52 }),
- tapPoint,
- waitForControl: vi.fn().mockResolvedValue({ x: 0.5, y: 0.5 }),
- waitForMatch
- }
- )
-
- expect(tapPoint).toHaveBeenNthCalledWith(1, args.emulator, {
- label: 'Continue',
- x: 0.5,
- y: 0.9
- })
- expect(tapPoint).toHaveBeenNthCalledWith(2, args.emulator, {
- label: 'Don’t Allow',
- x: 0.3,
- y: 0.6
- })
- expect(tapPoint).toHaveBeenNthCalledWith(3, args.emulator, {
- label: 'Continue',
- x: 0.5,
- y: 0.9
- })
- })
-
- it('retries when Photos onboarding appears after the first photo tap', async () => {
- const waitForMatch = vi
- .fn()
- .mockResolvedValueOnce({ label: 'Select', x: 0.8, y: 0.1 })
- .mockResolvedValueOnce({ label: 'Continue', x: 0.5, y: 0.9 })
- .mockResolvedValueOnce({ label: 'Select', x: 0.8, y: 0.1 })
- const tapLastControl = vi.fn().mockResolvedValue({ x: 0.16, y: 0.52 })
- const tapPoint = vi.fn().mockResolvedValue(undefined)
- const waitForControl = vi
- .fn()
- .mockRejectedValueOnce(new Error('lazy onboarding'))
- .mockResolvedValue({ x: 0.5, y: 0.5 })
-
- await copyHostedIosPhotoFixtureToClipboard(
- {
- deviceUdid: 'simulator',
- emulator: args.emulator,
- fixturePath: '/repo/mobile/assets/favicon.png',
- timeoutMs: args.timeoutMs
- },
- {
- readFixture: vi.fn().mockResolvedValue(fixtureBytes),
- runCommand: vi.fn().mockResolvedValue({ stdout: '', stderr: '' }),
- stageFixture: vi.fn().mockResolvedValue('/repo/mobile/assets/favicon.png'),
- tapControl: vi.fn().mockResolvedValue({ x: 0.5, y: 0.5 }),
- tapLastControl,
- tapPoint,
- waitForControl,
- waitForMatch
- }
- )
-
- expect(tapLastControl).toHaveBeenCalledTimes(2)
- expect(tapPoint).toHaveBeenCalledWith(args.emulator, {
- label: 'Continue',
- x: 0.5,
- y: 0.9
- })
- })
-
- it('requires pixel-identical shell upload and terminal-only path exposure', async () => {
- const beforeTerminal = ['ORCA_HOSTED_CLIPBOARD_TEXT_PASTE']
- const afterTerminal = [...beforeTerminal, uploadedPath]
- const operations = {
- allowPaste: vi.fn().mockResolvedValue('allowed'),
- copyFixture: vi.fn().mockResolvedValue({
- copyPoint: { x: 0.15, y: 0.66 },
- fixtureName: 'favicon.png',
- fixturePixelIdentity
- }),
- readState: vi.fn().mockResolvedValue({ bodyText: 'Mobile Emulator' }),
- readTerminal: vi
- .fn()
- .mockResolvedValueOnce(beforeTerminal)
- .mockResolvedValueOnce(afterTerminal),
- readUploadedFile: vi.fn().mockResolvedValue(fixtureBytes),
- tapControl: vi.fn().mockResolvedValue({ x: 0.3, y: 0.9 }),
- waitForDocument: vi.fn().mockResolvedValue(sessionDocument)
- }
-
- await expect(verifyHostedIosTerminalClipboardImagePaste(args, operations)).resolves.toEqual({
- evidence: {
- copyPoint: { x: 0.15, y: 0.66 },
- fixtureName: 'favicon.png',
- height: 1,
- pasteControlPoint: { x: 0.3, y: 0.9 },
- pastePermissionPrompt: 'allowed',
- pixelSha256: fixturePixelIdentity.sha256,
- privilegedPageMarkers: 'absent',
- route: sessionDocument.href,
- size: fixtureBytes.byteLength,
- terminalPathInjected: true,
- width: 1
- },
- sessionDocument
- })
- expect(operations.readUploadedFile).toHaveBeenCalledWith(uploadedPath)
- })
-
- it('rejects a clipboard upload with different decoded pixels', async () => {
- const operations = {
- allowPaste: vi.fn().mockResolvedValue('not-shown'),
- copyFixture: vi.fn().mockResolvedValue({
- copyPoint: { x: 0.15, y: 0.66 },
- fixtureName: 'favicon.png',
- fixturePixelIdentity
- }),
- readState: vi.fn().mockResolvedValue({ bodyText: 'Mobile Emulator' }),
- readTerminal: vi.fn().mockResolvedValueOnce([]).mockResolvedValueOnce([uploadedPath]),
- readUploadedFile: vi.fn().mockResolvedValue(createPng([255, 0, 0, 255])),
- tapControl: vi.fn().mockResolvedValue({ x: 0.3, y: 0.9 }),
- waitForDocument: vi.fn().mockResolvedValue(sessionDocument)
- }
-
- await expect(verifyHostedIosTerminalClipboardImagePaste(args, operations)).rejects.toThrow(
- 'did not preserve the fixture pixels'
- )
- })
-})
-
-function createPng(rgba: [number, number, number, number]): Buffer {
- const png = new PNG({ height: 1, width: 1 })
- png.data.set(rgba)
- return PNG.sync.write(png)
-}
diff --git a/mobile/src/mobile-web/hosted-ios-terminal-clipboard-paste.test.ts b/mobile/src/mobile-web/hosted-ios-terminal-clipboard-paste.test.ts
deleted file mode 100644
index ded7968d137..00000000000
--- a/mobile/src/mobile-web/hosted-ios-terminal-clipboard-paste.test.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-const mocks = vi.hoisted(() => ({
- activateControl: vi.fn()
-}))
-
-vi.mock('../../scripts/hosted-webview-cdp-session.mjs', () => ({
- activateHostedWebViewControl: mocks.activateControl,
- waitForVisibleHostedWebView: vi.fn()
-}))
-
-import {
- verifyHostedIosTerminalClipboardPaste,
- writeHostedIosSimulatorPasteboard
-} from '../../scripts/hosted-ios-terminal-clipboard-paste.mjs'
-
-describe('hosted iOS terminal clipboard paste', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- })
-
- it('uses the unchanged Paste control and requires the marker in the Desktop terminal', async () => {
- const workspaceDocument = { href: 'orca-mobile-web://build/h/host' }
- const sessionDocument = {
- href: 'orca-mobile-web://build/h/host/session/workspace'
- }
- const writePasteboard = vi.fn().mockResolvedValue(undefined)
- const activateWorkspace = vi
- .fn()
- .mockImplementation(async (_document, _workspace, _activate, _timeout, waitForWorkspace) => {
- await waitForWorkspace()
- })
- const waitForDocument = vi
- .fn()
- .mockResolvedValueOnce(workspaceDocument)
- .mockResolvedValueOnce(sessionDocument)
- const tapControl = vi
- .fn()
- .mockResolvedValueOnce({ x: 0.75, y: 0.9 })
- .mockResolvedValueOnce({ x: 0.5, y: 0.6 })
- const readTerminal = vi.fn().mockResolvedValue('terminal-handle')
-
- await expect(
- verifyHostedIosTerminalClipboardPaste(
- {
- deviceUdid: 'simulator',
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- expectedWorkspace: 'mobile-rearch',
- orcaCli: './config/scripts/orca-dev.mjs',
- pairingRuntimeUserDataPath: '/tmp/pairing/userData',
- timeoutMs: 30_000,
- workspaceDocument,
- worktree: '/repo/mobile-rearch'
- },
- {
- activateWorkspace,
- readTerminal,
- tapControl,
- waitForDocument,
- writePasteboard
- }
- )
- ).resolves.toEqual({
- evidence: {
- activationAttempts: 1,
- marker: 'ORCA_HOSTED_CLIPBOARD_TEXT_PASTE',
- pasteControlPoint: { x: 0.75, y: 0.9 },
- pastePermissionPrompt: 'allowed',
- route: sessionDocument.href,
- terminalHandle: 'terminal-handle'
- },
- sessionDocument
- })
-
- expect(writePasteboard).toHaveBeenCalledWith('simulator', 'ORCA_HOSTED_CLIPBOARD_TEXT_PASTE')
- expect(activateWorkspace).toHaveBeenCalledWith(
- workspaceDocument,
- 'mobile-rearch',
- mocks.activateControl,
- 30_000,
- expect.any(Function)
- )
- expect(tapControl).toHaveBeenNthCalledWith(1, { deviceUdid: 'simulator' }, 'Paste', 30_000)
- expect(tapControl).toHaveBeenNthCalledWith(2, { deviceUdid: 'simulator' }, 'Allow Paste', 3_000)
- expect(readTerminal).toHaveBeenCalledWith({
- marker: 'ORCA_HOSTED_CLIPBOARD_TEXT_PASTE',
- orcaCli: './config/scripts/orca-dev.mjs',
- pairingRuntimeUserDataPath: '/tmp/pairing/userData',
- timeoutMs: 10_000,
- worktree: '/repo/mobile-rearch'
- })
- })
-
- it('writes text to the selected simulator pasteboard', async () => {
- const stdin = { end: vi.fn() }
- const child = {
- once: vi.fn((event, listener) => {
- if (event === 'exit') {
- listener(0)
- }
- }),
- stderr: { on: vi.fn(), setEncoding: vi.fn() },
- stdin
- }
- const spawnProcess = vi.fn().mockReturnValue(child)
-
- await expect(
- writeHostedIosSimulatorPasteboard('simulator', 'marker', spawnProcess)
- ).resolves.toBeUndefined()
-
- expect(spawnProcess).toHaveBeenCalledWith('xcrun', ['simctl', 'pbcopy', 'simulator'], {
- stdio: ['pipe', 'ignore', 'pipe']
- })
- expect(stdin.end).toHaveBeenCalledWith('marker')
- })
-
- it('retries a silent native tap before accepting terminal evidence', async () => {
- const sessionDocument = {
- href: 'orca-mobile-web://build/h/host/session/workspace'
- }
- const tapControl = vi
- .fn()
- .mockResolvedValueOnce({ x: 0.25, y: 0.9 })
- .mockRejectedValueOnce(new Error('prompt not shown'))
- .mockResolvedValueOnce({ x: 0.25, y: 0.9 })
- .mockRejectedValueOnce(new Error('prompt not shown'))
- const readTerminal = vi
- .fn()
- .mockRejectedValueOnce(new Error('marker missing'))
- .mockResolvedValueOnce('terminal-handle')
-
- const result = await verifyHostedIosTerminalClipboardPaste(
- {
- deviceUdid: 'simulator',
- discoveryUrl: 'http://127.0.0.1:9222',
- emulator: { deviceUdid: 'simulator' },
- expectedWorkspace: 'mobile-rearch',
- orcaCli: './config/scripts/orca-dev.mjs',
- pairingRuntimeUserDataPath: '/tmp/pairing/userData',
- timeoutMs: 30_000,
- workspaceDocument: { href: 'orca-mobile-web://build/h/host' },
- worktree: '/repo/mobile-rearch'
- },
- {
- activateWorkspace: vi.fn().mockResolvedValue(undefined),
- readTerminal,
- tapControl,
- waitForDocument: vi.fn().mockResolvedValue(sessionDocument),
- writePasteboard: vi.fn().mockResolvedValue(undefined)
- }
- )
-
- expect(result.evidence).toMatchObject({
- activationAttempts: 2,
- pastePermissionPrompt: 'not-shown',
- terminalHandle: 'terminal-handle'
- })
- expect(readTerminal).toHaveBeenCalledTimes(2)
- expect(tapControl).toHaveBeenCalledTimes(4)
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-terminal-device-input-journey.test.ts b/mobile/src/mobile-web/hosted-ios-terminal-device-input-journey.test.ts
deleted file mode 100644
index b25af06283a..00000000000
--- a/mobile/src/mobile-web/hosted-ios-terminal-device-input-journey.test.ts
+++ /dev/null
@@ -1,183 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-const mocks = vi.hoisted(() => ({
- verifyClipboardImagePaste: vi.fn(),
- verifyDocumentUpload: vi.fn(),
- verifyClipboardPaste: vi.fn(),
- verifyPhotoDenial: vi.fn(),
- verifyPhotoRevocation: vi.fn()
-}))
-
-vi.mock('../../scripts/hosted-ios-document-upload.mjs', () => ({
- verifyHostedIosDocumentUpload: mocks.verifyDocumentUpload
-}))
-vi.mock('../../scripts/hosted-ios-terminal-clipboard-paste.mjs', () => ({
- verifyHostedIosTerminalClipboardPaste: mocks.verifyClipboardPaste
-}))
-vi.mock('../../scripts/hosted-ios-terminal-clipboard-image-paste.mjs', () => ({
- verifyHostedIosTerminalClipboardImagePaste: mocks.verifyClipboardImagePaste
-}))
-vi.mock('../../scripts/hosted-ios-photo-permission-denial.mjs', () => ({
- verifyHostedIosPhotoPermissionDenial: mocks.verifyPhotoDenial
-}))
-vi.mock('../../scripts/hosted-ios-photo-permission-revocation.mjs', () => ({
- verifyHostedIosPhotoPermissionRevocation: mocks.verifyPhotoRevocation
-}))
-
-import {
- verifyHostedIosTerminalDeviceInputJourney,
- verifyHostedIosTerminalInputJourney
-} from '../../scripts/hosted-ios-terminal-device-input-journey.mjs'
-
-describe('hosted iOS terminal device-input journey', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- })
-
- it('carries the exact session and terminal through every device-input operation', async () => {
- const args = {
- deviceUdid: 'simulator',
- emulator: {},
- worktree: '/repo/mobile-rearch',
- workspaceDocument: { href: 'workspace' }
- }
- const documentFixture = {
- destinationPath: '/simulator/fixture.png',
- fixtureName: 'fixture.png'
- }
- const seedFixture = vi.fn().mockResolvedValue(documentFixture)
- const removeFixture = vi.fn().mockResolvedValue(undefined)
- const terminalClipboardPaste = {
- evidence: { terminalHandle: 'terminal-handle' },
- sessionDocument: { href: 'session' }
- }
- const photoPermissionDenial = {
- evidence: { toast: 'Photo permission denied' },
- sessionDocument: { href: 'session-after-photo-denial' }
- }
- const photoPermissionRevocation = {
- evidence: { permissionState: 'revoked-after-grant' },
- sessionDocument: { href: 'session-after-photo-revocation' }
- }
- const documentUpload = {
- evidence: { terminalPathInjected: true },
- sessionDocument: { href: 'session-after-document-upload' }
- }
- const terminalClipboardImagePaste = {
- evidence: { terminalPathInjected: true },
- sessionDocument: { href: 'session-after-clipboard-image-paste' }
- }
- mocks.verifyClipboardPaste.mockResolvedValue(terminalClipboardPaste)
- mocks.verifyClipboardImagePaste.mockResolvedValue(terminalClipboardImagePaste)
- mocks.verifyPhotoDenial.mockResolvedValue(photoPermissionDenial)
- mocks.verifyPhotoRevocation.mockResolvedValue(photoPermissionRevocation)
- mocks.verifyDocumentUpload.mockResolvedValue(documentUpload)
-
- await expect(
- verifyHostedIosTerminalDeviceInputJourney(args, {
- removeFixture,
- seedFixture
- })
- ).resolves.toEqual({
- documentUpload,
- photoPermissionDenial,
- photoPermissionRevocation,
- terminalClipboardImagePaste,
- terminalClipboardPaste
- })
- expect(seedFixture).toHaveBeenCalledWith({
- deviceUdid: 'simulator',
- fixturePath: '/repo/mobile-rearch/mobile/assets/favicon.png'
- })
- expect(mocks.verifyPhotoDenial).toHaveBeenCalledWith({
- ...args,
- sessionDocument: documentUpload.sessionDocument,
- terminalHandle: 'terminal-handle'
- })
- expect(mocks.verifyDocumentUpload).toHaveBeenCalledWith({
- ...args,
- documentFixture,
- sessionDocument: terminalClipboardPaste.sessionDocument,
- terminalHandle: 'terminal-handle'
- })
- expect(mocks.verifyPhotoRevocation).toHaveBeenCalledWith({
- ...args,
- sessionDocument: photoPermissionDenial.sessionDocument,
- terminalHandle: 'terminal-handle'
- })
- expect(mocks.verifyClipboardImagePaste).toHaveBeenCalledWith({
- ...args,
- sessionDocument: photoPermissionRevocation.sessionDocument,
- terminalHandle: 'terminal-handle'
- })
- expect(removeFixture).toHaveBeenCalledWith(documentFixture.destinationPath)
- })
-
- it('runs clipboard-image verification without unrelated picker stages', async () => {
- const args = {
- deviceUdid: 'simulator',
- emulator: {},
- worktree: '/repo/mobile-rearch',
- workspaceDocument: { href: 'workspace' }
- }
- const terminalClipboardPaste = {
- evidence: { terminalHandle: 'terminal-handle' },
- sessionDocument: { href: 'session' }
- }
- const terminalClipboardImagePaste = {
- evidence: { terminalPathInjected: true },
- sessionDocument: { href: 'session-after-image-paste' }
- }
- mocks.verifyClipboardPaste.mockResolvedValue(terminalClipboardPaste)
- mocks.verifyClipboardImagePaste.mockResolvedValue(terminalClipboardImagePaste)
-
- await expect(
- verifyHostedIosTerminalInputJourney(args, { clipboardImageOnly: true })
- ).resolves.toEqual({
- terminalClipboardImagePaste,
- terminalClipboardPaste
- })
- expect(mocks.verifyClipboardImagePaste).toHaveBeenCalledWith({
- ...args,
- sessionDocument: terminalClipboardPaste.sessionDocument,
- terminalHandle: 'terminal-handle'
- })
- expect(mocks.verifyDocumentUpload).not.toHaveBeenCalled()
- expect(mocks.verifyPhotoDenial).not.toHaveBeenCalled()
- expect(mocks.verifyPhotoRevocation).not.toHaveBeenCalled()
- })
-
- it('runs Photos revocation without unrelated picker stages', async () => {
- const args = {
- deviceUdid: 'simulator',
- emulator: {},
- worktree: '/repo/mobile-rearch',
- workspaceDocument: { href: 'workspace' }
- }
- const terminalClipboardPaste = {
- evidence: { terminalHandle: 'terminal-handle' },
- sessionDocument: { href: 'session' }
- }
- const photoPermissionRevocation = {
- evidence: { permissionState: 'revoked-after-grant' },
- sessionDocument: { href: 'session-after-photo-revocation' }
- }
- mocks.verifyClipboardPaste.mockResolvedValue(terminalClipboardPaste)
- mocks.verifyPhotoRevocation.mockResolvedValue(photoPermissionRevocation)
-
- await expect(
- verifyHostedIosTerminalInputJourney(args, { photosRevocationOnly: true })
- ).resolves.toEqual({
- photoPermissionRevocation,
- terminalClipboardPaste
- })
- expect(mocks.verifyPhotoRevocation).toHaveBeenCalledWith({
- ...args,
- sessionDocument: terminalClipboardPaste.sessionDocument,
- terminalHandle: 'terminal-handle'
- })
- expect(mocks.verifyDocumentUpload).not.toHaveBeenCalled()
- expect(mocks.verifyPhotoDenial).not.toHaveBeenCalled()
- expect(mocks.verifyClipboardImagePaste).not.toHaveBeenCalled()
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-terminal-link-point-alignment.test.ts b/mobile/src/mobile-web/hosted-ios-terminal-link-point-alignment.test.ts
deleted file mode 100644
index fb6b5388ed2..00000000000
--- a/mobile/src/mobile-web/hosted-ios-terminal-link-point-alignment.test.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import {
- alignHostedIosSessionPoint,
- alignHostedIosTerminalLinkPoints
-} from '../../scripts/hosted-ios-adversarial-terminal-links.mjs'
-
-describe('hosted iOS terminal link point alignment', () => {
- it('removes the transient viewport displacement before a native tap', () => {
- expect(
- alignHostedIosTerminalLinkPoints(
- {
- file: { x: 0.07, y: 0.72 },
- http: { x: 0.07, y: 0.79 },
- javascript: { x: 0.07, y: 0.65 }
- },
- { x: 0.4, y: 0.5 },
- { x: 0.4, y: 0.14 }
- )
- ).toEqual({
- points: {
- file: { x: 0.07, y: 0.36 },
- http: { x: 0.07, y: 0.43000000000000005 },
- javascript: { x: 0.07, y: 0.29000000000000004 }
- },
- yOffset: -0.36
- })
- })
-
- it('rejects an alignment that leaves the native screen', () => {
- expect(() =>
- alignHostedIosTerminalLinkPoints(
- { file: { x: 0.2, y: 0.1 } },
- { x: 0.2, y: 0.8 },
- { x: 0.2, y: 0.1 }
- )
- ).toThrow('alignment is invalid')
- })
-
- it('applies the measured offset only to Session-origin taps', () => {
- const point = { x: 0.8, y: 0.7 }
- expect(
- alignHostedIosSessionPoint(point, -0.36, { href: 'orca://host/session/opaque' })
- ).toEqual({ x: 0.8, y: 0.33999999999999997 })
- expect(
- alignHostedIosSessionPoint(point, -0.36, { href: 'orca://host/source-control/opaque' })
- ).toBe(point)
- })
-})
diff --git a/mobile/src/mobile-web/hosted-ios-workspace-parity.test.ts b/mobile/src/mobile-web/hosted-ios-workspace-parity.test.ts
deleted file mode 100644
index 2f5efed23dd..00000000000
--- a/mobile/src/mobile-web/hosted-ios-workspace-parity.test.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-const mocks = vi.hoisted(() => ({
- captureScreenshot: vi.fn(),
- compareScreenshots: vi.fn(),
- dismissDeveloperMenu: vi.fn(),
- readTextPoint: vi.fn(),
- waitForControl: vi.fn()
-}))
-
-vi.mock('node:child_process', () => ({
- execFile: mocks.captureScreenshot
-}))
-vi.mock('../../scripts/emulator-developer-menu-dismissal.mjs', () => ({
- dismissEmulatorDeveloperMenuIfPresent: mocks.dismissDeveloperMenu
-}))
-vi.mock('../../scripts/hosted-ios-emulator-accessibility.mjs', () => ({
- waitForHostedIosAccessibilityControl: mocks.waitForControl
-}))
-vi.mock('../../scripts/hosted-ios-screenshot-parity.mjs', () => ({
- assertHostedIosScreenshotParity: mocks.compareScreenshots
-}))
-vi.mock('../../scripts/hosted-webview-cdp-session.mjs', () => ({
- readHostedWebViewTextPoint: mocks.readTextPoint
-}))
-
-import {
- captureHostedWorkspaceParity,
- captureNativeWorkspaceBaseline
-} from '../../scripts/hosted-ios-workspace-parity.mjs'
-
-describe('hosted iOS workspace parity', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- mocks.captureScreenshot.mockImplementation((_command, _args, callback) =>
- callback(null, '', '')
- )
- mocks.compareScreenshots.mockResolvedValue({ changedPixelRatio: 0.01 })
- mocks.readTextPoint.mockResolvedValue({ x: 0.2, y: 0.1 })
- mocks.waitForControl.mockResolvedValue({ x: 0.2, y: 0.1 })
- })
-
- it('captures the unchanged native workspace screen', async () => {
- const baseline = await captureNativeWorkspaceBaseline({
- deviceUdid: 'simulator',
- emulator: { deviceUdid: 'simulator' },
- runtimeDirectory: '/tmp/parity',
- timeoutMs: 30_000
- })
-
- expect(mocks.waitForControl).toHaveBeenCalledWith({ deviceUdid: 'simulator' }, 'Filter', 30_000)
- expect(baseline).toEqual({
- filterPoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-workspace-portrait.png'
- })
- })
-
- it('compares the unchanged hosted workspace screen', async () => {
- const evidence = await captureHostedWorkspaceParity({
- deviceUdid: 'simulator',
- document: { href: 'orca-mobile-web://build/h/host' },
- nativeBaseline: {
- filterPoint: { x: 0.2, y: 0.1 },
- screenshot: '/tmp/parity/native-workspace-portrait.png'
- },
- runtimeDirectory: '/tmp/parity',
- timeoutMs: 30_000
- })
-
- expect(mocks.readTextPoint).toHaveBeenCalledWith(expect.anything(), 'Filter')
- expect(mocks.compareScreenshots).toHaveBeenCalledOnce()
- expect(evidence).toEqual({
- nativeScreenshot: 'native-workspace-portrait.png',
- hostedScreenshot: 'hosted-workspace-portrait.png',
- nativeFilterPoint: { x: 0.2, y: 0.1 },
- hostedFilterPoint: { x: 0.2, y: 0.1 },
- screenshotParity: { changedPixelRatio: 0.01 }
- })
- })
-})
diff --git a/mobile/src/mobile-web/hosted-mobile-e2e-launch.test.ts b/mobile/src/mobile-web/hosted-mobile-e2e-launch.test.ts
deleted file mode 100644
index d32b85220b5..00000000000
--- a/mobile/src/mobile-web/hosted-mobile-e2e-launch.test.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import {
- HOSTED_MOBILE_APP_ROUTE_URL,
- hostedMobileMetroArguments
-} from '../../scripts/hosted-mobile-e2e-launch.mjs'
-
-describe('hosted mobile E2E launch', () => {
- it('uses an Expo Router path URL', () => {
- expect(HOSTED_MOBILE_APP_ROUTE_URL).toBe('orca:///hybrid')
- })
-
- it('invalidates Metro transforms when the public host identity changes', () => {
- expect(hostedMobileMetroArguments(8081, true)).toEqual([
- 'start',
- '--host',
- 'lan',
- '--port',
- '8081',
- '--clear'
- ])
- expect(hostedMobileMetroArguments(8081, false)).not.toContain('--clear')
- })
-})
diff --git a/mobile/src/mobile-web/hosted-route-native-api-census.test.ts b/mobile/src/mobile-web/hosted-route-native-api-census.test.ts
deleted file mode 100644
index 940a379ac4c..00000000000
--- a/mobile/src/mobile-web/hosted-route-native-api-census.test.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { readFileSync } from 'node:fs'
-import { join, relative } from 'node:path'
-import { describe, expect, it } from 'vitest'
-import { hostedModuleGraph, hostedRoot, mobileRoot } from './hosted-module-graph.test-support'
-
-const HOSTED_SESSION_ROUTE = join(hostedRoot, 'h', '[hostId]', 'session', '[worktreeId].tsx')
-
-// Native-only entry points: on the hosted page these either throw or silently do nothing.
-const BANNED_IN_HOSTED_BUNDLE = ['Clipboard.setStringAsync']
-const BANNED_IN_SESSION_ROUTE = [
- ...BANNED_IN_HOSTED_BUNDLE,
- 'Linking.openURL',
- "/transport/host-store'"
-]
-
-describe('hosted route native API census', () => {
- it('keeps native-only APIs out of every module the hosted session route reaches', () => {
- const graph = hostedModuleGraph([HOSTED_SESSION_ROUTE])
-
- // Presence precondition: the walk must actually reach the session screen's own modules.
- expect(graph.length).toBeGreaterThan(500)
- expect(graph.map((path) => relative(mobileRoot, path))).toEqual(
- expect.arrayContaining([
- join('app', 'h', '[hostId]', 'session', '[worktreeId].tsx'),
- join('src', 'session', 'MobileTerminalInputActions.tsx')
- ])
- )
- expect(offenders(graph, BANNED_IN_SESSION_ROUTE)).toEqual([])
- })
-
- it('keeps native-only APIs out of every module any hosted route reaches', () => {
- const graph = hostedModuleGraph()
-
- expect(graph.length).toBeGreaterThan(500)
- expect(graph).toContain(join(hostedRoot, 'terminal-settings.tsx'))
- // Terminal settings now has a real hosted route; it is no longer a native-only destination.
- // AsyncStorage is deliberately not censused: it is reachable today through the shared
- // storage and transport modules, and on web it resolves to localStorage.
- expect(offenders(graph, BANNED_IN_HOSTED_BUNDLE)).toEqual([])
- })
-})
-
-function offenders(graph: string[], banned: string[]): string[] {
- const found: string[] = []
- for (const modulePath of graph) {
- readFileSync(modulePath, 'utf8')
- .split('\n')
- .forEach((line, index) => {
- if (banned.some((pattern) => line.includes(pattern))) {
- found.push(`${relative(mobileRoot, modulePath)}:${index + 1}`)
- }
- })
- }
- return found
-}
diff --git a/mobile/src/mobile-web/hosted-terminal-link-locator.test.ts b/mobile/src/mobile-web/hosted-terminal-link-locator.test.ts
deleted file mode 100644
index bd4baa493dd..00000000000
--- a/mobile/src/mobile-web/hosted-terminal-link-locator.test.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import {
- describeHostedTerminalLinkPoint,
- hostedTerminalLinkPointsFromGeometry,
- readHostedTerminalLinkPoints
-} from '../../scripts/hosted-terminal-link-locator.mjs'
-
-const geometry = {
- cellHeight: 20,
- cellWidth: 10,
- cursorTop: 460,
- innerHeight: 740,
- screenRect: {
- height: 600,
- width: 400
- },
- terminalRect: {
- bottom: 700,
- height: 610,
- left: 0,
- top: 90,
- width: 400
- },
- screenHeight: 800,
- screenWidth: 400
-}
-
-describe('hosted terminal link locator', () => {
- it('maps fixed cursor-relative rows through the resized xterm grid', () => {
- expect(hostedTerminalLinkPointsFromGeometry(geometry)).toEqual({
- javascript: { x: 0.0875, y: 0.530625 },
- file: { x: 0.0875, y: 0.606875 },
- fileAlternate: { x: 0.0875, y: 0.6322916666666667 },
- http: { x: 0.0875, y: 0.683125 }
- })
- })
-
- it('uses rendered link rows when the cursor moved after staging', () => {
- expect(
- hostedTerminalLinkPointsFromGeometry({
- ...geometry,
- cursorTop: 20,
- linkRows: { javascript: 4, file: 7, fileAlternate: 8, http: 10 }
- })
- ).toEqual({
- javascript: { x: 0.0875, y: 0.301875 },
- file: { x: 0.0875, y: 0.378125 },
- fileAlternate: { x: 0.0875, y: 0.40354166666666663 },
- http: { x: 0.0875, y: 0.454375 }
- })
- })
-
- it('anchors the staged corpus to the viewport bottom when xterm is blurred', () => {
- expect(hostedTerminalLinkPointsFromGeometry({ ...geometry, cursorTop: 0 })).toEqual({
- javascript: { x: 0.0875, y: 0.683125 },
- file: { x: 0.0875, y: 0.759375 },
- fileAlternate: { x: 0.0875, y: 0.7847916666666666 },
- http: { x: 0.0875, y: 0.835625 }
- })
- })
-
- it('rejects an empty discovered xterm instead of tapping a guessed row', () => {
- expect(() =>
- hostedTerminalLinkPointsFromGeometry({
- ...geometry,
- cursorTop: 0,
- linkRows: { javascript: null, file: null, fileAlternate: null, http: null },
- xtermFound: true
- })
- ).toThrow('corpus is not present')
- })
-
- it('fails when the rendered cell geometry cannot describe the screen', () => {
- expect(() =>
- hostedTerminalLinkPointsFromGeometry({
- ...geometry,
- cellHeight: 70
- })
- ).toThrow('grid is invalid')
- })
-
- it('reports the DOM target under a normalized emulator point', async () => {
- const evaluate = async (_document: unknown, expression: string) => {
- expect(expression).toContain('"x":0.25')
- expect(expression).toContain('document.elementFromPoint')
- return JSON.stringify({ clientX: 100, clientY: 200, terminalHit: true })
- }
-
- await expect(
- describeHostedTerminalLinkPoint({}, { x: 0.25, y: 0.5 }, { evaluate })
- ).resolves.toEqual({
- clientX: 100,
- clientY: 200,
- terminalHit: true
- })
- })
-
- it('reads rendered link rows from the visible xterm buffer', async () => {
- const evaluate = async (_document: unknown, expression: string) => {
- expect(expression).toContain('xterm.buffer.active')
- return JSON.stringify({
- ...geometry,
- cursorTop: 0,
- linkRows: { javascript: 4, file: 7, fileAlternate: 8, http: 10 }
- })
- }
-
- await expect(readHostedTerminalLinkPoints({}, { evaluate })).resolves.toEqual(
- hostedTerminalLinkPointsFromGeometry({
- ...geometry,
- cursorTop: 0,
- linkRows: { javascript: 4, file: 7, fileAlternate: 8, http: 10 }
- })
- )
- })
-})
diff --git a/mobile/src/mobile-web/hosted-webview-adversarial-content.test.ts b/mobile/src/mobile-web/hosted-webview-adversarial-content.test.ts
deleted file mode 100644
index dd81575b689..00000000000
--- a/mobile/src/mobile-web/hosted-webview-adversarial-content.test.ts
+++ /dev/null
@@ -1,266 +0,0 @@
-import { execFile } from 'node:child_process'
-import { readFileSync } from 'node:fs'
-import { readFile } from 'node:fs/promises'
-import path from 'node:path'
-import { fileURLToPath } from 'node:url'
-import { promisify } from 'node:util'
-import { afterEach, describe, expect, it } from 'vitest'
-import {
- createHostedAdversarialRepositoryFixture,
- HOSTED_ADVERSARIAL_CONTENT,
- HOSTED_ADVERSARIAL_CONTENT_MARKER,
- HOSTED_ADVERSARIAL_FILENAME,
- HOSTED_ADVERSARIAL_FILENAME_MARKER,
- HOSTED_ADVERSARIAL_HTML_FILENAME,
- HOSTED_ADVERSARIAL_HTML_MARKER,
- HOSTED_ADVERSARIAL_IMAGE_FILENAME,
- HOSTED_ADVERSARIAL_IMAGE_MARKER,
- HOSTED_ADVERSARIAL_MARKDOWN_FILENAME,
- HOSTED_ADVERSARIAL_MARKDOWN_MARKER,
- HOSTED_ADVERSARIAL_SVG_FILENAME,
- HOSTED_ADVERSARIAL_SVG_MARKER,
- HOSTED_ADVERSARIAL_WORKSPACE_ROW,
- readHostedAdversarialRepositoryContent,
- removeHostedAdversarialRepositoryFixture
-} from '../../scripts/hosted-adversarial-repository-fixture.mjs'
-import {
- hostedWebViewAdversarialContentEvidence,
- hostedWebViewAdversarialContentObservations,
- verifyHostedWebViewAdversarialContent
-} from '../../scripts/hosted-webview-adversarial-content.mjs'
-import { hostedAdversarialFileExecutionEvidence } from '../../scripts/hosted-webview-adversarial-files.mjs'
-
-const execFileAsync = promisify(execFile)
-const fixtures: Awaited>[] = []
-const androidHarnessSource = readFileSync(
- new URL('../../scripts/run-hosted-android-source-control-review-e2e.mjs', import.meta.url),
- 'utf8'
-)
-const androidHarnessPath = fileURLToPath(
- new URL('../../scripts/run-hosted-android-source-control-review-e2e.mjs', import.meta.url)
-)
-const iosHarnessSource = readFileSync(
- new URL('../../scripts/run-hosted-webview-simulator-e2e.mjs', import.meta.url),
- 'utf8'
-)
-
-afterEach(async () => {
- await Promise.all(fixtures.splice(0).map(removeHostedAdversarialRepositoryFixture))
-})
-
-describe('hosted WebView adversarial content', () => {
- it('creates one disposable hostile filename and diff', async () => {
- const fixture = await createHostedAdversarialRepositoryFixture({
- probePort: 54321
- })
- fixtures.push(fixture)
-
- const status = await execFileAsync('git', ['status', '--short'], {
- cwd: fixture.root,
- encoding: 'utf8'
- })
- expect(status.stdout).toContain(HOSTED_ADVERSARIAL_FILENAME_MARKER)
- const diff = await execFileAsync('git', ['diff', '--'], {
- cwd: fixture.root,
- encoding: 'utf8'
- })
- expect(diff.stdout).toContain(HOSTED_ADVERSARIAL_CONTENT_MARKER)
- expect(await readHostedAdversarialRepositoryContent(fixture)).toBe(
- `${HOSTED_ADVERSARIAL_CONTENT}\n`
- )
- const branch = await execFileAsync('git', ['symbolic-ref', '--short', 'HEAD'], {
- cwd: fixture.root,
- encoding: 'utf8'
- })
- expect(branch.stdout.trim()).toBe(HOSTED_ADVERSARIAL_WORKSPACE_ROW)
- expect(fixture.workspaceRowName).toBe(HOSTED_ADVERSARIAL_WORKSPACE_ROW)
- expect(fixture.repositoryFiles.map(({ filename }) => filename)).toEqual([
- HOSTED_ADVERSARIAL_MARKDOWN_FILENAME,
- HOSTED_ADVERSARIAL_HTML_FILENAME,
- HOSTED_ADVERSARIAL_SVG_FILENAME,
- HOSTED_ADVERSARIAL_IMAGE_FILENAME
- ])
- expect(fixture.repositoryFiles.map(({ marker }) => marker)).toEqual([
- HOSTED_ADVERSARIAL_MARKDOWN_MARKER,
- HOSTED_ADVERSARIAL_HTML_MARKER,
- HOSTED_ADVERSARIAL_SVG_MARKER,
- HOSTED_ADVERSARIAL_IMAGE_MARKER
- ])
- for (const file of fixture.repositoryFiles) {
- const content = await readFile(path.join(fixture.root, file.filename))
- expect(content).toEqual(Buffer.from(file.content))
- expect(content.includes(file.marker)).toBe(true)
- expect(content.includes('http://127.0.0.1:54321/')).toBe(true)
- }
- })
-
- it('accepts literal markers without created elements or execution', () => {
- expect(
- hostedWebViewAdversarialContentEvidence({
- text: `${HOSTED_ADVERSARIAL_FILENAME}\n${HOSTED_ADVERSARIAL_CONTENT}`,
- execution: {
- filenameExecuted: false,
- contentExecuted: false,
- injectedImageCount: 0
- }
- })
- ).toEqual({
- filenameRenderedAsText: true,
- diffRenderedAsText: true,
- injectedImageCount: 0,
- scriptMarkersExecuted: false
- })
- })
-
- it('aggregates literal markers across route documents', async () => {
- const values = [
- JSON.stringify({
- href: 'https://orca-mobile-web.invalid/source-control',
- bodyText: HOSTED_ADVERSARIAL_FILENAME,
- labels: [],
- placeholders: []
- }),
- JSON.stringify({
- filenameExecuted: false,
- contentExecuted: false,
- injectedImageCount: 0
- }),
- JSON.stringify({
- href: 'https://orca-mobile-web.invalid/session',
- bodyText: HOSTED_ADVERSARIAL_CONTENT,
- labels: [],
- placeholders: []
- }),
- JSON.stringify({
- filenameExecuted: false,
- contentExecuted: false,
- injectedImageCount: 0
- })
- ]
- class FakeWebSocket {
- private message: ((data: Buffer) => void) | undefined
-
- once(event: string, listener: () => void) {
- if (event === 'open') {
- queueMicrotask(listener)
- }
- }
-
- on(event: string, listener: (data: Buffer) => void) {
- if (event === 'message') {
- this.message = listener
- }
- }
-
- send(payload: string) {
- const id = JSON.parse(payload).id
- const value = values.shift()
- queueMicrotask(() => {
- this.message?.(
- Buffer.from(JSON.stringify({ id, result: { result: { value: value ?? '' } } }))
- )
- })
- }
-
- close() {}
- }
-
- await expect(
- verifyHostedWebViewAdversarialContent({
- documents: [
- { webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/source' },
- { webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/session' }
- ],
- WebSocketCtor: FakeWebSocket as never
- })
- ).resolves.toMatchObject({
- filenameRenderedAsText: true,
- diffRenderedAsText: true,
- scriptMarkersExecuted: false
- })
- })
-
- it('rejects missing literal markers and executed content', () => {
- expect(() =>
- hostedWebViewAdversarialContentEvidence({
- text: HOSTED_ADVERSARIAL_FILENAME,
- execution: {
- filenameExecuted: false,
- contentExecuted: false,
- injectedImageCount: 0
- }
- })
- ).toThrow('was not rendered')
- expect(() =>
- hostedWebViewAdversarialContentEvidence({
- text: `${HOSTED_ADVERSARIAL_FILENAME}\n${HOSTED_ADVERSARIAL_CONTENT}`,
- execution: {
- filenameExecuted: false,
- contentExecuted: true,
- injectedImageCount: 1
- }
- })
- ).toThrow('content executed')
- })
-
- it('rejects malformed execution observations without coercion', () => {
- expect(() =>
- hostedWebViewAdversarialContentObservations([
- {
- state: {
- bodyText: `${HOSTED_ADVERSARIAL_FILENAME}\n${HOSTED_ADVERSARIAL_CONTENT}`,
- labels: []
- },
- execution: {
- filenameExecuted: 'false',
- contentExecuted: false,
- injectedImageCount: 0
- }
- }
- ])
- ).toThrow('content executed')
- })
-
- it('rejects malformed repository-file execution evidence', () => {
- expect(
- hostedAdversarialFileExecutionEvidence({
- markers: [false, false, false, false],
- injectedElementCount: 0
- })
- ).toEqual({
- injectedElementCount: 0,
- repositoryFileScriptMarkersExecuted: false
- })
- expect(() =>
- hostedAdversarialFileExecutionEvidence({
- markers: [false, 'false', false, false],
- injectedElementCount: 0
- })
- ).toThrow('repository file executed')
- })
-
- it('keeps the disposable corpus wired into the exact Android route gate', () => {
- expect(androidHarnessSource).toContain('createHostedAdversarialRuntimeFixture')
- expect(androidHarnessSource).toContain('captureHostedWebViewAdversarialObservation')
- expect(androidHarnessSource).toContain('hostedWebViewAdversarialContentObservations')
- expect(androidHarnessSource).toContain('removeHostedAdversarialRuntimeFixture')
- })
-
- it('rejects conflicting Android security journeys before device access', async () => {
- await expect(
- execFileAsync(process.execPath, [
- androidHarnessPath,
- '--adversarial-content',
- '--security-only'
- ])
- ).rejects.toMatchObject({
- stderr: expect.stringContaining('mutually exclusive')
- })
- })
-
- it('keeps the same disposable corpus wired into the exact iOS route gate', () => {
- expect(iosHarnessSource).toContain('registerHostedIosAdversarialRepository')
- expect(iosHarnessSource).toContain('createHostedIosAdversarialContentInspector')
- expect(iosHarnessSource).toContain('removeHostedAdversarialRuntimeFixture')
- })
-})
diff --git a/mobile/src/mobile-web/hosted-webview-cdp-session.test.ts b/mobile/src/mobile-web/hosted-webview-cdp-session.test.ts
deleted file mode 100644
index d5c8c998cbd..00000000000
--- a/mobile/src/mobile-web/hosted-webview-cdp-session.test.ts
+++ /dev/null
@@ -1,804 +0,0 @@
-import { readFileSync } from 'node:fs'
-import { describe, expect, it } from 'vitest'
-import {
- activateHostedWebViewControl,
- assertNoHostedMobileWebCdpTarget,
- isHostedMobileWebUrl,
- readHostedWebViewState,
- readHostedWebViewTextPoint,
- selectVisibleHostedWebView,
- setHostedWebViewInput,
- startHostedWebViewConnectionObservation,
- terminateHostedWebViewProcess,
- waitForHostedWebViewConnectionSequence,
- verifyHostedWebViewNavigationIsolation,
- verifyHostedWebViewNetworkIsolation
-} from '../../scripts/hosted-webview-cdp-session.mjs'
-import { createHostedIosNativeBaselineStep } from '../../scripts/hosted-ios-native-baseline-step.mjs'
-import { verifyHostedWebViewExecutableIsolation } from '../../scripts/hosted-webview-executable-isolation.mjs'
-import {
- FakeCdpSocket,
- FakeProcessTerminationSocket,
- fakeCdpConstructor
-} from './hosted-webview-cdp-test-fakes'
-
-const iosShellSource = readFileSync(
- new URL('../../packages/expo-mobile-web-shell/ios/MobileWebShellView.swift', import.meta.url),
- 'utf8'
-)
-const androidShellSource = readFileSync(
- new URL(
- '../../packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebShellView.kt',
- import.meta.url
- ),
- 'utf8'
-)
-const androidProbeSource = readFileSync(
- new URL(
- '../../packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebDebugIsolationProbe.kt',
- import.meta.url
- ),
- 'utf8'
-)
-// Read patches through pnpm-workspace.yaml so a dependency bump can't leave a patch undeclared.
-const workspaceManifest = readFileSync(
- new URL('../../pnpm-workspace.yaml', import.meta.url),
- 'utf8'
-)
-function readPatchedDependencySource(packageName: string): string {
- const entry = workspaceManifest.match(
- new RegExp(`^\\s+'?${packageName.replace(/[/@.]/g, '\\$&')}@[^':]+'?:\\s*(\\S+)$`, 'm')
- )
- if (!entry) {
- throw new Error(`no patchedDependencies entry for ${packageName}`)
- }
- return readFileSync(new URL(`../../${entry[1]}`, import.meta.url), 'utf8')
-}
-const expoLogBoxPatch = readPatchedDependencySource('@expo/log-box')
-const expoDomWebViewPatch = readPatchedDependencySource('@expo/dom-webview')
-const reactNativeWebViewPatch = readPatchedDependencySource('react-native-webview')
-const simulatorHarnessSource = readFileSync(
- new URL('../../scripts/run-hosted-webview-simulator-e2e.mjs', import.meta.url),
- 'utf8'
-)
-const simulatorSecurityEvidenceSource = readFileSync(
- new URL('../../scripts/hosted-webview-security-evidence.mjs', import.meta.url),
- 'utf8'
-)
-const simulatorReportSource = readFileSync(
- new URL('../../scripts/hosted-webview-e2e-report.mjs', import.meta.url),
- 'utf8'
-)
-const simulatorAppBuildSource = readFileSync(
- new URL('../../scripts/hosted-ios-simulator-app-build.mjs', import.meta.url),
- 'utf8'
-)
-const simulatorAppPreparationSource = readFileSync(
- new URL('../../scripts/hosted-ios-simulator-app-preparation.mjs', import.meta.url),
- 'utf8'
-)
-const simulatorLauncherSource = readFileSync(
- new URL('../../scripts/hosted-ios-mobile-launcher.mjs', import.meta.url),
- 'utf8'
-)
-const androidSecurityHarnessSource = readFileSync(
- new URL('../../scripts/run-hosted-android-webview-security-e2e.mjs', import.meta.url),
- 'utf8'
-)
-const androidReleaseHarnessSource = readFileSync(
- new URL('../../scripts/verify-hosted-android-release-webview.mjs', import.meta.url),
- 'utf8'
-)
-const androidCrashLoopHarnessSource = readFileSync(
- new URL('../../scripts/run-hosted-android-webview-crash-loop.mjs', import.meta.url),
- 'utf8'
-)
-
-function probe(overrides: Record = {}) {
- return {
- targetId: 'target-a',
- href: 'orca-mobile-web://session-a/',
- visibility: 'visible',
- focused: true,
- bridgeListening: true,
- bodyText: 'mobile-rearch',
- buttonCount: 3,
- ...overrides
- }
-}
-
-describe('hosted WebView CDP target selection', () => {
- it('waits for the workspace route when the departing session has the same text', () => {
- const session = probe({ href: 'orca-mobile-web://session-a/h/host/session/workspace' })
- const workspace = probe({ href: 'orca-mobile-web://session-a/h/host', focused: false })
- expect(
- selectVisibleHostedWebView([session], 'mobile-rearch', undefined, true, '/h/host')
- ).toBeNull()
- expect(
- selectVisibleHostedWebView([session, workspace], 'mobile-rearch', undefined, true, '/h/host')
- ).toBe(workspace)
- })
-
- it('recognizes only the platform private asset origins', () => {
- expect(isHostedMobileWebUrl('orca-mobile-web://session-a/')).toBe(true)
- expect(isHostedMobileWebUrl('https://session-a.orca-mobile-web.invalid/#session-a')).toBe(true)
- expect(isHostedMobileWebUrl('https://orca-mobile-web.invalid/#session-a')).toBe(true)
- expect(isHostedMobileWebUrl('https://orca-mobile-web.invalid.evil.test/')).toBe(false)
- expect(isHostedMobileWebUrl('https://session-a.orca-mobile-web.invalid.evil.test/')).toBe(false)
- expect(isHostedMobileWebUrl('http://orca-mobile-web.invalid/')).toBe(false)
- expect(isHostedMobileWebUrl('https://session-a.orca-mobile-web.invalid.evil/')).toBe(false)
- })
-
- it('proves native baselines have no hosted private-origin CDP target', async () => {
- const target = (url: string) => ({
- id: 'target',
- type: 'page',
- url,
- webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/target'
- })
- const fetchTargets = (urls: string[]) => async () =>
- new Response(JSON.stringify(urls.map(target)))
-
- await expect(
- assertNoHostedMobileWebCdpTarget({
- discoveryUrl: 'http://127.0.0.1:9222',
- fetchImpl: fetchTargets(['about:blank', 'file:///native-terminal.html'])
- })
- ).resolves.toBeUndefined()
- await expect(
- assertNoHostedMobileWebCdpTarget({
- discoveryUrl: 'http://127.0.0.1:9222',
- fetchImpl: fetchTargets(Array.from({ length: 17 }, () => 'about:blank'))
- })
- ).rejects.toThrow('Native baseline CDP target count exceeded its inspection limit')
- for (const url of [
- 'orca-mobile-web://session-a/',
- 'https://orca-mobile-web.invalid/#session-a'
- ]) {
- await expect(
- assertNoHostedMobileWebCdpTarget({
- discoveryUrl: 'http://127.0.0.1:9222',
- fetchImpl: fetchTargets([url])
- })
- ).rejects.toThrow('Native baseline has a hosted mobile WebView CDP target')
- }
- await expect(
- assertNoHostedMobileWebCdpTarget({
- discoveryUrl: 'http://127.0.0.1:9222',
- fetchImpl: async () =>
- new Response(
- JSON.stringify([
- {
- id: 'uninspectable-hosted-target',
- type: 'page',
- url: 'orca-mobile-web://session-a/'
- }
- ])
- )
- })
- ).rejects.toThrow('Native baseline has a hosted mobile WebView CDP target')
- const staleDiscoverySocket = new FakeCdpSocket([JSON.stringify(probe())])
- await expect(
- assertNoHostedMobileWebCdpTarget({
- discoveryUrl: 'http://127.0.0.1:9222',
- fetchImpl: fetchTargets(['about:blank']),
- WebSocketCtor: fakeCdpConstructor(staleDiscoverySocket)
- })
- ).rejects.toThrow('Native baseline has a hosted mobile WebView CDP target')
- })
-
- it('asserts target exclusion around every native baseline capture', async () => {
- const events: string[] = []
- const nativeBaselineStep = createHostedIosNativeBaselineStep({
- assertNoHostedMobileWebCdpTarget: async () => {
- events.push('assert')
- },
- discoveryUrl: 'http://127.0.0.1:9222',
- evidenceStep: async (label: string, run: () => Promise) => {
- events.push(label)
- return run()
- }
- })
-
- await expect(
- nativeBaselineStep('native workspace baseline', async () => {
- events.push('capture')
- return 'captured'
- })
- ).resolves.toBe('captured')
- expect(events).toEqual([
- 'native workspace baseline hosted target exclusion',
- 'assert',
- 'native workspace baseline',
- 'capture',
- 'native workspace baseline hosted target exclusion after capture',
- 'assert'
- ])
- })
-
- it('selects only a visible interactive hosted document with expected UI text', () => {
- expect(
- selectVisibleHostedWebView(
- [
- probe({ href: 'https://untrusted.example/' }),
- probe({ visibility: 'hidden' }),
- probe({ bridgeListening: false }),
- probe({ bodyText: 'another workspace' }),
- probe()
- ],
- 'mobile-rearch'
- )
- ).toMatchObject({ targetId: 'target-a' })
- })
-
- it('prefers the focused generation when WebKit briefly reports two visible documents', () => {
- expect(
- selectVisibleHostedWebView(
- [probe({ targetId: 'old', focused: false }), probe({ targetId: 'current', focused: true })],
- 'mobile-rearch'
- )
- ).toMatchObject({ targetId: 'current' })
- })
-
- it('rejects a document without user-observable controls', () => {
- expect(selectVisibleHostedWebView([probe({ buttonCount: 0 })], 'mobile-rearch')).toBeNull()
- })
-
- it('can select a ready terminal document without raw DOM buttons', () => {
- expect(
- selectVisibleHostedWebView(
- [probe({ buttonCount: 0, href: 'orca-mobile-web://session-a/h/host/session/worktree' })],
- 'mobile-rearch',
- '/session/',
- false
- )
- ).toMatchObject({ targetId: 'target-a' })
- })
-
- it('can require the visible document to be on the expected route', () => {
- expect(
- selectVisibleHostedWebView(
- [
- probe({
- href: 'orca-mobile-web://session-a/h/host/agent-history/worktree'
- }),
- probe({
- href: 'orca-mobile-web://session-a/h/host/session/worktree'
- })
- ],
- 'mobile-rearch',
- '/session/'
- )
- ).toMatchObject({
- href: 'orca-mobile-web://session-a/h/host/session/worktree'
- })
- })
-
- it('starts the document probe and requires its completion marker', async () => {
- const socket = new FakeCdpSocket(['probe-a', 'probe-a'])
- const result = await verifyHostedWebViewNetworkIsolation({
- document: {
- webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/current'
- },
- probeId: 'probe-a',
- settleDelayMs: 0,
- WebSocketCtor: class {
- constructor() {
- return socket
- }
- }
- })
-
- expect(result).toEqual({
- fetch: 'attempted',
- xhr: 'attempted',
- webSocket: 'attempted',
- image: 'attempted'
- })
- expect(socket.evaluations).toHaveLength(2)
- const expressions = socket.evaluations
- .map((evaluation) => evaluation.params.expression)
- .join('\n')
- expect(expressions).toContain('__orcaRunSecurityProbe')
- expect(expressions).toContain('__orcaDebugNetworkProbeCompletion')
- })
-
- it('requires an exact probe token', async () => {
- await expect(
- verifyHostedWebViewNetworkIsolation({
- document: {
- webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/current'
- },
- probeId: ''
- })
- ).rejects.toThrow('token is unavailable')
- })
-
- it('requires all adversarial navigation attempts to fail closed', async () => {
- const socket = new FakeCdpSocket([
- JSON.stringify({
- token: 'probe-a',
- documentRetained: true,
- popupBlocked: true,
- serviceWorkerBlocked: true,
- redirectFrameAttempted: true,
- downloadAttempted: true,
- externalSchemeAttempted: true
- })
- ])
- const result = await verifyHostedWebViewNavigationIsolation({
- document: {
- webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/current'
- },
- probeId: 'probe-a',
- settleDelayMs: 0,
- WebSocketCtor: class {
- constructor() {
- return socket
- }
- }
- })
-
- expect(result).toEqual({
- documentRetained: true,
- popupBlocked: true,
- serviceWorkerBlocked: true,
- redirectFrameAttempted: true,
- downloadAttempted: true,
- externalSchemeAttempted: true
- })
- expect(socket.evaluations[0]?.params.expression).toContain(
- '__orcaDebugNavigationProbeCompletion'
- )
- })
-
- it('requires the active script and blocks an undeclared executable asset', async () => {
- const socket = new FakeCdpSocket([
- JSON.stringify({
- token: 'probe-a',
- activeDeclaredScriptLoaded: true,
- undeclaredScriptBlocked: true,
- documentRetained: true,
- bridgeListening: true,
- scriptPaths: ['/h/host/review/assets/bundle.js']
- })
- ])
-
- await expect(
- verifyHostedWebViewExecutableIsolation({
- document: {
- webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/current'
- },
- probeId: 'probe-a',
- settleDelayMs: 0,
- WebSocketCtor: class {
- constructor() {
- return socket
- }
- }
- })
- ).resolves.toEqual({
- activeDeclaredScriptLoaded: true,
- undeclaredScriptBlocked: true,
- documentRetained: true
- })
- expect(socket.evaluations[0]?.params.expression).toContain(
- '__orcaDebugExecutableProbeCompletion'
- )
- })
-
- it('rejects incomplete executable isolation evidence', async () => {
- const socket = new FakeCdpSocket([
- JSON.stringify({
- token: 'probe-a',
- activeDeclaredScriptLoaded: true,
- undeclaredScriptBlocked: false,
- documentRetained: true
- })
- ])
-
- await expect(
- verifyHostedWebViewExecutableIsolation({
- document: {
- webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/current'
- },
- probeId: 'probe-a',
- settleDelayMs: 0,
- WebSocketCtor: class {
- constructor() {
- return socket
- }
- }
- })
- ).rejects.toThrow('executable isolation failed')
- })
-
- it('reads a normalized hosted text landmark', async () => {
- const socket = new FakeCdpSocket([JSON.stringify({ x: 0.25, y: 0.125 })])
-
- await expect(
- readHostedWebViewTextPoint(
- { webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/current' },
- 'Agent Session History',
- fakeCdpConstructor(socket),
- { horizontalPosition: 0.15, ignoreCase: true, occurrence: 1, reveal: true }
- )
- ).resolves.toEqual({ x: 0.25, y: 0.125 })
- expect(socket.evaluations[0]?.params.expression).toContain('getBoundingClientRect')
- expect(socket.evaluations[0]?.params.expression).toContain("style.visibility !== 'hidden'")
- expect(socket.evaluations[0]?.params.expression).toContain('toLocaleLowerCase')
- expect(socket.evaluations[0]?.params.expression).toContain('matches[1]')
- expect(socket.evaluations[0]?.params.expression).toContain('rect.width * 0.15')
- expect(socket.evaluations[0]?.params.expression).toContain(
- "scrollIntoView({ block: 'nearest', inline: 'nearest' })"
- )
- })
-
- it('rejects incomplete adversarial navigation evidence', async () => {
- const socket = new FakeCdpSocket([
- JSON.stringify({
- token: 'probe-a',
- documentRetained: true,
- popupBlocked: false
- })
- ])
- await expect(
- verifyHostedWebViewNavigationIsolation({
- document: {
- webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/current'
- },
- probeId: 'probe-a',
- settleDelayMs: 0,
- WebSocketCtor: class {
- constructor() {
- return socket
- }
- }
- })
- ).rejects.toThrow('navigation isolation failed')
- })
-
- it('accepts an Android popup proxy only without a second page target', async () => {
- const socket = new FakeCdpSocket([
- JSON.stringify({
- token: 'probe-a',
- documentRetained: true,
- popupBlocked: false,
- serviceWorkerBlocked: true,
- redirectFrameAttempted: true,
- downloadAttempted: true,
- externalSchemeAttempted: true
- })
- ])
- const fetchImpl = async () =>
- new Response(
- JSON.stringify([
- {
- id: 'current',
- type: 'page',
- url: 'https://orca-mobile-web.invalid/#session',
- webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/current'
- }
- ])
- )
-
- await expect(
- verifyHostedWebViewNavigationIsolation({
- document: {
- targetId: 'current',
- webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/current'
- },
- discoveryUrl: 'http://127.0.0.1:9222',
- probeId: 'probe-a',
- settleDelayMs: 0,
- WebSocketCtor: class {
- constructor() {
- return socket
- }
- },
- fetchImpl
- })
- ).resolves.toMatchObject({ popupBlocked: true })
- })
-
- it('reads bounded UI facts and activates controls without page-owned selectors', async () => {
- const socket = new FakeCdpSocket([
- JSON.stringify({
- href: 'orca-mobile-web://session-a/',
- bodyText: 'Agent Session History',
- labels: ['Back', 'Refresh agent sessions'],
- placeholders: ['Search sessions, repo:, path:']
- }),
- JSON.stringify({ found: true }),
- JSON.stringify({ activated: true }),
- JSON.stringify({ updated: true }),
- JSON.stringify({ found: true }),
- JSON.stringify({ activated: true })
- ])
- const document = {
- webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/current'
- }
-
- await expect(
- readHostedWebViewState(document, fakeCdpConstructor(socket))
- ).resolves.toMatchObject({
- bodyText: 'Agent Session History',
- labels: ['Back', 'Refresh agent sessions']
- })
- await expect(
- activateHostedWebViewControl(
- document,
- { kind: 'label', value: 'More session actions', reveal: true },
- fakeCdpConstructor(socket)
- )
- ).resolves.toBeUndefined()
- await expect(
- setHostedWebViewInput(
- document,
- { placeholder: 'Search sessions, repo:, path:', value: 'fixture' },
- fakeCdpConstructor(socket)
- )
- ).resolves.toBeUndefined()
- await expect(
- activateHostedWebViewControl(
- document,
- { kind: 'text', value: 'mobile-rearch' },
- fakeCdpConstructor(socket)
- )
- ).resolves.toBeUndefined()
- const expressions = socket.evaluations
- .map((evaluation) => evaluation.params.expression)
- .join('\n')
- expect(expressions).toContain('input[placeholder],textarea[placeholder]')
- expect(expressions).toContain('element.getBoundingClientRect()')
- expect(expressions).toContain('rect.top < innerHeight')
- expect(expressions).toContain('More session actions')
- expect(expressions).toContain('closest(\'button,[role="button"],a,[tabindex]\')')
- expect(expressions).toContain('getBoundingClientRect()')
- expect(expressions).toContain("scrollIntoView({ block: 'nearest', inline: 'nearest' })")
- expect(expressions).toContain('data-orca-cdp-activation')
- expect(expressions).toContain('__orcaCdpActivationLedger')
- expect(expressions).toContain("Object.getOwnPropertyDescriptor(prototype, 'value')")
- expect(expressions).toContain("new InputEvent('input'")
- expect(expressions).toContain('element.click()')
- })
-
- it('requires renderer-loss evidence after requesting a debug process crash', async () => {
- const socket = new FakeProcessTerminationSocket()
- await expect(
- terminateHostedWebViewProcess(
- { webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/current' },
- fakeCdpConstructor(socket)
- )
- ).resolves.toBeUndefined()
- expect(socket.command).toEqual({ id: 1, method: 'Page.crash' })
- })
-
- it('observes an ordered reconnect while recording retained route evidence', async () => {
- const entries = [
- {
- state: 'recovering',
- href: 'orca-mobile-web://session-a/h/host/agent-history/worktree',
- retainedExpectedText: true,
- retainedExpectedRoute: true
- },
- {
- state: 'connected',
- href: 'orca-mobile-web://session-a/h/host/agent-history/worktree',
- retainedExpectedText: true,
- retainedExpectedRoute: true
- }
- ]
- const socket = new FakeCdpSocket([JSON.stringify({ started: true }), JSON.stringify(entries)])
- const document = {
- webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/current'
- }
- const WebSocketCtor = fakeCdpConstructor(socket)
-
- await startHostedWebViewConnectionObservation(
- document,
- {
- expectedText: 'Hybrid Agent History Fixture',
- expectedHrefIncludes: '/agent-history/'
- },
- WebSocketCtor
- )
- await expect(
- waitForHostedWebViewConnectionSequence(
- document,
- ['recovering', 'connected'],
- 1_000,
- WebSocketCtor
- )
- ).resolves.toEqual(entries)
-
- const expressions = socket.evaluations
- .map((evaluation) => evaluation.params.expression)
- .join('\n')
- expect(expressions).toContain("addEventListener('message'")
- expect(expressions).toContain("message?.type !== 'connection'")
- expect(expressions).toContain('requestAnimationFrame')
- expect(expressions).toContain('retainedExpectedText')
- expect(expressions).toContain('retainedExpectedRoute')
- })
-
- it('keeps the native probe DEBUG-only, loopback-only, and completion-marked', () => {
- const probeStart = iosShellSource.indexOf(
- 'private func mobileWebNetworkIsolationProbeUserScript()'
- )
- const debugStart = iosShellSource.lastIndexOf('#if DEBUG', probeStart)
- const debugEnd = iosShellSource.indexOf('#endif', probeStart)
- const probeSource = iosShellSource.slice(debugStart, debugEnd)
-
- expect(debugStart).toBeGreaterThanOrEqual(0)
- expect(debugEnd).toBeGreaterThan(probeStart)
- expect(probeSource).toContain('ORCA_E2E_MOBILE_WEB_NETWORK_PROBE_PORT')
- expect(probeSource).toContain('ORCA_E2E_MOBILE_WEB_NETWORK_PROBE_TOKEN')
- expect(probeSource).toContain('http://127.0.0.1:')
- expect(probeSource).toContain('ws://127.0.0.1:')
- expect(probeSource).toContain('completed===4')
- expect(probeSource).toContain('__orcaDebugNavigationProbeCompletion')
- expect(probeSource).toContain('__orcaDebugExecutableProbeCompletion')
- expect(probeSource).toContain('activeDeclaredScriptLoaded')
- expect(probeSource).toContain('undeclaredScriptBlocked')
- expect(probeSource).toContain('[a-f0-9]{64}\\\\.js$')
- expect(probeSource).toContain("script.getAttribute('src')")
- expect(probeSource).toContain('new URL(declaredScriptPath(activeScript),location.origin)')
- expect(probeSource).toContain("window.open(probeBase+'/popup-probe','_blank')")
- expect(probeSource).toContain("frame.src=probeBase+'/redirect-probe'")
- expect(probeSource).toContain("download.href=probeBase+'/download-probe'")
- expect(probeSource).toContain("navigator.serviceWorker.register(probeBase+'/worker-probe')")
- expect(probeSource).toContain("location.assign('orca-security-probe://blocked')")
- expect(probeSource).toContain('forMainFrameOnly: true')
- expect(iosShellSource).toContain(
- '#if DEBUG\n if let networkProbe = mobileWebNetworkIsolationProbeUserScript()'
- )
- })
-
- it('keeps the Android probe debuggable-only and installs it at document start', () => {
- expect(androidProbeSource).toContain('BuildConfig.DEBUG')
- expect(androidProbeSource).toContain('ApplicationInfo.FLAG_DEBUGGABLE')
- expect(androidProbeSource).toContain(
- 'WebView.setWebContentsDebuggingEnabled(isMobileWebInspectionEnabled(applicationFlags))'
- )
- expect(androidProbeSource).toContain('if (!BuildConfig.DEBUG || !isDebuggable) return null')
- expect(androidProbeSource).toContain('ORCA_E2E_MOBILE_WEB_NETWORK_PROBE_PORT')
- expect(androidProbeSource).toContain('ORCA_E2E_MOBILE_WEB_NETWORK_PROBE_TOKEN')
- expect(androidProbeSource).toContain('http://127.0.0.1:')
- expect(androidProbeSource).toContain('ws://127.0.0.1:')
- expect(androidProbeSource).toContain('globalThis.__orcaRunSecurityProbe=function()')
- expect(androidProbeSource).toContain('globalThis.__orcaMobileWebShellListening!==true')
- expect(androidProbeSource).toContain('completed===4')
- expect(androidProbeSource).toContain('__orcaDebugNavigationProbeCompletion')
- expect(androidProbeSource).toContain('__orcaDebugExecutableProbeCompletion')
- expect(androidProbeSource).toContain('activeDeclaredScriptLoaded')
- expect(androidProbeSource).toContain('undeclaredScriptBlocked')
- expect(androidProbeSource).toContain('[a-f0-9]{64}\\.js')
- expect(androidProbeSource).toContain("script.getAttribute('src')")
- expect(androidProbeSource).toContain(
- 'new URL(declaredScriptPath(activeScript),location.origin)'
- )
- expect(androidProbeSource).toContain("window.open(probeBase+'/popup-probe','_blank')")
- expect(androidProbeSource).toContain("frame.src=probeBase+'/redirect-probe'")
- expect(androidProbeSource).toContain("download.href=probeBase+'/download-probe'")
- expect(androidProbeSource).toContain(
- "navigator.serviceWorker.register(probeBase+'/worker-probe')"
- )
- expect(androidProbeSource).toContain("location.assign('orca-security-probe://blocked')")
- expect(androidProbeSource).toContain('WebViewFeature.DOCUMENT_START_SCRIPT')
- expect(androidProbeSource).toContain('WebViewCompat.addDocumentStartJavaScript')
- expect(androidProbeSource).toContain('allowedOrigin: String')
- expect(androidProbeSource).toContain('setOf(allowedOrigin)')
- expect(androidProbeSource).not.toContain('setOf(MOBILE_WEB_ORIGIN)')
- expect(androidShellSource).toContain(
- 'installMobileWebDebugIsolationProbe(\n webView,\n appContext,\n mobileWebOriginForSession(sessionId)'
- )
- expect(androidShellSource).toContain('override fun onAttachedToWindow()')
- expect(androidShellSource).toContain('addBridgeMessageListener(sessionId)')
- expect(androidShellSource).toContain(
- '!documentLoaded || currentUrl == null || !isAllowedDocumentUrl(currentUrl)'
- )
- expect(expoLogBoxPatch).toContain(
- 'context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0'
- )
- expect(expoLogBoxPatch).toContain('- setWebContentsDebuggingEnabled(true)')
- expect(expoLogBoxPatch).toContain('+ setWebContentsDebuggingEnabled(isDebuggable)')
- expect(expoDomWebViewPatch).toContain(
- '+ WebView.setWebContentsDebuggingEnabled(webviewDebuggingEnabled && isDebuggable)'
- )
- expect(reactNativeWebViewPatch).toContain(
- '+ RNCWebView.setWebContentsDebuggingEnabled(enabled && isDebuggable)'
- )
- expect(reactNativeWebViewPatch).toContain(
- '+ if (ReactBuildConfig.DEBUG && isDebuggable) {'
- )
- })
-
- it('requires a production Android image for the Release inspector gate', () => {
- expect(androidReleaseHarnessSource).toContain("buildType !== 'user'")
- expect(androidReleaseHarnessSource).toContain("debuggable !== '0'")
- expect(androidReleaseHarnessSource).toContain("!fingerprint.includes(':user/')")
- expect(androidReleaseHarnessSource).toContain("packageFlags.includes('DEBUGGABLE')")
- expect(androidReleaseHarnessSource).toContain("'am', 'force-stop'")
- expect(androidReleaseHarnessSource).toContain("'uiautomator', 'dump', '/dev/tty'")
- expect(androidReleaseHarnessSource).toContain('assertNoInspectorSocket(options, pid)')
- expect(androidReleaseHarnessSource).toContain(
- 'Android Release DevTools discovery endpoint is accessible'
- )
- })
-
- it('builds and installs the exact native shell before the isolated live gate', () => {
- expect(simulatorHarnessSource).toContain('hostedIosSimulatorAppPreparation')
- expect(simulatorHarnessSource).toContain(
- 'const appPreparation = hostedIosSimulatorAppPreparation'
- )
- expect(simulatorHarnessSource).toContain(
- 'nativeAppPath = await evidenceStep(appPreparation.label, appPreparation.run)'
- )
- expect(simulatorAppPreparationSource).toContain("label: 'existing native simulator app'")
- expect(simulatorAppPreparationSource).toContain("label: 'cached native simulator app install'")
- expect(simulatorAppPreparationSource).toContain("label: 'native simulator app build'")
- expect(simulatorHarnessSource).toContain('options.securityOnly')
- expect(simulatorHarnessSource).toContain(
- "expectedText: options.sourceControlOnly ? '1 tab' : '2 tabs'"
- )
- expect(simulatorHarnessSource).toContain(
- "await evidenceStep('hosted terminal device input journey'"
- )
- expect(simulatorHarnessSource).toContain(
- 'terminalDeviceInput.photoPermissionRevocation?.sessionDocument'
- )
- expect(simulatorReportSource).toContain(
- 'terminalDeviceInput?.terminalClipboardImagePaste?.evidence'
- )
- expect(simulatorHarnessSource).toContain('captureHostedWebViewSecurityEvidence')
- expect(simulatorSecurityEvidenceSource).toContain('verifyHostedWebViewExecutableIsolation')
- expect(simulatorSecurityEvidenceSource).toContain('verifyHostedWebViewPrivacyIsolation')
- expect(simulatorHarnessSource).toContain("await evidenceStep('Photos permission reset'")
- expect(simulatorHarnessSource).toContain('await clearHostedIosWebViewSecurityProbe(deviceUdid)')
- expect(simulatorAppBuildSource).toContain("'xcodebuild'")
- expect(simulatorAppBuildSource).toContain("'simctl', 'install', deviceUdid, appPath")
- expect(simulatorAppBuildSource).not.toContain('CODE_SIGNING_ALLOWED=NO')
- expect(simulatorLauncherSource).toContain("EXPO_PUBLIC_ORCA_E2E_MOBILE_NATIVE_BASELINE: '1'")
- const inspectorStart = simulatorHarnessSource.indexOf('inspector = await startCdpServer')
- const nativeCapture = simulatorHarnessSource.indexOf("'native workspace baseline'")
- const exclusion = simulatorHarnessSource.indexOf(
- 'assertNoHostedMobileWebCdpTarget',
- inspectorStart
- )
- const handoff = simulatorHarnessSource.indexOf("'native hybrid route handoff'")
- const hostedWait = simulatorHarnessSource.indexOf('let workspaceDocument = await')
- expect(inspectorStart).toBeGreaterThanOrEqual(0)
- expect(exclusion).toBeGreaterThan(inspectorStart)
- expect(nativeCapture).toBeGreaterThan(exclusion)
- expect(handoff).toBeGreaterThan(nativeCapture)
- expect(hostedWait).toBeGreaterThan(handoff)
- })
-
- it('installs and launches the exact Android shell with a proven sentinel', () => {
- expect(androidSecurityHarnessSource).toContain("['install', '-r', '-t', options.apk]")
- expect(androidSecurityHarnessSource).toContain(
- "['reverse', `tcp:${probe.port}`, `tcp:${probe.port}`]"
- )
- expect(androidSecurityHarnessSource).toContain("'nc', '-z', '-w', '5', '127.0.0.1'")
- expect(androidSecurityHarnessSource).toContain("includes('tcp:connection')")
- expect(androidSecurityHarnessSource).toContain('probe.reset()')
- expect(androidSecurityHarnessSource).toContain("'ORCA_E2E_MOBILE_WEB_NETWORK_PROBE_PORT'")
- expect(androidSecurityHarnessSource).toContain("'ORCA_E2E_MOBILE_WEB_NETWORK_PROBE_TOKEN'")
- expect(androidSecurityHarnessSource).toContain('waitForVisibleHostedWebView')
- expect(androidSecurityHarnessSource).toContain('verifyHostedWebViewNetworkIsolation')
- expect(androidSecurityHarnessSource).toContain('verifyHostedWebViewNavigationIsolation')
- expect(androidSecurityHarnessSource).toContain('verifyHostedWebViewExecutableIsolation')
- expect(androidSecurityHarnessSource).toContain('verifyHostedWebViewPrivacyIsolation')
- expect(androidSecurityHarnessSource).toContain('probe.observations.length > 0')
- })
-
- it('crashes three Android renderers and requires native activation rollback', () => {
- expect(androidCrashLoopHarnessSource).toContain('const failureCount = 3')
- expect(androidCrashLoopHarnessSource).toContain('terminateHostedWebViewProcess(document)')
- expect(androidCrashLoopHarnessSource).toContain('initial.previous')
- expect(androidCrashLoopHarnessSource).toContain('waitForAndroidActivation(')
- expect(androidCrashLoopHarnessSource).toContain('>= 60_000')
- expect(androidCrashLoopHarnessSource).toContain('documents.at(-1)?.href === documents[0]?.href')
- })
-})
diff --git a/mobile/src/mobile-web/hosted-webview-cdp-target-replacement.test.ts b/mobile/src/mobile-web/hosted-webview-cdp-target-replacement.test.ts
deleted file mode 100644
index 03b8f150dc6..00000000000
--- a/mobile/src/mobile-web/hosted-webview-cdp-target-replacement.test.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import { waitForHostedWebViewConnectionSequence } from '../../scripts/hosted-webview-cdp-session.mjs'
-
-describe('hosted WebView CDP target replacement', () => {
- it('reacquires the document while observing reconnect', async () => {
- const recovering = connectionEntry('recovering')
- const connected = connectionEntry('connected')
- const stale = new TargetSocket([
- JSON.stringify([recovering]),
- new Error('target replaced'),
- new Error('target replaced'),
- new Error('target replaced')
- ])
- const replacement = new TargetSocket([JSON.stringify([connected])])
- const WebSocketCtor = class {
- constructor(endpoint: string) {
- return endpoint.endsWith('/stale') ? stale : replacement
- }
- }
- const reacquireDocument = vi.fn().mockResolvedValue({
- webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/replacement'
- })
-
- await expect(
- waitForHostedWebViewConnectionSequence(
- { webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/stale' },
- ['recovering', 'connected'],
- 5_000,
- { WebSocketCtor, reacquireDocument }
- )
- ).resolves.toEqual([recovering, connected])
- expect(reacquireDocument).toHaveBeenCalledOnce()
- })
-})
-
-function connectionEntry(state: string) {
- return {
- state,
- href: 'orca-mobile-web://session-a/h/host/agent-history/worktree',
- retainedExpectedText: true,
- retainedExpectedRoute: true
- }
-}
-
-class TargetSocket {
- private readonly values: (string | Error)[]
- private errorListener: ((error: Error) => void) | undefined
- private messageListener: ((data: Buffer) => void) | undefined
-
- constructor(values: (string | Error)[]) {
- this.values = [...values]
- }
-
- once(event: string, listener: (value?: unknown) => void): void {
- if (event === 'open') {
- queueMicrotask(listener)
- } else if (event === 'error') {
- this.errorListener = listener as (error: Error) => void
- }
- }
-
- on(event: string, listener: (data: Buffer) => void): void {
- if (event === 'message') {
- this.messageListener = listener
- }
- }
-
- send(): void {
- const value = this.values.shift() ?? ''
- queueMicrotask(() => {
- if (value instanceof Error) {
- this.errorListener?.(value)
- return
- }
- this.messageListener?.(Buffer.from(JSON.stringify({ id: 1, result: { result: { value } } })))
- })
- }
-
- close(): void {}
-}
diff --git a/mobile/src/mobile-web/hosted-webview-cdp-test-fakes.ts b/mobile/src/mobile-web/hosted-webview-cdp-test-fakes.ts
deleted file mode 100644
index 0ee81bb5c12..00000000000
--- a/mobile/src/mobile-web/hosted-webview-cdp-test-fakes.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-export class FakeCdpSocket {
- evaluations: {
- id: number
- params: { expression: string; returnByValue: boolean }
- }[] = []
- private readonly values: string[]
- private messageListener: ((data: Buffer) => void) | undefined
-
- constructor(values: string[]) {
- this.values = [...values]
- }
-
- once(event: string, listener: (value?: unknown) => void): void {
- if (event === 'open') {
- queueMicrotask(listener)
- }
- }
-
- on(event: string, listener: (data: Buffer) => void): void {
- if (event === 'message') {
- this.messageListener = listener
- }
- }
-
- send(payload: string): void {
- this.evaluations.push(JSON.parse(payload))
- const value = this.values.shift() ?? ''
- queueMicrotask(() => {
- this.messageListener?.(Buffer.from(JSON.stringify({ id: 1, result: { result: { value } } })))
- })
- }
-
- close(): void {}
-}
-
-export class FakeProcessTerminationSocket {
- command: unknown
- private messageListener: ((data: Buffer) => void) | undefined
-
- once(event: string, listener: (value?: unknown) => void): void {
- if (event === 'open') {
- queueMicrotask(listener)
- }
- }
-
- on(event: string, listener: (data: Buffer) => void): void {
- if (event === 'message') {
- this.messageListener = listener
- }
- }
-
- send(payload: string): void {
- this.command = JSON.parse(payload)
- queueMicrotask(() => {
- this.messageListener?.(
- Buffer.from(
- JSON.stringify({
- method: 'Inspector.detached',
- params: { reason: 'Render process gone.' }
- })
- )
- )
- })
- }
-
- close(): void {}
-}
-
-export function fakeCdpConstructor(socket: FakeCdpSocket | FakeProcessTerminationSocket) {
- return class {
- constructor() {
- return socket
- }
- }
-}
diff --git a/mobile/src/mobile-web/hosted-webview-long-press.test.ts b/mobile/src/mobile-web/hosted-webview-long-press.test.ts
deleted file mode 100644
index 0fd3b4a5405..00000000000
--- a/mobile/src/mobile-web/hosted-webview-long-press.test.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import { dispatchHostedWebViewLongPress } from '../../scripts/hosted-webview-long-press.mjs'
-
-describe('hosted WebView long press', () => {
- it('holds the unchanged RNW control after native touch grants shell authority', async () => {
- const evaluate = vi
- .fn()
- .mockResolvedValueOnce('hosted-long-press-123')
- .mockResolvedValueOnce('hosted-long-press-123')
- const wait = vi.fn().mockResolvedValue(undefined)
-
- await dispatchHostedWebViewLongPress(
- { href: 'orca-mobile-web://build/session/workspace' },
- 'Attach a photo',
- { evaluate, now: () => 123, wait }
- )
-
- expect(evaluate.mock.calls[0]?.[1]).toContain(
- "element.dispatchEvent(pointerEvent(element, 'mousedown', 1))"
- )
- expect(evaluate.mock.calls[0]?.[1]).toContain('document.getSelection()?.removeAllRanges()')
- expect(wait).toHaveBeenCalledWith(600)
- expect(evaluate.mock.calls[1]?.[1]).toContain("new MouseEvent('mouseup'")
- })
-
- it('fails closed when the hosted control cannot be located', async () => {
- await expect(
- dispatchHostedWebViewLongPress({}, 'Attach a photo', {
- evaluate: vi.fn().mockResolvedValue(''),
- now: () => 123,
- wait: vi.fn()
- })
- ).rejects.toThrow('Hosted WebView control was not found')
- })
-})
diff --git a/mobile/src/mobile-web/hosted-webview-privacy-isolation.test.ts b/mobile/src/mobile-web/hosted-webview-privacy-isolation.test.ts
deleted file mode 100644
index 74ffca59e1c..00000000000
--- a/mobile/src/mobile-web/hosted-webview-privacy-isolation.test.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import { verifyHostedWebViewPrivacyIsolation } from '../../scripts/hosted-webview-privacy-isolation.mjs'
-
-describe('hosted WebView privacy isolation', () => {
- it('accepts an empty private-origin page state', async () => {
- const socket = new FakeCdpSocket({
- href: 'orca-mobile-web://session-a/h/paired-orca-desktop',
- htmlBytes: 4096,
- domMarkers: [],
- historyMarkers: [],
- localStorageEntries: 0,
- sessionStorageEntries: 0,
- cookieBytes: 0
- })
-
- await expect(
- verifyHostedWebViewPrivacyIsolation({
- document: { webSocketDebuggerUrl: 'ws://localhost/page' },
- WebSocketCtor: fakeCdpConstructor(socket)
- })
- ).resolves.toMatchObject({
- privateOrigin: true,
- credentialedUrl: false,
- domCredentialMarkers: 0
- })
- expect(socket.expression).toContain('document.documentElement?.outerHTML')
- })
-
- it.each([
- { domMarkers: ['devicetoken'] },
- { historyMarkers: ['publickeyb64'] },
- { localStorageEntries: 1 },
- { sessionStorageEntries: 1 },
- { cookieBytes: 1 },
- { href: 'https://user:secret@orca-mobile-web.invalid/' },
- { href: 'https://orca-mobile-web.invalid/?credential=secret' }
- ])('rejects privileged page state: %j', async (overrides) => {
- const socket = new FakeCdpSocket({
- href: 'https://orca-mobile-web.invalid/#session-a',
- htmlBytes: 4096,
- domMarkers: [],
- historyMarkers: [],
- localStorageEntries: 0,
- sessionStorageEntries: 0,
- cookieBytes: 0,
- ...overrides
- })
-
- await expect(
- verifyHostedWebViewPrivacyIsolation({
- document: { webSocketDebuggerUrl: 'ws://localhost/page' },
- WebSocketCtor: fakeCdpConstructor(socket)
- })
- ).rejects.toThrow('privacy isolation failed')
- })
-})
-
-class FakeCdpSocket {
- expression = ''
- private messageListener: ((data: Buffer) => void) | undefined
-
- constructor(private readonly value: unknown) {}
-
- once(event: string, listener: () => void): void {
- if (event === 'open') {
- queueMicrotask(listener)
- }
- }
-
- on(event: string, listener: (data: Buffer) => void): void {
- if (event === 'message') {
- this.messageListener = listener
- }
- }
-
- send(payload: string): void {
- const request = JSON.parse(payload)
- this.expression = request.params.expression
- queueMicrotask(() => {
- this.messageListener?.(
- Buffer.from(
- JSON.stringify({
- id: 1,
- result: { result: { value: JSON.stringify(this.value) } }
- })
- )
- )
- })
- }
-
- close(): void {}
-}
-
-function fakeCdpConstructor(socket: FakeCdpSocket) {
- return class {
- constructor() {
- return socket
- }
- }
-}
diff --git a/mobile/src/mobile-web/hosted-webview-route-exception-evidence.test.ts b/mobile/src/mobile-web/hosted-webview-route-exception-evidence.test.ts
deleted file mode 100644
index f86f3bc0987..00000000000
--- a/mobile/src/mobile-web/hosted-webview-route-exception-evidence.test.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-const { evaluate } = vi.hoisted(() => ({ evaluate: vi.fn() }))
-
-vi.mock('../../scripts/hosted-webview-cdp-session.mjs', () => ({
- evaluateHostedDocumentWithRetry: evaluate
-}))
-
-import {
- installHostedWebViewRouteExceptionCapture,
- readHostedWebViewRouteExceptionEvidence
-} from '../../scripts/hosted-webview-route-exception-evidence.mjs'
-
-describe('hosted WebView route exception evidence', () => {
- beforeEach(() => {
- evaluate.mockReset()
- })
-
- it('installs a bounded error-only capture in the hosted document', async () => {
- evaluate.mockResolvedValue('installed')
-
- const WebSocketCtor = class {}
- await installHostedWebViewRouteExceptionCapture({ targetId: 'hosted' }, WebSocketCtor)
-
- expect(evaluate).toHaveBeenCalledWith(
- { targetId: 'hosted' },
- expect.stringContaining("append('unhandled-rejection'"),
- WebSocketCtor
- )
- expect(evaluate.mock.calls[0][1]).toContain('entries.length > 24')
- expect(evaluate.mock.calls[0][1]).toContain('.slice(0, 4096)')
- })
-
- it('returns only bounded exception evidence entries', async () => {
- evaluate.mockResolvedValue(
- JSON.stringify([
- { kind: 'console-error', text: 'TypeError: failed' },
- { kind: 'other', text: 'ignored' },
- { kind: 'window-error', text: 'x'.repeat(4_097) }
- ])
- )
-
- const WebSocketCtor = class {}
- await expect(
- readHostedWebViewRouteExceptionEvidence({ targetId: 'hosted' }, WebSocketCtor)
- ).resolves.toEqual([{ kind: 'console-error', text: 'TypeError: failed' }])
- })
-})
diff --git a/mobile/src/mobile-web/hosted-webview-route-navigation.test.ts b/mobile/src/mobile-web/hosted-webview-route-navigation.test.ts
deleted file mode 100644
index 2748ac831db..00000000000
--- a/mobile/src/mobile-web/hosted-webview-route-navigation.test.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import { navigateHostedWebViewRoute } from '../../scripts/hosted-webview-route-navigation.mjs'
-
-describe('hosted WebView route navigation', () => {
- it('moves within the authenticated hosted SPA without a document navigation', async () => {
- const socket = cdpSocket('/h/host/review/workspace')
-
- await expect(
- navigateHostedWebViewRoute(
- { webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/1' },
- '/h/host/review/workspace?scope=all',
- socket.WebSocket
- )
- ).resolves.toBeUndefined()
-
- expect(socket.expression()).toContain('history.pushState')
- expect(socket.expression()).toContain("dispatchEvent(new PopStateEvent('popstate'")
- })
-
- it('rejects routes outside the hosted app graph', async () => {
- const socket = cdpSocket('')
-
- await expect(
- navigateHostedWebViewRoute(
- { webSocketDebuggerUrl: 'ws://127.0.0.1/devtools/page/1' },
- 'https://example.test/',
- socket.WebSocket
- )
- ).rejects.toThrow('route is invalid')
- expect(socket.expression()).toBe('')
- })
-})
-
-function cdpSocket(value: string) {
- let expression = ''
- class FakeWebSocket {
- private message: ((data: Buffer) => void) | undefined
-
- once(event: string, listener: (error?: Error) => void) {
- if (event === 'open') {
- queueMicrotask(listener)
- }
- }
-
- on(event: string, listener: (data: Buffer) => void) {
- if (event === 'message') {
- this.message = listener
- }
- }
-
- send(payload: string) {
- expression = JSON.parse(payload).params.expression
- queueMicrotask(() => {
- this.message?.(Buffer.from(JSON.stringify({ id: 1, result: { result: { value } } })))
- })
- }
-
- close() {}
- }
-
- return {
- WebSocket: FakeWebSocket as never,
- expression: vi.fn(() => expression)
- }
-}
diff --git a/mobile/src/mobile-web/hosted-webview-runtime-directory.test.ts b/mobile/src/mobile-web/hosted-webview-runtime-directory.test.ts
deleted file mode 100644
index 22650012998..00000000000
--- a/mobile/src/mobile-web/hosted-webview-runtime-directory.test.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import path from 'node:path'
-import { describe, expect, it } from 'vitest'
-import { resolveHostedWebViewRuntimeDirectory } from '../../scripts/hosted-webview-runtime-directory.mjs'
-
-describe('hosted WebView runtime directory', () => {
- it('isolates each run while keeping the socket path short', () => {
- const first = resolveHostedWebViewRuntimeDirectory({
- worktree: '/repo/mobile-rearch',
- runNonce: 'run-one'
- })
- const second = resolveHostedWebViewRuntimeDirectory({
- worktree: '/repo/mobile-rearch',
- runNonce: 'run-two'
- })
-
- expect(first).not.toBe(second)
- expect(first).toMatch(/^\/tmp\/orca-mw-[a-f0-9]{8}-run-one$/)
- expect(
- path.join(first, 'emulator-control/userData/daemon/daemon-v27.sock').length
- ).toBeLessThan(104)
- })
-
- it('honors an explicit evidence directory', () => {
- expect(
- resolveHostedWebViewRuntimeDirectory({
- worktree: '/repo/mobile-rearch',
- override: './evidence',
- runNonce: 'ignored'
- })
- ).toBe(path.resolve('./evidence'))
- })
-})
diff --git a/mobile/src/mobile-web/hosted-webview-simulator-e2e-options.test.ts b/mobile/src/mobile-web/hosted-webview-simulator-e2e-options.test.ts
deleted file mode 100644
index 427af713b48..00000000000
--- a/mobile/src/mobile-web/hosted-webview-simulator-e2e-options.test.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import { parseHostedWebViewSimulatorE2eOptions } from '../../scripts/hosted-webview-simulator-e2e-options.mjs'
-
-describe('hosted WebView simulator E2E options', () => {
- it('retains bounded defaults', () => {
- expect(parseHostedWebViewSimulatorE2eOptions([])).toEqual({
- accountsOnly: false,
- adversarialContent: false,
- clipboardImageOnly: false,
- device: 'iPhone 17 Pro',
- expectedBuild: undefined,
- filesPreviewOnly: false,
- isolationOnly: false,
- nativeSettingsOnly: false,
- photosRevocationOnly: false,
- reuseNativeInstall: false,
- securityOnly: false,
- skipNativeBuild: false,
- sourceControlOnly: false,
- timeoutMs: 180_000
- })
- })
-
- it('reuses an installed app without selecting the cached reinstall path', () => {
- expect(parseHostedWebViewSimulatorE2eOptions(['--reuse-native-install'])).toMatchObject({
- reuseNativeInstall: true,
- skipNativeBuild: false
- })
- expect(() =>
- parseHostedWebViewSimulatorE2eOptions(['--reuse-native-install', '--skip-native-build'])
- ).toThrow('mutually exclusive')
- })
-
- it('accepts only canonical expected build identities', () => {
- const buildId = 'a'.repeat(64)
- expect(parseHostedWebViewSimulatorE2eOptions(['--expected-build', buildId])).toMatchObject({
- expectedBuild: buildId
- })
- expect(() =>
- parseHostedWebViewSimulatorE2eOptions(['--expected-build', 'A'.repeat(64)])
- ).toThrow('lowercase SHA-256')
- })
-
- it('parses the focused cached-app journey', () => {
- expect(
- parseHostedWebViewSimulatorE2eOptions([
- '--',
- '--device',
- 'simulator-id',
- '--timeout-ms',
- '60000',
- '--native-settings-only',
- '--skip-native-build'
- ])
- ).toMatchObject({
- device: 'simulator-id',
- nativeSettingsOnly: true,
- skipNativeBuild: true,
- timeoutMs: 60_000
- })
- })
-
- it('maps the clipboard-image journey onto focused security setup', () => {
- expect(parseHostedWebViewSimulatorE2eOptions(['--clipboard-image-only'])).toMatchObject({
- clipboardImageOnly: true,
- securityOnly: true
- })
- })
-
- it('maps the Photos revocation journey onto focused security setup', () => {
- expect(parseHostedWebViewSimulatorE2eOptions(['--photos-revocation-only'])).toMatchObject({
- photosRevocationOnly: true,
- securityOnly: true
- })
- })
-
- it('maps WebView isolation onto focused security setup', () => {
- expect(parseHostedWebViewSimulatorE2eOptions(['--isolation-only'])).toMatchObject({
- isolationOnly: true,
- securityOnly: true
- })
- })
-
- it('maps adversarial content onto the focused Source Control journey', () => {
- expect(parseHostedWebViewSimulatorE2eOptions(['--adversarial-content'])).toMatchObject({
- adversarialContent: true,
- sourceControlOnly: true
- })
- })
-
- it('rejects mutually exclusive focused journeys', () => {
- expect(() =>
- parseHostedWebViewSimulatorE2eOptions(['--accounts-only', '--source-control-only'])
- ).toThrow('mutually exclusive')
- expect(() =>
- parseHostedWebViewSimulatorE2eOptions(['--clipboard-image-only', '--security-only'])
- ).toThrow('mutually exclusive')
- expect(() =>
- parseHostedWebViewSimulatorE2eOptions(['--clipboard-image-only', '--photos-revocation-only'])
- ).toThrow('mutually exclusive')
- expect(() =>
- parseHostedWebViewSimulatorE2eOptions(['--isolation-only', '--security-only'])
- ).toThrow('mutually exclusive')
- })
-
- it('keeps the real chat/session journey exclusive from OTA and adversarial runs', () => {
- expect(
- parseHostedWebViewSimulatorE2eOptions(['--chat-only', '--reuse-native-install'])
- ).toMatchObject({ chatOnly: true, reuseNativeInstall: true })
- expect(() => parseHostedWebViewSimulatorE2eOptions(['--chat-only', '--ota-only'])).toThrow(
- 'mutually exclusive'
- )
- expect(() =>
- parseHostedWebViewSimulatorE2eOptions(['--chat-only', '--adversarial-content'])
- ).toThrow('mutually exclusive')
- })
-
- it('rejects invalid timeouts and unknown arguments', () => {
- expect(() => parseHostedWebViewSimulatorE2eOptions(['--timeout-ms', '9999'])).toThrow(
- 'at least 10000'
- )
- expect(() => parseHostedWebViewSimulatorE2eOptions(['--unknown'])).toThrow('Unknown argument')
- })
-})
diff --git a/mobile/src/mobile-web/hosted-webview-workspace-activation.test.ts b/mobile/src/mobile-web/hosted-webview-workspace-activation.test.ts
deleted file mode 100644
index f6a07d7a371..00000000000
--- a/mobile/src/mobile-web/hosted-webview-workspace-activation.test.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-import { describe, expect, it, vi } from 'vitest'
-import { activateHostedWorkspaceRow } from '../../scripts/hosted-webview-workspace-activation.mjs'
-
-describe('hosted WebView workspace activation', () => {
- it('prefers the existing accessible workspace-row label', async () => {
- const activate = vi.fn().mockResolvedValue(undefined)
- const document = { href: 'orca-mobile-web://build/' }
-
- await activateHostedWorkspaceRow(document, 'mobile-rearch', activate)
-
- expect(activate).toHaveBeenCalledOnce()
- expect(activate).toHaveBeenCalledWith(document, {
- kind: 'label',
- value: 'Open mobile-rearch',
- reveal: true
- })
- })
-
- it('reveals a grouped text row when accessible row labels are absent', async () => {
- const missing = new Error('Hosted WebView control was not found: mobile-rearch')
- const activate = vi
- .fn()
- .mockRejectedValueOnce(missing)
- .mockRejectedValueOnce(missing)
- .mockResolvedValueOnce(undefined)
- .mockResolvedValueOnce(undefined)
-
- await activateHostedWorkspaceRow({}, 'mobile-rearch', activate)
-
- expect(activate).toHaveBeenNthCalledWith(
- 2,
- {},
- {
- kind: 'text',
- value: 'mobile-rearch',
- ignoreCase: true,
- occurrence: 1,
- reveal: true
- }
- )
- expect(activate).toHaveBeenNthCalledWith(
- 3,
- {},
- {
- kind: 'text',
- value: 'mobile-rearch',
- ignoreCase: true,
- reveal: true
- }
- )
- expect(activate).toHaveBeenNthCalledWith(
- 4,
- {},
- {
- kind: 'text',
- value: 'mobile-rearch',
- ignoreCase: true,
- occurrence: 1,
- reveal: true
- }
- )
- })
-
- it('reacquires a replaced WebKit document before retrying', async () => {
- const stale = new Error('WebKit CDP connection closed')
- const activate = vi.fn().mockRejectedValueOnce(stale).mockResolvedValueOnce(undefined)
- const replacement = { href: 'orca-mobile-web://replacement/' }
- const resolveDocument = vi.fn().mockResolvedValue(replacement)
-
- await activateHostedWorkspaceRow(
- { href: 'orca-mobile-web://stale/' },
- 'mobile-rearch',
- activate,
- 1_000,
- resolveDocument
- )
-
- expect(resolveDocument).toHaveBeenCalledOnce()
- expect(activate).toHaveBeenLastCalledWith(replacement, {
- kind: 'label',
- value: 'Open mobile-rearch',
- reveal: true
- })
- })
-
- it('reacquires an Android target replaced during its inspector handshake', async () => {
- const stale = new Error('Unexpected server response: 500')
- const activate = vi.fn().mockRejectedValueOnce(stale).mockResolvedValueOnce(undefined)
- const replacement = { href: 'https://orca-mobile-web.invalid/' }
- const resolveDocument = vi.fn().mockResolvedValue(replacement)
-
- await activateHostedWorkspaceRow(
- { href: 'https://orca-mobile-web.invalid/' },
- 'mobile-rearch',
- activate,
- 1_000,
- resolveDocument
- )
-
- expect(resolveDocument).toHaveBeenCalledOnce()
- expect(activate).toHaveBeenLastCalledWith(replacement, {
- kind: 'label',
- value: 'Open mobile-rearch',
- reveal: true
- })
- })
-
- it('retries after a grouped row is not visible immediately', async () => {
- const missing = new Error('Hosted WebView control was not found: mobile-rearch')
- const activate = vi
- .fn()
- .mockRejectedValueOnce(missing)
- .mockRejectedValueOnce(missing)
- .mockResolvedValueOnce(undefined)
- .mockRejectedValueOnce(missing)
- .mockRejectedValueOnce(missing)
- .mockResolvedValueOnce(undefined)
-
- await activateHostedWorkspaceRow({}, 'mobile-rearch', activate, 1_000)
-
- expect(activate).toHaveBeenCalledTimes(6)
- expect(activate).toHaveBeenLastCalledWith({}, expect.objectContaining({ reveal: true }))
- })
-
- it('accepts activation of the only matching text row', async () => {
- const missing = new Error('Hosted WebView control was not found: mobile-rearch')
- const stale = new Error('WebKit CDP connection closed')
- const activate = vi
- .fn()
- .mockRejectedValueOnce(missing)
- .mockRejectedValueOnce(missing)
- .mockResolvedValueOnce(undefined)
- .mockRejectedValueOnce(stale)
-
- await activateHostedWorkspaceRow({}, 'mobile-rearch', activate)
-
- expect(activate).toHaveBeenCalledTimes(4)
- })
-
- it('accepts an SPA route transition after the activated row disappears', async () => {
- const missing = new Error('Hosted WebView control was not found: mobile-rearch')
- const activate = vi
- .fn()
- .mockRejectedValueOnce(missing)
- .mockRejectedValueOnce(missing)
- .mockResolvedValueOnce(undefined)
- .mockRejectedValueOnce(missing)
- const resolveDocument = vi.fn().mockResolvedValue({
- href: 'https://orca-mobile-web.invalid/h/host/session/workspace'
- })
-
- await activateHostedWorkspaceRow(
- { href: 'https://orca-mobile-web.invalid/h/host' },
- 'mobile-rearch',
- activate,
- 1_000,
- resolveDocument
- )
-
- expect(resolveDocument).toHaveBeenCalledOnce()
- })
-})
diff --git a/mobile/src/mobile-web/mobile-web-shell-response-schema-corpus.test.ts b/mobile/src/mobile-web/mobile-web-shell-response-schema-corpus.test.ts
deleted file mode 100644
index 2d9afceacf8..00000000000
--- a/mobile/src/mobile-web/mobile-web-shell-response-schema-corpus.test.ts
+++ /dev/null
@@ -1,248 +0,0 @@
-///
-
-import { describe, expect, it, vi } from 'vitest'
-import { z, ZodType } from 'zod'
-import {
- MOBILE_WEB_BRIDGE_OPERATIONS,
- MOBILE_WEB_BRIDGE_PROTOCOL_VERSION,
- parseMobileWebBridgeShellMessage,
- type MobileWebBridgePageMessage,
- type MobileWebBridgeShellMessage
-} from '../../../src/shared/mobile-web/bridge-contract'
-import { MobileWebBridgeSubscriptionClient } from '../../../src/mobile-web/src/mobile-web-bridge-subscription-client'
-import type { MobileWebBridgeSubscriptionSetup } from '../../../src/mobile-web/src/mobile-web-bridge-subscription-setup'
-import { MobileWebOneShotRequestClient } from '../../../src/mobile-web/src/mobile-web-one-shot-request-client'
-import { MobileWebHostResultSchema } from '../../../src/shared/mobile-web/host-rpc-contract'
-import { MOBILE_WEB_PRODUCTION_GRANTS } from './mobile-web-production-grants'
-
-const CONTEXT = {
- shellSessionId: 'S'.repeat(43),
- buildId: 'a'.repeat(64)
-}
-const REQUEST_ID = 'R'.repeat(22)
-const SUBSCRIPTION_ID = 'U'.repeat(22)
-const RESPONSE_PAYLOAD_CORPUS: unknown[] = [
- null,
- true,
- false,
- 0,
- 1.5,
- '',
- 'payload',
- [],
- {},
- { unexpected: true }
-]
-const contractModules = import.meta.glob(
- [
- '../../../src/shared/mobile-web/*-contract.ts',
- '!../../../src/shared/mobile-web/bridge-operation-contract.ts'
- ],
- { eager: true }
-)
-
-type NamedSchema = { name: string; schema: ZodType }
-
-describe('mobile web shell response schema corpus', () => {
- const resultSchemas = namedSchemas('ResultSchema')
- const eventSchemas = namedSchemas('EventSchema')
-
- it('keeps the result and event schema corpus aligned with production grant kinds', () => {
- const registeredOperations = Object.values(MOBILE_WEB_BRIDGE_OPERATIONS).flatMap((operations) =>
- Object.keys(operations)
- )
- const oneShotGrants = MOBILE_WEB_PRODUCTION_GRANTS.filter(
- (grant) => grant.operation !== 'subscribe'
- )
- const subscriptionGrants = MOBILE_WEB_PRODUCTION_GRANTS.filter(
- (grant) => grant.operation === 'subscribe'
- )
-
- expect(resultSchemas.length).toBeGreaterThanOrEqual(150)
- expect(oneShotGrants.length).toBeGreaterThan(100)
- expect(eventSchemas.length).toBeGreaterThanOrEqual(subscriptionGrants.length)
- expect(MOBILE_WEB_PRODUCTION_GRANTS).toHaveLength(registeredOperations.length)
- expect(new Set(resultSchemas.map(({ name }) => name))).toHaveLength(resultSchemas.length)
- expect(new Set(eventSchemas.map(({ name }) => name))).toHaveLength(eventSchemas.length)
- })
-
- it('rejects invalid success payloads through every one-shot result schema', async () => {
- let caseCount = 0
- for (const { name, schema } of resultSchemas) {
- // Domain payloads on the generic lane are interpreted by the matching hosted page.
- if (schema === MobileWebHostResultSchema) {
- continue
- }
- const rejected = RESPONSE_PAYLOAD_CORPUS.filter((payload) => pageRejects(schema, payload))
- expect(rejected.length, name).toBeGreaterThanOrEqual(8)
- for (const payload of rejected) {
- await expectOneShotRejection(schema, payload, name)
- caseCount += 1
- }
- }
- expect(caseCount).toBeGreaterThan(1_200)
- })
-
- it('keeps generic host domain results opaque to the shell contract', () => {
- expect(resultSchemas.filter(({ schema }) => schema === MobileWebHostResultSchema)).toHaveLength(
- 1
- )
- for (const payload of RESPONSE_PAYLOAD_CORPUS) {
- expect(MobileWebHostResultSchema.parse(payload)).toEqual(payload)
- }
- })
-
- it('retires every subscription after an invalid event payload', async () => {
- let caseCount = 0
- for (const { name, schema } of eventSchemas) {
- const rejected = RESPONSE_PAYLOAD_CORPUS.filter((payload) => pageRejects(schema, payload))
- expect(rejected.length, name).toBeGreaterThanOrEqual(8)
- for (const payload of rejected) {
- await expectSubscriptionRejection(schema, payload, name)
- caseCount += 1
- }
- }
- expect(caseCount).toBeGreaterThanOrEqual(64)
- })
-})
-
-function pageRejects(schema: ZodType, payload: unknown): boolean {
- return !schema.safeParse(payload).success
-}
-
-function namedSchemas(suffix: 'ResultSchema' | 'EventSchema'): NamedSchema[] {
- const schemas: NamedSchema[] = []
- for (const [path, module] of Object.entries(contractModules)) {
- for (const [name, schema] of Object.entries(module as Record)) {
- if (name.endsWith(suffix) && schema instanceof ZodType) {
- schemas.push({ name: `${path}:${name}`, schema })
- }
- }
- }
- return schemas.sort((left, right) => left.name.localeCompare(right.name))
-}
-
-async function expectOneShotRejection(
- resultSchema: ZodType,
- payload: unknown,
- label: string
-): Promise {
- const client = new MobileWebOneShotRequestClient({
- getGrant: () => grant('snapshot'),
- postMessage: () => true,
- envelope: () => ({ version: MOBILE_WEB_BRIDGE_PROTOCOL_VERSION, ...CONTEXT }),
- createRequestId: () => REQUEST_ID,
- otherPendingCount: () => 0
- })
- const result = client.request('workspace', 'snapshot', {}, z.object({}).strict(), resultSchema)
- const rejection = expect(result, `${label}: ${JSON.stringify(payload)}`).rejects.toMatchObject({
- code: 'invalid_message',
- retryable: false
- })
-
- expect(client.receive(parsedShellMessage(successResponse(payload)))).toBe(true)
- await rejection
- expect(client.pendingCount()).toBe(0)
-}
-
-async function expectSubscriptionRejection(
- eventSchema: ZodType,
- payload: unknown,
- label: string
-): Promise {
- const messages: MobileWebBridgePageMessage[] = []
- const onEvent = vi.fn()
- const onError = vi.fn()
- const ids = [REQUEST_ID, SUBSCRIPTION_ID]
- const client = new MobileWebBridgeSubscriptionClient({
- getGrant: () => grant('subscribe'),
- postMessage: (message) => {
- messages.push(message)
- return true
- },
- envelope: () => ({ version: MOBILE_WEB_BRIDGE_PROTOCOL_VERSION, ...CONTEXT }),
- createMessageId: () => ids.shift() ?? 'Z'.repeat(22),
- otherPendingCount: () => 0
- })
- const subscription = exposeSubscribeWith(client)({
- capability: 'workspace',
- payload: {},
- payloadSchema: z.object({}).strict(),
- eventSchema,
- onEvent,
- onError
- })
- client.receive(parsedShellMessage(successResponse(null)))
- await subscription.ready
-
- expect(client.receive(parsedShellMessage(eventMessage(payload)))).toBe(true)
- expect(onEvent, label).not.toHaveBeenCalled()
- expect(onError, label).toHaveBeenCalledWith(
- expect.objectContaining({ code: 'invalid_message', retryable: false })
- )
- expect(messages.at(-1)).toMatchObject({
- type: 'cancel',
- target: 'subscription',
- id: SUBSCRIPTION_ID
- })
-}
-
-function exposeSubscribeWith(client: MobileWebBridgeSubscriptionClient): (
- setup: MobileWebBridgeSubscriptionSetup
-) => {
- ready: Promise
- unsubscribe: () => void
- subscriptionId: string
-} {
- return (
- client as unknown as {
- subscribeWith: ReturnType
- }
- ).subscribeWith.bind(client)
-}
-
-function grant(operation: 'snapshot' | 'subscribe') {
- return {
- capability: 'workspace',
- operation,
- limits: {
- maxRequestBytes: 1024,
- maxResponseBytes: 1024,
- maxConcurrent: 2,
- rateCapacity: 4,
- rateRefillPerSecond: 1
- }
- } as const
-}
-
-function successResponse(
- payload: unknown
-): Extract {
- return {
- version: MOBILE_WEB_BRIDGE_PROTOCOL_VERSION,
- ...CONTEXT,
- type: 'response',
- requestId: REQUEST_ID,
- status: 'success',
- payload
- }
-}
-
-function eventMessage(payload: unknown): Extract {
- return {
- version: MOBILE_WEB_BRIDGE_PROTOCOL_VERSION,
- ...CONTEXT,
- type: 'event',
- subscriptionId: SUBSCRIPTION_ID,
- sequence: 0,
- payload
- }
-}
-
-function parsedShellMessage(message: MobileWebBridgeShellMessage): MobileWebBridgeShellMessage {
- const parsed = parseMobileWebBridgeShellMessage(JSON.stringify(message), CONTEXT)
- if (!parsed.ok) {
- throw new Error(`Corpus produced ${parsed.error}`)
- }
- return parsed.value
-}
diff --git a/mobile/src/session/mobile-web-session-screen-binding-source.test.ts b/mobile/src/session/mobile-web-session-screen-binding-source.test.ts
index d0d1422ce21..a7ebaf5c752 100644
--- a/mobile/src/session/mobile-web-session-screen-binding-source.test.ts
+++ b/mobile/src/session/mobile-web-session-screen-binding-source.test.ts
@@ -43,8 +43,6 @@ describe('mobile web session screen binding', () => {
expect(hostedSessionRoute).toContain('lastConnectedAt={shell.lastConnectedAt}')
expect(hostedSessionRoute).toContain(' = {
- 'account.consumeResetCredit': ['scope'],
- 'file.markdownDraftRead': ['relativePath', 'tabId', 'workspaceId'],
- 'file.markdownRead': ['relativePath', 'tabId', 'workspaceId'],
- 'file.markdownSave': ['relativePath', 'tabId', 'workspaceId'],
- 'file.readTerminalArtifactChunk': ['offset', 'tabId', 'token', 'workspaceId'],
- 'file.resolveTerminalPath': ['workspaceId'],
- 'file.write': ['byteLength', 'relativePath', 'revision', 'workspaceId'],
- 'provider.manageReview': ['action', 'provider', 'reviewNumber', 'workspaceId'],
- 'provider.mutateReview': ['provider', 'reviewNumber', 'workspaceId'],
- 'provider.review': ['branch', 'observedHead', 'workspaceId'],
- 'provider.reviewCreate': ['provider', 'workspaceId'],
- 'provider.reviewCreationEligibility': ['branch', 'observedHead', 'workspaceId'],
- 'provider.reviewDiff': [
- 'branch',
- 'observedHead',
- 'path',
- 'provider',
- 'reviewHead',
- 'reviewNumber',
- 'workspaceId'
- ],
- 'provider.reviewGenerateFields': ['workspaceId'],
- 'provider.reviewQuery': ['provider', 'query', 'reviewNumber', 'workspaceId'],
- 'provider.submitReview': [
- 'action',
- 'expectedReviewHead',
- 'provider',
- 'reviewNumber',
- 'submissionId',
- 'submittedCommentIds',
- 'workspaceId'
- ],
- 'sourceControl.abort': ['operation', 'previousBranch', 'previousHead', 'workspaceId'],
- 'sourceControl.branch': ['branch', 'operation', 'previousBranch', 'previousHead', 'workspaceId'],
- 'sourceControl.branchCompare': ['baseRef', 'offset', 'revision', 'workspaceId'],
- 'sourceControl.branches': ['workspaceId'],
- 'sourceControl.cancelCommitMessageGeneration': ['workspaceId'],
- 'sourceControl.commit': ['previousHead', 'workspaceId'],
- 'sourceControl.commitCompare': ['commitId', 'workspaceId'],
- 'sourceControl.discard': ['operation', 'relativePaths.length', 'workspaceId'],
- 'sourceControl.fetch': ['operation', 'previousBranch', 'previousHead', 'workspaceId'],
- 'sourceControl.generateCommitMessage': ['previousHead', 'workspaceId'],
- 'sourceControl.history': ['limit', 'workspaceId'],
- 'sourceControl.pull': ['operation', 'previousBranch', 'previousHead', 'workspaceId'],
- 'sourceControl.push': ['operation', 'previousBranch', 'previousHead', 'workspaceId'],
- 'sourceControl.rebase': ['operation', 'previousBranch', 'previousHead', 'workspaceId'],
- 'sourceControl.reviewDiff': ['relativePath', 'scope', 'workspaceId'],
- 'sourceControl.reviewLink': ['workspaceId'],
- 'sourceControl.reviewLinkUpdate': ['workspaceId'],
- 'sourceControl.reviewMetadata': ['workspaceId'],
- 'sourceControl.reviewMetadataUpdate': ['workspaceId'],
- 'sourceControl.stage': ['operation', 'relativePaths.length', 'workspaceId'],
- 'sourceControl.unstage': ['operation', 'relativePaths.length', 'workspaceId'],
- 'sourceControl.upstream': ['workspaceId'],
- 'task.loadLinearDetail': ['issue.targetId'],
- 'task.loadLinearIssue': ['issue.targetId'],
- 'task.projectTable': ['project', 'selectedView.id'],
- 'task.resolveProjectRef': ['host'],
- 'workspace.activate': ['workspaceId'],
- 'workspace.creationSaveSparsePreset': ['directories', 'id', 'name', 'repoId'],
- 'workspace.creationSparsePresets': ['repoId'],
- 'workspace.creationSshConnect': ['targetId'],
- 'workspace.creationSshState': ['targetId'],
- 'workspace.remove': ['workspaceId'],
- 'workspace.update': ['workspaceId']
-}
-
-/** Echo helpers shared across request clients, and the result fields each one compares. Resolved
- * by name because the derivation below only walks helpers defined in the same file. */
-const SHARED_ECHO_HELPERS: Record = {
- requireEchoedWorkspaceId: ['workspaceId']
-}
-
-function balancedBody(text: string, from: number): string | null {
- let depth = 0
- let index = from
- let opened = false
- for (; index < text.length; index++) {
- const character = text[index]
- if (character === '(') {
- depth++
- opened = true
- } else if (character === ')') {
- depth--
- if (opened && depth === 0) {
- index++
- break
- }
- }
- }
- const start = text.indexOf('{', index)
- if (start === -1) {
- return null
- }
- let braces = 0
- for (let cursor = start; cursor < text.length; cursor++) {
- if (text[cursor] === '{') {
- braces++
- } else if (text[cursor] === '}') {
- braces--
- if (braces === 0) {
- return text.slice(start, cursor + 1)
- }
- }
- }
- return null
-}
-
-type Method = { name: string; body: string }
-
-function readClient(name: string): { text: string; methods: Method[]; helpers: Method[] } {
- const text = readFileSync(join(PAGE_DIR, name), 'utf8')
- const lines = text.split('\n')
- const offsets: number[] = []
- let offset = 0
- for (const line of lines) {
- offsets.push(offset)
- offset += line.length + 1
- }
- const methods: Method[] = []
- for (let index = 0; index < lines.length; index++) {
- const match =
- /^ {2}(?:(?:private|protected|public|async)\s+)*([A-Za-z][A-Za-z0-9]*)\s*[(<]/.exec(
- lines[index]!
- )
- if (!match || /^\s*(?:if|for|while|switch|catch|return)\b/.test(lines[index]!)) {
- continue
- }
- const body = balancedBody(text, offsets[index]!)
- if (!body) {
- continue
- }
- methods.push({ name: match[1]!, body })
- index += body.split('\n').length - 1
- }
- const helpers: Method[] = []
- for (const match of text.matchAll(/\nfunction ([A-Za-z0-9_]+)\s*[<(]/g)) {
- const body = balancedBody(text, match.index + match[0].length - 1)
- if (body) {
- helpers.push({ name: match[1]!, body })
- }
- }
- return { text, methods, helpers }
-}
-
-/** Echo comparisons today, plus any `echo: [...]` declaration a factory-built method carries, so a
- * declarative rewrite that drops a field still shrinks this set. */
-function echoFields(body: string, helpers: Method[], seen: Set): Set {
- const fields = new Set()
- for (const match of body.matchAll(
- /\b(?:result|preset|value)\.((?:[A-Za-z0-9_]+\.)*[A-Za-z0-9_]+)\s*!==/g
- )) {
- fields.add(match[1]!)
- }
- for (const match of body.matchAll(
- /!same[A-Za-z0-9_]*\(\s*(?:result|preset|value)\.([A-Za-z0-9_]+)/g
- )) {
- fields.add(match[1]!)
- }
- for (const match of body.matchAll(/\becho:\s*\[([^\]]*)\]/g)) {
- for (const field of match[1]!.matchAll(/'([A-Za-z0-9_.]+)'/g)) {
- fields.add(field[1]!)
- }
- }
- for (const [helper, declared] of Object.entries(SHARED_ECHO_HELPERS)) {
- if (new RegExp(`\\b${helper}\\(`).test(body)) {
- for (const field of declared) {
- fields.add(field)
- }
- }
- }
- for (const match of body.matchAll(/\b((?:matching|same)[A-Za-z0-9_]*)\s*\(/g)) {
- const helper = helpers.find((candidate) => candidate.name === match[1])
- if (!helper || seen.has(helper.name)) {
- continue
- }
- seen.add(helper.name)
- for (const field of echoFields(helper.body, helpers, seen)) {
- fields.add(field)
- }
- }
- return fields
-}
-
-/** Operations a method names directly in a `request` call. */
-function directOperations(body: string): string[] {
- return [...body.matchAll(/\.request\(\s*'([A-Za-z]+)',\s*'([A-Za-z0-9]+)'/g)].map(
- (match) => `${match[1]}.${match[2]}`
- )
-}
-
-/** Operations a method hands to a private helper that forwards a variable operation on. */
-function forwardedOperations(body: string, forwarders: Map): string[] {
- const operations: string[] = []
- for (const [helper, capability] of forwarders) {
- for (const match of body.matchAll(
- new RegExp(`\\bthis\\.${helper}(?:<[^(]*>)?\\(\\s*'([A-Za-z0-9]+)'`, 'g')
- )) {
- operations.push(`${capability}.${match[1]}`)
- }
- }
- return operations
-}
-
-function derivedEchoCensus(): Record {
- const census: Record> = {}
- const files = readdirSync(PAGE_DIR).filter(
- (name) => name.endsWith('-request-client.ts') && !name.includes('.test.')
- )
- for (const name of files) {
- const { methods, helpers } = readClient(name)
- const forwarders = new Map()
- for (const method of methods) {
- const match = /\.request\(\s*'([A-Za-z]+)',\s*(?!')[A-Za-z]/.exec(method.body)
- if (match) {
- forwarders.set(method.name, match[1]!)
- }
- }
- const forwarderFields = new Map>()
- for (const method of methods) {
- const fields = echoFields(method.body, helpers, new Set())
- if (forwarders.has(method.name)) {
- forwarderFields.set(method.name, fields)
- continue
- }
- if (fields.size === 0) {
- continue
- }
- for (const operation of [
- ...directOperations(method.body),
- ...forwardedOperations(method.body, forwarders)
- ]) {
- census[operation] = new Set([...(census[operation] ?? []), ...fields])
- }
- }
- for (const [helper, fields] of forwarderFields) {
- if (fields.size === 0) {
- continue
- }
- for (const method of methods) {
- if (method.name === helper) {
- continue
- }
- for (const operation of forwardedOperations(
- method.body,
- new Map([[helper, forwarders.get(helper)!]])
- )) {
- census[operation] = new Set([...(census[operation] ?? []), ...fields])
- }
- }
- }
- }
- return Object.fromEntries(
- Object.entries(census).map(([operation, fields]) => [operation, [...fields].sort()])
- )
-}
-
-describe('mobile web bridge operation echo census', () => {
- const derived = derivedEchoCensus()
-
- it('checks the recorded echo fields on every operation that guards its own identity', () => {
- expect(derived).toEqual(
- Object.fromEntries(
- Object.entries(EXPECTED_ECHO_FIELDS).map(([operation, fields]) => [operation, [...fields]])
- )
- )
- })
-
- it('keeps every shared echo helper checking the fields it is credited with', () => {
- const text = readFileSync(join(PAGE_DIR, 'mobile-web-result-echo.ts'), 'utf8')
-
- for (const [helper, declared] of Object.entries(SHARED_ECHO_HELPERS)) {
- expect(text).toContain(`export function ${helper}`)
- for (const field of declared) {
- expect(text).toContain(`result.${field} !==`)
- }
- }
- })
-
- it('records echo fields only for registered operations', () => {
- const registered = new Set(
- Object.entries(MOBILE_WEB_BRIDGE_OPERATIONS).flatMap(([capability, operations]) =>
- Object.keys(operations).map((operation) => `${capability}.${operation}`)
- )
- )
-
- expect(Object.keys(EXPECTED_ECHO_FIELDS).filter((key) => !registered.has(key))).toEqual([])
- expect(Object.keys(EXPECTED_ECHO_FIELDS).length).toBeGreaterThanOrEqual(49)
- })
-
- it('guards the page workspace handle on every workspace-scoped echo it records', () => {
- const workspaceScoped = Object.entries(EXPECTED_ECHO_FIELDS).filter(([, fields]) =>
- fields.some((field) => field === 'workspaceId')
- )
-
- expect(workspaceScoped.length).toBeGreaterThanOrEqual(40)
- })
-})
diff --git a/src/shared/mobile-web/bridge-operation-registry-census.test.ts b/src/shared/mobile-web/bridge-operation-registry-census.test.ts
deleted file mode 100644
index 8fc802d9ec8..00000000000
--- a/src/shared/mobile-web/bridge-operation-registry-census.test.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { readFileSync, readdirSync } from 'node:fs'
-import { join, resolve } from 'node:path'
-import { describe, expect, it } from 'vitest'
-import { MOBILE_WEB_BRIDGE_OPERATIONS } from './bridge-operation-registry'
-
-const REPO_ROOT = resolve(__dirname, '..', '..', '..')
-const PAGE_DIR = join(REPO_ROOT, 'src', 'mobile-web', 'src')
-const SHELL_DIR = join(REPO_ROOT, 'mobile', 'src', 'mobile-web')
-
-const registered = new Set(
- Object.entries(MOBILE_WEB_BRIDGE_OPERATIONS).flatMap(([capability, operations]) =>
- Object.keys(operations).map((operation) => `${capability}.${operation}`)
- )
-)
-
-function sources(dir: string, keep: (name: string) => boolean): { name: string; text: string }[] {
- return readdirSync(dir)
- .filter((name) => name.endsWith('.ts') && !name.includes('.test.') && keep(name))
- .map((name) => ({ name, text: readFileSync(join(dir, name), 'utf8') }))
-}
-
-describe('mobile web bridge operation registry census', () => {
- it('registers every capability and operation pair a page request client names', () => {
- const files = sources(PAGE_DIR, () => true)
- const named = new Set()
- for (const { text } of files) {
- for (const match of text.matchAll(/\.request\(\s*'([A-Za-z]+)',\s*'([A-Za-z]+)'/g)) {
- named.add(`${match[1]}.${match[2]}`)
- }
- }
-
- expect(files.length).toBeGreaterThanOrEqual(40)
- expect(named.size).toBeGreaterThanOrEqual(126)
- expect([...named].filter((pair) => !registered.has(pair))).toEqual([])
- })
-
- // A bare `string` here is how `workspace.creationRetiredNames` shipped unregistered: the helper
- // erased the operation name before the compiler could check it against the registry.
- it('types every page request client operation parameter against the registry', () => {
- const files = sources(PAGE_DIR, (name) => name.endsWith('-request-client.ts'))
- const untyped = files
- .filter(({ text }) => /\boperation\??:\s*string\b/.test(text))
- .map(({ name }) => name)
-
- expect(files.map(({ name }) => name)).toContain('mobile-web-one-shot-request-client.ts')
- expect(files.length).toBeGreaterThanOrEqual(20)
- expect(untyped).toEqual([])
- })
-
- // A page client that reaches one operation through two different schema pairs has an arm nobody
- // reviews; a factory rewrite that swaps a schema shows up here as a second pair.
- it('binds every directly named operation to exactly one payload and result schema', () => {
- const pairs = new Map>()
- for (const { text } of sources(PAGE_DIR, () => true)) {
- for (const match of text.matchAll(
- /\.request(?:<[^(]*>)?\(\s*'([A-Za-z]+)',\s*'([A-Za-z0-9]+)',([\s\S]{0,400}?)\n\s*\)/g
- )) {
- const schemas = [...match[3]!.matchAll(/\b([A-Za-z0-9_]*Schema)\b/g)].map((name) => name[1])
- const key = `${match[1]}.${match[2]}`
- pairs.set(key, (pairs.get(key) ?? new Set()).add(schemas.join('|')))
- }
- }
-
- expect(pairs.size).toBeGreaterThanOrEqual(126)
- expect([...pairs].filter(([, schemas]) => schemas.size !== 1).map(([key]) => key)).toEqual([])
- expect([...pairs.keys()].filter((key) => !registered.has(key))).toEqual([])
- expect(
- [...pairs]
- .filter(([, schemas]) => [...schemas].some((pair) => pair.split('|').length !== 2))
- .map(([key]) => key)
- ).toEqual([])
- })
-
- // Capability-level routing is proven against the real table in the shell's dispatch census. This
- // covers the step inside an arm: the operation name has to be matched somewhere, not merely
- // quoted, which a stray comment or an unrelated string used to satisfy.
- it('matches every registered operation at a shell dispatch position', () => {
- const shell = sources(SHELL_DIR, (name) => !name.startsWith('mobile-web-production-'))
- const undispatched = [...registered].filter((pair) => {
- const operation = pair.slice(pair.indexOf('.') + 1)
- const patterns = [
- new RegExp(`operation === '${operation}'`),
- new RegExp(`case '${operation}':`),
- new RegExp(`'${operation}'(?=[,\\]])`),
- new RegExp(`^\\s*'${operation}',?$`, 'm'),
- new RegExp(`\\bstartsWith\\('${operation}'\\)`),
- new RegExp(`\\b${operation}:\\s`)
- ]
- return !shell.some(({ text }) => patterns.some((pattern) => pattern.test(text)))
- })
-
- expect(shell.length).toBeGreaterThanOrEqual(100)
- expect(undispatched).toEqual([])
- })
-})