11import { afterEach , describe , expect , it , vi } from 'vitest'
22import type { PairingOffer } from '../../shared/pairing'
3+ import type {
4+ BrowserClientHostCommandEvent ,
5+ BrowserClientHostCommandResult
6+ } from '../../shared/browser-client-host-protocol'
37import { BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY } from '../../shared/protocol-version'
48import 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
398561function 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