diff --git a/mobile/src/mobile-web/mobile-web-account-roundtrip.test.ts b/mobile/src/mobile-web/mobile-web-account-roundtrip.test.ts index 742b27d9b6c..f9cf3e8e833 100644 --- a/mobile/src/mobile-web/mobile-web-account-roundtrip.test.ts +++ b/mobile/src/mobile-web/mobile-web-account-roundtrip.test.ts @@ -1,19 +1,8 @@ import { expect, it, vi } from 'vitest' -import { - parseMobileWebBridgePageMessage, - parseMobileWebBridgeShellMessage -} from '../../../src/shared/mobile-web/bridge-contract' -import { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' import type { RpcClient } from '../transport/rpc-client' -import { MobileWebCapabilityBroker } from './mobile-web-capability-broker' - -const CONTEXT = { - shellSessionId: 'S'.repeat(43), - buildId: 'a'.repeat(64) -} +import { createMobileWebBridgeRoundtripFixture } from './mobile-web-bridge-roundtrip-fixture' it('round trips typed account reads, selection, and snapshots through the production bridge', async () => { - let broker: MobileWebCapabilityBroker let hostListener: ((event: unknown) => void) | null = null const hostUnsubscribe = vi.fn() const sendRequest = vi.fn(async (method: string) => { @@ -31,46 +20,16 @@ it('round trips typed account reads, selection, and snapshots through the produc } as unknown as RpcClient const requestIds = ['A', 'B', 'C', 'D', 'E'] let requestIndex = 0 - const client = new MobileWebBridgeClient({ - context: CONTEXT, + const { client, pageMessages, shellMessages } = createMobileWebBridgeRoundtripFixture({ grants: [accountGrant('snapshot'), accountGrant('select'), accountGrant('subscribe')], + rpcClient, createRequestId: () => requestIds[requestIndex++]!.repeat(22), - postMessage: (message) => { - const parsed = parseMobileWebBridgePageMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - return false - } - void broker.handle(parsed.value) - return true - } - }) - broker = new MobileWebCapabilityBroker({ - context: CONTEXT, - getClient: () => rpcClient, - isConnected: () => true, - isActive: () => true, - postMessage: (message) => { - const parsed = parseMobileWebBridgeShellMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - throw new Error(parsed.error) - } - client.receive(parsed.value) - }, - nativeAuthority: { - hapticFeedback: vi.fn(), - clipboardWrite: vi.fn(), - openExternal: vi.fn(), - terminalPreferences: vi.fn(), - terminalTextScaleUpdate: vi.fn() - }, navigationAuthority: { route: vi.fn(), reconnect: vi.fn(), removeHost: vi.fn(), consumeRecentUserGesture: () => true - }, - terminalClientId: 'device-token', - randomBytes: (length) => new Uint8Array(length).fill(1) + } }) await expect(client.account.snapshot()).resolves.toMatchObject({ @@ -79,6 +38,17 @@ it('round trips typed account reads, selection, and snapshots through the produc activeAccountId: 'claude-1' } }) + expect(pageMessages[0]).toMatchObject({ + type: 'request', + requestId: 'A'.repeat(22), + capability: 'account', + operation: 'snapshot' + }) + expect(shellMessages[0]).toMatchObject({ + type: 'response', + requestId: 'A'.repeat(22), + status: 'success' + }) await expect( client.account.select({ provider: 'codex', accountId: 'codex-1' }) ).resolves.toBeNull() diff --git a/mobile/src/mobile-web/mobile-web-agent-history-roundtrip.test.ts b/mobile/src/mobile-web/mobile-web-agent-history-roundtrip.test.ts index 1a2e6f278ca..5547649bb0f 100644 --- a/mobile/src/mobile-web/mobile-web-agent-history-roundtrip.test.ts +++ b/mobile/src/mobile-web/mobile-web-agent-history-roundtrip.test.ts @@ -1,19 +1,8 @@ import { expect, it, vi } from 'vitest' -import { - parseMobileWebBridgePageMessage, - parseMobileWebBridgeShellMessage -} from '../../../src/shared/mobile-web/bridge-contract' -import { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' import type { RpcClient } from '../transport/rpc-client' -import { MobileWebCapabilityBroker } from './mobile-web-capability-broker' - -const CONTEXT = { - shellSessionId: 'S'.repeat(43), - buildId: 'a'.repeat(64) -} +import { createMobileWebBridgeRoundtripFixture } from './mobile-web-bridge-roundtrip-fixture' it('round trips opaque agent history and gesture-gates resume through the production bridge', async () => { - let broker: MobileWebCapabilityBroker const sendRequest = vi.fn(async (method: string) => { if (method === 'status.get') { return { @@ -35,49 +24,20 @@ it('round trips opaque agent history and gesture-gates resume through the produc const rpcClient = { sendRequest } as unknown as RpcClient const requestIds = ['A', 'B', 'C'] let requestIndex = 0 - const client = new MobileWebBridgeClient({ - context: CONTEXT, + const { broker, client } = createMobileWebBridgeRoundtripFixture({ grants: [ agentHistoryGrant('snapshot'), agentHistoryGrant('preview'), agentHistoryGrant('resume') ], + rpcClient, createRequestId: () => requestIds[requestIndex++]!.repeat(22), - postMessage: (message) => { - const parsed = parseMobileWebBridgePageMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - return false - } - void broker.handle(parsed.value) - return true - } - }) - broker = new MobileWebCapabilityBroker({ - context: CONTEXT, - getClient: () => rpcClient, - isConnected: () => true, - isActive: () => true, - postMessage: (message) => { - const parsed = parseMobileWebBridgeShellMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - throw new Error(parsed.error) - } - client.receive(parsed.value) - }, - nativeAuthority: { - hapticFeedback: vi.fn(), - clipboardWrite: vi.fn(), - openExternal: vi.fn(), - terminalPreferences: vi.fn(), - terminalTextScaleUpdate: vi.fn() - }, navigationAuthority: { route: vi.fn(), reconnect: vi.fn(), removeHost: vi.fn(), consumeRecentUserGesture: () => false }, - terminalClientId: 'device-token', randomBytes: (length) => new Uint8Array(length).fill(5) }) const route = await broker.resolveNavigationRoute('host-workspace') diff --git a/mobile/src/mobile-web/mobile-web-bridge-roundtrip-fixture.ts b/mobile/src/mobile-web/mobile-web-bridge-roundtrip-fixture.ts new file mode 100644 index 00000000000..1aa55e9d403 --- /dev/null +++ b/mobile/src/mobile-web/mobile-web-bridge-roundtrip-fixture.ts @@ -0,0 +1,91 @@ +import { onTestFinished } from 'vitest' +import { + parseMobileWebBridgePageMessage, + parseMobileWebBridgeShellMessage, + type MobileWebBridgeMessageContext, + type MobileWebBridgePageMessage, + type MobileWebBridgeShellMessage +} from '../../../src/shared/mobile-web/bridge-contract' +import { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' +import type { RpcClient } from '../transport/rpc-client' +import { MobileWebCapabilityBroker } from './mobile-web-capability-broker' +import type { MobileWebNativeCapabilityAuthority } from './mobile-web-native-capability-authority' +import type { MobileWebNavigationAuthority } from './mobile-web-navigation-operations' + +type InitMessage = Extract + +export const MOBILE_WEB_BRIDGE_ROUNDTRIP_CONTEXT = { + shellSessionId: 'S'.repeat(43), + buildId: 'a'.repeat(64) +} + +export function createMobileWebBridgeRoundtripFixture(options: { + grants: InitMessage['grants'] + rpcClient?: RpcClient | null + context?: MobileWebBridgeMessageContext + createRequestId?: () => string + nativeAuthority?: Partial + navigationAuthority?: MobileWebNavigationAuthority + isActive?: () => boolean + isConnected?: () => boolean + terminalClientId?: string + randomBytes?: (length: number) => Uint8Array +}) { + const context = options.context ?? MOBILE_WEB_BRIDGE_ROUNDTRIP_CONTEXT + const pageMessages: MobileWebBridgePageMessage[] = [] + const shellMessages: MobileWebBridgeShellMessage[] = [] + let broker: MobileWebCapabilityBroker + const client = new MobileWebBridgeClient({ + context, + grants: options.grants, + createRequestId: options.createRequestId, + postMessage(message) { + const parsed = parseMobileWebBridgePageMessage(JSON.stringify(message), context) + if (!parsed.ok) { + return false + } + pageMessages.push(parsed.value) + void broker.handle(parsed.value) + return true + } + }) + broker = new MobileWebCapabilityBroker({ + context, + getClient: () => options.rpcClient ?? null, + isConnected: options.isConnected ?? (() => options.rpcClient != null), + isActive: options.isActive ?? (() => true), + nativeAuthority: { ...defaultNativeAuthority(), ...options.nativeAuthority }, + navigationAuthority: options.navigationAuthority, + terminalClientId: options.terminalClientId ?? 'roundtrip-device', + randomBytes: options.randomBytes ?? ((length) => new Uint8Array(length).fill(1)), + postMessage(message) { + const parsed = parseMobileWebBridgeShellMessage(JSON.stringify(message), context) + if (!parsed.ok) { + throw new Error(parsed.error) + } + shellMessages.push(parsed.value) + client.receive(parsed.value) + } + }) + const dispose = () => { + client.dispose() + broker.dispose() + } + onTestFinished(dispose) + return { broker, client, dispose, pageMessages, shellMessages } +} + +function defaultNativeAuthority(): MobileWebNativeCapabilityAuthority { + return { + hapticFeedback: () => {}, + clipboardAvailability: async () => ({ hasText: false, hasImage: false }), + clipboardWrite: async () => ({ confirmation: 'in-app' }), + openExternal: async () => {}, + terminalPreferences: async () => ({ + textScale: 1, + autocompleteEnabled: true, + linkOpenMode: 'phone-browser' + }), + terminalTextScaleUpdate: async () => {} + } +} diff --git a/mobile/src/mobile-web/mobile-web-bridge-roundtrip.test.ts b/mobile/src/mobile-web/mobile-web-bridge-roundtrip.test.ts index a39837c2315..2b3d68a0489 100644 --- a/mobile/src/mobile-web/mobile-web-bridge-roundtrip.test.ts +++ b/mobile/src/mobile-web/mobile-web-bridge-roundtrip.test.ts @@ -1,15 +1,11 @@ import { describe, expect, it, vi } from 'vitest' import { MOBILE_WEB_BRIDGE_PROTOCOL_VERSION, - parseMobileWebBridgePageMessage, parseMobileWebBridgeShellMessage } from '../../../src/shared/mobile-web/bridge-contract' -import { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' import type { RpcClient } from '../transport/rpc-client' -import { - MOBILE_WEB_PRODUCTION_GRANTS, - MobileWebCapabilityBroker -} from './mobile-web-capability-broker' +import { MOBILE_WEB_PRODUCTION_GRANTS } from './mobile-web-capability-broker' +import { createMobileWebBridgeRoundtripFixture } from './mobile-web-bridge-roundtrip-fixture' const CONTEXT = { shellSessionId: 'S'.repeat(43), @@ -149,7 +145,6 @@ describe('mobile web bridge round trip', () => { .mockResolvedValueOnce({ ok: true, result: { browserPageId: 'browser-2' } }) .mockResolvedValueOnce({ ok: true, result: { closed: true } }) .mockResolvedValueOnce({ ok: true, result: { opened: true } }) - let broker: MobileWebCapabilityBroker let hostSubscriptionListener: ((event: unknown) => void) | undefined const hostUnsubscribe = vi.fn() const subscribe = vi @@ -160,41 +155,13 @@ describe('mobile web bridge round trip', () => { }) const requestIds = ['R', 'Q', 'T', 'U', 'V', 'W', 'X', 'Y', 'A', 'B'] let requestIndex = 0 - const client = new MobileWebBridgeClient({ + const rpcClient = { sendRequest, subscribe } as unknown as RpcClient + const { client } = createMobileWebBridgeRoundtripFixture({ context: CONTEXT, grants: [...MOBILE_WEB_PRODUCTION_GRANTS], + rpcClient, createRequestId: () => (requestIds[requestIndex++] ?? 'Z').repeat(22), - postMessage: (message) => { - const parsed = parseMobileWebBridgePageMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - return false - } - void broker.handle(parsed.value) - return true - } - }) - const rpcClient = { sendRequest, subscribe } as unknown as RpcClient - broker = new MobileWebCapabilityBroker({ - context: CONTEXT, - getClient: () => rpcClient, - isConnected: () => true, - isActive: () => true, - nativeAuthority: { - hapticFeedback: vi.fn(), - clipboardWrite: vi.fn(), - openExternal: vi.fn(), - terminalPreferences: vi.fn(), - terminalTextScaleUpdate: vi.fn() - }, - terminalClientId: 'device-token', - randomBytes: (length) => new Uint8Array(length).fill(1), - postMessage: (message) => { - const parsed = parseMobileWebBridgeShellMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - throw new Error(parsed.error) - } - client.receive(parsed.value) - } + terminalClientId: 'device-token' }) const firstWorkspacePage = await client.workspaceSnapshot({ limit: 1 }) @@ -422,8 +389,5 @@ describe('mobile web bridge round trip', () => { expect(liveErrors).toEqual([]) subscription.unsubscribe() expect(hostUnsubscribe).toHaveBeenCalledOnce() - - client.dispose() - broker.dispose() }) }) diff --git a/mobile/src/mobile-web/mobile-web-native-roundtrip.test.ts b/mobile/src/mobile-web/mobile-web-native-roundtrip.test.ts index 78e70533b4b..647a608e3a0 100644 --- a/mobile/src/mobile-web/mobile-web-native-roundtrip.test.ts +++ b/mobile/src/mobile-web/mobile-web-native-roundtrip.test.ts @@ -1,17 +1,7 @@ import { describe, expect, it, vi } from 'vitest' -import { - parseMobileWebBridgePageMessage, - parseMobileWebBridgeShellMessage -} from '../../../src/shared/mobile-web/bridge-contract' -import { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' -import { MobileWebCapabilityBroker } from './mobile-web-capability-broker' +import { createMobileWebBridgeRoundtripFixture } from './mobile-web-bridge-roundtrip-fixture' import { MOBILE_WEB_PRODUCTION_NATIVE_GRANTS } from './mobile-web-production-native-grants' -const CONTEXT = { - shellSessionId: 'S'.repeat(43), - buildId: 'a'.repeat(64) -} - describe('mobile web native capability round trip', () => { it('keeps device effects in the shell behind typed grants and gestures', async () => { const hapticFeedback = vi.fn() @@ -30,26 +20,11 @@ describe('mobile web native capability round trip', () => { const terminalCustomKeysUpdate = vi.fn().mockResolvedValue(undefined) const terminalTextScaleUpdate = vi.fn().mockResolvedValue(undefined) const consumeRecentUserGesture = vi.fn(() => true) - let broker: MobileWebCapabilityBroker let requestIndex = 0 - const client = new MobileWebBridgeClient({ - context: CONTEXT, + const { client } = createMobileWebBridgeRoundtripFixture({ grants: [...MOBILE_WEB_PRODUCTION_NATIVE_GRANTS], createRequestId: () => String.fromCharCode(65 + requestIndex++).repeat(22), - postMessage(message) { - const parsed = parseMobileWebBridgePageMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - return false - } - void broker.handle(parsed.value) - return true - } - }) - broker = new MobileWebCapabilityBroker({ - context: CONTEXT, - getClient: () => null, isConnected: () => false, - isActive: () => true, nativeAuthority: { hapticFeedback, clipboardWrite, @@ -64,15 +39,6 @@ describe('mobile web native capability round trip', () => { reconnect: vi.fn(), removeHost: vi.fn(), consumeRecentUserGesture - }, - terminalClientId: 'native-only-device', - randomBytes: (length) => new Uint8Array(length), - postMessage(message) { - const parsed = parseMobileWebBridgeShellMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - throw new Error(parsed.error) - } - client.receive(parsed.value) } }) @@ -108,7 +74,5 @@ describe('mobile web native capability round trip', () => { { id: 'custom-2', label: 'Test', bytes: 'pnpm test\r', enter: false } ]) expect(consumeRecentUserGesture).toHaveBeenCalledTimes(4) - client.dispose() - broker.dispose() }) }) diff --git a/mobile/src/mobile-web/mobile-web-navigation-roundtrip.test.ts b/mobile/src/mobile-web/mobile-web-navigation-roundtrip.test.ts index adb0f52ba8f..1daa7f62e1d 100644 --- a/mobile/src/mobile-web/mobile-web-navigation-roundtrip.test.ts +++ b/mobile/src/mobile-web/mobile-web-navigation-roundtrip.test.ts @@ -1,58 +1,23 @@ import { describe, expect, it, vi } from 'vitest' -import { - parseMobileWebBridgePageMessage, - parseMobileWebBridgeShellMessage -} from '../../../src/shared/mobile-web/bridge-contract' -import { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' -import { MobileWebCapabilityBroker } from './mobile-web-capability-broker' +import { createMobileWebBridgeRoundtripFixture } from './mobile-web-bridge-roundtrip-fixture' import { MOBILE_WEB_PRODUCTION_NAVIGATION_GRANTS } from './mobile-web-production-navigation-grants' -const CONTEXT = { - shellSessionId: 'S'.repeat(43), - buildId: 'a'.repeat(64) -} - describe('mobile web navigation round trip', () => { it('carries named shell intent without host identity', async () => { const route = vi.fn() const reconnect = vi.fn() const removeHost = vi.fn() const consumeRecentUserGesture = vi.fn(() => true) - let broker: MobileWebCapabilityBroker let requestIndex = 0 - const client = new MobileWebBridgeClient({ - context: CONTEXT, + const { client } = createMobileWebBridgeRoundtripFixture({ grants: [...MOBILE_WEB_PRODUCTION_NAVIGATION_GRANTS], createRequestId: () => String.fromCharCode(82 + requestIndex++).repeat(22), - postMessage(message) { - const parsed = parseMobileWebBridgePageMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - return false - } - void broker.handle(parsed.value) - return true - } - }) - broker = new MobileWebCapabilityBroker({ - context: CONTEXT, - getClient: () => null, isConnected: () => false, - isActive: () => true, - nativeAuthority: nativeAuthority(), navigationAuthority: { route, reconnect, removeHost, consumeRecentUserGesture - }, - terminalClientId: 'native-only-device', - randomBytes: (length) => new Uint8Array(length), - postMessage(message) { - const parsed = parseMobileWebBridgeShellMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - throw new Error(parsed.error) - } - client.receive(parsed.value) } }) @@ -68,17 +33,5 @@ describe('mobile web navigation round trip', () => { expect(reconnect).toHaveBeenCalledWith() expect(removeHost).toHaveBeenCalledWith() expect(consumeRecentUserGesture).toHaveBeenCalledTimes(3) - client.dispose() - broker.dispose() }) }) - -function nativeAuthority() { - return { - hapticFeedback: vi.fn(), - clipboardWrite: vi.fn(), - openExternal: vi.fn(), - terminalPreferences: vi.fn(), - terminalTextScaleUpdate: vi.fn() - } -} diff --git a/mobile/src/mobile-web/mobile-web-task-item-mutation-roundtrip.test.ts b/mobile/src/mobile-web/mobile-web-task-item-mutation-roundtrip.test.ts index bc6b00fe76e..9f5faea48ea 100644 --- a/mobile/src/mobile-web/mobile-web-task-item-mutation-roundtrip.test.ts +++ b/mobile/src/mobile-web/mobile-web-task-item-mutation-roundtrip.test.ts @@ -1,9 +1,4 @@ import { expect, it, vi } from 'vitest' -import { - parseMobileWebBridgePageMessage, - parseMobileWebBridgeShellMessage -} from '../../../src/shared/mobile-web/bridge-contract' -import { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' import { webHostTaskItemFileOperations } from '../tasks/web-host-task-item-file-operations' import { webHostTaskItemMutationOperations } from '../tasks/web-host-task-item-mutation-operations' import { webHostTaskItemReviewOperations } from '../tasks/web-host-task-item-review-operations' @@ -12,7 +7,7 @@ import { webHostTaskProviderWriteOperations } from '../tasks/web-host-task-provi import { webHostTaskListOperations } from '../tasks/web-host-task-list-operations' import { webHostTaskReadOperations } from '../tasks/web-host-task-read-operations' import type { RpcClient } from '../transport/rpc-client' -import { MobileWebCapabilityBroker } from './mobile-web-capability-broker' +import { createMobileWebBridgeRoundtripFixture } from './mobile-web-bridge-roundtrip-fixture' import { taskRoundtripHostResponse } from './mobile-web-task-roundtrip-host-fixtures' const CONTEXT = { @@ -21,11 +16,10 @@ const CONTEXT = { } it('revalidates opaque hosted task targets before provider writes', async () => { - let broker: MobileWebCapabilityBroker const sendRequest = vi.fn(async (method: string) => taskRoundtripHostResponse(method)) const rpcClient = { sendRequest } as unknown as RpcClient let requestIndex = 0 - const client = new MobileWebBridgeClient({ + const { broker, client } = createMobileWebBridgeRoundtripFixture({ context: CONTEXT, grants: [ taskGrant('repositories'), @@ -56,36 +50,9 @@ it('revalidates opaque hosted task targets before provider writes', async () => taskGrant('createProviderIssue'), taskGrant('updateIssueSource') ], + rpcClient, createRequestId: () => 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'[requestIndex++]!.repeat(22), - postMessage: (message) => { - const parsed = parseMobileWebBridgePageMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - return false - } - void broker.handle(parsed.value) - return true - } - }) - broker = new MobileWebCapabilityBroker({ - context: CONTEXT, - getClient: () => rpcClient, - isConnected: () => true, - isActive: () => true, - postMessage: (message) => { - const parsed = parseMobileWebBridgeShellMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - throw new Error(parsed.error) - } - client.receive(parsed.value) - }, - nativeAuthority: { - hapticFeedback: vi.fn(), - clipboardWrite: vi.fn(), - openExternal: vi.fn(), - terminalPreferences: vi.fn(), - terminalTextScaleUpdate: vi.fn() - }, terminalClientId: 'device-token', randomBytes: (length) => new Uint8Array(length).fill(4) }) diff --git a/mobile/src/mobile-web/mobile-web-task-project-mutation-roundtrip.test.ts b/mobile/src/mobile-web/mobile-web-task-project-mutation-roundtrip.test.ts index e98ff671539..d946fb5fc3a 100644 --- a/mobile/src/mobile-web/mobile-web-task-project-mutation-roundtrip.test.ts +++ b/mobile/src/mobile-web/mobile-web-task-project-mutation-roundtrip.test.ts @@ -1,15 +1,10 @@ import { expect, it, vi } from 'vitest' -import { - parseMobileWebBridgePageMessage, - parseMobileWebBridgeShellMessage -} from '../../../src/shared/mobile-web/bridge-contract' -import { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' import { webHostTaskProjectFileOperations } from '../tasks/web-host-task-project-file-operations' import { webHostTaskProjectMutationOperations } from '../tasks/web-host-task-project-mutation-operations' import { webHostTaskProjectReadOperations } from '../tasks/web-host-task-project-read-operations' import { webHostTaskReadOperations } from '../tasks/web-host-task-read-operations' import type { RpcClient } from '../transport/rpc-client' -import { MobileWebCapabilityBroker } from './mobile-web-capability-broker' +import { createMobileWebBridgeRoundtripFixture } from './mobile-web-bridge-roundtrip-fixture' import { taskRoundtripHostResponse } from './mobile-web-task-roundtrip-host-fixtures' const CONTEXT = { @@ -18,11 +13,10 @@ const CONTEXT = { } it('revalidates opaque GitHub Project mutation targets before every write', async () => { - let broker: MobileWebCapabilityBroker const sendRequest = vi.fn(async (method: string) => taskRoundtripHostResponse(method)) const rpcClient = { sendRequest } as unknown as RpcClient let requestIndex = 0 - const client = new MobileWebBridgeClient({ + const { broker, client } = createMobileWebBridgeRoundtripFixture({ context: CONTEXT, grants: [ taskGrant('repositories'), @@ -45,35 +39,8 @@ it('revalidates opaque GitHub Project mutation targets before every write', asyn taskGrant('loadProjectFileContents'), taskGrant('addProjectInlineComment') ], + rpcClient, createRequestId: () => String.fromCharCode(65 + requestIndex++).repeat(22), - postMessage: (message) => { - const parsed = parseMobileWebBridgePageMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - return false - } - void broker.handle(parsed.value) - return true - } - }) - broker = new MobileWebCapabilityBroker({ - context: CONTEXT, - getClient: () => rpcClient, - isConnected: () => true, - isActive: () => true, - postMessage: (message) => { - const parsed = parseMobileWebBridgeShellMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - throw new Error(parsed.error) - } - client.receive(parsed.value) - }, - nativeAuthority: { - hapticFeedback: vi.fn(), - clipboardWrite: vi.fn(), - openExternal: vi.fn(), - terminalPreferences: vi.fn(), - terminalTextScaleUpdate: vi.fn() - }, terminalClientId: 'device-token', randomBytes: (length) => new Uint8Array(length).fill(3) }) diff --git a/mobile/src/mobile-web/mobile-web-task-read-roundtrip.test.ts b/mobile/src/mobile-web/mobile-web-task-read-roundtrip.test.ts index 3f27bd199f4..a6f93685a08 100644 --- a/mobile/src/mobile-web/mobile-web-task-read-roundtrip.test.ts +++ b/mobile/src/mobile-web/mobile-web-task-read-roundtrip.test.ts @@ -1,16 +1,11 @@ import { expect, it, vi } from 'vitest' -import { - parseMobileWebBridgePageMessage, - parseMobileWebBridgeShellMessage -} from '../../../src/shared/mobile-web/bridge-contract' -import { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' import { webHostTaskPreferenceOperations } from '../tasks/web-host-task-preference-operations' import { webHostTaskDetailOperations } from '../tasks/web-host-task-detail-operations' import { webHostTaskProjectReadOperations } from '../tasks/web-host-task-project-read-operations' import { webHostTaskListOperations } from '../tasks/web-host-task-list-operations' import { webHostTaskReadOperations } from '../tasks/web-host-task-read-operations' import type { RpcClient } from '../transport/rpc-client' -import { MobileWebCapabilityBroker } from './mobile-web-capability-broker' +import { createMobileWebBridgeRoundtripFixture } from './mobile-web-bridge-roundtrip-fixture' import { TASK_ROUNDTRIP_HOST_REPO_ID, taskRoundtripHostResponse @@ -21,11 +16,10 @@ const CONTEXT = { buildId: 'a'.repeat(64) } it('round trips bounded task bootstrap reads through opaque repository authority', async () => { - let broker: MobileWebCapabilityBroker const sendRequest = vi.fn(async (method: string) => taskRoundtripHostResponse(method)) const rpcClient = { sendRequest } as unknown as RpcClient let requestIndex = 0 - const client = new MobileWebBridgeClient({ + const { broker, client } = createMobileWebBridgeRoundtripFixture({ context: CONTEXT, grants: [ taskGrant('bootstrap'), @@ -53,37 +47,9 @@ it('round trips bounded task bootstrap reads through opaque repository authority taskGrant('projectItemAssignableUsers'), taskGrant('projectIssueTypes') ], + rpcClient, createRequestId: () => String.fromCharCode(65 + requestIndex++).repeat(22), - postMessage: (message) => { - const parsed = parseMobileWebBridgePageMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - return false - } - void broker.handle(parsed.value) - return true - } - }) - broker = new MobileWebCapabilityBroker({ - context: CONTEXT, - getClient: () => rpcClient, - isConnected: () => true, - isActive: () => true, - postMessage: (message) => { - const parsed = parseMobileWebBridgeShellMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - throw new Error(parsed.error) - } - client.receive(parsed.value) - }, - nativeAuthority: { - hapticFeedback: vi.fn(), - clipboardWrite: vi.fn(), - openExternal: vi.fn(), - terminalPreferences: vi.fn(), - terminalTextScaleUpdate: vi.fn() - }, - terminalClientId: 'device-token', - randomBytes: (length) => new Uint8Array(length).fill(1) + terminalClientId: 'device-token' }) const operations = webHostTaskReadOperations(client) const lists = webHostTaskListOperations(client) diff --git a/mobile/src/mobile-web/mobile-web-workspace-creation-roundtrip.test.ts b/mobile/src/mobile-web/mobile-web-workspace-creation-roundtrip.test.ts index f01bbb6d96c..da819a6689d 100644 --- a/mobile/src/mobile-web/mobile-web-workspace-creation-roundtrip.test.ts +++ b/mobile/src/mobile-web/mobile-web-workspace-creation-roundtrip.test.ts @@ -1,23 +1,11 @@ import { describe, expect, it, vi } from 'vitest' -import { - parseMobileWebBridgePageMessage, - parseMobileWebBridgeShellMessage, - type MobileWebBridgeShellMessage -} from '../../../src/shared/mobile-web/bridge-contract' -import { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' import type { RpcClient } from '../transport/rpc-client' import { MOBILE_WORKTREE_CREATE_IDEMPOTENCY_CAPABILITY } from '../tasks/worktree-create-capability' -import { MobileWebCapabilityBroker } from './mobile-web-capability-broker' +import { createMobileWebBridgeRoundtripFixture } from './mobile-web-bridge-roundtrip-fixture' import { MOBILE_WEB_PRODUCTION_WORKSPACE_CREATION_GRANTS } from './mobile-web-production-workspace-creation-grants' -const CONTEXT = { - shellSessionId: 'S'.repeat(43), - buildId: 'a'.repeat(64) -} - describe('mobile web workspace creation round trip', () => { it('carries page requests through schemas and resolves host authority only in native', async () => { - const shellMessages: MobileWebBridgeShellMessage[] = [] const consumeRecentUserGesture = vi.fn(() => true) const sendRequest = vi.fn(async (method: string) => { if (method === 'repo.list') { @@ -47,48 +35,17 @@ describe('mobile web workspace creation round trip', () => { throw new Error(`Unexpected method ${method}`) }) const hostClient = { sendRequest } as unknown as RpcClient - let broker: MobileWebCapabilityBroker let requestIndex = 0 - const pageClient = new MobileWebBridgeClient({ - context: CONTEXT, + const { client: pageClient, shellMessages } = createMobileWebBridgeRoundtripFixture({ grants: [...MOBILE_WEB_PRODUCTION_WORKSPACE_CREATION_GRANTS], + rpcClient: hostClient, createRequestId: () => String.fromCharCode(65 + requestIndex++).repeat(22), - postMessage(message) { - const parsed = parseMobileWebBridgePageMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - return false - } - void broker.handle(parsed.value) - return true - } - }) - broker = new MobileWebCapabilityBroker({ - context: CONTEXT, - getClient: () => hostClient, - isConnected: () => true, - isActive: () => true, - nativeAuthority: { - hapticFeedback: vi.fn(), - clipboardWrite: vi.fn(), - openExternal: vi.fn(), - terminalPreferences: vi.fn(), - terminalTextScaleUpdate: vi.fn() - }, - terminalClientId: 'native-device-secret', randomBytes: (length) => new Uint8Array(length).fill(5), navigationAuthority: { route: vi.fn(), reconnect: vi.fn(), removeHost: vi.fn(), consumeRecentUserGesture - }, - postMessage(message) { - shellMessages.push(message) - const parsed = parseMobileWebBridgeShellMessage(JSON.stringify(message), CONTEXT) - if (!parsed.ok) { - throw new Error(parsed.error) - } - pageClient.receive(parsed.value) } }) @@ -132,8 +89,5 @@ describe('mobile web workspace creation round trip', () => { expect(createParams).not.toHaveProperty('startupCommand') expect(sendRequest).not.toHaveBeenCalledWith('settings.get') expect(JSON.stringify(shellMessages)).not.toMatch(/repo-secret|worktree-secret|ssh-private-id/) - - pageClient.dispose() - broker.dispose() }) })