@@ -2,10 +2,13 @@ import { expect, it } from "@effect/vitest";
22import {
33 EnvironmentId ,
44 PreviewAutomationNoFocusedOwnerError ,
5+ PreviewAutomationUnavailableError ,
56 ProviderInstanceId ,
67 ThreadId ,
8+ type PreviewAutomationOwner ,
79} from "@t3tools/contracts" ;
810import * as Effect from "effect/Effect" ;
11+ import * as Fiber from "effect/Fiber" ;
912import * as Stream from "effect/Stream" ;
1013
1114import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts" ;
@@ -20,11 +23,22 @@ const scope = {
2023 expiresAt : 2 ,
2124} ;
2225
23- it . effect ( "routes a request to the focused owner and correlates its response" , ( ) =>
26+ const makeOwner = ( overrides : Partial < PreviewAutomationOwner > = { } ) : PreviewAutomationOwner => ( {
27+ clientId : "client-1" ,
28+ environmentId : scope . environmentId ,
29+ threadId : scope . threadId ,
30+ tabId : null ,
31+ visible : false ,
32+ supportsAutomation : true ,
33+ focusedAt : "2026-06-11T00:00:00.000Z" ,
34+ ...overrides ,
35+ } ) ;
36+
37+ it . effect ( "atomically registers a connected owner and correlates its response" , ( ) =>
2438 Effect . scoped (
2539 Effect . gen ( function * ( ) {
2640 const broker = yield * PreviewAutomationBroker . __testing . make ;
27- const requests = yield * broker . connect ( "client-1" ) ;
41+ const requests = yield * broker . connect ( makeOwner ( ) ) ;
2842 yield * Stream . runForEach ( requests , ( request ) =>
2943 broker . respond ( {
3044 requestId : request . requestId ,
@@ -33,15 +47,6 @@ it.effect("routes a request to the focused owner and correlates its response", (
3347 } ) ,
3448 ) . pipe ( Effect . forkScoped ) ;
3549 yield * Effect . yieldNow ;
36- yield * broker . reportOwner ( {
37- clientId : "client-1" ,
38- environmentId : scope . environmentId ,
39- threadId : scope . threadId ,
40- tabId : null ,
41- visible : false ,
42- supportsAutomation : true ,
43- focusedAt : "2026-06-11T00:00:00.000Z" ,
44- } ) ;
4550
4651 const result = yield * broker . invoke < { available : boolean } > ( {
4752 scope,
@@ -68,22 +73,119 @@ it.effect("routes interactive commands to a hidden durable browser host", () =>
6873 Effect . scoped (
6974 Effect . gen ( function * ( ) {
7075 const broker = yield * PreviewAutomationBroker . __testing . make ;
71- const requests = yield * broker . connect ( "client-hidden" ) ;
76+ const requests = yield * broker . connect (
77+ makeOwner ( { clientId : "client-hidden" , tabId : "tab-hidden" } ) ,
78+ ) ;
79+ yield * Stream . runForEach ( requests , ( request ) =>
80+ broker . respond ( { requestId : request . requestId , ok : true } ) ,
81+ ) . pipe ( Effect . forkScoped ) ;
82+ yield * Effect . yieldNow ;
83+
84+ yield * broker . invoke < void > ( { scope, operation : "click" , input : { x : 10 , y : 10 } } ) ;
85+ } ) ,
86+ ) ,
87+ ) ;
88+
89+ it . effect ( "lets the browser host resolve an active tab that has not been reported yet" , ( ) =>
90+ Effect . scoped (
91+ Effect . gen ( function * ( ) {
92+ const broker = yield * PreviewAutomationBroker . __testing . make ;
93+ const requests = yield * broker . connect ( makeOwner ( { tabId : null } ) ) ;
94+ let routedTabId : string | undefined ;
95+ yield * Stream . runForEach ( requests , ( request ) => {
96+ routedTabId = request . tabId ;
97+ return broker . respond ( { requestId : request . requestId , ok : true } ) ;
98+ } ) . pipe ( Effect . forkScoped ) ;
99+ yield * Effect . yieldNow ;
100+
101+ yield * broker . invoke < void > ( { scope, operation : "click" , input : { x : 10 , y : 10 } } ) ;
102+
103+ expect ( routedTabId ) . toBeUndefined ( ) ;
104+ } ) ,
105+ ) ,
106+ ) ;
107+
108+ it . effect ( "preserves current owner metadata when its request stream reconnects" , ( ) =>
109+ Effect . scoped (
110+ Effect . gen ( function * ( ) {
111+ const broker = yield * PreviewAutomationBroker . __testing . make ;
112+ const firstRequests = yield * broker . connect ( makeOwner ( ) ) ;
113+ yield * Stream . runDrain ( firstRequests ) . pipe ( Effect . forkScoped ) ;
114+ yield * broker . reportOwner ( makeOwner ( { tabId : "tab-current" , visible : true } ) ) ;
115+
116+ const reconnectedRequests = yield * broker . connect ( makeOwner ( ) ) ;
117+ let routedTabId : string | undefined ;
118+ yield * Stream . runForEach ( reconnectedRequests , ( request ) => {
119+ routedTabId = request . tabId ;
120+ return broker . respond ( { requestId : request . requestId , ok : true } ) ;
121+ } ) . pipe ( Effect . forkScoped ) ;
122+ yield * Effect . yieldNow ;
123+
124+ yield * broker . invoke < void > ( { scope, operation : "click" , input : { x : 10 , y : 10 } } ) ;
125+
126+ expect ( routedTabId ) . toBe ( "tab-current" ) ;
127+ } ) ,
128+ ) ,
129+ ) ;
130+
131+ it . effect ( "ignores stale owner cleanup after the client moves to another thread" , ( ) =>
132+ Effect . scoped (
133+ Effect . gen ( function * ( ) {
134+ const broker = yield * PreviewAutomationBroker . __testing . make ;
135+ const requests = yield * broker . connect ( makeOwner ( ) ) ;
72136 yield * Stream . runForEach ( requests , ( request ) =>
73137 broker . respond ( { requestId : request . requestId , ok : true } ) ,
74138 ) . pipe ( Effect . forkScoped ) ;
75139 yield * Effect . yieldNow ;
76- yield * broker . reportOwner ( {
77- clientId : "client-hidden" ,
140+
141+ yield * broker . clearOwner ( {
142+ clientId : "client-1" ,
78143 environmentId : scope . environmentId ,
79- threadId : scope . threadId ,
80- tabId : "tab-hidden" ,
81- visible : false ,
82- supportsAutomation : true ,
83- focusedAt : "2026-06-11T00:00:00.000Z" ,
144+ threadId : ThreadId . make ( "thread-stale" ) ,
84145 } ) ;
85146
86- yield * broker . invoke < void > ( { scope, operation : "click" , input : { x : 10 , y : 10 } } ) ;
147+ yield * broker . invoke < void > ( { scope, operation : "status" , input : { } } ) ;
148+ } ) ,
149+ ) ,
150+ ) ;
151+
152+ it . effect ( "fails requests assigned to a browser stream when that stream reconnects" , ( ) =>
153+ Effect . scoped (
154+ Effect . gen ( function * ( ) {
155+ const broker = yield * PreviewAutomationBroker . __testing . make ;
156+ const _requests = yield * broker . connect ( makeOwner ( ) ) ;
157+ const pending = yield * broker
158+ . invoke < void > ( { scope, operation : "status" , input : { } } )
159+ . pipe ( Effect . flip , Effect . forkScoped ) ;
160+ yield * Effect . yieldNow ;
161+
162+ const _replacementRequests = yield * broker . connect ( makeOwner ( ) ) ;
163+
164+ const error = yield * Fiber . join ( pending ) ;
165+ expect ( error ) . toBeInstanceOf ( PreviewAutomationUnavailableError ) ;
166+ } ) ,
167+ ) ,
168+ ) ;
169+
170+ it . effect ( "falls back to an older connected owner when a newer report is not connected" , ( ) =>
171+ Effect . scoped (
172+ Effect . gen ( function * ( ) {
173+ const broker = yield * PreviewAutomationBroker . __testing . make ;
174+ const requests = yield * broker . connect ( makeOwner ( { clientId : "client-connected" } ) ) ;
175+ yield * Stream . runForEach ( requests , ( request ) =>
176+ broker . respond ( { requestId : request . requestId , ok : true , result : "connected" } ) ,
177+ ) . pipe ( Effect . forkScoped ) ;
178+ yield * Effect . yieldNow ;
179+ yield * broker . reportOwner (
180+ makeOwner ( {
181+ clientId : "client-report-only" ,
182+ focusedAt : "2026-06-11T00:00:01.000Z" ,
183+ } ) ,
184+ ) ;
185+
186+ const result = yield * broker . invoke < string > ( { scope, operation : "status" , input : { } } ) ;
187+
188+ expect ( result ) . toBe ( "connected" ) ;
87189 } ) ,
88190 ) ,
89191) ;
0 commit comments