Skip to content

Commit b4b0e97

Browse files
committed
feat(browser): settle host commands on lease socket
1 parent 61fcd62 commit b4b0e97

8 files changed

Lines changed: 954 additions & 43 deletions

config/reliability-gates.jsonc

Lines changed: 33 additions & 5 deletions
Large diffs are not rendered by default.

src/main/browser/paired-runtime-browser-host-lease.test.ts

Lines changed: 232 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { afterEach, describe, expect, it, vi } from 'vitest'
22
import type { PairingOffer } from '../../shared/pairing'
3+
import type {
4+
BrowserClientHostCommandEvent,
5+
BrowserClientHostCommandResult
6+
} from '../../shared/browser-client-host-protocol'
37
import { BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY } from '../../shared/protocol-version'
48
import type {
59
RemoteRuntimeSubscription,
@@ -75,8 +79,8 @@ describe('PairedRuntimeBrowserHostLease', () => {
7579
})
7680

7781
it('uses page commands only after the host echoes the requested protocol', async () => {
78-
const { callbacks, close } = await subscribeLease()
79-
const onPageCommand = vi.fn()
82+
const { callbacks, close, sendRequest } = await subscribeLease()
83+
const onPageCommand = vi.fn(() => ({ status: 'completed' as const }))
8084
const lease = createLease({ pageCommandProtocolVersion: 1, onPageCommand })
8185
const starting = lease.start()
8286
await vi.waitFor(() => expect(callbacks.current).toBeDefined())
@@ -101,11 +105,13 @@ describe('PairedRuntimeBrowserHostLease', () => {
101105
expect.any(Object),
102106
expect.any(Object)
103107
)
104-
callbacks.current!.onResponse({
105-
id: 'browser-host',
106-
ok: true,
107-
result: {
108-
type: 'command',
108+
callbacks.current!.onResponse(commandResponse())
109+
110+
expect(onPageCommand).toHaveBeenCalledOnce()
111+
await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledOnce())
112+
expect(sendRequest).toHaveBeenCalledWith(
113+
'browser.clientHost.commandResult',
114+
{
109115
pageCommandProtocolVersion: 1,
110116
authorityRuntimeId: 'runtime-a',
111117
authorityEpoch: 'epoch-a',
@@ -115,22 +121,164 @@ describe('PairedRuntimeBrowserHostLease', () => {
115121
pageHostGeneration: 1,
116122
commandSequence: 1,
117123
commandId: 'command-a',
118-
command: {
119-
type: 'createPage',
120-
browserProfileId: 'default',
121-
executionHostKey: 'native:runtime-a:1'
122-
}
124+
result: { status: 'completed' }
123125
},
124-
_meta: { runtimeId: 'runtime-a' }
126+
15_000
127+
)
128+
expect(close).not.toHaveBeenCalled()
129+
await lease.close()
130+
})
131+
132+
it.each([
133+
['completed', { status: 'completed' } as const],
134+
['failed', { status: 'failed', errorCode: 'navigation_failed' } as const]
135+
])('submits a validated %s page command result', async (_caseName, result) => {
136+
const { callbacks, close, sendRequest } = await subscribeLease()
137+
const lease = createLease({
138+
pageCommandProtocolVersion: 1,
139+
onPageCommand: () => result
125140
})
141+
const starting = lease.start()
142+
await vi.waitFor(() => expect(callbacks.current).toBeDefined())
143+
callbacks.current!.onResponse(readyResponse({ pageCommandProtocolVersion: 1 }))
144+
await starting
126145

127-
expect(onPageCommand).toHaveBeenCalledOnce()
146+
callbacks.current!.onResponse(commandResponse())
147+
148+
await vi.waitFor(() =>
149+
expect(sendRequest).toHaveBeenCalledWith(
150+
'browser.clientHost.commandResult',
151+
expect.objectContaining({ result }),
152+
15_000
153+
)
154+
)
155+
expect(close).not.toHaveBeenCalled()
156+
await lease.close()
157+
})
158+
159+
it('accepts exact duplicate result acknowledgement without fencing the lease', async () => {
160+
const { callbacks, close, sendRequest } = await subscribeLease()
161+
sendRequest
162+
.mockResolvedValueOnce(commandResultAck(true))
163+
.mockResolvedValueOnce(commandResultAck(false))
164+
const lease = createLease({
165+
pageCommandProtocolVersion: 1,
166+
onPageCommand: () => ({ status: 'completed' })
167+
})
168+
const starting = lease.start()
169+
await vi.waitFor(() => expect(callbacks.current).toBeDefined())
170+
callbacks.current!.onResponse(readyResponse({ pageCommandProtocolVersion: 1 }))
171+
await starting
172+
173+
callbacks.current!.onResponse(commandResponse())
174+
await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(1))
175+
callbacks.current!.onResponse(commandResponse())
176+
177+
await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(2))
128178
expect(close).not.toHaveBeenCalled()
129179
await lease.close()
130180
})
131181

182+
it.each([
183+
[
184+
'server rejection',
185+
vi.fn().mockResolvedValue({
186+
id: 'command-result',
187+
ok: false,
188+
error: { code: 'runtime_error', message: 'command result rejected' },
189+
_meta: { runtimeId: 'runtime-a' }
190+
}),
191+
'command result rejected'
192+
],
193+
[
194+
'transport timeout',
195+
vi.fn().mockRejectedValue(new Error('command result timed out')),
196+
'command result timed out'
197+
],
198+
[
199+
'malformed acknowledgement',
200+
vi.fn().mockResolvedValue({
201+
id: 'command-result',
202+
ok: true,
203+
result: { accepted: 'yes' },
204+
_meta: { runtimeId: 'runtime-a' }
205+
}),
206+
'Invalid browser host command result acknowledgement'
207+
],
208+
[
209+
'wrong runtime acknowledgement',
210+
vi.fn().mockResolvedValue(commandResultAck(true, 'runtime-b')),
211+
'Invalid browser host command result acknowledgement'
212+
]
213+
])('fails closed on %s', async (_caseName, sendRequest, expectedMessage) => {
214+
const { callbacks, close } = await subscribeLease({ sendRequest })
215+
const onError = vi.fn()
216+
const lease = createLease({
217+
pageCommandProtocolVersion: 1,
218+
onPageCommand: () => ({ status: 'completed' }),
219+
onError
220+
})
221+
const starting = lease.start()
222+
await vi.waitFor(() => expect(callbacks.current).toBeDefined())
223+
callbacks.current!.onResponse(readyResponse({ pageCommandProtocolVersion: 1 }))
224+
await starting
225+
226+
callbacks.current!.onResponse(commandResponse())
227+
228+
await vi.waitFor(() => expect(close).toHaveBeenCalledOnce())
229+
expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: expectedMessage }))
230+
})
231+
232+
it('fails closed when a v1 subscription lacks the same-socket request sender', async () => {
233+
const { callbacks, close } = await subscribeLease({ sendRequest: undefined })
234+
const onError = vi.fn()
235+
const lease = createLease({
236+
pageCommandProtocolVersion: 1,
237+
onPageCommand: () => ({ status: 'completed' }),
238+
onError
239+
})
240+
const starting = lease.start()
241+
await vi.waitFor(() => expect(callbacks.current).toBeDefined())
242+
callbacks.current!.onResponse(readyResponse({ pageCommandProtocolVersion: 1 }))
243+
244+
await expect(starting).rejects.toThrow('Browser host command result transport unavailable')
245+
await vi.waitFor(() => expect(close).toHaveBeenCalledOnce())
246+
expect(onError).toHaveBeenCalledWith(
247+
expect.objectContaining({ message: 'Browser host command result transport unavailable' })
248+
)
249+
})
250+
251+
it.each([
252+
[
253+
'throws',
254+
'command handler threw',
255+
() => {
256+
throw new Error('command handler threw')
257+
}
258+
],
259+
[
260+
'rejects',
261+
'command handler rejected',
262+
() => Promise.reject(new Error('command handler rejected'))
263+
]
264+
])('fails closed when the page command handler %s', async (_caseName, message, onPageCommand) => {
265+
const { callbacks, close, sendRequest } = await subscribeLease()
266+
const onError = vi.fn()
267+
const lease = createLease({ pageCommandProtocolVersion: 1, onPageCommand, onError })
268+
const starting = lease.start()
269+
await vi.waitFor(() => expect(callbacks.current).toBeDefined())
270+
callbacks.current!.onResponse(readyResponse({ pageCommandProtocolVersion: 1 }))
271+
await starting
272+
273+
callbacks.current!.onResponse(commandResponse())
274+
275+
await vi.waitFor(() => expect(close).toHaveBeenCalledOnce())
276+
expect(sendRequest).not.toHaveBeenCalled()
277+
expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message }))
278+
})
279+
132280
it('rejects page commands when an old host did not negotiate them', async () => {
133-
const { callbacks, close } = await subscribeLease()
281+
const { callbacks, close } = await subscribeLease({ sendRequest: undefined })
134282
const onError = vi.fn()
135283
const onPageCommand = vi.fn()
136284
const lease = createLease({ pageCommandProtocolVersion: 1, onPageCommand, onError })
@@ -380,25 +528,42 @@ describe('PairedRuntimeBrowserHostLease', () => {
380528
})
381529
})
382530

383-
async function subscribeLease(): Promise<{
531+
async function subscribeLease(options?: { sendRequest?: ReturnType<typeof vi.fn> }): Promise<{
384532
callbacks: { current?: RemoteRuntimeSubscriptionCallbacks }
385533
close: ReturnType<typeof vi.fn>
534+
sendRequest: ReturnType<typeof vi.fn>
386535
}> {
387536
const callbacks: { current?: RemoteRuntimeSubscriptionCallbacks } = {}
388537
const close = vi.fn()
538+
const sendRequest =
539+
options && 'sendRequest' in options
540+
? options.sendRequest
541+
: vi.fn().mockResolvedValue(commandResultAck(true))
389542
subscribeRemoteRuntimeRequestMock.mockImplementationOnce(
390543
async (...args: unknown[]): Promise<RemoteRuntimeSubscription> => {
391544
callbacks.current = args[4] as RemoteRuntimeSubscriptionCallbacks
392-
return { requestId: 'browser-host', close, sendBinary: () => false }
545+
const subscription: RemoteRuntimeSubscription = {
546+
requestId: 'browser-host',
547+
close,
548+
sendBinary: () => false,
549+
...(sendRequest
550+
? {
551+
sendRequest: sendRequest as unknown as RemoteRuntimeSubscription['sendRequest']
552+
}
553+
: {})
554+
}
555+
return subscription
393556
}
394557
)
395-
return { callbacks, close }
558+
return { callbacks, close, sendRequest: sendRequest ?? vi.fn() }
396559
}
397560

398561
function createLease(
399562
overrides: {
400563
onError?: (error: Error) => void
401-
onPageCommand?: (command: unknown) => void | Promise<void>
564+
onPageCommand?: (
565+
command: BrowserClientHostCommandEvent
566+
) => BrowserClientHostCommandResult | Promise<BrowserClientHostCommandResult>
402567
pageCommandProtocolVersion?: 1
403568
} = {}
404569
): PairedRuntimeBrowserHostLease {
@@ -410,3 +575,51 @@ function createLease(
410575
...overrides
411576
})
412577
}
578+
579+
function readyResponse(overrides: { pageCommandProtocolVersion?: 1 } = {}) {
580+
return {
581+
id: 'browser-host',
582+
ok: true as const,
583+
result: {
584+
type: 'ready' as const,
585+
authorityEpoch: 'epoch-a',
586+
browserHostGeneration: 4,
587+
...overrides
588+
},
589+
_meta: { runtimeId: 'runtime-a' }
590+
}
591+
}
592+
593+
function commandResponse() {
594+
return {
595+
id: 'browser-host',
596+
ok: true as const,
597+
result: {
598+
type: 'command' as const,
599+
pageCommandProtocolVersion: 1 as const,
600+
authorityRuntimeId: 'runtime-a',
601+
authorityEpoch: 'epoch-a',
602+
browserHostClientId: 'host-a',
603+
browserHostGeneration: 4,
604+
browserPageId: 'page-a',
605+
pageHostGeneration: 1,
606+
commandSequence: 1,
607+
commandId: 'command-a',
608+
command: {
609+
type: 'createPage' as const,
610+
browserProfileId: 'default',
611+
executionHostKey: 'native:runtime-a:1'
612+
}
613+
},
614+
_meta: { runtimeId: 'runtime-a' }
615+
}
616+
}
617+
618+
function commandResultAck(accepted: boolean, runtimeId = 'runtime-a') {
619+
return {
620+
id: 'command-result',
621+
ok: true as const,
622+
result: { accepted },
623+
_meta: { runtimeId }
624+
}
625+
}

0 commit comments

Comments
 (0)