@@ -111,6 +111,80 @@ test('screenshot resolves relative positional path against request cwd', async (
111111 expect ( recordedAction ?. positionals ) . toEqual ( [ path . join ( callerCwd , 'evidence/test.png' ) ] ) ;
112112} ) ;
113113
114+ test ( 'router serializes concurrent commands for the same device across sessions' , async ( ) => {
115+ const sessionStore = makeStore ( ) ;
116+ sessionStore . set ( 'session-a' , makeSession ( 'session-a' ) ) ;
117+ sessionStore . set ( 'session-b' , makeSession ( 'session-b' ) ) ;
118+
119+ const order : string [ ] = [ ] ;
120+ let active = 0 ;
121+ let maxActive = 0 ;
122+ const gates : Array < ( ) => void > = [ ] ;
123+
124+ mockDispatch . mockImplementation ( async ( _device , command ) => {
125+ order . push ( `start-${ command } ` ) ;
126+ active += 1 ;
127+ maxActive = Math . max ( maxActive , active ) ;
128+ await new Promise < void > ( ( resolve ) => {
129+ gates . push ( ( ) => {
130+ active -= 1 ;
131+ order . push ( `end-${ command } ` ) ;
132+ resolve ( ) ;
133+ } ) ;
134+ } ) ;
135+ return { } ;
136+ } ) ;
137+
138+ const handler = createRequestHandler ( {
139+ logPath : path . join ( os . tmpdir ( ) , 'daemon.log' ) ,
140+ token : 'test-token' ,
141+ sessionStore,
142+ leaseRegistry : new LeaseRegistry ( ) ,
143+ trackDownloadableArtifact : ( ) => 'artifact-id' ,
144+ } ) ;
145+
146+ const screenshotRequest = handler ( {
147+ token : 'test-token' ,
148+ session : 'session-a' ,
149+ command : 'screenshot' ,
150+ positionals : [ '/tmp/first.png' ] ,
151+ meta : { requestId : 'req-lock-1' } ,
152+ } ) ;
153+
154+ await vi . waitFor ( ( ) => {
155+ expect ( order ) . toEqual ( [ 'start-screenshot' ] ) ;
156+ } ) ;
157+
158+ const scrollRequest = handler ( {
159+ token : 'test-token' ,
160+ session : 'session-b' ,
161+ command : 'scroll' ,
162+ positionals : [ 'down' ] ,
163+ meta : { requestId : 'req-lock-2' } ,
164+ } ) ;
165+
166+ await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
167+ expect ( order ) . toEqual ( [ 'start-screenshot' ] ) ;
168+
169+ gates . shift ( ) ?.( ) ;
170+
171+ await vi . waitFor ( ( ) => {
172+ expect ( order ) . toEqual ( [ 'start-screenshot' , 'end-screenshot' , 'start-scroll' ] ) ;
173+ } ) ;
174+
175+ gates . shift ( ) ?.( ) ;
176+
177+ const [ screenshotResponse , scrollResponse ] = await Promise . all ( [
178+ screenshotRequest ,
179+ scrollRequest ,
180+ ] ) ;
181+
182+ expect ( screenshotResponse . ok ) . toBe ( true ) ;
183+ expect ( scrollResponse . ok ) . toBe ( true ) ;
184+ expect ( maxActive ) . toBe ( 1 ) ;
185+ expect ( order ) . toEqual ( [ 'start-screenshot' , 'end-screenshot' , 'start-scroll' , 'end-scroll' ] ) ;
186+ } ) ;
187+
114188test ( 'screenshot forwards macOS session surface to dispatch' , async ( ) => {
115189 const sessionStore = makeStore ( ) ;
116190 sessionStore . set ( 'default' , makeMacOsMenubarSession ( 'default' ) ) ;
0 commit comments