From 80bf1a53b63a1744e838ba3c899ead5b247925b7 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Mon, 7 Sep 2026 14:45:03 -0400 Subject: [PATCH 1/3] test(mobile): drop the source-text census and schema-corpus suites Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- .../hosted-route-native-api-census.test.ts | 55 ---- ...e-web-shell-response-schema-corpus.test.ts | 251 -------------- ...-web-session-screen-binding-source.test.ts | 2 - .../bridge-operation-echo-census.test.ts | 309 ------------------ .../bridge-operation-registry-census.test.ts | 95 ------ 5 files changed, 712 deletions(-) delete mode 100644 mobile/src/mobile-web/hosted-route-native-api-census.test.ts delete mode 100644 mobile/src/mobile-web/mobile-web-shell-response-schema-corpus.test.ts delete mode 100644 src/shared/mobile-web/bridge-operation-echo-census.test.ts delete mode 100644 src/shared/mobile-web/bridge-operation-registry-census.test.ts 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/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 a58ce9558f6..00000000000 --- a/mobile/src/mobile-web/mobile-web-shell-response-schema-corpus.test.ts +++ /dev/null @@ -1,251 +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 { tolerantMobileWebShellPayload } from '../../../src/shared/mobile-web/shell-payload-tolerance' -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) - }) -}) - -/** What the page actually applies to a shell payload, so "cannot parse" here is the page's verdict - * rather than the authoring schema's. */ -function pageRejects(schema: ZodType, payload: unknown): boolean { - return !tolerantMobileWebShellPayload(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([]) - }) -}) From 094706a5bd071c2ad562860854155720739f2666 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Mon, 7 Sep 2026 14:45:21 -0400 Subject: [PATCH 2/3] test(mobile): drop the hosted e2e-script unit tests These 52 files unit-tested mobile/scripts/*.mjs runners, CDP fakes, emulator drivers, and repository fixtures rather than any page or shell product code. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- ...osted-adversarial-provider-fixture.test.ts | 112 --- .../hosted-adversarial-terminal-links.test.ts | 299 ------- ...android-adversarial-terminal-links.test.ts | 97 --- ...sted-android-agent-history-journey.test.ts | 103 --- ...ted-android-emulator-accessibility.test.ts | 75 -- .../hosted-android-emulator-session.test.ts | 83 -- ...sted-android-hardware-back-journey.test.ts | 157 ---- .../hosted-android-mobile-web-cache.test.ts | 71 -- .../hosted-android-privacy-audit.test.ts | 115 --- ...osted-android-workspace-activation.test.ts | 90 -- .../hosted-ios-accounts-parity.test.ts | 101 --- ...d-ios-adversarial-terminal-staging.test.ts | 34 - .../hosted-ios-agent-history-parity.test.ts | 66 -- .../hosted-ios-build-activation.test.ts | 115 --- .../hosted-ios-core-route-parity.test.ts | 213 ----- .../hosted-ios-document-upload.test.ts | 277 ------ .../hosted-ios-emulator-accessibility.test.ts | 426 ---------- .../hosted-ios-emulator-controller.test.ts | 50 -- .../hosted-ios-emulator-long-press.test.ts | 92 -- .../hosted-ios-files-preview-parity.test.ts | 187 ---- ...host-origin-source-control-journey.test.ts | 159 ---- .../hosted-ios-hybrid-route-handoff.test.ts | 12 - .../hosted-ios-mobile-web-cache.test.ts | 87 -- .../hosted-ios-native-alert-journey.test.ts | 66 -- .../hosted-ios-native-onboarding.test.ts | 63 -- ...hosted-ios-native-settings-handoff.test.ts | 86 -- ...hosted-ios-photo-permission-denial.test.ts | 141 --- ...ed-ios-photo-permission-revocation.test.ts | 331 ------- .../hosted-ios-privacy-log-audit.test.ts | 42 - .../hosted-ios-screenshot-parity.test.ts | 131 --- .../hosted-ios-simulator-app-build.test.ts | 54 -- ...sted-ios-simulator-app-preparation.test.ts | 72 -- ...-ios-source-control-review-journey.test.ts | 531 ------------ ...d-ios-source-control-review-parity.test.ts | 203 ----- ...ios-terminal-clipboard-image-paste.test.ts | 247 ------ ...osted-ios-terminal-clipboard-paste.test.ts | 162 ---- ...-ios-terminal-device-input-journey.test.ts | 183 ---- ...-ios-terminal-link-point-alignment.test.ts | 48 -- .../hosted-ios-workspace-parity.test.ts | 80 -- .../hosted-mobile-e2e-launch.test.ts | 23 - .../hosted-terminal-link-locator.test.ts | 116 --- ...hosted-webview-adversarial-content.test.ts | 266 ------ .../hosted-webview-cdp-session.test.ts | 804 ------------------ ...ted-webview-cdp-target-replacement.test.ts | 80 -- .../hosted-webview-cdp-test-fakes.ts | 75 -- .../hosted-webview-long-press.test.ts | 35 - .../hosted-webview-privacy-isolation.test.ts | 100 --- ...d-webview-route-exception-evidence.test.ts | 48 -- .../hosted-webview-route-navigation.test.ts | 65 -- .../hosted-webview-runtime-directory.test.ts | 32 - ...sted-webview-simulator-e2e-options.test.ts | 124 --- ...osted-webview-workspace-activation.test.ts | 162 ---- 52 files changed, 7391 deletions(-) delete mode 100644 mobile/src/mobile-web/hosted-adversarial-provider-fixture.test.ts delete mode 100644 mobile/src/mobile-web/hosted-adversarial-terminal-links.test.ts delete mode 100644 mobile/src/mobile-web/hosted-android-adversarial-terminal-links.test.ts delete mode 100644 mobile/src/mobile-web/hosted-android-agent-history-journey.test.ts delete mode 100644 mobile/src/mobile-web/hosted-android-emulator-accessibility.test.ts delete mode 100644 mobile/src/mobile-web/hosted-android-emulator-session.test.ts delete mode 100644 mobile/src/mobile-web/hosted-android-hardware-back-journey.test.ts delete mode 100644 mobile/src/mobile-web/hosted-android-mobile-web-cache.test.ts delete mode 100644 mobile/src/mobile-web/hosted-android-privacy-audit.test.ts delete mode 100644 mobile/src/mobile-web/hosted-android-workspace-activation.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-accounts-parity.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-adversarial-terminal-staging.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-agent-history-parity.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-build-activation.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-core-route-parity.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-document-upload.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-emulator-accessibility.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-emulator-controller.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-emulator-long-press.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-files-preview-parity.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-host-origin-source-control-journey.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-hybrid-route-handoff.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-mobile-web-cache.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-native-alert-journey.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-native-onboarding.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-native-settings-handoff.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-photo-permission-denial.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-photo-permission-revocation.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-privacy-log-audit.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-screenshot-parity.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-simulator-app-build.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-simulator-app-preparation.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-source-control-review-journey.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-source-control-review-parity.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-terminal-clipboard-image-paste.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-terminal-clipboard-paste.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-terminal-device-input-journey.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-terminal-link-point-alignment.test.ts delete mode 100644 mobile/src/mobile-web/hosted-ios-workspace-parity.test.ts delete mode 100644 mobile/src/mobile-web/hosted-mobile-e2e-launch.test.ts delete mode 100644 mobile/src/mobile-web/hosted-terminal-link-locator.test.ts delete mode 100644 mobile/src/mobile-web/hosted-webview-adversarial-content.test.ts delete mode 100644 mobile/src/mobile-web/hosted-webview-cdp-session.test.ts delete mode 100644 mobile/src/mobile-web/hosted-webview-cdp-target-replacement.test.ts delete mode 100644 mobile/src/mobile-web/hosted-webview-cdp-test-fakes.ts delete mode 100644 mobile/src/mobile-web/hosted-webview-long-press.test.ts delete mode 100644 mobile/src/mobile-web/hosted-webview-privacy-isolation.test.ts delete mode 100644 mobile/src/mobile-web/hosted-webview-route-exception-evidence.test.ts delete mode 100644 mobile/src/mobile-web/hosted-webview-route-navigation.test.ts delete mode 100644 mobile/src/mobile-web/hosted-webview-runtime-directory.test.ts delete mode 100644 mobile/src/mobile-web/hosted-webview-simulator-e2e-options.test.ts delete mode 100644 mobile/src/mobile-web/hosted-webview-workspace-activation.test.ts 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-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() - }) -}) From 05ed3770f873c4fe8a912e71d8a7c9d68073118e Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Mon, 7 Sep 2026 14:47:20 -0400 Subject: [PATCH 3/3] docs(mobile): drop the deleted CDP-session spec from the DevTools gate A vitest path that no longer exists is a filter, not an error, so the gate would have kept reporting green over half a run. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- docs/reference/mobile-hybrid-webview-devtools.md | 1 - 1 file changed, 1 deletion(-) 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