test(mobile): consolidate bridge roundtrip fixture

This commit is contained in:
Jinwoo-H
2026-08-23 05:16:01 -04:00
parent 35630bd1db
commit 0820c97a15
10 changed files with 132 additions and 376 deletions
@@ -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()
@@ -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')
@@ -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<MobileWebBridgeShellMessage, { type: 'init' }>
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<MobileWebNativeCapabilityAuthority>
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 () => {}
}
}
@@ -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()
})
})
@@ -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()
})
})
@@ -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()
}
}
@@ -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)
})
@@ -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)
})
@@ -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)
@@ -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()
})
})